Files
lmnp-organizer/frontend/src/app/components/lease-finalization-page/lease-finalization-page.spec.ts
T

195 lines
6.7 KiB
TypeScript

import { TestBed } from '@angular/core/testing';
import { ActivatedRoute, provideRouter } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { of, Subject, throwError } from 'rxjs';
import { vi } from 'vitest';
import {
FinalizeLeaseRequest,
FinalizationMetadata,
LeaseFinalizationService,
LeaseFinalizationState,
} from '../../services/lease-finalization.service';
import { WorkspaceService } from '../../services/workspace.service';
import { LeaseFinalizationPage } from './lease-finalization-page';
describe('LeaseFinalizationPage', () => {
const revision = {
number: 4,
templateVersion: '2026.10',
sourceFingerprint: 'source',
sha256: 'sha256',
generatedAt: '2026-07-19T10:00:00Z',
authorId: 'owner-1',
furnitureInventory: { templateVersion: 'FR-MEUBLE-2025-01', lines: [] },
privateAreas: [],
annexes: [],
warningCodes: [],
warnings: [],
};
const state: LeaseFinalizationState = {
revision,
guaranteeSimulatorUrl: 'https://www.service-public.fr/simulateur',
warningCodes: ['SECURITY_DEPOSIT_ABOVE_TWO_MONTHS'],
warnings: [{ code: 'SECURITY_DEPOSIT_ABOVE_TWO_MONTHS', ruleVersion: '2026-07-01' }],
guaranteeRequirements: [],
suggestedInventoryCorrections: [],
finalization: null,
};
const finalization: FinalizationMetadata = {
revisionNumber: 4,
templateVersion: '2026.10',
lastSignatureDate: '2026-07-19',
signedFileName: 'bail-signe.pdf',
signedSha256: 'signed-sha256',
authorId: 'owner-1',
finalizedAt: '2026-07-19T10:30:00Z',
overrides: [],
entryInventoryDiscrepancies: false,
inventoryCorrections: [],
allInventoryCorrectionsInitialed: false,
};
const lease = {
id: 'lease-1',
apartmentId: 'apartment-1',
tenantIds: ['tenant-1'],
guarantors: [],
specialClauses: [],
leaseType: 'STANDARD' as const,
startDate: '2026-07-01',
endDate: '2027-06-30',
monthlyRent: 800,
monthlyCharges: 50,
securityDeposit: 800,
dueDay: 5,
chargeMethod: 'FLAT_RATE' as const,
indexFlatRate: false,
status: 'ACTIVE' as const,
};
const getState = vi.fn();
const uploadGuaranteeDeed = vi.fn();
const finalize = vi.fn();
const workspace = { acceptLeaseFinalization: vi.fn() };
const dialog = { open: vi.fn() };
beforeEach(() => {
vi.clearAllMocks();
getState.mockReturnValue(of(state));
dialog.open.mockReturnValue({ afterClosed: () => of(true) });
TestBed.configureTestingModule({
providers: [
provideRouter([]),
{
provide: ActivatedRoute,
useValue: { snapshot: { paramMap: { get: () => 'lease-1' } } },
},
{
provide: LeaseFinalizationService,
useValue: { getState, uploadGuaranteeDeed, finalize },
},
{ provide: WorkspaceService, useValue: workspace },
{ provide: MatDialog, useValue: dialog },
],
});
});
it('shows a skeleton, then a retryable load error', () => {
const response = new Subject<LeaseFinalizationState>();
getState.mockReturnValueOnce(response);
const fixture = TestBed.createComponent(LeaseFinalizationPage);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.finalization-skeleton')).not.toBeNull();
response.error({ error: { message: 'Bail introuvable' } });
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Bail introuvable');
getState.mockReturnValueOnce(of(state));
fixture.nativeElement.querySelector('.load-error button').click();
fixture.detectChanges();
expect(getState).toHaveBeenCalledTimes(2);
expect(fixture.nativeElement.textContent).toContain('Révision 4 · modèle 2026.10');
});
it('instructs the landlord to generate a revision when none exists', () => {
getState.mockReturnValueOnce(of({ ...state, revision: null }));
const fixture = TestBed.createComponent(LeaseFinalizationPage);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain("Générez d'abord le bail");
expect(fixture.nativeElement.querySelector('app-lease-finalization-form')).toBeNull();
});
it('asks for final confirmation, prevents duplicates, and publishes success', () => {
const response = new Subject<{
lease: typeof lease;
rentDues: [];
finalization: FinalizationMetadata;
}>();
finalize.mockReturnValue(response);
const fixture = TestBed.createComponent(LeaseFinalizationPage);
fixture.detectChanges();
const request: FinalizeLeaseRequest = {
signedLease: new File(['lease'], 'bail-signe.pdf', { type: 'application/pdf' }),
revisionNumber: 4,
lastSignatureDate: '2026-07-19',
allPartiesSigned: true,
revisionMatches: true,
confirmedWarningCodes: ['SECURITY_DEPOSIT_ABOVE_TWO_MONTHS'],
entryInventoryDiscrepancies: false,
inventoryCorrections: [],
allInventoryCorrectionsInitialed: false,
};
fixture.componentInstance.requestFinalization(request);
fixture.componentInstance.requestFinalization(request);
expect(dialog.open).toHaveBeenCalledOnce();
expect(finalize).toHaveBeenCalledWith('lease-1', request);
expect(fixture.componentInstance.submitting()).toBe(true);
response.next({ lease, rentDues: [], finalization });
response.complete();
fixture.detectChanges();
expect(workspace.acceptLeaseFinalization).toHaveBeenCalledWith(lease, []);
expect(fixture.nativeElement.textContent).toContain('Bail finalisé et désormais immuable');
expect(fixture.nativeElement.querySelector('app-lease-finalization-form')).toBeNull();
});
it('renders an immutable summary when the lease was already finalized', () => {
getState.mockReturnValueOnce(of({ ...state, finalization }));
const fixture = TestBed.createComponent(LeaseFinalizationPage);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('bail-signe.pdf');
expect(fixture.nativeElement.textContent).toContain('signed-sha256');
expect(fixture.nativeElement.querySelector('input')).toBeNull();
});
it('keeps a finalization failure actionable without showing success', () => {
finalize.mockReturnValue(
throwError(() => ({ status: 409, error: { message: 'PDF signé refusé' } })),
);
const fixture = TestBed.createComponent(LeaseFinalizationPage);
fixture.detectChanges();
const request: FinalizeLeaseRequest = {
signedLease: new File(['lease'], 'bail-signe.pdf', { type: 'application/pdf' }),
revisionNumber: 4,
lastSignatureDate: '2026-07-19',
allPartiesSigned: true,
revisionMatches: true,
confirmedWarningCodes: ['SECURITY_DEPOSIT_ABOVE_TWO_MONTHS'],
entryInventoryDiscrepancies: false,
inventoryCorrections: [],
allInventoryCorrectionsInitialed: false,
};
fixture.componentInstance.requestFinalization(request);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('PDF signé refusé');
expect(fixture.nativeElement.textContent).not.toContain('désormais immuable');
expect(fixture.componentInstance.submitting()).toBe(false);
});
});