Aller au contenu

7b/ Revue des Tests — PD-32

Tu es QA Engineer senior, spécialisé dans les tests automatisés NestJS/Jest.

TÂCHE

Produis une revue des tests pour l'implémentation PD-32 (endpoints GET/PUT /user/profile).

Évalue : - Couverture des cas nominaux et d'erreur - Pertinence des assertions - Robustesse (edge cases, données limites) - Isolation et indépendance des tests

Verdict : ✅ CONFORME / ⚠️ RÉSERVES / ❌ NON_CONFORME

AXES D'ANALYSE

  • Tous les TC-* de la matrice sont couverts
  • Assertions pertinentes (pas de toBeTruthy() générique)
  • Tests isolés (pas de dépendance inter-tests)
  • Mocking approprié (services, repositories)
  • Edge cases couverts (null, vide, limites)
  • Tests négatifs (erreurs attendues)
  • Nommage descriptif des tests

TESTS CONTRACTUELS (référence)

Matrice de couverture

ID Invariant ID Critère ID Test Couverture Commentaire
INV-32-01 CA-32-01 TC-ERR-01 Oui GET sans JWT refusé
INV-32-01 CA-32-02 TC-ERR-02 Oui PUT sans JWT refusé
INV-32-02 CA-32-03 TC-ERR-03 Oui Accès croisé lecture refusé
INV-32-02 CA-32-03 TC-ERR-04 Oui Accès croisé écriture refusé
INV-32-03 CA-32-04 TC-NOM-01 Oui Réponse profil non sensible
INV-32-04 CA-32-04 TC-INV-01 Oui Champs sensibles/protégés absents
INV-32-05 CA-32-06 TC-NOM-02 Oui Mise à jour name autorisée
INV-32-05 CA-32-07 TC-NOM-03 Oui Mise à jour avatar_url autorisée
INV-32-05 CA-32-08 TC-NOM-04 Oui Mise à jour preferences autorisée
INV-32-06 CA-32-10 TC-ERR-06 Oui Rejet modification email/plan
INV-32-06 CA-32-11 TC-ERR-07 Oui Rejet modification id/status/srpSalt/passwordHash
INV-32-07 CA-32-09 TC-ERR-05 Oui Rejet clé inconnue dans preferences
INV-32-08 CA-32-05 TC-NOM-01 Oui Contrat PD-106 respecté
INV-32-09 CA-32-12 TC-ERR-08 Oui Rate limit global appliqué
INV-32-10 CA-32-13 TC-INV-02 Oui Erreurs explicites sans mutation partielle
INV-32-10 CA-32-13 TC-ERR-09 Oui Validation invalide : échec explicite
INV-32-10 CA-32-13 TC-ERR-10 Oui Erreur interne : échec explicite

TESTS IMPLÉMENTÉS À REVIEWER

update-user-profile.dto.spec.ts (Tests unitaires DTO)

import { plainToInstance } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';

import { UpdateUserProfileDto } from './update-user-profile.dto';

describe('UpdateUserProfileDto', () => {
  async function validateDto(dto: object): Promise<ValidationError[]> {
    const instance = plainToInstance(UpdateUserProfileDto, dto);
    return validate(instance, { whitelist: true, forbidNonWhitelisted: true });
  }

  function hasPropertyError(errors: ValidationError[], property: string): boolean {
    return errors.some((e) => e.property === property);
  }

  describe('PD-32 Valid payloads', () => {
    it('should accept valid update with all fields', async () => {
      const dto = {
        name: 'Jean Dupont',
        avatarUrl: 'https://example.com/avatar.png',
        preferences: {
          locale: 'fr',
          timezone: 'Europe/Paris',
        },
      };

      const errors = await validateDto(dto);
      expect(errors).toHaveLength(0);
    });

    it('should accept update with only name', async () => {
      const dto = { name: 'Jean Dupont' };
      const errors = await validateDto(dto);
      expect(errors).toHaveLength(0);
    });

    it('should accept update with only avatarUrl', async () => {
      const dto = { avatarUrl: 'https://cdn.example.com/images/user123.jpg' };
      const errors = await validateDto(dto);
      expect(errors).toHaveLength(0);
    });

    it('should accept update with only preferences', async () => {
      const dto = {
        preferences: {
          locale: 'en',
          notifications: { email: true },
        },
      };
      const errors = await validateDto(dto);
      expect(errors).toHaveLength(0);
    });

    it('should accept empty object (no changes)', async () => {
      const dto = {};
      const errors = await validateDto(dto);
      expect(errors).toHaveLength(0);
    });
  });

  describe('INV-32-05: Validates name field', () => {
    it('should accept valid name', async () => {
      const dto = { name: 'John Doe' };
      const errors = await validateDto(dto);
      expect(hasPropertyError(errors, 'name')).toBe(false);
    });

    it('should reject name exceeding 100 characters', async () => {
      const dto = { name: 'a'.repeat(101) };
      const errors = await validateDto(dto);
      expect(hasPropertyError(errors, 'name')).toBe(true);
    });

    it('should accept name with exactly 100 characters', async () => {
      const dto = { name: 'a'.repeat(100) };
      const errors = await validateDto(dto);
      expect(hasPropertyError(errors, 'name')).toBe(false);
    });

    it('should reject name as number', async () => {
      const dto = { name: 12345 };
      const errors = await validateDto(dto);
      expect(hasPropertyError(errors, 'name')).toBe(true);
    });
  });

  describe('INV-32-05: Validates avatarUrl field', () => {
    it('should accept valid HTTPS URL', async () => {
      const dto = { avatarUrl: 'https://example.com/avatar.png' };
      const errors = await validateDto(dto);
      expect(hasPropertyError(errors, 'avatarUrl')).toBe(false);
    });

    it('should reject HTTP URL (not HTTPS)', async () => {
      const dto = { avatarUrl: 'http://example.com/avatar.png' };
      const errors = await validateDto(dto);
      expect(hasPropertyError(errors, 'avatarUrl')).toBe(true);
    });

    it('should reject URL without protocol', async () => {
      const dto = { avatarUrl: 'example.com/avatar.png' };
      const errors = await validateDto(dto);
      expect(hasPropertyError(errors, 'avatarUrl')).toBe(true);
    });

    it('should reject invalid URL', async () => {
      const dto = { avatarUrl: 'not-a-url' };
      const errors = await validateDto(dto);
      expect(hasPropertyError(errors, 'avatarUrl')).toBe(true);
    });

    it('should reject avatarUrl exceeding 2048 characters', async () => {
      const longUrl = 'https://example.com/' + 'a'.repeat(2030);
      const dto = { avatarUrl: longUrl };
      const errors = await validateDto(dto);
      expect(hasPropertyError(errors, 'avatarUrl')).toBe(true);
    });
  });

  describe('INV-32-06: Rejects protected fields', () => {
    it('should reject email field (protected)', async () => {
      const dto = { name: 'John', email: 'new@email.com' };
      const errors = await validateDto(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it('should reject plan field (protected)', async () => {
      const dto = { name: 'John', plan: 'premium' };
      const errors = await validateDto(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it('should reject id field (protected)', async () => {
      const dto = { name: 'John', id: '550e8400-e29b-41d4-a716-446655440000' };
      const errors = await validateDto(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it('should reject status field (protected)', async () => {
      const dto = { name: 'John', status: 'ACTIVE' };
      const errors = await validateDto(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it('should reject passwordHash field (sensitive)', async () => {
      const dto = { name: 'John', passwordHash: 'somehash' };
      const errors = await validateDto(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it('should reject srpSalt field (sensitive)', async () => {
      const dto = { name: 'John', srpSalt: 'somesalt' };
      const errors = await validateDto(dto);
      expect(errors.length).toBeGreaterThan(0);
    });
  });

  describe('INV-32-07: Validates preferences structure', () => {
    it('should accept valid preferences with locale', async () => {
      const dto = { preferences: { locale: 'fr' } };
      const errors = await validateDto(dto);
      expect(errors).toHaveLength(0);
    });

    it('should accept valid preferences with timezone', async () => {
      const dto = { preferences: { timezone: 'Europe/Paris' } };
      const errors = await validateDto(dto);
      expect(errors).toHaveLength(0);
    });

    it('should accept valid preferences with security settings', async () => {
      const dto = {
        preferences: {
          security: { loginNotifications: true, sessionTimeout: 30 },
        },
      };
      const errors = await validateDto(dto);
      expect(errors).toHaveLength(0);
    });

    it('should accept valid preferences with notifications', async () => {
      const dto = {
        preferences: { notifications: { email: true, push: false } },
      };
      const errors = await validateDto(dto);
      expect(errors).toHaveLength(0);
    });

    it('should reject invalid locale value', async () => {
      const dto = { preferences: { locale: 'invalid' } };
      const errors = await validateDto(dto);
      expect(errors.length).toBeGreaterThan(0);
    });

    it('should accept locale "en"', async () => {
      const dto = { preferences: { locale: 'en' } };
      const errors = await validateDto(dto);
      expect(errors).toHaveLength(0);
    });
  });
});

user-profile.service.spec.ts (Tests unitaires Service)

import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { DataSource, EntityManager } from 'typeorm';

import { RlsQueryService } from '../../../database/services/rls-query.service';
import { User } from '../../auth/entities/user.entity';
import { UserProfileService } from './user-profile.service';
import { UpdateUserProfileDto } from '../dto/update-user-profile.dto';

describe('UserProfileService', () => {
  let service: UserProfileService;
  let rlsQueryService: jest.Mocked<RlsQueryService>;
  let dataSource: jest.Mocked<DataSource>;
  let entityManager: jest.Mocked<EntityManager>;

  const mockUserId = '550e8400-e29b-41d4-a716-446655440000';
  const mockUser: Partial<User> = {
    id: mockUserId,
    email: 'test@example.com',
    name: 'John Doe',
    avatarUrl: 'https://example.com/avatar.png',
    preferences: { locale: 'fr', timezone: 'Europe/Paris' },
  };

  beforeEach(async () => {
    rlsQueryService = {
      findOneWithRls: jest.fn(),
    } as unknown as jest.Mocked<RlsQueryService>;

    entityManager = {
      query: jest.fn(),
      findOne: jest.fn(),
      save: jest.fn(),
    } as unknown as jest.Mocked<EntityManager>;

    dataSource = {
      transaction: jest.fn((callback) => callback(entityManager)),
    } as unknown as jest.Mocked<DataSource>;

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserProfileService,
        { provide: RlsQueryService, useValue: rlsQueryService },
        { provide: DataSource, useValue: dataSource },
      ],
    }).compile();

    service = module.get<UserProfileService>(UserProfileService);
  });

  describe('getProfile', () => {
    it('should return user profile when user exists', async () => {
      rlsQueryService.findOneWithRls.mockResolvedValue(mockUser as User);

      const result = await service.getProfile(mockUserId);

      expect(rlsQueryService.findOneWithRls).toHaveBeenCalledWith(User, {
        where: { id: mockUserId },
        select: ['id', 'name', 'email', 'avatarUrl', 'preferences'],
      });
      expect(result).toEqual({
        name: 'John Doe',
        email: 'test@example.com',
        avatarUrl: 'https://example.com/avatar.png',
        preferences: { locale: 'fr', timezone: 'Europe/Paris' },
      });
    });

    it('should throw NotFoundException when user does not exist', async () => {
      rlsQueryService.findOneWithRls.mockResolvedValue(null);
      await expect(service.getProfile(mockUserId)).rejects.toThrow(NotFoundException);
    });

    it('should return profile with empty preferences when preferences is null', async () => {
      const userWithoutPrefs = { ...mockUser, preferences: null };
      rlsQueryService.findOneWithRls.mockResolvedValue(userWithoutPrefs as unknown as User);

      const result = await service.getProfile(mockUserId);
      expect(result.preferences).toEqual({});
    });
  });

  describe('updateProfile', () => {
    const updateDto: UpdateUserProfileDto = {
      name: 'Jane Doe',
      avatarUrl: 'https://new.example.com/avatar.jpg',
      preferences: { locale: 'en' },
    };

    beforeEach(() => {
      entityManager.findOne.mockResolvedValue(mockUser as User);
      entityManager.save.mockImplementation(async (_, user) => user as User);
    });

    it('should update user profile and return updated data', async () => {
      const result = await service.updateProfile(mockUserId, updateDto);

      expect(entityManager.query).toHaveBeenCalledWith(
        'SET LOCAL app.current_user_id = $1',
        [mockUserId],
      );
      expect(entityManager.save).toHaveBeenCalled();
      expect(result.name).toBe('Jane Doe');
      expect(result.avatarUrl).toBe('https://new.example.com/avatar.jpg');
    });

    it('should throw NotFoundException when user does not exist', async () => {
      entityManager.findOne.mockResolvedValue(null);
      await expect(service.updateProfile(mockUserId, updateDto)).rejects.toThrow(NotFoundException);
    });

    it('should merge preferences instead of replacing them', async () => {
      const existingUser = {
        ...mockUser,
        preferences: {
          locale: 'fr',
          timezone: 'Europe/Paris',
          notifications: { email: true },
        },
      };
      entityManager.findOne.mockResolvedValue(existingUser as User);
      entityManager.save.mockImplementation(async (_, user) => user as User);

      const partialUpdate: UpdateUserProfileDto = { preferences: { locale: 'en' } };
      await service.updateProfile(mockUserId, partialUpdate);

      expect(entityManager.save).toHaveBeenCalledWith(
        User,
        expect.objectContaining({
          preferences: expect.objectContaining({
            locale: 'en',
            timezone: 'Europe/Paris',
            notifications: { email: true },
          }),
        }),
      );
    });

    it('should only update provided fields', async () => {
      const existingUser = { ...mockUser };
      entityManager.findOne.mockResolvedValue(existingUser as User);
      entityManager.save.mockImplementation(async (_, user) => user as User);

      const partialUpdate: UpdateUserProfileDto = { name: 'Only Name Updated' };
      await service.updateProfile(mockUserId, partialUpdate);

      expect(entityManager.save).toHaveBeenCalledWith(
        User,
        expect.objectContaining({
          name: 'Only Name Updated',
          avatarUrl: mockUser.avatarUrl,
        }),
      );
    });

    it('should handle undefined fields without changing them', async () => {
      const existingUser = { ...mockUser };
      entityManager.findOne.mockResolvedValue(existingUser as User);
      entityManager.save.mockImplementation(async (_, user) => user as User);

      const emptyUpdate: UpdateUserProfileDto = {};
      await service.updateProfile(mockUserId, emptyUpdate);

      expect(entityManager.save).toHaveBeenCalledWith(
        User,
        expect.objectContaining({
          name: mockUser.name,
          avatarUrl: mockUser.avatarUrl,
          preferences: mockUser.preferences,
        }),
      );
    });
  });

  describe('INV-32-02: RLS enforcement', () => {
    it('getProfile should use RLS-aware query service', async () => {
      rlsQueryService.findOneWithRls.mockResolvedValue(mockUser as User);
      await service.getProfile(mockUserId);
      expect(rlsQueryService.findOneWithRls).toHaveBeenCalled();
    });

    it('updateProfile should set RLS context before query', async () => {
      entityManager.findOne.mockResolvedValue(mockUser as User);
      entityManager.save.mockImplementation(async (_, user) => user as User);

      await service.updateProfile(mockUserId, { name: 'Test' });

      const calls = entityManager.query.mock.calls;
      expect(calls[0]).toEqual(['SET LOCAL app.current_user_id = $1', [mockUserId]]);
    });
  });
});

user-profile.e2e-spec.ts (Tests E2E)

/**
 * UserProfile E2E Tests - PD-32
 */
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { JwtService } from '@nestjs/jwt';
import { DataSource } from 'typeorm';

import { AppModule } from '../src/app.module';
import { User } from '../src/modules/auth/entities/user.entity';

describe('UserProfile (e2e) - PD-32', () => {
  let app: INestApplication;
  let jwtService: JwtService;
  let dataSource: DataSource;
  let testUser: User;
  let authToken: string;

  const TEST_EMAIL = `test-${Date.now()}@example.com`;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(
      new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
      }),
    );
    await app.init();

    jwtService = app.get(JwtService);
    dataSource = app.get(DataSource);

    const userRepo = dataSource.getRepository(User);
    testUser = userRepo.create({
      email: TEST_EMAIL,
      srpSalt: Buffer.from('test-salt'),
      passwordHash: Buffer.from('test-hash'),
      name: 'Initial Name',
      avatarUrl: 'https://initial.example.com/avatar.png',
      preferences: { locale: 'fr' },
    });
    await userRepo.save(testUser);

    authToken = jwtService.sign({
      sub: testUser.id,
      email: testUser.email,
    });
  });

  afterAll(async () => {
    if (dataSource && testUser) {
      const userRepo = dataSource.getRepository(User);
      await userRepo.delete({ id: testUser.id });
    }
    if (app) {
      await app.close();
    }
  });

  describe('GET /user/profile', () => {
    describe('TC-NOM-01: Successful profile retrieval', () => {
      it('should return user profile with correct fields', async () => {
        const response = await request(app.getHttpServer())
          .get('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .expect(200);

        expect(response.body).toHaveProperty('email', TEST_EMAIL);
        expect(response.body).toHaveProperty('name');
        expect(response.body).toHaveProperty('avatarUrl');
        expect(response.body).toHaveProperty('preferences');
      });
    });

    describe('TC-INV-32-03/04: Sensitive fields exclusion', () => {
      it('should NOT expose passwordHash', async () => {
        const response = await request(app.getHttpServer())
          .get('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .expect(200);
        expect(response.body).not.toHaveProperty('passwordHash');
      });

      it('should NOT expose srpSalt', async () => {
        const response = await request(app.getHttpServer())
          .get('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .expect(200);
        expect(response.body).not.toHaveProperty('srpSalt');
      });

      it('should NOT expose validationToken', async () => {
        const response = await request(app.getHttpServer())
          .get('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .expect(200);
        expect(response.body).not.toHaveProperty('validationToken');
      });

      it('should NOT expose id', async () => {
        const response = await request(app.getHttpServer())
          .get('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .expect(200);
        expect(response.body).not.toHaveProperty('id');
      });

      it('should NOT expose plan', async () => {
        const response = await request(app.getHttpServer())
          .get('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .expect(200);
        expect(response.body).not.toHaveProperty('plan');
      });

      it('should NOT expose status', async () => {
        const response = await request(app.getHttpServer())
          .get('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .expect(200);
        expect(response.body).not.toHaveProperty('status');
      });
    });

    describe('TC-ERR-01: Authentication required', () => {
      it('should return 401 without Authorization header', async () => {
        await request(app.getHttpServer()).get('/user/profile').expect(401);
      });

      it('should return 401 with invalid token', async () => {
        await request(app.getHttpServer())
          .get('/user/profile')
          .set('Authorization', 'Bearer invalid-token')
          .expect(401);
      });
    });
  });

  describe('PUT /user/profile', () => {
    describe('TC-NOM-02: Successful profile update', () => {
      it('should update name', async () => {
        const response = await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ name: 'Updated Name' })
          .expect(200);
        expect(response.body.name).toBe('Updated Name');
      });

      it('should update avatarUrl', async () => {
        const response = await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ avatarUrl: 'https://new.example.com/avatar.jpg' })
          .expect(200);
        expect(response.body.avatarUrl).toBe('https://new.example.com/avatar.jpg');
      });

      it('should update preferences', async () => {
        const response = await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ preferences: { locale: 'en', timezone: 'America/New_York' } })
          .expect(200);
        expect(response.body.preferences.locale).toBe('en');
        expect(response.body.preferences.timezone).toBe('America/New_York');
      });
    });

    describe('TC-INV-32-06: Protected fields rejection', () => {
      it('should reject email in payload (400 Bad Request)', async () => {
        await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ name: 'Test', email: 'hacker@evil.com' })
          .expect(400);
      });

      it('should reject plan in payload (400 Bad Request)', async () => {
        await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ name: 'Test', plan: 'business' })
          .expect(400);
      });

      it('should reject id in payload (400 Bad Request)', async () => {
        await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ name: 'Test', id: '550e8400-e29b-41d4-a716-446655440000' })
          .expect(400);
      });

      it('should reject status in payload (400 Bad Request)', async () => {
        await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ name: 'Test', status: 'ACTIVE' })
          .expect(400);
      });

      it('should reject passwordHash in payload (400 Bad Request)', async () => {
        await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ name: 'Test', passwordHash: 'some-hash' })
          .expect(400);
      });
    });

    describe('TC-ERR-02: Validation errors', () => {
      it('should reject non-HTTPS avatarUrl', async () => {
        await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ avatarUrl: 'http://insecure.com/avatar.png' })
          .expect(400);
      });

      it('should reject invalid locale in preferences', async () => {
        await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ preferences: { locale: 'invalid' } })
          .expect(400);
      });

      it('should reject name exceeding 100 chars', async () => {
        await request(app.getHttpServer())
          .put('/user/profile')
          .set('Authorization', `Bearer ${authToken}`)
          .send({ name: 'a'.repeat(101) })
          .expect(400);
      });
    });

    describe('TC-ERR-01: Authentication required', () => {
      it('should return 401 without Authorization header', async () => {
        await request(app.getHttpServer())
          .put('/user/profile')
          .send({ name: 'Test' })
          .expect(401);
      });
    });
  });

  describe('RLS Cross-access protection', () => {
    let otherUser: User;
    let otherUserToken: string;

    beforeAll(async () => {
      const userRepo = dataSource.getRepository(User);
      otherUser = userRepo.create({
        email: `other-${Date.now()}@example.com`,
        srpSalt: Buffer.from('other-salt'),
        passwordHash: Buffer.from('other-hash'),
        name: 'Other User',
        preferences: {},
      });
      await userRepo.save(otherUser);

      otherUserToken = jwtService.sign({
        sub: otherUser.id,
        email: otherUser.email,
      });
    });

    afterAll(async () => {
      if (dataSource && otherUser) {
        const userRepo = dataSource.getRepository(User);
        await userRepo.delete({ id: otherUser.id });
      }
    });

    it('TC-ERR-03: User A cannot see User B profile via their token', async () => {
      const responseA = await request(app.getHttpServer())
        .get('/user/profile')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      const responseB = await request(app.getHttpServer())
        .get('/user/profile')
        .set('Authorization', `Bearer ${otherUserToken}`)
        .expect(200);

      expect(responseA.body.email).toBe(TEST_EMAIL);
      expect(responseB.body.email).toBe(otherUser.email);
      expect(responseA.body.email).not.toBe(responseB.body.email);
    });

    it('TC-ERR-04: User A update does not affect User B', async () => {
      const uniqueName = `UniqueNameA-${Date.now()}`;

      await request(app.getHttpServer())
        .put('/user/profile')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: uniqueName })
        .expect(200);

      const responseB = await request(app.getHttpServer())
        .get('/user/profile')
        .set('Authorization', `Bearer ${otherUserToken}`)
        .expect(200);

      expect(responseB.body.name).not.toBe(uniqueName);
    });
  });
});

ARTEFACT À PRODUIRE

➡️ PD-32-review-tests.md

FORMAT DE SORTIE

# PD-32 — Revue des Tests

## Résumé
| Critère | Statut |
|---------|--------|
| Couverture TC-* | X/Y |
| Qualité assertions | ✅/⚠️/❌ |
| Isolation | ✅/⚠️/❌ |
| Edge cases | ✅/⚠️/❌ |

**Verdict** : ...

## Matrice de couverture
| TC-ID | Implémenté | Fichier | Commentaire |
|-------|------------|---------|-------------|
| TC-NOM-01 | ✅/❌ | ... | ... |

## Points à améliorer
| ID | Description | Gravité |
|----|-------------|---------|
| T-01 | ... | MAJEUR/MINEUR |

## Tests manquants
- ...