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>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { ConflictException, NotFoundException } from '@nestjs/common';
|
||||
import { ApisService } from './apis.service';
|
||||
import { ApiType } from '@datacat/shared';
|
||||
|
||||
@@ -154,3 +155,193 @@ describe('ApisService.parseSearchTokens()', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException, Inject, forwardRef } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, ConflictException, Inject, forwardRef } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ApiEntryEntity } from './api-entry.entity';
|
||||
@@ -25,6 +25,22 @@ export class ApisService {
|
||||
) {}
|
||||
|
||||
async create(dto: CreateApiEntryDto): Promise<ApiEntry> {
|
||||
// Vérifier qu'il n'existe pas déjà une entrée avec le même tuple (name, convention, version)
|
||||
const duplicate = await this.repo.findOne({
|
||||
where: {
|
||||
name: dto.name,
|
||||
convention: dto.convention,
|
||||
versionMajor: dto.versionMajor,
|
||||
versionMinor: dto.versionMinor,
|
||||
versionPatch: dto.versionPatch,
|
||||
},
|
||||
});
|
||||
if (duplicate) {
|
||||
throw new ConflictException(
|
||||
`La version ${dto.versionMajor}.${dto.versionMinor}.${dto.versionPatch} existe déjà pour l'API "${dto.name}".`,
|
||||
);
|
||||
}
|
||||
|
||||
// Première version si aucune entrée avec le même name + convention
|
||||
const existingCount = await this.repo.count({
|
||||
where: { name: dto.name, convention: dto.convention },
|
||||
|
||||
Reference in New Issue
Block a user