fix(ci): restore ImageMagick acceptance and real E2E flows
This commit is contained in:
1 parent
d7555a469a
commit
12b102f2bc
13 files changed
+128
-50
No files matched your search
+8
-1
@@ -10,7 +10,10 @@ RUN mvn clean package -DskipTests
|
||||
# Runtime shared by acceptance and the final image
|
||||
FROM docker.io/library/eclipse-temurin:25-jre-alpine AS runtime
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache imagemagick imagemagick-heic imagemagick-jpeg imagemagick-webp \
|
||||
# ponytail: only ImageMagick uses edge until stable ships the 7.1.2-32 path-policy fix; then remove the tag.
|
||||
RUN printf '%s\n' '@imagemagick https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories \
|
||||
&& apk add --no-cache 'imagemagick@imagemagick>=7.1.2.32' 'imagemagick-libs@imagemagick>=7.1.2.32' \
|
||||
imagemagick-heic@imagemagick imagemagick-jpeg@imagemagick imagemagick-webp@imagemagick \
|
||||
&& magick -version \
|
||||
&& magick identify -list format | awk '$1 ~ /^JPEG/ && $3 ~ /r/ { jpeg=1 } $1 ~ /^PNG/ && $3 ~ /rw/ { png=1 } $1 ~ /^WEBP/ && $3 ~ /rw/ { webp=1 } $1 ~ /^HEIC/ && $3 ~ /r/ { heic=1 } END { exit !(jpeg && png && webp && heic) }' \
|
||||
&& addgroup -S lmnp \
|
||||
@@ -70,6 +73,10 @@ RUN set -eu; \
|
||||
test "$(magick identify -quiet -format '%m|%wx%h|%[orientation]' /tmp/lmnp-condition-photos/build-check/small.webp)" = 'WEBP|800x600|Undefined'; \
|
||||
! magick identify -verbose /tmp/lmnp-condition-photos/build-check/small.webp | grep -Eiq 'exif:|xmp:|profile-|comment:'; \
|
||||
printf invalid > /tmp/lmnp-condition-photos/build-check/invalid.upload; \
|
||||
cp /tmp/lmnp-condition-photos/build-check/small.png /tmp/lmnp-policy-outside.png; \
|
||||
if magick identify -quiet /tmp/lmnp-policy-outside.png; then echo 'ImageMagick allowed an outside read' >&2; exit 1; fi; \
|
||||
if magick /tmp/lmnp-condition-photos/build-check/small.png /tmp/lmnp-policy-outside-output.png; then echo 'ImageMagick allowed an outside write' >&2; exit 1; fi; \
|
||||
rm /tmp/lmnp-policy-outside.png; \
|
||||
! magick identify -quiet /etc/passwd; \
|
||||
! magick identify -quiet -; \
|
||||
magick identify -list policy | grep -q '/tmp/lmnp-condition-photos'; \
|
||||
|
||||
@@ -6,6 +6,8 @@ Inspection-photo uploads are auto-oriented from EXIF, fitted without upscaling i
|
||||
|
||||
The server delegates this normalization to ImageMagick installed in the runtime image and invoked directly without a shell. Its security policy denies every coder and delegate by default, enables only the input formats explicitly accepted by the product and WebP/PNG output, restricts paths to application-controlled temporary files, and bounds dimensions, sequence length, memory, mapped memory, disk, threads, and execution time. The application additionally enforces a process timeout, validates both resulting formats and dimensions, and publishes the normalized files only after successful verification. This avoids depending on small Java WebP wrappers while keeping the image processor replaceable behind one adapter.
|
||||
|
||||
The runtime requires ImageMagick and its core library at version 7.1.2-32 or later: version 7.1.2-30 accumulates canonical path denies and rejects the temporary-directory allowlist ([upstream correction](https://github.com/ImageMagick/ImageMagick/blob/7.1.2-32/MagickCore/policy.c#L887-L895)). Until Alpine stable carries that correction, only ImageMagick packages use a tagged edge community repository. Container acceptance checks exercise image processing inside the allowed directory and reject reading and writing valid PNG files outside it.
|
||||
|
||||
Accepted uploads are single-image JPEG, PNG, WebP, and HEIC/HEIF files, identified from their decoded content rather than trusted extensions or client-supplied media types. Animated or multi-image inputs and every other format are rejected. HEIC/HEIF support is supplied by the runtime ImageMagick build through its system `libheif` codec; the container build verifies that the four accepted decoders and the WebP and PNG encoders are actually available.
|
||||
|
||||
Upload byte counts and source pixel dimensions are operational resource controls, not domain rules. ImageMagick limits remain configurable and their defaults are calibrated with representative and adversarial images under the target container memory and expected concurrency; they are not inferred from codec capabilities. Exceeding a resource limit or failing normalization deletes the temporary source, leaves the report draft unchanged, and returns an actionable retry error. No stable photo reference is created until the normalized result has passed format and dimension verification.
|
||||
@@ -17,14 +17,14 @@ test('Journey 12: prepares, downloads, files, and personally reports the 2025 ta
|
||||
});
|
||||
await page.goto('/tax');
|
||||
await expect(page.getByRole('heading', { name: 'Mes exercices fiscaux' })).toBeVisible();
|
||||
await page.getByLabel('SIRET').fill('12345678901234');
|
||||
await page.getByLabel('SIRET').fill('73282932000074');
|
||||
await page.getByLabel('Date de début d’activité').fill('2025-01-01');
|
||||
const profileResponse = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === 'PUT' &&
|
||||
new URL(response.url()).pathname === '/api/tax/profile',
|
||||
);
|
||||
await page.getByRole('button', { name: 'Confirmer le profil fiscal' }).click();
|
||||
await page.getByRole('button', { name: 'Enregistrer le profil fiscal' }).click();
|
||||
expect((await profileResponse).status()).toBe(200);
|
||||
|
||||
await page.goto('/tax/2025');
|
||||
|
||||
@@ -4,6 +4,7 @@ import { businessTest } from './support/business';
|
||||
import { authStatePath, BASE_URL } from './support/config';
|
||||
import { expect } from './support/diagnostics';
|
||||
import { createActiveJourney7Lease } from './support/journey-7';
|
||||
import { installControlledWebAuthn } from './support/vault';
|
||||
import {
|
||||
getJson,
|
||||
nullableString,
|
||||
@@ -55,6 +56,7 @@ test('Journey 13: presents the six live Calendar concepts as one isolated workfl
|
||||
browser,
|
||||
}) => {
|
||||
const api = new RealApiClient(page.context());
|
||||
await installControlledWebAuthn(page.context(), 'alice');
|
||||
const calendarBeforeTenantArrival = await readCalendar(api, '2026-09-02', '2026-09-02');
|
||||
const lease = await createActiveJourney7Lease(api, {
|
||||
marker: LEASE_LABEL,
|
||||
@@ -67,11 +69,21 @@ test('Journey 13: presents the six live Calendar concepts as one isolated workfl
|
||||
.filter((entry) => !existingEntryIds.has(entry.id))
|
||||
.map((entry) => entry.kind),
|
||||
).toEqual(['RENT_DUE']);
|
||||
await postJson(api, `/api/leases/${lease.leaseId}/end/agreed-early-end`, {
|
||||
timeZone: 'Europe/Paris',
|
||||
endOn: SEPTEMBER_TO,
|
||||
reason: 'Accord des parties pour le parcours calendrier',
|
||||
});
|
||||
await page.goto(`/leases/${lease.leaseId}/end`);
|
||||
await expect(page.getByRole('heading', { name: 'Bail actif' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Déverrouiller le coffre' }).click();
|
||||
await page.getByLabel('Date de fin convenue').fill('30/09/2027');
|
||||
await page
|
||||
.getByLabel('Raison de l’accord')
|
||||
.fill('Accord des parties pour le parcours calendrier');
|
||||
const agreementResponse = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === 'POST' &&
|
||||
new URL(response.url()).pathname ===
|
||||
`/api/leases/${lease.leaseId}/end/agreed-early-end`,
|
||||
);
|
||||
await page.getByRole('button', { name: 'Enregistrer l’accord' }).click();
|
||||
expect((await agreementResponse).status()).toBe(200);
|
||||
const mortgage = readRecord(
|
||||
await postJson(api, '/api/mortgages', {
|
||||
label: MORTGAGE_LABEL,
|
||||
|
||||
@@ -659,10 +659,18 @@ async function completeLeaseAndDocuments(
|
||||
await tenantOption.click();
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByRole('textbox', { name: "Date d'entrée" }).fill('01/09/2027');
|
||||
await page.getByRole('combobox', { name: 'Type de bail' }).click();
|
||||
await page.getByRole('option', { name: 'Location meublée classique' }).click();
|
||||
await page.getByRole('textbox', { name: "Date d'entrée" }).blur();
|
||||
await page.getByLabel('Loyer mensuel').fill('900');
|
||||
await page.getByLabel('Charges mensuelles').fill('50');
|
||||
await page.getByLabel('Dépôt de garantie').fill('900');
|
||||
await page.getByRole('combobox', { name: 'Mode de récupération des charges' }).press('Enter');
|
||||
await page.getByRole('option', { name: 'Forfait de charges' }).click();
|
||||
await page
|
||||
.getByRole('checkbox', { name: 'Indexer le forfait de charges avec le loyer' })
|
||||
.check();
|
||||
await page.getByLabel('Jour d’échéance').fill('1');
|
||||
await page.getByRole('button', { name: 'Enregistrer le brouillon' }).click();
|
||||
const leaseCard = page.locator('app-lease-card').filter({ hasText: apartment.name });
|
||||
await expect(leaseCard).toBeVisible();
|
||||
@@ -679,26 +687,33 @@ async function completeLeaseAndDocuments(
|
||||
expect(readArray(lease['tenantIds'], 'tenantIds', 'lease')).toContain(tenantId);
|
||||
expect(lease['status']).toBe('DRAFT');
|
||||
|
||||
await leaseCard.getByRole('button', { name: 'Inventaire du mobilier' }).click();
|
||||
await leaseCard.getByRole('button', { name: 'Plus d’actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Inventaire du mobilier' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Inventaire du mobilier' })).toBeVisible();
|
||||
const inventoryCards = page.locator('app-inventory-line-card');
|
||||
await expect(inventoryCards.first()).toBeVisible();
|
||||
const inventoryCount = await inventoryCards.count();
|
||||
expect(inventoryCount).toBeGreaterThan(0);
|
||||
for (let index = 0; index < inventoryCount; index += 1) {
|
||||
const card = inventoryCards.nth(index);
|
||||
const inventoryTitles = await inventoryCards.locator('mat-card-title').allTextContents();
|
||||
for (const title of inventoryTitles) {
|
||||
const card = inventoryCards.filter({ has: page.getByText(title.trim(), { exact: true }) });
|
||||
await card.getByRole('combobox', { name: 'Espace privatif' }).press('Enter');
|
||||
await page.getByRole('option', { name: 'Pièce principale', exact: true }).click();
|
||||
await card.getByRole('combobox', { name: 'Présence' }).press('Enter');
|
||||
await page
|
||||
.getByRole('listbox', { name: 'Présence' })
|
||||
.last()
|
||||
.getByRole('option', { name: 'Présent', exact: true })
|
||||
.getByRole('option', { name: 'Présente', exact: true })
|
||||
.click();
|
||||
}
|
||||
await page.getByRole('button', { name: 'Enregistrer et revenir aux baux' }).click();
|
||||
await page.getByRole('button', { name: 'Enregistrer', exact: true }).click();
|
||||
await expect(page.getByText('Modifications non enregistrées')).toBeHidden();
|
||||
await page.getByRole('link', { name: '← Retour aux baux' }).click();
|
||||
await expect(page).toHaveURL(/\/leases$/);
|
||||
|
||||
const currentLeaseCard = page.locator('app-lease-card').filter({ hasText: apartment.name });
|
||||
await currentLeaseCard.getByRole('button', { name: 'État des lieux d’entrée' }).click();
|
||||
await currentLeaseCard.getByRole('button', { name: 'Plus d’actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'État des lieux d’entrée' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Entrée' })).toBeVisible();
|
||||
const activeStep = page.locator('.mat-vertical-content-container-active');
|
||||
await activeStep.getByRole('button', { name: 'Confirmer cette étape' }).click();
|
||||
@@ -807,7 +822,7 @@ async function completeLeaseAndDocuments(
|
||||
).toBe(revisionNumber);
|
||||
|
||||
await page.goto('/rent-payments');
|
||||
await expect(page.getByRole('heading', { name: 'Mes loyers' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Échéances et règlements' })).toBeVisible();
|
||||
const rentCard = page.locator('.rent-payment').filter({ hasText: apartment.name }).first();
|
||||
await expect(rentCard).toBeVisible();
|
||||
await rentCard.getByRole('button', { name: 'Enregistrer un règlement' }).click();
|
||||
|
||||
@@ -52,6 +52,8 @@ businessTest(
|
||||
await page.getByLabel("Prix d'acquisition").fill('150000');
|
||||
await page.getByLabel('Date d’acquisition').fill('2024-06-15');
|
||||
await page.getByRole('button', { name: 'Enregistrer', exact: true }).click();
|
||||
await expect(page.getByRole('heading', { name: "Modifier l'appartement" })).toBeVisible();
|
||||
await page.getByRole('button', { name: '← Retour' }).click();
|
||||
await expect(page.getByRole('heading', { name: apartmentName })).toBeVisible();
|
||||
|
||||
await page
|
||||
@@ -104,12 +106,19 @@ businessTest(
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
const startDateInput = page.getByRole('textbox', { name: "Date d'entrée" });
|
||||
await page.getByRole('combobox', { name: 'Type de bail' }).click();
|
||||
await page.getByRole('option', { name: 'Location meublée classique' }).click();
|
||||
await startDateInput.fill('02/09/2027');
|
||||
await startDateInput.blur();
|
||||
await expect(page.getByText('1 septembre 2028', { exact: true })).toBeVisible();
|
||||
await page.getByLabel('Loyer mensuel').fill('850');
|
||||
await page.getByLabel('Charges mensuelles').fill('50');
|
||||
await page.getByLabel('Dépôt de garantie').fill('850');
|
||||
await page
|
||||
.getByRole('combobox', { name: 'Mode de récupération des charges' })
|
||||
.press('Enter');
|
||||
await page.getByRole('option', { name: 'Forfait de charges' }).click();
|
||||
await page.getByLabel('Jour d’échéance').fill('1');
|
||||
await page.getByRole('button', { name: 'Enregistrer le brouillon' }).click();
|
||||
|
||||
const leaseCard = page
|
||||
@@ -154,7 +163,8 @@ businessTest(
|
||||
await expect(page.getByRole('heading', { name: apartmentName })).toBeVisible();
|
||||
await page.goto('/leases');
|
||||
await expect(page.getByRole('heading', { name: apartmentName })).toBeVisible();
|
||||
await expect(leaseCard.getByRole('button', { name: 'Modifier' })).toBeVisible();
|
||||
await leaseCard.getByRole('button', { name: 'Plus d’actions' }).click();
|
||||
await expect(page.getByRole('menuitem', { name: 'Modifier' })).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -70,7 +70,9 @@ test('Journey 4: persists apartment and tenant document lifecycles through the U
|
||||
hasText: APARTMENT_NAME,
|
||||
});
|
||||
await expect(apartmentCard).toBeVisible();
|
||||
await apartmentCard.getByRole('button', { name: 'Modifier' }).click();
|
||||
await apartmentCard
|
||||
.getByRole('link', { name: `Ouvrir la fiche de ${APARTMENT_NAME}` })
|
||||
.click();
|
||||
|
||||
const apartmentDocuments = page.locator('app-apartment-document-manager');
|
||||
await expect(
|
||||
@@ -166,7 +168,9 @@ test('Journey 4: persists apartment and tenant document lifecycles through the U
|
||||
const persistedApartmentCard = page
|
||||
.locator('app-apartment-list .apartment-card')
|
||||
.filter({ hasText: APARTMENT_NAME });
|
||||
await persistedApartmentCard.getByRole('button', { name: 'Modifier' }).click();
|
||||
await persistedApartmentCard
|
||||
.getByRole('link', { name: `Ouvrir la fiche de ${APARTMENT_NAME}` })
|
||||
.click();
|
||||
const persistedApartmentDocuments = page.locator('app-apartment-document-manager');
|
||||
const persistedReplacementRow = persistedApartmentDocuments
|
||||
.locator('.document-list li')
|
||||
@@ -200,11 +204,14 @@ test('Journey 4: persists apartment and tenant document lifecycles through the U
|
||||
|
||||
const tenantForm = page.locator('app-tenant-form');
|
||||
await expect(tenantForm.getByRole('heading', { name: 'Pièces jointes' })).toBeVisible();
|
||||
await tenantForm.getByLabel('Ajouter des pièces jointes').setInputFiles({
|
||||
name: TENANT_FILE_NAME,
|
||||
mimeType: 'application/pdf',
|
||||
buffer: largePdf,
|
||||
});
|
||||
await tenantForm
|
||||
.locator('app-tenant-attachment-editor input[type=file]')
|
||||
.first()
|
||||
.setInputFiles({
|
||||
name: TENANT_FILE_NAME,
|
||||
mimeType: 'application/pdf',
|
||||
buffer: largePdf,
|
||||
});
|
||||
await tenantForm.getByRole('button', { name: 'Enregistrer', exact: true }).click();
|
||||
|
||||
const tenantAttachment = await waitForTenantAttachment(api, tenant.id, TENANT_FILE_NAME);
|
||||
|
||||
@@ -162,7 +162,7 @@ interface ApiInventoryLine {
|
||||
id: string;
|
||||
category: string | null;
|
||||
presence: 'PRESENT' | 'ABSENT' | null;
|
||||
privateAreaId: string;
|
||||
privateAreaId: string | null;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
@@ -437,7 +437,7 @@ function isApiInventoryLine(value: unknown): value is ApiInventoryLine {
|
||||
(value['presence'] === null ||
|
||||
value['presence'] === 'PRESENT' ||
|
||||
value['presence'] === 'ABSENT') &&
|
||||
typeof value['privateAreaId'] === 'string' &&
|
||||
(value['privateAreaId'] === null || typeof value['privateAreaId'] === 'string') &&
|
||||
typeof value['quantity'] === 'number'
|
||||
);
|
||||
}
|
||||
@@ -742,10 +742,20 @@ businessTest(
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByRole('textbox', { name: "Date d'entrée" }).fill('01/09/2027');
|
||||
await page.getByRole('combobox', { name: 'Type de bail' }).click();
|
||||
await page.getByRole('option', { name: 'Location meublée classique' }).click();
|
||||
await page.getByRole('textbox', { name: "Date d'entrée" }).blur();
|
||||
await page.getByLabel('Loyer mensuel').fill('900');
|
||||
await page.getByLabel('Charges mensuelles').fill('50');
|
||||
await page.getByLabel('Dépôt de garantie').fill('900');
|
||||
await page
|
||||
.getByRole('combobox', { name: 'Mode de récupération des charges' })
|
||||
.press('Enter');
|
||||
await page.getByRole('option', { name: 'Forfait de charges' }).click();
|
||||
await page
|
||||
.getByRole('checkbox', { name: 'Indexer le forfait de charges avec le loyer' })
|
||||
.check();
|
||||
await page.getByLabel('Jour d’échéance').fill('1');
|
||||
const visaleDeclaration = page.locator('app-visale-editor');
|
||||
await visaleDeclaration
|
||||
.getByRole('button', { name: 'Déclarer une garantie Visale' })
|
||||
@@ -781,7 +791,8 @@ businessTest(
|
||||
);
|
||||
const leaseId = lease.id;
|
||||
|
||||
await leaseCard.getByRole('button', { name: 'Modifier' }).click();
|
||||
await leaseCard.getByRole('button', { name: 'Plus d’actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Modifier' }).click();
|
||||
const visale = page.locator('app-visale-editor');
|
||||
await expect(
|
||||
visale.getByText('Le contrat Visale requis n’a pas encore été téléversé.'),
|
||||
@@ -801,7 +812,8 @@ businessTest(
|
||||
});
|
||||
await expect(visale.getByText('Copie du visa Visale : visa-visale.pdf')).toBeVisible();
|
||||
await page.reload();
|
||||
await leaseCard.getByRole('button', { name: 'Modifier' }).click();
|
||||
await leaseCard.getByRole('button', { name: 'Plus d’actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Modifier' }).click();
|
||||
const reloadedVisale = page.locator('app-visale-editor');
|
||||
await expect(
|
||||
reloadedVisale.getByRole('button', { name: 'Remplacer le contrat Visale' }),
|
||||
@@ -853,16 +865,24 @@ businessTest(
|
||||
assertEncryptedCiphertext(contractDocument, 'Visale surety contract document', minimalPdf);
|
||||
await page.goto('/leases');
|
||||
|
||||
await leaseCard.getByRole('button', { name: 'Inventaire du mobilier' }).click();
|
||||
await leaseCard.getByRole('button', { name: 'Plus d’actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Inventaire du mobilier' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Inventaire du mobilier' })).toBeVisible();
|
||||
const inventoryCards = page.locator('app-inventory-line-card');
|
||||
await expect(inventoryCards).toHaveCount(legalFurnitureCategories.length);
|
||||
for (let index = 0; index < legalFurnitureCategories.length; index++) {
|
||||
const card = inventoryCards.nth(index);
|
||||
const inventoryTitles = await inventoryCards.locator('mat-card-title').allTextContents();
|
||||
for (const title of inventoryTitles) {
|
||||
const card = inventoryCards.filter({
|
||||
has: page.getByText(title.trim(), { exact: true }),
|
||||
});
|
||||
await card.getByRole('combobox', { name: 'Espace privatif' }).click();
|
||||
await page.getByRole('option', { name: 'Pièce principale', exact: true }).click();
|
||||
await card.getByLabel('Présence').click();
|
||||
await page.getByRole('option', { name: 'Présent', exact: true }).click();
|
||||
await page.getByRole('option', { name: 'Présente', exact: true }).click();
|
||||
}
|
||||
await page.getByRole('button', { name: 'Enregistrer et revenir aux baux' }).click();
|
||||
await page.getByRole('button', { name: 'Enregistrer', exact: true }).click();
|
||||
await expect(page.getByText('Modifications non enregistrées')).toBeHidden();
|
||||
await page.getByRole('link', { name: '← Retour aux baux' }).click();
|
||||
await expect(page).toHaveURL(/\/leases$/);
|
||||
const inventoryResponse = await api.requireOk('GET', `/api/leases/${leaseId}/inventory`);
|
||||
const inventory = await parseJson(inventoryResponse, isApiInventory);
|
||||
@@ -872,7 +892,8 @@ businessTest(
|
||||
expect(inventory.readyForLeaseDocument).toBe(true);
|
||||
expect(inventory.missingCategories).toEqual([]);
|
||||
|
||||
await leaseCard.getByRole('button', { name: 'État des lieux d’entrée' }).click();
|
||||
await leaseCard.getByRole('button', { name: 'Plus d’actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'État des lieux d’entrée' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Entrée' })).toBeVisible();
|
||||
const activeStep = page.locator('.mat-vertical-content-container-active');
|
||||
await activeStep.getByRole('button', { name: 'Confirmer cette étape' }).click();
|
||||
@@ -1165,7 +1186,8 @@ businessTest(
|
||||
await unlock.click();
|
||||
await expect(unlock).toHaveCount(0);
|
||||
const zipDownloadPromise = page.waitForEvent('download');
|
||||
await activeCard.getByRole('button', { name: 'Télécharger le Dossier final' }).click();
|
||||
await activeCard.getByRole('button', { name: 'Plus d’actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Télécharger le Dossier final' }).click();
|
||||
const zipDownload = await zipDownloadPromise;
|
||||
expect(zipDownload.suggestedFilename()).toBe(`dossier-final-bail-${leaseId}.zip`);
|
||||
const zipPath = await zipDownload.path();
|
||||
|
||||
@@ -30,7 +30,7 @@ businessTest(
|
||||
});
|
||||
|
||||
await page.goto('/rent-payments');
|
||||
await expect(page.getByRole('heading', { name: 'Mes loyers' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Échéances et règlements' })).toBeVisible();
|
||||
|
||||
const firstDueCard = rentDueCard(page, setup.apartmentName, 'juillet 2027');
|
||||
let secondDueCard = rentDueCard(page, setup.apartmentName, 'août 2027');
|
||||
@@ -45,7 +45,7 @@ businessTest(
|
||||
firstPaymentDialog.getByRole('heading', { name: 'Enregistrer un règlement' }),
|
||||
).toBeVisible();
|
||||
await firstPaymentDialog.getByLabel('Montant reçu').fill('1000');
|
||||
await expect(firstPaymentDialog.getByText(/Surplus non affecté/)).toContainText('50,00');
|
||||
await expect(firstPaymentDialog.getByText(/Surplus non affecté/)).toContainText(/50\s€/);
|
||||
await firstPaymentDialog.getByRole('button', { name: 'Confirmer le règlement' }).click();
|
||||
await expect(page.getByRole('status')).toHaveText('Le règlement a été enregistré.');
|
||||
await expect(restAmount(firstDueCard)).toHaveText(euro(0));
|
||||
@@ -82,7 +82,7 @@ businessTest(
|
||||
secondPaymentDialog.getByRole('heading', { name: 'Enregistrer un règlement' }),
|
||||
).toBeVisible();
|
||||
await secondPaymentDialog.getByLabel('Montant reçu').fill('950');
|
||||
await expect(secondPaymentDialog.getByText(/Surplus non affecté/)).toContainText('50,00');
|
||||
await expect(secondPaymentDialog.getByText(/Surplus non affecté/)).toContainText(/50\s€/);
|
||||
await secondPaymentDialog.getByRole('button', { name: 'Confirmer le règlement' }).click();
|
||||
await expect(page.getByRole('status')).toHaveText('Le règlement a été enregistré.');
|
||||
await expect(restAmount(secondDueCard)).toHaveText(euro(0));
|
||||
@@ -97,7 +97,7 @@ businessTest(
|
||||
|
||||
await unlockVault(page, setup.apartmentName);
|
||||
await page.goto('/rent-payments');
|
||||
await expect(page.getByRole('heading', { name: 'Mes loyers' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Échéances et règlements' })).toBeVisible();
|
||||
await expectPdfDownload(
|
||||
page,
|
||||
paymentCard(page, setup.apartmentName, 1000).getByRole('button', {
|
||||
@@ -121,7 +121,7 @@ businessTest(
|
||||
`recu-reglement-${secondPayment.id}.pdf`,
|
||||
);
|
||||
await page.reload();
|
||||
await expect(page.getByRole('heading', { name: 'Mes loyers' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Échéances et règlements' })).toBeVisible();
|
||||
|
||||
const secondPaymentCard = paymentCard(page, setup.apartmentName, 950);
|
||||
await secondPaymentCard.getByRole('button', { name: 'Rembourser' }).click();
|
||||
@@ -129,7 +129,7 @@ businessTest(
|
||||
await expect(
|
||||
refundDialog.getByRole('heading', { name: 'Rembourser le surplus' }),
|
||||
).toBeVisible();
|
||||
await expect(refundDialog.getByText(/Solde non affecté disponible/)).toContainText('50,00');
|
||||
await expect(refundDialog.getByText(/Solde non affecté disponible/)).toContainText(/50\s€/);
|
||||
await refundDialog.getByLabel('Date du remboursement').fill('2027-09-02');
|
||||
await refundDialog.getByLabel('Montant remboursé').fill('20');
|
||||
await refundDialog.getByRole('button', { name: 'Confirmer le remboursement' }).click();
|
||||
@@ -171,7 +171,7 @@ businessTest(
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByRole('heading', { name: 'Mes loyers' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Échéances et règlements' })).toBeVisible();
|
||||
const unlockedSecondPaymentCard = paymentCard(page, setup.apartmentName, 950);
|
||||
await expect(
|
||||
unlockedSecondPaymentCard.getByText('Contrepassé', { exact: true }),
|
||||
@@ -275,7 +275,7 @@ function paymentCard(page: Page, apartmentName: string, amount: number) {
|
||||
|
||||
function euro(amount: number): RegExp {
|
||||
return new RegExp(
|
||||
`^${amount.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}\\s€$`,
|
||||
`^${amount.toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 2 })}\\s€$`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ test('Journey 7: calculates, applies, and persists an IRL rent revision with pro
|
||||
await expect(leaseCard.locator('.terms dd').nth(1)).toHaveText('50 € / mois');
|
||||
await unlockLeaseVault(page, leaseCard);
|
||||
|
||||
await leaseCard.getByRole('button', { name: 'Réviser le loyer selon l’IRL' }).click();
|
||||
await leaseCard.getByRole('button', { name: 'Plus d’actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Réviser le loyer selon l’IRL' }).click();
|
||||
const revisionPanel = leaseCard.locator('app-rent-revision-preview');
|
||||
await expect(
|
||||
revisionPanel.getByRole('heading', { name: 'Révision annuelle du loyer' }),
|
||||
@@ -89,7 +90,8 @@ test('Journey 7: calculates, applies, and persists an IRL rent revision with pro
|
||||
await page.reload();
|
||||
await expect(leaseCard).toBeVisible();
|
||||
await unlockLeaseVault(page, leaseCard);
|
||||
await leaseCard.getByRole('button', { name: 'Réviser le loyer selon l’IRL' }).click();
|
||||
await leaseCard.getByRole('button', { name: 'Plus d’actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Réviser le loyer selon l’IRL' }).click();
|
||||
const persistedRevisionItem = leaseCard
|
||||
.locator('app-rent-revision-preview app-rent-revision-proposals .revision-list li')
|
||||
.first();
|
||||
|
||||
@@ -164,6 +164,7 @@ export async function setupJourney6(context: BrowserContext): Promise<Journey6Se
|
||||
lines: inventory.lines.map((line) => ({
|
||||
...line,
|
||||
presence: 'PRESENT',
|
||||
privateAreaId: ENTRY_AREA_ID,
|
||||
quantity: 1,
|
||||
})),
|
||||
} satisfies InventoryUpdateRequest,
|
||||
@@ -424,7 +425,7 @@ interface InventoryLine {
|
||||
id: string;
|
||||
category: string | null;
|
||||
presence: string | null;
|
||||
privateAreaId: string;
|
||||
privateAreaId: string | null;
|
||||
designation: string;
|
||||
quantity: number;
|
||||
description: string;
|
||||
@@ -448,7 +449,7 @@ function isInventoryLine(value: unknown): value is InventoryLine {
|
||||
string(item['id']) &&
|
||||
(item['category'] === null || string(item['category'])) &&
|
||||
(item['presence'] === null || string(item['presence'])) &&
|
||||
string(item['privateAreaId']) &&
|
||||
(item['privateAreaId'] === null || string(item['privateAreaId'])) &&
|
||||
string(item['designation']) &&
|
||||
number(item['quantity']) &&
|
||||
typeof item['description'] === 'string'
|
||||
|
||||
@@ -41,7 +41,7 @@ interface InventoryLine {
|
||||
id: string;
|
||||
category: string | null;
|
||||
presence: string | null;
|
||||
privateAreaId: string;
|
||||
privateAreaId: string | null;
|
||||
designation: string;
|
||||
quantity: number;
|
||||
description: string;
|
||||
@@ -331,7 +331,7 @@ function parseInventory(value: unknown): InventoryResponse {
|
||||
id: readString(item['id'], 'id', `inventory line ${index}`),
|
||||
category: nullableString(item['category'], 'category', `inventory line ${index}`),
|
||||
presence: nullableString(item['presence'], 'presence', `inventory line ${index}`),
|
||||
privateAreaId: readString(
|
||||
privateAreaId: nullableString(
|
||||
item['privateAreaId'],
|
||||
'privateAreaId',
|
||||
`inventory line ${index}`,
|
||||
|
||||
@@ -63,7 +63,7 @@ interface InventoryLine {
|
||||
id: string;
|
||||
category: string | null;
|
||||
presence: string | null;
|
||||
privateAreaId: string;
|
||||
privateAreaId: string | null;
|
||||
designation: string;
|
||||
quantity: number;
|
||||
description: string;
|
||||
@@ -443,7 +443,7 @@ function parseInventory(value: unknown): InventoryResponse {
|
||||
id: readString(item['id'], 'id', `inventory line ${index}`),
|
||||
category: nullableString(item['category'], 'category', `inventory line ${index}`),
|
||||
presence: nullableString(item['presence'], 'presence', `inventory line ${index}`),
|
||||
privateAreaId: readString(
|
||||
privateAreaId: nullableString(
|
||||
item['privateAreaId'],
|
||||
'privateAreaId',
|
||||
`inventory line ${index}`,
|
||||
|
||||
Reference in new issue
Block a user