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(); 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); }); });