import { vi, describe, it, expect, beforeEach } from 'vitest'; import { ConflictException, NotFoundException } from '@nestjs/common'; import { ApisService } from './apis.service'; import { ApiType } from '@datacat/shared'; /** Accès à la méthode privée pour les tests unitaires */ function parse(raw: string) { return (ApisService.prototype as any).parseSearchTokens.call({}, raw) as { text: string; type?: ApiType; version?: string; latestOnly: boolean; }; } describe('ApisService.parseSearchTokens()', () => { describe('chaîne vide', () => { it('retourne text vide, pas de type, pas de version, latestOnly=false', () => { expect(parse('')).toEqual({ text: '', type: undefined, version: undefined, latestOnly: false }); }); it('gère les espaces seuls', () => { expect(parse(' ')).toEqual({ text: '', type: undefined, version: undefined, latestOnly: false }); }); }); describe('texte libre seul (sans version ni type)', () => { it('"auth" → latestOnly=true automatique', () => { expect(parse('auth')).toEqual({ text: 'auth', type: undefined, version: undefined, latestOnly: true }); }); it('"paiement commande" → texte libre multi-mots, latestOnly=true', () => { expect(parse('paiement commande')).toEqual({ text: 'paiement commande', type: undefined, version: undefined, latestOnly: true, }); }); }); describe('détection du type', () => { it('"async" → ASYNCAPI', () => { const r = parse('async'); expect(r.type).toBe('ASYNCAPI'); }); it('"asyncapi" → ASYNCAPI', () => { expect(parse('asyncapi').type).toBe('ASYNCAPI'); }); it('"openapi" → OPENAPI', () => { expect(parse('openapi').type).toBe('OPENAPI'); }); it('"open" → OPENAPI', () => { expect(parse('open').type).toBe('OPENAPI'); }); it('type seul → latestOnly=true (champ non vide, pas de version)', () => { expect(parse('async').latestOnly).toBe(true); }); it('mots de type ne font pas partie du texte libre', () => { expect(parse('auth async').text).toBe('auth'); }); }); describe('détection de version', () => { it('"v1" → version="1", latestOnly=false', () => { expect(parse('v1')).toEqual({ text: '', type: undefined, version: '1', latestOnly: false }); }); it('"1.2" → version="1.2"', () => { expect(parse('1.2').version).toBe('1.2'); }); it('"v1.2.3" → version="1.2.3" (préfixe v supprimé)', () => { expect(parse('v1.2.3').version).toBe('1.2.3'); }); it('"1.0.0" → version exacte', () => { expect(parse('1.0.0').version).toBe('1.0.0'); }); it('version présente → latestOnly=false', () => { expect(parse('auth v1').latestOnly).toBe(false); }); it('numéro de version ne fait pas partie du texte libre', () => { expect(parse('auth v1').text).toBe('auth'); }); }); describe('mot-clé "latest"', () => { it('"latest" seul → latestOnly=true, pas de texte', () => { expect(parse('latest')).toEqual({ text: '', type: undefined, version: undefined, latestOnly: true }); }); it('"latest" + type → latestOnly=true + type détecté', () => { const r = parse('latest async'); expect(r.latestOnly).toBe(true); expect(r.type).toBe('ASYNCAPI'); }); it('"latest" + version → les deux flags actifs', () => { const r = parse('latest v1'); expect(r.latestOnly).toBe(true); expect(r.version).toBe('1'); }); }); describe('combinaisons', () => { it('"auth async" → texte=auth, type=ASYNCAPI, latestOnly=true', () => { expect(parse('auth async')).toEqual({ text: 'auth', type: 'ASYNCAPI', version: undefined, latestOnly: true, }); }); it('"auth async v1" → texte=auth, type=ASYNCAPI, version=1, latestOnly=false', () => { expect(parse('auth async v1')).toEqual({ text: 'auth', type: 'ASYNCAPI', version: '1', latestOnly: false, }); }); it('"auth 1.0.0" → texte=auth, version=1.0.0, latestOnly=false', () => { expect(parse('auth 1.0.0')).toEqual({ text: 'auth', type: undefined, version: '1.0.0', latestOnly: false, }); }); it('"openapi latest" → type=OPENAPI, latestOnly=true', () => { expect(parse('openapi latest')).toEqual({ text: '', type: 'OPENAPI', version: undefined, latestOnly: true, }); }); it('insensible à la casse pour les mots-clés', () => { const r = parse('ASYNC Auth LATEST'); expect(r.type).toBe('ASYNCAPI'); expect(r.latestOnly).toBe(true); expect(r.text).toBe('Auth'); }); }); }); // ─── Mocks partagés pour les tests unitaires de ApisService ─────────────────── const mockRepo = { findOne: vi.fn(), count: vi.fn(), create: vi.fn(), save: vi.fn(), find: vi.fn(), update: vi.fn(), delete: vi.fn(), }; const mockDocsService = { generate: vi.fn() }; let service: ApisService; beforeEach(() => { vi.clearAllMocks(); service = new ApisService(mockRepo as any, mockDocsService as any); }); function makeEntity(overrides: Record = {}) { return { id: 'uuid-1', name: 'Petstore', convention: 'CONSULTER', versionMajor: 1, versionMinor: 0, versionPatch: 0, type: 'OPENAPI', provider: 'DINUM', description: null, yamlContent: 'openapi: "3.0.0"', htmlPath: null, status: 'PENDING', errorMessage: null, categoryId: null, isCurrent: true, contactFunctionalName: '', contactFunctionalEntity: '', contactFunctionalEmail: '', contactTechnicalName: '', contactTechnicalEntity: '', contactTechnicalEmail: '', createdAt: new Date(), updatedAt: new Date(), ...overrides, }; } const baseDto = { name: 'Petstore', convention: 'CONSULTER' as const, versionMajor: 1, versionMinor: 0, versionPatch: 0, type: 'OPENAPI' as const, provider: 'DINUM', yamlContent: 'openapi: "3.0.0"', contactFunctionalName: 'Alice', contactFunctionalEntity: 'DINUM', contactFunctionalEmail: 'alice@dinum.fr', contactTechnicalName: 'Bob', contactTechnicalEntity: 'DINUM', contactTechnicalEmail: 'bob@dinum.fr', }; // ─── ApisService.create() ───────────────────────────────────────────────────── describe('ApisService.create()', () => { it('crée l\'entrée et la retourne', async () => { const entity = makeEntity(); mockRepo.findOne.mockResolvedValue(null); mockRepo.count.mockResolvedValue(0); mockRepo.create.mockReturnValue(entity); mockRepo.save.mockResolvedValue(entity); const result = await service.create(baseDto as any); expect(result.id).toBe('uuid-1'); expect(result.title).toBe('Consulter Petstore'); expect(result.version).toBe('1.0.0'); }); it('lève ConflictException si doublon', async () => { mockRepo.findOne.mockResolvedValue(makeEntity()); await expect(service.create(baseDto as any)).rejects.toThrow(ConflictException); }); it('isCurrent=true si première version', async () => { const entity = makeEntity({ isCurrent: true }); mockRepo.findOne.mockResolvedValue(null); mockRepo.count.mockResolvedValue(0); mockRepo.create.mockReturnValue(entity); mockRepo.save.mockResolvedValue(entity); await service.create(baseDto as any); expect(mockRepo.create).toHaveBeenCalledWith(expect.objectContaining({ isCurrent: true })); }); it('isCurrent=false si version ultérieure', async () => { const entity = makeEntity({ isCurrent: false }); mockRepo.findOne.mockResolvedValue(null); mockRepo.count.mockResolvedValue(1); mockRepo.create.mockReturnValue(entity); mockRepo.save.mockResolvedValue(entity); await service.create(baseDto as any); expect(mockRepo.create).toHaveBeenCalledWith(expect.objectContaining({ isCurrent: false })); }); }); // ─── ApisService.findOneOrThrow() ──────────────────────────────────────────── describe('ApisService.findOneOrThrow()', () => { it('retourne l\'entrée mappée si trouvée', async () => { mockRepo.findOne.mockResolvedValue(makeEntity()); const result = await service.findOneOrThrow('uuid-1'); expect(result.id).toBe('uuid-1'); expect(result.title).toBe('Consulter Petstore'); expect(result.version).toBe('1.0.0'); }); it('lève NotFoundException si absente', async () => { mockRepo.findOne.mockResolvedValue(null); await expect(service.findOneOrThrow('uuid-unknown')).rejects.toThrow(NotFoundException); }); }); // ─── ApisService.remove() ──────────────────────────────────────────────────── describe('ApisService.remove()', () => { it('supprime l\'entrée si elle existe', async () => { mockRepo.findOne.mockResolvedValue(makeEntity()); mockRepo.delete.mockResolvedValue({ affected: 1 }); await service.remove('uuid-1'); expect(mockRepo.delete).toHaveBeenCalledWith('uuid-1'); }); it('lève NotFoundException si absente', async () => { mockRepo.findOne.mockResolvedValue(null); await expect(service.remove('uuid-unknown')).rejects.toThrow(NotFoundException); }); }); // ─── ApisService.findVersionsOf() ──────────────────────────────────────────── describe('ApisService.findVersionsOf()', () => { it('retourne toutes les versions (même name+convention)', async () => { const entity1 = makeEntity({ id: 'uuid-1', versionMajor: 2 }); const entity2 = makeEntity({ id: 'uuid-2', versionMajor: 1 }); mockRepo.findOne.mockResolvedValue(entity1); mockRepo.find.mockResolvedValue([entity1, entity2]); const result = await service.findVersionsOf('uuid-1'); expect(result).toHaveLength(2); expect(result[0].id).toBe('uuid-1'); expect(result[1].id).toBe('uuid-2'); }); it('lève NotFoundException si id inconnu', async () => { mockRepo.findOne.mockResolvedValue(null); await expect(service.findVersionsOf('uuid-unknown')).rejects.toThrow(NotFoundException); }); }); // ─── ApisService.setCurrent() ──────────────────────────────────────────────── describe('ApisService.setCurrent()', () => { it('met isCurrent=false sur les siblings puis true sur la cible', async () => { mockRepo.findOne.mockResolvedValue(makeEntity()); mockRepo.update.mockResolvedValue({ affected: 1 }); await service.setCurrent('uuid-1'); expect(mockRepo.update).toHaveBeenCalledWith( { name: 'Petstore', convention: 'CONSULTER' }, { isCurrent: false }, ); expect(mockRepo.update).toHaveBeenCalledWith( { id: 'uuid-1' }, { isCurrent: true }, ); }); }); // ─── ApisService.regenerate() ──────────────────────────────────────────────── describe('ApisService.regenerate()', () => { it('remet le statut à PENDING et appelle docsService.generate()', async () => { mockRepo.findOne.mockResolvedValue(makeEntity()); mockRepo.update.mockResolvedValue({ affected: 1 }); mockDocsService.generate.mockResolvedValue(undefined); await service.regenerate('uuid-1'); expect(mockRepo.update).toHaveBeenCalledWith('uuid-1', { status: 'PENDING', errorMessage: null }); expect(mockDocsService.generate).toHaveBeenCalledWith('uuid-1'); }); });