← Volver al listado de tecnologías

Capítulo 3: Una Sesión Real de Pair Programming

Por: Artiko
pair-programmingsesión-prácticaejemplo-realstrong-styledriver-navigatordiálogo

Capítulo 3: Una Sesión Real de Pair Programming

En este capítulo final, abandonaremos la teoría para sumergirnos en la práctica. Acompañaremos a Ana y Carlos durante una sesión completa de pair programming de dos horas, donde implementarán un sistema de autenticación JWT desde cero usando Strong-Style Pairing.

Esta no es una sesión idealizada o simplificada. Veremos los desafíos reales, las dudas, los momentos de frustración, las negociaciones técnicas y los pequeños triunfos que caracterizan una sesión genuina de pair programming. Observaremos cómo aplican las técnicas aprendidas, cómo se comunican, cómo manejan los desacuerdos y cómo llegan a soluciones mejores de las que cualquiera habría logrado solo.

Conociendo a los Protagonistas

Ana - La Senior Developer

Ana tiene 8 años de experiencia en desarrollo backend. Ha trabajado en startups y empresas grandes, especializándose en arquitectura de sistemas y seguridad. Es metódica, analítica y tiene una paciencia natural para enseñar. Sin embargo, a veces puede ser demasiado perfeccionista y le cuesta delegar.

Sus fortalezas incluyen:

Sus áreas de mejora:

Carlos - El Mid-Level Developer

Carlos tiene 3 años de experiencia, principalmente en frontend, pero últimamente ha estado trabajando más en full-stack. Es curioso, entusiasta y no teme hacer preguntas. A veces puede ser impulsivo y querer implementar la primera solución que se le ocurre.

Sus fortalezas incluyen:

Sus áreas de mejora:

El Contexto del Proyecto

La empresa donde trabajan necesita modernizar su sistema de autenticación. Actualmente usan sesiones basadas en cookies, pero quieren migrar a JWT para soportar mejor sus aplicaciones móviles y permitir una arquitectura más distribuida.

Los requisitos son:

  1. Registro de usuarios con validación de email
  2. Login con email y password
  3. Tokens JWT con refresh tokens
  4. Middleware para proteger rutas
  5. Manejo robusto de errores
  6. Tests básicos de la funcionalidad

Tienen 2 horas para crear un MVP funcional que demuestre la viabilidad del approach.

La Sesión: 9:00 AM - 11:00 AM

9:00 - 9:15: Setup y Preparación

Ana llega a su escritorio con su café matutino. Carlos ya está en línea, habiendo llegado unos minutos antes para preparar su ambiente.

Ana: “Buenos días, Carlos. ¿Listo para nuestra sesión de pairing?”

Carlos: “¡Buenos días! Sí, ya tengo el repo clonado. Creé una nueva rama feature/jwt-auth desde main.”

Ana: “Perfecto. ¿Tienes VS Code Live Share configurado?”

Carlos: “Sí, te envío el link… ahí está.”

Ana se une a la sesión compartida. Pueden ver los cursores del otro, los terminales están sincronizados, y ambos tienen acceso completo al código.

Ana: “Excelente. Hagamos un checklist rápido antes de empezar. ¿Timer pomodoro?”

Carlos: “Listo, 25 minutos configurados.”

Ana: “¿Slack y otras distracciones?”

Carlos: “En modo ‘No molestar’.”

Ana: “Genial. Entonces, hoy implementaremos autenticación JWT completa. He estado investigando y creo que deberíamos usar access tokens de corta duración con refresh tokens de larga duración.”

Carlos: “Tiene sentido. ¿Usamos la librería jsonwebtoken?”

Ana: “Sí, es la más estable. Para passwords usaremos bcrypt. ¿Te parece bien usar Strong-Style Pairing?”

Carlos: “Me parece bien. Tú empiezas como Navigator entonces, ¿verdad?”

Ana: “Exacto. Una cosa antes de empezar: voy a intentar comunicarte intenciones más que código específico. Si algo no está claro o tienes una mejor idea, por favor dilo. Esto es colaborativo.”

Carlos: “Entendido. Y si voy muy lento o no entiendo algo, te pregunto.”

Ana: “Perfecto. ¿Alguna pregunta antes de empezar?”

Carlos: “Solo una: ¿vamos a usar una base de datos real o mockearemos por ahora?”

Ana: “Buena pregunta. Crearemos interfaces para el repositorio, pero por ahora usaremos una implementación en memoria. Así nos enfocamos en la lógica de autenticación.”

Carlos: “Me parece bien. ¡Empecemos!“

9:15 - 9:40: Primera Sesión (Ana como Navigator, Carlos como Driver)

Ana: “Empecemos creando la estructura base. Necesitamos un servicio de autenticación que encapsule toda la lógica.”

Carlos: “Ok, creo un nuevo archivo… ¿authService.ts en una carpeta services?”

Ana: “Exacto.”

Carlos crea la estructura de carpetas y el archivo.

Ana: “Ahora, hagámoslo como una clase. Esto nos permitirá inyectar dependencias y hacer testing más fácil.”

Carlos: “Class AuthService entonces…”

Carlos comienza a escribir. Ana observa cómo Carlos estructura el código, resistiendo el impulso de dictar cada línea.

Ana: “Bien. Ahora pensemos qué dependencias necesitará este servicio.”

Carlos: “Definitivamente necesitamos jwt para los tokens… bcrypt para hashear passwords…”

Ana: “Correcto. ¿Qué más necesitaríamos?”

Carlos: “Mmm… acceso a la base de datos para guardar y buscar usuarios?”

Ana: “Exactamente. Pero no queremos que el servicio dependa directamente de la implementación de la base de datos.”

Carlos: “Ah, ¿inyección de dependencias? ¿Paso un repositorio en el constructor?”

Ana: “¡Perfecto! Eso es exactamente lo que tenía en mente.”

Carlos implementa el constructor con las dependencias. Ana nota que Carlos también agregó los secrets de JWT como parámetros del constructor, algo que ella no había mencionado explícitamente.

Ana: “Me gusta que hayas separado los secrets para access y refresh tokens. No lo había mencionado pero es una excelente práctica.”

Carlos: “Gracias. Lo vi en un tutorial hace poco y me pareció más seguro.”

Continúan trabajando en el método de registro. Ana guía la lógica general mientras Carlos implementa los detalles.

Ana: “Para el registro, primero necesitamos verificar si el usuario ya existe.”

Carlos: “Busco por email entonces?” escribe const existingUser = await this.userRepository.findByEmail(email);

Ana: “Exacto. Y si existe?”

Carlos: “Lanzo un error… pero espera, ¿no deberíamos tener errores personalizados para esto?”

Ana: “Muy buena observación. Por ahora usa un Error simple con un mensaje descriptivo. Podemos refactorizar después.”

Carlos continúa implementando. Hay un momento donde se confunde sobre el orden de las operaciones.

Carlos: “¿Hasheo el password antes o después de verificar si el usuario existe?”

Ana: “¿Qué crees que sería más eficiente?”

Carlos: “Ah, claro. Después. No tiene sentido hashear si vamos a rechazar el registro de todas formas.”

Ana: “Exactamente. Me gusta cómo estás pensando en la eficiencia.”

Después de 20 minutos, tienen el registro y login básicos implementados. Carlos ha estado escribiendo código fluídamente, haciendo preguntas cuando necesita clarificación, y Ana ha estado guiando sin microgestionar.

Ana: “Creo que es buen momento para hacer un commit antes de rotar. ¿Te parece?”

Carlos: “Sí, lo hago.” escribe el comando git add . && git commit -m "feat: implement base auth service with register and login"

Ana: “El mensaje de commit está bien, pero para el futuro, podríamos ser más específicos sobre qué incluye cada commit.”

Carlos: “Tienes razón. ¿Algo como ‘feat: add AuthService with register and login methods, using JWT and bcrypt’?”

Ana: “¡Perfecto! Usemos ese.”

Carlos actualiza el commit con —amend.

9:40 - 9:45: Break y Rotación

Ana: “Buen trabajo. Tomemos 5 minutos. Estira las piernas, toma agua.”

Carlos: “Ok. Oye, Ana, una pregunta rápida. Cuando sea Navigator, ¿qué tan específico debo ser?”

Ana: “Depende de lo que yo necesite. Si ves que implemento algo rápidamente, puedes ser más abstracto. Si me ves dudando, sé más específico. Es un balance que iremos encontrando.”

Carlos: “Entendido. También estaba pensando… deberíamos agregar refresh tokens, ¿no?”

Ana: “Definitivamente. Eso será prioritario en la siguiente sesión. ¿Tienes alguna idea de cómo implementarlo?”

Carlos: “Creo que deberíamos guardar los refresh tokens en la base de datos para poder revocarlos.”

Ana: “Excelente idea. Me gusta cómo estás pensando en la seguridad. Implementémoslo cuando retomemos.”

[Ambos se levantan, estiran, toman agua. Carlos revisa rápidamente su teléfono, Ana da una vuelta por la oficina.]

9:45 - 10:10: Segunda Sesión (Carlos como Navigator, Ana como Driver)

Carlos: “Ok, retomemos. Ahora necesitamos implementar refresh tokens. La idea es que cuando el access token expire, el cliente pueda usar el refresh token para obtener uno nuevo.”

Ana: “Entendido. ¿Empiezo creando un método para esto?”

Carlos: “Sí, llamémoslo refreshTokens. Debe recibir un refresh token y retornar un nuevo par de tokens.”

Ana comienza a implementar. Carlos nota que Ana escribe código muy rápido y eficientemente.

Carlos: “Primero necesitamos verificar que el refresh token sea válido.”

Ana implementa la verificación con jwt.verify, envolviendo todo en un try-catch.

Carlos: “Bien, pero también deberíamos verificar que ese refresh token esté en nuestra base de datos, ¿no? Para poder revocarlo si es necesario.”

Ana: “Claro, agrego esa validación.” implementa la verificación

Carlos: “Y si todo está bien, revocamos el token viejo y generamos uno nuevo.”

Ana implementa esto, pero luego se detiene.

Ana: “Carlos, hay un edge case aquí. ¿Qué pasa si el usuario intenta usar el mismo refresh token dos veces?”

Carlos: “Hmm… podría ser un ataque. ¿Deberíamos revocar todos los tokens del usuario en ese caso?”

Ana: “Esa es una excelente consideración de seguridad. Por ahora, simplemente lo marquemos como inválido y retornemos error. Podemos agregar esa lógica más adelante.”

Continúan trabajando. Carlos guía la creación del middleware de autenticación.

Carlos: “Ahora necesitamos un middleware para proteger rutas. Debería extraer el token del header Authorization.”

Ana: “¿El formato estándar ‘Bearer token’?”

Carlos: “Exacto. Y si no hay token o es inválido, retorna 401.”

Ana implementa el middleware rápidamente. Su experiencia se nota en cómo estructura el código y maneja los edge cases.

Carlos: “Espera, ¿no deberíamos diferenciar entre token expirado y token inválido?”

Ana: “¿Para qué el cliente sepa si debe intentar refresh?”

Carlos: “Exactamente.”

Ana: “Brillante. Déjame agregar eso.” modifica el código para retornar diferentes códigos/mensajes de error

Carlos nota que Ana agregó algo que él no mencionó.

Carlos: “Veo que agregaste logging. Buena idea.”

Ana: “Sí, siempre es útil para debugging en producción. ¿Te parece bien?”

Carlos: “Totalmente. De hecho, creo que deberíamos agregar más logging en otros lugares también.”

Trabajan unos minutos más en pulir el código.

Carlos: “Creo que deberíamos agregar una clase de errores personalizados ahora.”

Ana: “De acuerdo. ¿Cómo la estructurarías?”

Carlos: “Una clase base AuthError, y luego clases específicas como InvalidCredentialsError, TokenExpiredError, etc.”

Ana implementa rápidamente la jerarquía de errores siguiendo la visión de Carlos. La experiencia de Ana hace que la implementación sea rápida, pero Carlos mantiene el control de la dirección.

Carlos: “Perfecto. Hagamos otro commit aquí.”

Ana: ejecuta git add . && git commit -m "feat: add refresh token logic and auth middleware with custom errors"

10:10 - 10:15: Break Rápido

Carlos: “Vamos bien de tiempo. ¿Cómo te sientes siendo Driver?”

Ana: “Es interesante. Me obliga a no adelantarme a tus ideas. A veces quiero agregar algo que sé que necesitaremos, pero espero a que lo menciones o te pregunto.”

Carlos: “Sí, como el logging. Me gustó que preguntaras.”

Ana: “Es parte del Strong-Style. Tus ideas, mis manos. Aunque puedo sugerir mejoras.”

10:15 - 10:40: Tercera Sesión (Ana como Navigator, Carlos como Driver)

Ana: “Ahora conectemos todo con Express. Necesitamos controllers y rutas.”

Carlos: “Ok, creo la estructura… controllers/authController.ts?”

Ana: “Perfecto.”

Carlos comienza a implementar el controller. Ana guía la estructura general.

Ana: “El controller debería ser también una clase, para consistencia.”

Carlos: “Y recibe el AuthService en el constructor.”

Ana: “Exacto. Me gusta que ya estés pensando en inyección de dependencias.”

Implementan los métodos del controller: register, login, refreshToken. Carlos ocasionalmente hace preguntas sobre el manejo de errores y códigos de respuesta HTTP.

Carlos: “¿201 para registro exitoso?”

Ana: “Correcto, es un recurso creado. Me impresiona que conozcas los códigos apropiados.”

Carlos: “He estado estudiando REST últimamente.”

Mientras Carlos implementa, comete un pequeño error. Olvida await en una llamada asíncrona.

Ana: “Cuidado, falta algo en la línea 42.”

Carlos: “¿Línea 42…? Ah! El await. Gracias.”

Ana: “De nada. Esos son los beneficios del pair programming, cuatro ojos ven más que dos.”

Continúan con las rutas. Carlos sugiere una mejora.

Carlos: “¿No deberíamos validar el formato del email antes de llegar al servicio?”

Ana: “Buena idea. ¿Dónde lo pondrías?”

Carlos: “En el controller, antes de llamar al servicio. Así el servicio se mantiene limpio y enfocado en lógica de negocio.”

Ana: “Me gusta tu pensamiento. Implementémoslo.”

Carlos agrega validación básica en el controller. Ana sugiere usar una librería de validación para el futuro, pero acuerdan mantenerlo simple por ahora.

10:40 - 10:45: Integración Final

Ana: “Conectemos todo en el archivo principal de la aplicación.”

Carlos: “app.ts o server.ts?”

Ana: “app.ts para la configuración de Express, server.ts para levantar el servidor.”

Carlos implementa la configuración básica de Express, monta las rutas, agrega middleware de parsing de JSON.

Ana: “No olvides el error handler global.”

Carlos: “Cierto, para catches todos los errores no manejados.”

Implementan un error handler que convierte los errores personalizados en respuestas HTTP apropiadas.

10:45 - 10:55: Testing Rápido

Carlos (ahora como Navigator): “Deberíamos al menos escribir un par de tests básicos.”

Ana (como Driver): “De acuerdo. ¿Tests unitarios o de integración?”

Carlos: “Empecemos con un test unitario del servicio, solo para validar el happy path del registro.”

Ana configura rápidamente Jest y escribe un test básico. Su experiencia hace que sea rápido, pero Carlos guía qué testear.

Carlos: “Asegúrate de verificar que el password no se retorne en la respuesta.”

Ana: “Buen punto.” agrega la aserción

Carlos: “Y mockea el repositorio para que sea un test unitario real.”

Ana implementa los mocks. Trabajan juntos para agregar un par de tests más.

10:55 - 11:00: Retrospectiva

Ana: “Excelente trabajo, Carlos. Hagamos commit final y una retro rápida.”

Carlos: ejecuta git add . && git commit -m "feat: add controllers, routes, and basic tests"

Ana: “¿Cómo te sentiste con la sesión?”

Carlos: “Muy bien. Aprendí mucho sobre JWT y seguridad. El Strong-Style fue raro al principio, pero me obligó a entender todo lo que escribía.”

Ana: “¿Qué fue lo más desafiante?”

Carlos: “Como Navigator, encontrar el balance entre ser muy específico o muy vago. A veces no sabía cuánto detalle dar.”

Ana: “Es normal, viene con la práctica. Lo hiciste muy bien. ¿Qué te gustó más?”

Carlos: “Que realmente trabajamos juntos. No fue uno enseñando y otro aprendiendo, ambos contribuimos.”

Ana: “Totalmente de acuerdo. Tu sugerencia sobre los refresh tokens en la DB fue excelente, no lo había considerado inicialmente.”

Carlos: “Y tu experiencia con el manejo de errores nos ahorró mucho tiempo. ¿Hacemos pair programming más seguido?”

Ana: “Definitivamente. Quizás podemos institucionalizar ‘Pair Programming Fridays’ o algo así.”

Análisis de la Sesión

Lo Que Funcionó Bien

La sesión de Ana y Carlos demuestra varios aspectos positivos del pair programming bien ejecutado:

Comunicación Efectiva: Ambos comunicaron claramente sus ideas y dudas. No hubo momentos de silencio incómodo ni malentendidos significativos.

Aprendizaje Bidireccional: Ana aprendió sobre nuevas prácticas (como separar secrets de JWT) y Carlos aprendió sobre arquitectura y seguridad.

Ego Saludable: Ninguno intentó dominar la sesión. Ambos aceptaron sugerencias y admitieron cuando no sabían algo.

Ritmo Sostenible: Los breaks regulares y las rotaciones mantuvieron la energía y concentración.

Código de Calidad: El resultado fue código bien estructurado, con manejo de errores, tests, y consideraciones de seguridad.

Áreas de Mejora

También hubo aspectos que podrían mejorar:

Planificación Inicial: Podrían haber dedicado más tiempo a discutir la arquitectura general antes de empezar a codear.

Documentación: No escribieron documentación o comentarios. En una sesión real más larga, esto sería importante.

Testing Más Completo: Los tests fueron básicos. Idealmente habrían escrito tests de integración también.

Manejo del Tiempo: Casi no les alcanzó el tiempo para tests. Mejor estimación ayudaría.

Lecciones Clave

  1. El Strong-Style funciona: Forzar que las ideas pasen por otra persona realmente resulta en mejor comprensión mutua.

  2. La experiencia diferente es una ventaja: Ana aportó experiencia en arquitectura, Carlos frescura y nuevas perspectivas.

  3. Los desacuerdos constructivos mejoran el código: La discusión sobre refresh tokens resultó en una mejor solución.

  4. El pair programming es menos cansado de lo esperado: Con rotaciones y breaks apropiados, mantuvieron energía por 2 horas.

  5. La calidad emergente es superior: El código resultante fue mejor que lo que cualquiera habría producido solo.

Código Completo de la Sesión

El Servicio de Autenticación

// src/services/authService.ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import crypto from 'crypto';
import { UserRepository } from '../repositories/userRepository';
import { 
  InvalidCredentialsError, 
  EmailAlreadyExistsError,
  InvalidTokenError 
} from '../errors/authErrors';

interface User {
  id: string;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
  updatedAt: Date;
}

interface AuthTokens {
  accessToken: string;
  refreshToken: string;
}

interface JwtPayload {
  userId: string;
  email: string;
  tokenId?: string;
}

export class AuthService {
  constructor(
    private userRepository: UserRepository,
    private jwtSecret: string,
    private jwtRefreshSecret: string
  ) {}

  async register(name: string, email: string, password: string): Promise<Omit<User, 'password'>> {
    // Verificar si el usuario existe
    const existingUser = await this.userRepository.findByEmail(email);
    
    if (existingUser) {
      throw new EmailAlreadyExistsError();
    }
    
    // Hashear password
    const hashedPassword = await bcrypt.hash(password, 10);
    
    // Crear usuario
    const user = await this.userRepository.create({
      name,
      email,
      password: hashedPassword
    });
    
    // Retornar sin password
    const { password: _, ...userWithoutPassword } = user;
    return userWithoutPassword;
  }

  async login(email: string, password: string): Promise<AuthTokens> {
    // Buscar usuario
    const user = await this.userRepository.findByEmail(email);
    
    if (!user) {
      throw new InvalidCredentialsError();
    }
    
    // Verificar password
    const isPasswordValid = await bcrypt.compare(password, user.password);
    
    if (!isPasswordValid) {
      throw new InvalidCredentialsError();
    }
    
    // Generar tokens
    return this.generateTokens(user);
  }

  async refreshTokens(refreshToken: string): Promise<AuthTokens> {
    try {
      // Verificar y decodificar token
      const decoded = jwt.verify(
        refreshToken,
        this.jwtRefreshSecret
      ) as JwtPayload;
      
      // Verificar en DB
      const isValid = await this.userRepository.validateRefreshToken(
        decoded.userId,
        decoded.tokenId!
      );
      
      if (!isValid) {
        throw new InvalidTokenError();
      }
      
      // Obtener usuario actualizado
      const user = await this.userRepository.findById(decoded.userId);
      
      if (!user) {
        throw new InvalidTokenError();
      }
      
      // Revocar token anterior
      await this.userRepository.revokeRefreshToken(decoded.tokenId!);
      
      // Generar nuevos tokens
      return this.generateTokens(user);
      
    } catch (error) {
      throw new InvalidTokenError();
    }
  }

  private async generateTokens(user: User): Promise<AuthTokens> {
    const payload: JwtPayload = {
      userId: user.id,
      email: user.email
    };
    
    // Access token - 15 minutos
    const accessToken = jwt.sign(
      payload,
      this.jwtSecret,
      { expiresIn: '15m' }
    );
    
    // Refresh token - 7 días
    const refreshTokenId = crypto.randomUUID();
    const refreshToken = jwt.sign(
      { ...payload, tokenId: refreshTokenId },
      this.jwtRefreshSecret,
      { expiresIn: '7d' }
    );
    
    // Guardar refresh token
    await this.userRepository.saveRefreshToken(
      user.id,
      refreshTokenId,
      refreshToken
    );
    
    return { accessToken, refreshToken };
  }
}

El Middleware de Autenticación

// src/middleware/authMiddleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

interface AuthRequest extends Request {
  user?: {
    userId: string;
    email: string;
  };
}

export function createAuthMiddleware(jwtSecret: string) {
  return async (
    req: AuthRequest,
    res: Response,
    next: NextFunction
  ) => {
    try {
      // Extraer token
      const authHeader = req.headers.authorization;
      
      if (!authHeader?.startsWith('Bearer ')) {
        return res.status(401).json({ 
          error: 'No token provided' 
        });
      }
      
      const token = authHeader.substring(7);
      
      // Verificar token
      const decoded = jwt.verify(token, jwtSecret) as {
        userId: string;
        email: string;
      };
      
      // Adjuntar usuario al request
      req.user = decoded;
      next();
      
    } catch (error) {
      if (error instanceof jwt.TokenExpiredError) {
        return res.status(401).json({ 
          error: 'Token expired',
          code: 'TOKEN_EXPIRED'
        });
      }
      
      return res.status(401).json({ 
        error: 'Invalid token',
        code: 'INVALID_TOKEN'
      });
    }
  };
}

El Controller

// src/controllers/authController.ts
import { Request, Response } from 'express';
import { AuthService } from '../services/authService';
import { 
  EmailAlreadyExistsError,
  InvalidCredentialsError,
  InvalidTokenError 
} from '../errors/authErrors';

export class AuthController {
  constructor(private authService: AuthService) {}
  
  register = async (req: Request, res: Response) => {
    try {
      const { name, email, password } = req.body;
      
      // Validaciones básicas
      if (!name || !email || !password) {
        return res.status(400).json({ 
          error: 'Name, email and password are required' 
        });
      }
      
      // Validar formato email
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(email)) {
        return res.status(400).json({ 
          error: 'Invalid email format' 
        });
      }
      
      // Validar longitud password
      if (password.length < 8) {
        return res.status(400).json({ 
          error: 'Password must be at least 8 characters' 
        });
      }
      
      // Registrar usuario
      const user = await this.authService.register(name, email, password);
      
      res.status(201).json({ 
        message: 'User registered successfully',
        user 
      });
      
    } catch (error) {
      if (error instanceof EmailAlreadyExistsError) {
        return res.status(409).json({ 
          error: error.message 
        });
      }
      
      console.error('Registration error:', error);
      res.status(500).json({ 
        error: 'Internal server error' 
      });
    }
  };
  
  login = async (req: Request, res: Response) => {
    try {
      const { email, password } = req.body;
      
      if (!email || !password) {
        return res.status(400).json({ 
          error: 'Email and password are required' 
        });
      }
      
      const tokens = await this.authService.login(email, password);
      
      res.json({ 
        message: 'Login successful',
        ...tokens 
      });
      
    } catch (error) {
      if (error instanceof InvalidCredentialsError) {
        return res.status(401).json({ 
          error: error.message 
        });
      }
      
      console.error('Login error:', error);
      res.status(500).json({ 
        error: 'Internal server error' 
      });
    }
  };
  
  refreshToken = async (req: Request, res: Response) => {
    try {
      const { refreshToken } = req.body;
      
      if (!refreshToken) {
        return res.status(400).json({ 
          error: 'Refresh token is required' 
        });
      }
      
      const tokens = await this.authService.refreshTokens(refreshToken);
      
      res.json({ 
        message: 'Tokens refreshed successfully',
        ...tokens 
      });
      
    } catch (error) {
      return res.status(401).json({ 
        error: 'Invalid refresh token' 
      });
    }
  };
}

Los Tests

// src/services/authService.test.ts
import { AuthService } from './authService';
import { UserRepository } from '../repositories/userRepository';
import bcrypt from 'bcrypt';

// Mock bcrypt
jest.mock('bcrypt');

describe('AuthService', () => {
  let authService: AuthService;
  let mockUserRepo: jest.Mocked<UserRepository>;
  
  beforeEach(() => {
    // Setup mocks
    mockUserRepo = {
      findByEmail: jest.fn(),
      create: jest.fn(),
      findById: jest.fn(),
      saveRefreshToken: jest.fn(),
      validateRefreshToken: jest.fn(),
      revokeRefreshToken: jest.fn()
    } as any;
    
    authService = new AuthService(
      mockUserRepo,
      'test-secret',
      'test-refresh-secret'
    );
    
    // Mock bcrypt
    (bcrypt.hash as jest.Mock).mockResolvedValue('hashed-password');
    (bcrypt.compare as jest.Mock).mockResolvedValue(true);
  });
  
  describe('register', () => {
    it('should register a new user successfully', async () => {
      // Arrange
      mockUserRepo.findByEmail.mockResolvedValue(null);
      
      const newUser = {
        id: '123',
        name: 'Test User',
        email: '[email protected]',
        password: 'hashed-password',
        createdAt: new Date(),
        updatedAt: new Date()
      };
      
      mockUserRepo.create.mockResolvedValue(newUser);
      
      // Act
      const result = await authService.register(
        'Test User',
        '[email protected]',
        'password123'
      );
      
      // Assert
      expect(result).not.toHaveProperty('password');
      expect(result.email).toBe('[email protected]');
      expect(result.name).toBe('Test User');
      expect(mockUserRepo.create).toHaveBeenCalledWith({
        name: 'Test User',
        email: '[email protected]',
        password: 'hashed-password'
      });
    });
    
    it('should throw error if email already exists', async () => {
      // Arrange
      mockUserRepo.findByEmail.mockResolvedValue({
        id: '123',
        email: '[email protected]'
      } as any);
      
      // Act & Assert
      await expect(
        authService.register(
          'Test User',
          '[email protected]',
          'password123'
        )
      ).rejects.toThrow('Email already registered');
    });
  });
  
  describe('login', () => {
    it('should login successfully with valid credentials', async () => {
      // Arrange
      const user = {
        id: '123',
        email: '[email protected]',
        password: 'hashed-password',
        name: 'Test User'
      };
      
      mockUserRepo.findByEmail.mockResolvedValue(user as any);
      mockUserRepo.saveRefreshToken.mockResolvedValue(undefined);
      
      // Act
      const result = await authService.login(
        '[email protected]',
        'password123'
      );
      
      // Assert
      expect(result).toHaveProperty('accessToken');
      expect(result).toHaveProperty('refreshToken');
      expect(bcrypt.compare).toHaveBeenCalledWith(
        'password123',
        'hashed-password'
      );
    });
    
    it('should throw error with invalid credentials', async () => {
      // Arrange
      mockUserRepo.findByEmail.mockResolvedValue(null);
      
      // Act & Assert
      await expect(
        authService.login('[email protected]', 'wrong')
      ).rejects.toThrow('Invalid credentials');
    });
  });
});

Reflexiones Finales sobre la Sesión

El Valor Real del Pair Programming

La sesión de Ana y Carlos ilustra perfectamente que el pair programming no es simplemente “dos personas escribiendo código juntas”. Es un proceso complejo de:

Los Momentos Clave

Varios momentos en la sesión fueron particularmente valiosos:

  1. Cuando Carlos sugirió guardar refresh tokens en la DB: Mostró que el “junior” puede tener insights valiosos
  2. Cuando Ana resistió agregar features sin que Carlos las mencionara: Demostró disciplina en Strong-Style
  3. Cuando discutieron dónde poner validaciones: Colaboración genuina para encontrar la mejor solución
  4. Cuando Ana cachó el await faltante: El valor de tener dos pares de ojos
  5. Cuando ambos admitieron aprender del otro: Humildad y apertura

El Código Como Artefacto Compartido

El código resultante no “pertenece” a Ana o Carlos; es genuinamente colaborativo. Cada línea fue discutida, entendida y acordada por ambos. Esto tiene implicaciones profundas:

Guía para Tu Primera Sesión

Basándote en la experiencia de Ana y Carlos, aquí hay una guía para tu primera sesión de pair programming:

Antes de la Sesión

  1. Define el objetivo claramente: ¿Qué van a construir?
  2. Acuerda el estilo: Strong-Style es recomendado para empezar
  3. Prepara el ambiente: Herramientas, repos, documentación
  4. Elimina distracciones: Modo “No molestar” en todo
  5. Ten agua/café a mano: Hidratación es importante

Durante la Sesión

  1. Comunica constantemente: Silencio prolongado es una señal de alerta
  2. Rota religiosamente: No te saltes las rotaciones
  3. Toma breaks: Son esenciales para mantener energía
  4. Haz commits frecuentes: En cada rotación idealmente
  5. Sé paciente: Especialmente las primeras veces

Después de la Sesión

  1. Haz retrospectiva: ¿Qué funcionó? ¿Qué mejorar?
  2. Documenta decisiones importantes: Mientras están frescas
  3. Planea la siguiente: El pair programming mejora con práctica
  4. Comparte aprendizajes: Con el equipo o comunidad
  5. Celebra el éxito: Reconoce el buen trabajo

Ejercicios Prácticos

Ejercicio 1: Replica la Sesión

Con un compañero, intenta replicar exactamente la sesión de Ana y Carlos:

Ejercicio 2: Variación del Proyecto

Usa la misma estructura de sesión pero con un proyecto diferente:

Ejercicio 3: Análisis de Diálogos

Lee nuevamente los diálogos e identifica:

Ejercicio 4: Role Play

Practica situaciones difíciles:

Conclusión

La sesión de Ana y Carlos nos muestra que el pair programming, especialmente con Strong-Style, es una práctica poderosa que va más allá de simplemente escribir código juntos. Es una forma de:

El código que produjeron en 2 horas no solo funciona; es mantenible, está bien estructurado, considera seguridad, incluye tests, y lo más importante: ambos lo entienden completamente.

Tu Turno

Ahora que has visto una sesión completa de pair programming, es tu momento de practicar. Encuentra un compañero, elige un proyecto pequeño, define 2 horas, y aplica lo aprendido.

Recuerda: la primera vez será incómoda, la segunda será mejor, y para la décima vez, no querrás programar de otra manera.

El pair programming no es sobre escribir código más rápido; es sobre escribir mejor código, juntos.


“El mejor código que he escrito, no lo escribí yo solo.” - Ana y Carlos