- 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>
348 lines
12 KiB
TypeScript
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');
|
|
});
|
|
});
|