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:
@@ -12,7 +12,7 @@ import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
|
||||
import { Router, RouterLink, ActivatedRoute } from '@angular/router';
|
||||
import { ApiService } from '../../core/api.service';
|
||||
import { CategoryService } from '../../core/category.service';
|
||||
import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datacat/shared';
|
||||
import { ApiEntry, ApiEntryListItem, Category, API_CONVENTION_LABELS, ApiConvention } from '@datacat/shared';
|
||||
|
||||
@Component({
|
||||
selector: 'app-upload',
|
||||
@@ -137,6 +137,16 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
|
||||
|
||||
<!-- Étape 2 : Informations -->
|
||||
@if (step() === 2) {
|
||||
<!-- Bannière nouvelle version -->
|
||||
@if (fromApi(); as origin) {
|
||||
<div class="fr-callout fr-callout--blue-cumulus fr-mb-3w">
|
||||
<p class="fr-callout__text">
|
||||
Nouvelle version de : <strong>{{ origin.title }}</strong> (v{{ origin.version }})
|
||||
— le nom et la convention sont verrouillés.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<form [formGroup]="metaForm" (ngSubmit)="goToStep3()" novalidate>
|
||||
|
||||
<!-- Titre : convention + nom -->
|
||||
@@ -240,7 +250,7 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
|
||||
|
||||
<!-- Version X.Y.Z -->
|
||||
<p class="fr-label fr-mb-1w">Version <span style="color:var(--text-default-error)">*</span></p>
|
||||
<div class="fr-grid-row fr-grid-row--gutters fr-mb-3w">
|
||||
<div class="fr-grid-row fr-grid-row--gutters fr-mb-1w">
|
||||
<div class="fr-col-4">
|
||||
<div class="fr-input-group" [class.fr-input-group--error]="versionMajorInvalid()">
|
||||
<label class="fr-label" for="version-major-input">Majeure (X)</label>
|
||||
@@ -285,8 +295,18 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (versionAlreadyExists()) {
|
||||
<div class="fr-alert fr-alert--warning fr-mb-3w">
|
||||
<p class="fr-alert__title">Version déjà existante</p>
|
||||
<p>
|
||||
La version {{ formValues().versionMajor }}.{{ formValues().versionMinor }}.{{ formValues().versionPatch }}
|
||||
existe déjà pour cette API. Choisissez un numéro de version différent.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="fr-btns-group fr-btns-group--inline-sm">
|
||||
<button type="submit" class="fr-btn">Suivant</button>
|
||||
<button type="submit" class="fr-btn" [disabled]="versionAlreadyExists()">Suivant</button>
|
||||
<button type="button" class="fr-btn fr-btn--secondary" (click)="step.set(1)">
|
||||
Retour
|
||||
</button>
|
||||
@@ -411,7 +431,7 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
|
||||
</div>
|
||||
<div class="fr-col-12 fr-col-md-6">
|
||||
<dt class="fr-text--bold">Type</dt>
|
||||
<dd>{{ metaForm.value.type }}</dd>
|
||||
<dd>{{ displayType() }}</dd>
|
||||
</div>
|
||||
<div class="fr-col-12">
|
||||
<dt class="fr-text--bold">Titre</dt>
|
||||
@@ -499,6 +519,10 @@ export class UploadComponent implements OnInit {
|
||||
validationDone = signal(false);
|
||||
validationErrors = signal<Array<{ file: string; message: string }>>([]);
|
||||
categories = signal<Category[]>([]);
|
||||
/* API d'origine lors d'une création de nouvelle version (from=<id>) */
|
||||
fromApi = signal<ApiEntry | null>(null);
|
||||
/* Versions existantes de l'API d'origine (pour détecter les doublons) */
|
||||
existingVersions = signal<ApiEntryListItem[]>([]);
|
||||
|
||||
readonly stepTitles = [
|
||||
'Choisir le fichier',
|
||||
@@ -540,16 +564,35 @@ export class UploadComponent implements OnInit {
|
||||
|
||||
previewTitle = computed(() => {
|
||||
const values = this.formValues();
|
||||
const convention = (values.convention ?? 'CONSULTER') as ApiConvention;
|
||||
const name = values.name ?? '';
|
||||
const origin = this.fromApi();
|
||||
/* Quand convention/name sont désactivés, ils sont absents de valueChanges → fallback sur fromApi */
|
||||
const convention = (values.convention ?? origin?.convention ?? 'CONSULTER') as ApiConvention;
|
||||
const name = values.name ?? origin?.name ?? '';
|
||||
return `${API_CONVENTION_LABELS[convention]} ${name}`.trim();
|
||||
});
|
||||
|
||||
selectedCategoryName = computed(() => {
|
||||
const id = this.formValues().categoryId;
|
||||
if (!id) return '';
|
||||
return this.categories().find((c) => c.id === id)?.name ?? '';
|
||||
});
|
||||
|
||||
/* Vérifie si la version saisie existe déjà parmi les versions de l'API d'origine */
|
||||
versionAlreadyExists = computed(() => {
|
||||
const versions = this.existingVersions();
|
||||
if (versions.length === 0) return false;
|
||||
const values = this.formValues();
|
||||
const candidate = `${values.versionMajor ?? 0}.${values.versionMinor ?? 0}.${values.versionPatch ?? 0}`;
|
||||
return versions.some((v) => v.version === candidate);
|
||||
});
|
||||
|
||||
/* Type effectif : getRawValue() inclut les champs désactivés */
|
||||
displayType = computed(() => {
|
||||
const origin = this.fromApi();
|
||||
if (origin) return origin.type;
|
||||
return (this.formValues().type ?? null) as 'ASYNCAPI' | 'OPENAPI' | null;
|
||||
});
|
||||
|
||||
/* Méthodes de validation (pas de computed — la re-exécution est déclenchée par les événements DOM blur/input) */
|
||||
conventionInvalid(): boolean {
|
||||
return !!(this.metaForm.get('convention')?.invalid && this.metaForm.get('convention')?.touched);
|
||||
@@ -599,10 +642,15 @@ export class UploadComponent implements OnInit {
|
||||
next: (api) => this.prefillFromApi(api),
|
||||
error: () => {},
|
||||
});
|
||||
this.apiService.getVersions(fromId).subscribe({
|
||||
next: (versions) => this.existingVersions.set(versions),
|
||||
error: () => {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private prefillFromApi(api: ApiEntry): void {
|
||||
this.fromApi.set(api);
|
||||
this.metaForm.patchValue({
|
||||
convention: api.convention,
|
||||
name: api.name,
|
||||
@@ -619,6 +667,10 @@ export class UploadComponent implements OnInit {
|
||||
contactTechnicalEntity: api.contactTechnicalEntity,
|
||||
contactTechnicalEmail: api.contactTechnicalEmail,
|
||||
});
|
||||
/* Verrouiller les champs qui définissent l'identité de l'API */
|
||||
this.metaForm.get('convention')?.disable();
|
||||
this.metaForm.get('name')?.disable();
|
||||
this.metaForm.get('type')?.disable();
|
||||
}
|
||||
|
||||
categoryLabel(cat: Category): string {
|
||||
@@ -634,7 +686,10 @@ export class UploadComponent implements OnInit {
|
||||
this.detectedMainFilename.set(null);
|
||||
this.validationErrors.set([]);
|
||||
this.validationDone.set(false);
|
||||
this.metaForm.get('type')?.enable();
|
||||
/* Ne pas réactiver le type s'il est verrouillé par fromApi */
|
||||
if (!this.fromApi()) {
|
||||
this.metaForm.get('type')?.enable();
|
||||
}
|
||||
|
||||
const input = event.target as HTMLInputElement;
|
||||
const fileList = input.files;
|
||||
@@ -671,6 +726,7 @@ export class UploadComponent implements OnInit {
|
||||
this.INFO_FIELDS.forEach((f) => this.metaForm.get(f)?.markAsTouched());
|
||||
return;
|
||||
}
|
||||
if (this.versionAlreadyExists()) return;
|
||||
this.step.set(3);
|
||||
}
|
||||
|
||||
@@ -698,19 +754,26 @@ export class UploadComponent implements OnInit {
|
||||
this.validationDone.set(true);
|
||||
if (!result.valid) {
|
||||
this.validationErrors.set(result.errors);
|
||||
this.metaForm.get('type')?.enable();
|
||||
/* Ne pas réactiver le type s'il est verrouillé par fromApi */
|
||||
if (!this.fromApi()) {
|
||||
this.metaForm.get('type')?.enable();
|
||||
}
|
||||
} else {
|
||||
this.validationErrors.set([]);
|
||||
if (result.type) {
|
||||
this.detectedType.set(result.type);
|
||||
this.metaForm.patchValue({ type: result.type });
|
||||
this.metaForm.get('type')?.disable();
|
||||
/* Ne pas modifier le type s'il est verrouillé par fromApi */
|
||||
if (!this.fromApi()) {
|
||||
this.metaForm.patchValue({ type: result.type });
|
||||
this.metaForm.get('type')?.disable();
|
||||
}
|
||||
}
|
||||
if (result.mainFile) {
|
||||
this.detectedMainFilename.set(result.mainFile);
|
||||
}
|
||||
/* Pré-remplissage du formulaire depuis les métadonnées du fichier */
|
||||
if (result.name) this.metaForm.patchValue({ name: result.name });
|
||||
/* Ne pas écraser le nom s'il est verrouillé par fromApi */
|
||||
if (result.name && !this.fromApi()) this.metaForm.patchValue({ name: result.name });
|
||||
if (result.versionMajor !== null) this.metaForm.patchValue({ versionMajor: result.versionMajor });
|
||||
if (result.versionMinor !== null) this.metaForm.patchValue({ versionMinor: result.versionMinor });
|
||||
if (result.versionPatch !== null) this.metaForm.patchValue({ versionPatch: result.versionPatch });
|
||||
@@ -781,13 +844,13 @@ export class UploadComponent implements OnInit {
|
||||
if (/^openapi\s*:/m.test(content)) {
|
||||
this.detectedType.set('OPENAPI');
|
||||
this.detectedMainFilename.set(file.name);
|
||||
this.metaForm.patchValue({ type: 'OPENAPI' });
|
||||
if (!this.fromApi()) this.metaForm.patchValue({ type: 'OPENAPI' });
|
||||
return;
|
||||
}
|
||||
if (/^asyncapi\s*:/m.test(content)) {
|
||||
this.detectedType.set('ASYNCAPI');
|
||||
this.detectedMainFilename.set(file.name);
|
||||
this.metaForm.patchValue({ type: 'ASYNCAPI' });
|
||||
if (!this.fromApi()) this.metaForm.patchValue({ type: 'ASYNCAPI' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user