Files
datacat/back/src/modules/apis/apis.service.spec.ts
z3n b6a52ce554 test(apis): ajouter tests unitaires ApisService + fix upload nouvelle version
- 21 nouveaux tests couvrant create, findOneOrThrow, remove,
  findVersionsOf, setCurrent et regenerate (pattern vi.fn() sans NestJS TestingModule)
- apis.service.ts : ConflictException sur doublon de version (name+convention+version)
- upload.component.ts : verrouillage convention/name/type lors d'une nouvelle
  version, bannière d'info, alerte et blocage si version déjà existante

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-06-24 12:55:03 +00:00

348 lines
12 KiB
TypeScript

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<string, unknown> = {}) {
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');
});
});