Enhance Docker and PowerShell scripts for improved functionality and maintainability
All checks were successful
Publish FaceAI Container / publish (push) Successful in 6m52s
All checks were successful
Publish FaceAI Container / publish (push) Successful in 6m52s
- Updated Dockerfile to include default MySQL client for better database interaction. - Modified entrypoint.sh to support additional workspace for legacy applications and added MySQL readiness check before startup. - Enhanced PowerShell script for trimming MySQL dumps to include overlay dumps and improved error handling for missing race and user IDs. - Added new image files and face encoding pickles for various projects, ensuring comprehensive data availability. - Removed outdated face encoding pickle from PISA directory to maintain data relevance. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
c227fce036
commit
dd7d4c865b
54 changed files with 492 additions and 144 deletions
|
|
@ -7,8 +7,7 @@ It includes:
|
|||
- a Vue frontend for the FaceAI upload and polling flow
|
||||
- a Node/Express backend for session exchange, queueing, and return handoff
|
||||
- a dedicated processor runner that consumes matcher jobs from Redis and executes `face_matcher`
|
||||
- a local legacy simulator so the launch and return flow can be tested without the old Java site
|
||||
- a Dockerized PHP Apache stack for exercising the real `www/faceai_handoff.php` and `www/faceai_return.php` bridge files
|
||||
- a local Dockerized Tomcat/JSP stack so the launch and return flow can be tested against the real legacy race pages under `www/`
|
||||
|
||||
## Structure
|
||||
|
||||
|
|
@ -24,32 +23,38 @@ faceai/
|
|||
|
||||
## Runtime Topology
|
||||
|
||||
The scaffold currently expects four runtime roles:
|
||||
The production-oriented application topology is still three services:
|
||||
|
||||
- `faceai`: public HTTP service on port `3001`, serving the built Vue app and the authenticated API
|
||||
- `processor`: background matcher runner consuming BullMQ jobs from Redis and executing the Linux `face_matcher` binary
|
||||
- `redis`: short-lived queue and search-state store
|
||||
- `legacy-php`: local-only PHP Apache simulator for exercising the real bridge files under `www/`
|
||||
|
||||
For hosted deployment, the long-lived application topology is `faceai` + `processor` + `redis`. The PHP simulator stays local-only and the real legacy site remains on its existing stack.
|
||||
The checked-in development override now expands that into the full local integration stack:
|
||||
|
||||
- `tomcat-www`: local Tomcat runtime serving the real legacy JSP race pages from `www/`
|
||||
- `mysql`: local legacy database used by the Tomcat stack
|
||||
- `maildump`: local SMTP sink and viewer for the Tomcat stack
|
||||
|
||||
That means the local FaceAI flow now runs against the same legacy page type the real site uses instead of a separate PHP bridge container.
|
||||
|
||||
## What The End-To-End Local Test Covers
|
||||
|
||||
The local simulator exercises the exact flow the plan is aiming for:
|
||||
The local Tomcat stack exercises the exact flow the current integration is aiming for:
|
||||
|
||||
1. a legacy-like race page loads the original `www/_js/rus-ecom-240621.js` script and shows a `Face ID` button instead of `tipoPuntoFoto`
|
||||
2. clicking it hits the real PHP handoff bridge at `www/faceai_handoff.php`
|
||||
2. clicking it uses the handoff URL emitted by the real JSP race page and launches FaceAI through `http://localhost:3001/dev/legacy/launch`
|
||||
3. the backend signs a short-lived handoff token and redirects to the Vue app
|
||||
4. the Vue app exchanges the token for its own FaceAI session cookie
|
||||
5. the user uploads a selfie and starts a Redis-backed race-scoped search
|
||||
6. the frontend polls until the job completes
|
||||
7. FaceAI requests a signed return URL
|
||||
8. the browser is redirected back to the real PHP return bridge at `www/faceai_return.php`
|
||||
9. the PHP bridge fetches the signed result from FaceAI and renders a filtered legacy-like race page
|
||||
8. the browser is redirected back to the real Tomcat race page with `faceaiResultId` and `faceaiToken`
|
||||
9. the legacy race-page JavaScript fetches the signed result from FaceAI, stores the hydrated match payload in browser storage, and reloads the cleaned race URL
|
||||
10. the same legacy race page renders the FaceAI-filtered gallery view from that stored payload
|
||||
|
||||
## Local Testing With The Legacy PHP Simulator
|
||||
## Local Testing With The Merged Legacy JSP Stack
|
||||
|
||||
This is the recommended local test path because it exercises the public site, the processor, Redis, and the real PHP bridge files together.
|
||||
This is the recommended local test path because it exercises the public site, the processor, Redis, and the real legacy JSP page flow together.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
|
|
@ -78,13 +83,15 @@ The local development stack started by the command above combines the base file
|
|||
- FaceAI public site on `http://localhost:3001`
|
||||
- processor runner on the internal Compose network
|
||||
- Redis on the internal Compose network
|
||||
- PHP Apache serving `../www` on `http://localhost:8080`
|
||||
- Tomcat serving the real local legacy site on `http://localhost:8080`
|
||||
- MySQL for the local legacy stack
|
||||
- Maildump for the local legacy stack on `http://localhost:8025`
|
||||
|
||||
The local stack also mounts:
|
||||
|
||||
- `../test_pkl` into both the public FaceAI container and the processor container as the shared read-only PKL dataset root
|
||||
- `./logs` into both the public FaceAI container and the processor container as the persistent diagnostics directory
|
||||
- `../www` into the PHP container so the real bridge files are used
|
||||
- `../www` into the Tomcat build/runtime inputs so the real legacy JSP pages and assets are used
|
||||
|
||||
The `processor` service is built from `docker/processor.Dockerfile` using the repository root as Docker build context. That image copies only the checked-in Unix `face_matcher` into the image, so the matcher is baked into the processor runtime without bringing along the other Unix or Windows binaries.
|
||||
|
||||
|
|
@ -110,22 +117,23 @@ The current bundled Linux `face_matcher` binary is a PyInstaller build that requ
|
|||
Open:
|
||||
|
||||
```text
|
||||
http://localhost:8080/faceai_simulator.php?raceId=202&lang=it
|
||||
http://localhost:8080/Foto2.abl?id_gara=1018557&pageRow=96&pageNumber=1
|
||||
```
|
||||
|
||||
That page simulates the legacy race page, loads the original race-page JavaScript from `www/_js/rus-ecom-240621.js`, lets the script replace the visible `tipoPuntoFoto` selector with the new `Face ID` button, and launches the real PHP handoff bridge at `www/faceai_handoff.php`.
|
||||
That is the real local Tomcat race page. It loads the original race-page JavaScript from `www/_js/rus-ecom-240621.js`, lets the script replace the visible `tipoPuntoFoto` selector with the new `Face ID` button, and launches the backend handoff endpoint configured through the JSP page.
|
||||
|
||||
### Expected Local Flow
|
||||
|
||||
Use the page above and verify this sequence:
|
||||
|
||||
1. the simulator page renders on port `8080`
|
||||
1. the real local race page renders on port `8080`
|
||||
2. the visible checkpoint selector is replaced with the `Face ID` launch button
|
||||
3. clicking `Face ID` redirects through `faceai_handoff.php` into `http://localhost:3001/auth/callback?token=...`
|
||||
3. clicking `Face ID` redirects through `http://localhost:3001/dev/legacy/launch` into `http://localhost:3001/auth/callback?token=...`
|
||||
4. the FaceAI app establishes its session and loads the upload flow
|
||||
5. uploading a selfie creates a queued search that the processor picks up
|
||||
6. when polling completes, FaceAI redirects back to `http://localhost:8080/faceai_return.php?...`
|
||||
7. the PHP return page renders the filtered photo list from the FaceAI result payload
|
||||
6. when polling completes, FaceAI redirects back to the same race page with `faceaiResultId` and `faceaiToken`
|
||||
7. the race-page JavaScript hydrates the result from FaceAI and reloads the cleaned legacy URL
|
||||
8. the filtered photo list renders from the hydrated FaceAI result payload
|
||||
|
||||
### Rebuild Notes
|
||||
|
||||
|
|
@ -143,7 +151,7 @@ docker compose --env-file .env.development down
|
|||
|
||||
### Automated End-To-End Test
|
||||
|
||||
The workspace now includes a Playwright suite that drives the PHP simulator, the FaceAI app, and the processor end to end.
|
||||
The workspace now includes a Playwright suite that drives the real local Tomcat race page, the FaceAI app, and the processor end to end.
|
||||
|
||||
From this folder, run:
|
||||
|
||||
|
|
@ -157,10 +165,10 @@ The suite will:
|
|||
|
||||
- build the frontend bundle
|
||||
- start `docker compose --env-file .env.development up --build -d`
|
||||
- open `http://localhost:8080/faceai_simulator.php?raceId=202&lang=it`
|
||||
- open `http://localhost:8080/Foto2.abl?id_gara=1018557&pageRow=96&pageNumber=1`
|
||||
- click the `Face ID` launch button injected by `www/_js/rus-ecom-240621.js`
|
||||
- upload `test_pkl/test_images/DSC_1960.JPG`
|
||||
- wait for the processor to complete and for FaceAI to redirect to `faceai_return.php`
|
||||
- wait for the processor to complete and for FaceAI to redirect back to the legacy race page
|
||||
- assert the filtered legacy result contains the expected `6` matches and includes `DSC_1960.JPG`
|
||||
- validate `faceai/logs/backend.log`, `faceai/logs/processor.log`, and the per-search `worker.log` and `matcher.log` for the run
|
||||
- stop the Compose stack automatically when the suite finishes
|
||||
|
|
@ -238,7 +246,7 @@ If a running processor still reports `ENOENT`, the deployed image was built befo
|
|||
|
||||
## Optional Backend And Frontend Dev Loop
|
||||
|
||||
If you only want to iterate on the app without the PHP simulator, you can still run the public site and the processor separately. The queue-backed flow now requires Redis and the processor, so `npm run dev` alone is no longer the full stack.
|
||||
If you only want to iterate on the app without the local Tomcat stack, you can still run the public site and the processor separately. The queue-backed flow now requires Redis and the processor, so `npm run dev` alone is no longer the full stack.
|
||||
|
||||
One workable loop is:
|
||||
|
||||
|
|
@ -252,7 +260,7 @@ Then start the processor in a second shell, either with its own local environmen
|
|||
|
||||
## Docker Compose Deployment For The Public Site And Matcher Runner
|
||||
|
||||
The checked-in `docker-compose.yml` is now the production-ready base stack for hosted deployment. The checked-in `docker-compose.override.yml` is the development overlay that restores the local PHP simulator, workspace bind mounts, and development-oriented commands.
|
||||
The checked-in `docker-compose.yml` is now the production-ready base stack for hosted deployment. The checked-in `docker-compose.override.yml` is the development overlay that restores the local Tomcat/JSP stack, workspace bind mounts, and development-oriented commands.
|
||||
|
||||
Because Docker Compose auto-loads `docker-compose.override.yml` when it is present in the same directory, production-style runs from this workspace must explicitly select only the base file.
|
||||
|
||||
|
|
@ -367,8 +375,8 @@ After the Compose stack is up, validate at least the following:
|
|||
2. The legacy handoff endpoint redirects to `https://faceai.../auth/callback?token=...`.
|
||||
3. FaceAI can exchange the token and establish a session.
|
||||
4. A search is enqueued in Redis and picked up by the processor.
|
||||
5. Completing a search produces a redirect URL that points to `FACEAI_LEGACY_RETURN_URL`.
|
||||
6. The legacy return endpoint can resolve the signed result and render the filtered race page.
|
||||
5. Completing a search produces a redirect URL that points to the intended legacy return target.
|
||||
6. The legacy page can resolve the signed result and render the filtered race page.
|
||||
|
||||
### Current Production Limitations
|
||||
|
||||
|
|
@ -411,9 +419,10 @@ NODE_ENV=development
|
|||
FACEAI_PORT=3001
|
||||
FACEAI_FRONTEND_URL=http://localhost:3001
|
||||
FACEAI_PUBLIC_BASE_URL=http://localhost:3001
|
||||
FACEAI_LEGACY_RETURN_URL=http://localhost:8080/faceai_return.php
|
||||
FACEAI_LEGACY_RETURN_MODE=direct
|
||||
FACEAI_LEGACY_HOME_URL=http://localhost:8080/index.jsp
|
||||
FACEAI_FEATURE_ENABLED=1
|
||||
FACEAI_HANDOFF_URL=http://localhost:3001/dev/legacy/launch
|
||||
FACEAI_SHARED_SECRET=change-me
|
||||
FACEAI_SESSION_COOKIE=rus_faceai_session
|
||||
FACEAI_REDIS_URL=redis://redis:6379
|
||||
|
|
@ -425,12 +434,13 @@ FACEAI_PKL_ROOT=/data/pkl
|
|||
FACEAI_MATCHER_BINARY=/app/bin/face_matcher
|
||||
```
|
||||
|
||||
If you want FaceAI to return through the new PHP bridge prepared under `www`, point `FACEAI_LEGACY_RETURN_URL` to that endpoint instead, for example `http://localhost/faceai_return.php` or the equivalent URL in your local PHP setup.
|
||||
If you want FaceAI to force the older bridge-style return path instead, set `FACEAI_LEGACY_RETURN_MODE=bridge` and point `FACEAI_LEGACY_RETURN_URL` at the appropriate legacy bridge endpoint.
|
||||
|
||||
In the development override, that wiring is already done with:
|
||||
In the current development override, the local legacy handoff wiring is already done with:
|
||||
|
||||
```text
|
||||
FACEAI_LEGACY_RETURN_URL=http://localhost:8080/faceai_return.php
|
||||
FACEAI_HANDOFF_URL=http://localhost:3001/dev/legacy/launch
|
||||
FACEAI_LEGACY_RETURN_MODE=direct
|
||||
FACEAI_LEGACY_HOME_URL=http://localhost:8080/index.jsp
|
||||
```
|
||||
|
||||
|
|
@ -438,18 +448,17 @@ The development override also keeps the log wiring with `./logs:/data/logs`, so
|
|||
|
||||
The Compose contract now also includes an HTTP healthcheck on the public FaceAI service and a Redis readiness check. That makes `docker compose ps` meaningful during rollout: `faceai` only becomes healthy after `GET /health` returns `{"ok":true}`, and both the public site and the processor wait for Redis readiness before their own startup sequence begins.
|
||||
|
||||
The local PHP simulator also needs the legacy bridge feature flag enabled:
|
||||
The local Tomcat stack also needs the legacy bridge feature flag enabled on the JSP side:
|
||||
|
||||
```text
|
||||
FACEAI_FEATURE_ENABLED=1
|
||||
```
|
||||
|
||||
The checked-in `docker-compose.override.yml` sets that on the `legacy-php` service so the simulator can launch the FaceAI handoff flow locally.
|
||||
The checked-in `docker-compose.override.yml` sets that on the local `tomcat-www` service so the race page can launch the FaceAI handoff flow locally.
|
||||
|
||||
## Notes
|
||||
|
||||
- Search orchestration now uses Redis and a dedicated processor worker.
|
||||
- The checked-in base Compose file is production-oriented, while the checked-in override is development-only.
|
||||
- The local legacy simulator is intentionally backend-driven so the handoff can be tested without compiling the existing Java application.
|
||||
- `www/faceai_simulator.php` exists only for local testing. It does not replace the actual JSP race page.
|
||||
- The local development flow now uses the actual Tomcat-served JSP race page instead of a separate PHP simulator bridge.
|
||||
- The final legacy integration still needs a real signed identity source and a real return-filter implementation on the old site.
|
||||
|
|
|
|||
|
|
@ -86,11 +86,12 @@ services:
|
|||
FACEAI_DEV_DISPLAY_NAME: ${FACEAI_DEV_DISPLAY_NAME:-Local Model User}
|
||||
FACEAI_DEV_EMAIL: ${FACEAI_DEV_EMAIL:-local.model.user@example.invalid}
|
||||
FACEAI_DEV_MEMBERSHIP_STATUS: ${FACEAI_DEV_MEMBERSHIP_STATUS:-active}
|
||||
LOCAL_APP_INSTANCE: ${LOCAL_APP_INSTANCE:-local-model}
|
||||
LOCAL_APP_INSTANCE: ${LOCAL_APP_INSTANCE:-main}
|
||||
LOCAL_DOCBASE: /data/docbase/
|
||||
FACEAI_FEATURE_ENABLED: ${FACEAI_FEATURE_ENABLED:-1}
|
||||
volumes:
|
||||
- ../www:/workspace/www:ro
|
||||
- ../rus:/workspace/rus:ro
|
||||
- ../test_pkl:/workspace/test_pkl:ro
|
||||
- ../local-jsp-docker/runtime/docbase:/data/docbase
|
||||
- ../local-jsp-docker/runtime/tomcat-work:/usr/local/tomcat/work
|
||||
|
|
@ -111,15 +112,21 @@ services:
|
|||
- --max_allowed_packet=1G
|
||||
- --net_read_timeout=600
|
||||
- --net_write_timeout=600
|
||||
- --lower_case_table_names=1
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: pg
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
LOCAL_DB_SEED_DUMP: ${LOCAL_DB_SEED_DUMP:-pg-model-seed-trimmed-20260421.sql}
|
||||
LOCAL_DB_SEED_DUMP: ${LOCAL_DB_SEED_DUMP:-pg-local-purpose-seed-20260422.sql}
|
||||
LOCAL_DB_OVERLAY_DUMP: ${LOCAL_DB_OVERLAY_DUMP:-}
|
||||
LOCAL_DOCBASE: /data/docbase/
|
||||
LOCAL_MAIL_SMTP_HOST: maildump
|
||||
LOCAL_MAIL_SMTP_PORT: 1025
|
||||
LOCAL_SOURCE_DIR: /workspace/www/
|
||||
LOCAL_TEST_USER_ID: ${LOCAL_TEST_USER_ID:-2}
|
||||
LOCAL_TEST_USER_LOGIN: ${LOCAL_TEST_USER_LOGIN:-test}
|
||||
LOCAL_TEST_USER_PASSWORD: ${LOCAL_TEST_USER_PASSWORD:-test1}
|
||||
LOCAL_TEST_USER_EMAIL: ${LOCAL_TEST_USER_EMAIL:-localtest@regalamiunsorriso.test}
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- ../db:/seed:ro
|
||||
|
|
@ -128,11 +135,9 @@ services:
|
|||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- mysqladmin
|
||||
- ping
|
||||
- -h
|
||||
- 127.0.0.1
|
||||
- -proot
|
||||
- sh
|
||||
- -lc
|
||||
- mysql -uroot -p"$$MYSQL_ROOT_PASSWORD" -Nse "SELECT CASE WHEN EXISTS (SELECT 1 FROM pg.parm WHERE codice = 'REWRITE_URL_ENABLE' AND numero = 1) AND EXISTS (SELECT 1 FROM pg.gara LIMIT 1) THEN 1 ELSE 0 END" | grep -qx 1
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
const {
|
||||
ensureLocalAuthenticatedRacePage,
|
||||
EXPECTED_MATCH_COUNT,
|
||||
FACEAI_BASE_URL,
|
||||
LEGACY_RACE_ID,
|
||||
SELFIE_NAME,
|
||||
buildHandoffUrl,
|
||||
buildSimulatorUrl,
|
||||
getSearchArtifacts,
|
||||
|
|
@ -61,6 +63,7 @@ async function readLaunchUrlFromLegacyPage(page) {
|
|||
}
|
||||
|
||||
async function startSearch(page, selfieName) {
|
||||
const selfieLabel = selfieName.split(/[\\/]+/u).pop();
|
||||
const createResponsePromise = page.waitForResponse((response) => {
|
||||
return response.url().includes('/api/searches')
|
||||
&& response.request().method() === 'POST'
|
||||
|
|
@ -68,7 +71,7 @@ async function startSearch(page, selfieName) {
|
|||
});
|
||||
|
||||
await page.locator('input[type="file"]').setInputFiles(getSelfiePath(selfieName));
|
||||
await expect(page.getByText(selfieName)).toBeVisible();
|
||||
await expect(page.getByText(selfieLabel)).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Avvia ricerca Face ID' }).click();
|
||||
|
||||
const createResponse = await createResponsePromise;
|
||||
|
|
@ -165,40 +168,40 @@ test('runs the legacy Tomcat flow through FaceAI and returns to the filtered leg
|
|||
|
||||
await launchFromSimulator(page, {
|
||||
raceId: LEGACY_RACE_ID,
|
||||
raceSlug: 'livorno',
|
||||
raceName: 'Livorno',
|
||||
raceFolder: 'LIVORNO'
|
||||
raceSlug: 'isolotto',
|
||||
raceName: 'Festa sociale UP Isolotto',
|
||||
raceFolder: 'ISOLOTTO'
|
||||
});
|
||||
|
||||
const search = await startSearch(page, 'DSC_1960.JPG');
|
||||
const search = await startSearch(page, SELFIE_NAME);
|
||||
|
||||
await waitForLegacyResult(page, LEGACY_RACE_ID, EXPECTED_MATCH_COUNT);
|
||||
|
||||
await verifySearchLogs(search.id, {
|
||||
expectedMatchCount: EXPECTED_MATCH_COUNT,
|
||||
expectedSelfieName: 'DSC_1960.JPG'
|
||||
expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop()
|
||||
});
|
||||
});
|
||||
|
||||
test('builds the legacy FaceAI handoff URL with the exact local race storage metadata', async ({ page }) => {
|
||||
await page.goto(buildSimulatorUrl({
|
||||
raceId: LEGACY_RACE_ID,
|
||||
raceSlug: 'livorno',
|
||||
raceName: 'Livorno',
|
||||
raceSlug: 'isolotto',
|
||||
raceName: 'Festa sociale UP Isolotto',
|
||||
raceYear: '2026',
|
||||
raceMonthFolder: '04.APRILE',
|
||||
raceFolder: 'LIVORNO'
|
||||
raceFolder: 'ISOLOTTO'
|
||||
}), { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await expect(page.locator('#faceAiRaceYear')).toHaveValue('2026');
|
||||
await expect(page.locator('#faceAiRaceMonthFolder')).toHaveValue('04.APRILE');
|
||||
await expect(page.locator('#faceAiRaceFolder')).toHaveValue('LIVORNO');
|
||||
await expect(page.locator('#faceAiRaceFolder')).toHaveValue('ISOLOTTO');
|
||||
|
||||
const launchUrl = await readLaunchUrlFromLegacyPage(page);
|
||||
expect(launchUrl.searchParams.get('raceYear')).toBe('2026');
|
||||
expect(launchUrl.searchParams.get('raceMonthFolder')).toBe('04.APRILE');
|
||||
expect(launchUrl.searchParams.get('raceFolder')).toBe('LIVORNO');
|
||||
expect(launchUrl.searchParams.get('raceStorageRelativeDir')).toBe('2026/04.APRILE/LIVORNO');
|
||||
expect(launchUrl.searchParams.get('raceFolder')).toBe('ISOLOTTO');
|
||||
expect(launchUrl.searchParams.get('raceStorageRelativeDir')).toBe('2026/04.APRILE/ISOLOTTO');
|
||||
});
|
||||
|
||||
test('shows the unsupported-race message when the current race has no PKL data and lets the user go back', async ({ page }) => {
|
||||
|
|
@ -300,6 +303,19 @@ test('rejects a not-logged-in user after clicking the Face ID button and sends t
|
|||
await expect(page.locator('#faceAiErrorModalMessage')).toContainText('Il servizio Face ID non e al momento disponibile. Riprova piu tardi.');
|
||||
});
|
||||
|
||||
test('authenticates with the seeded local user and lets that user browse and launch the Livorno race page', async ({ page }) => {
|
||||
await ensureLocalAuthenticatedRacePage(page, {
|
||||
raceId: '1018557',
|
||||
raceName: 'VIVICITTA LIVORNO',
|
||||
raceYear: '2026',
|
||||
raceMonthFolder: '04.APRILE',
|
||||
raceFolder: 'LIVORNO'
|
||||
});
|
||||
|
||||
await page.locator('#faceaiLaunchButton').click();
|
||||
await waitForFaceAiHome(page);
|
||||
});
|
||||
|
||||
test('shows the no-face message and allows the user to return to the race page', async ({ page }) => {
|
||||
test.slow();
|
||||
|
||||
|
|
@ -341,8 +357,8 @@ test('opens the file chooser when the user clicks the upload button', async ({ p
|
|||
await page.getByRole('button', { name: 'Scegli immagine' }).click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
|
||||
await fileChooser.setFiles(getSelfiePath('DSC_1960.JPG'));
|
||||
await expect(page.getByText('DSC_1960.JPG')).toBeVisible();
|
||||
await fileChooser.setFiles(getSelfiePath(SELFIE_NAME));
|
||||
await expect(page.getByText(SELFIE_NAME.split(/[\\/]+/u).pop())).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Avvia ricerca Face ID' })).toBeEnabled();
|
||||
});
|
||||
|
||||
|
|
@ -364,7 +380,7 @@ test('lets the user retry with a valid photo after a no-face upload and then ret
|
|||
await expect(page.locator('.faceai-feedback')).toContainText('Nessun volto rilevato nella foto caricata');
|
||||
await expect(page.locator('input[type="file"]')).toBeEnabled();
|
||||
|
||||
const retrySearch = await startSearch(page, 'DSC_1960.JPG');
|
||||
const retrySearch = await startSearch(page, SELFIE_NAME);
|
||||
await waitForLegacyResult(page, '202', EXPECTED_MATCH_COUNT);
|
||||
|
||||
await verifySearchLogs(noFaceSearch.id, {
|
||||
|
|
@ -373,7 +389,7 @@ test('lets the user retry with a valid photo after a no-face upload and then ret
|
|||
});
|
||||
await verifySearchLogs(retrySearch.id, {
|
||||
expectedMatchCount: EXPECTED_MATCH_COUNT,
|
||||
expectedSelfieName: 'DSC_1960.JPG'
|
||||
expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop()
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -403,7 +419,7 @@ test('allows two users to process different photos at the same time', async ({ b
|
|||
]);
|
||||
|
||||
const [searchOne, searchTwo] = await Promise.all([
|
||||
startSearch(pages[0], 'DSC_1960.JPG'),
|
||||
startSearch(pages[0], SELFIE_NAME),
|
||||
startSearch(pages[1], 'DSC_1987.JPG')
|
||||
]);
|
||||
|
||||
|
|
@ -413,7 +429,7 @@ test('allows two users to process different photos at the same time', async ({ b
|
|||
]);
|
||||
|
||||
await Promise.all([
|
||||
verifySearchLogs(searchOne.id, { expectedSelfieName: 'DSC_1960.JPG' }),
|
||||
verifySearchLogs(searchOne.id, { expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }),
|
||||
verifySearchLogs(searchTwo.id, { expectedSelfieName: 'DSC_1987.JPG' })
|
||||
]);
|
||||
} finally {
|
||||
|
|
@ -436,7 +452,7 @@ test('queues the third user until a worker is free and then completes all three
|
|||
]);
|
||||
|
||||
const [searchOne, searchTwo, searchThree] = await Promise.all([
|
||||
startSearch(pages[0], 'DSC_1960.JPG'),
|
||||
startSearch(pages[0], SELFIE_NAME),
|
||||
startSearch(pages[1], 'DSC_1987.JPG'),
|
||||
startSearch(pages[2], 'DSC_2058.JPG')
|
||||
]);
|
||||
|
|
@ -477,7 +493,7 @@ test('queues the third user until a worker is free and then completes all three
|
|||
await Promise.all(pages.map((page) => waitForLegacyResult(page, '202')));
|
||||
|
||||
await Promise.all([
|
||||
verifySearchLogs(searchOne.id, { expectedSelfieName: 'DSC_1960.JPG' }),
|
||||
verifySearchLogs(searchOne.id, { expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }),
|
||||
verifySearchLogs(searchTwo.id, { expectedSelfieName: 'DSC_1987.JPG' }),
|
||||
verifySearchLogs(searchThree.id, { expectedSelfieName: 'DSC_2058.JPG' })
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ const WORKSPACE_ROOT = path.resolve(ROOT_DIR, '..');
|
|||
const LOG_ROOT = path.join(ROOT_DIR, 'logs');
|
||||
const SEARCH_LOG_ROOT = path.join(LOG_ROOT, 'searches');
|
||||
const FACEAI_BASE_URL = process.env.FACEAI_E2E_BASE_URL || 'http://127.0.0.1:3001';
|
||||
const SIMULATOR_URL = process.env.FACEAI_E2E_SIMULATOR_URL || 'http://127.0.0.1:8080/Foto2.abl?id_gara=1018557&pageRow=96&pageNumber=1';
|
||||
const SIMULATOR_URL = process.env.FACEAI_E2E_SIMULATOR_URL || 'http://127.0.0.1:8080/Foto2.abl?id_gara=1018547&pageRow=96&pageNumber=1';
|
||||
const LEGACY_BASE_URL = process.env.FACEAI_E2E_LEGACY_BASE_URL || 'http://127.0.0.1:8080';
|
||||
const LEGACY_HOME_URL = process.env.FACEAI_E2E_LEGACY_HOME_URL || `${LEGACY_BASE_URL}/index.jsp`;
|
||||
const SELFIE_NAME = process.env.FACEAI_E2E_SELFIE || 'DSC_1960.JPG';
|
||||
const LEGACY_LOGIN_URL = process.env.FACEAI_E2E_LEGACY_LOGIN_URL || `${LEGACY_BASE_URL}/login_clienti.html`;
|
||||
const LEGACY_USERNAME = process.env.FACEAI_E2E_LEGACY_USERNAME || 'test';
|
||||
const LEGACY_PASSWORD = process.env.FACEAI_E2E_LEGACY_PASSWORD || 'test1';
|
||||
const SELFIE_NAME = process.env.FACEAI_E2E_SELFIE || '2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8459.JPG';
|
||||
const EXPECTED_MATCH_COUNT = Number(process.env.FACEAI_E2E_EXPECTED_MATCH_COUNT || '6');
|
||||
const LEGACY_RACE_ID = process.env.FACEAI_E2E_RACE_ID || '1018557';
|
||||
const LEGACY_RACE_ID = process.env.FACEAI_E2E_RACE_ID || '1018547';
|
||||
|
||||
function quoteShellArg(value) {
|
||||
if (!/[\s"]/u.test(value)) {
|
||||
|
|
@ -114,17 +117,22 @@ async function waitForHttp(url, validate, timeoutMs = 3 * 60 * 1000) {
|
|||
}
|
||||
|
||||
function getSelfiePath(fileName = SELFIE_NAME) {
|
||||
const relativeSegments = fileName.split(/[\\/]+/u);
|
||||
if (relativeSegments.length > 1) {
|
||||
return path.join(WORKSPACE_ROOT, 'test_pkl', ...relativeSegments);
|
||||
}
|
||||
|
||||
return path.join(WORKSPACE_ROOT, 'test_pkl', 'test_images', fileName);
|
||||
}
|
||||
|
||||
function buildSimulatorUrl({
|
||||
raceId = LEGACY_RACE_ID,
|
||||
lang = 'it',
|
||||
raceSlug = 'livorno',
|
||||
raceName = 'Livorno',
|
||||
raceSlug = 'isolotto',
|
||||
raceName = 'Festa sociale UP Isolotto',
|
||||
raceYear = '2026',
|
||||
raceMonthFolder = '04.APRILE',
|
||||
raceFolder = 'LIVORNO',
|
||||
raceFolder = 'ISOLOTTO',
|
||||
pageRow = '96',
|
||||
pageNumber = '1'
|
||||
} = {}) {
|
||||
|
|
@ -139,11 +147,11 @@ function buildSimulatorUrl({
|
|||
function buildHandoffUrl({
|
||||
raceId = LEGACY_RACE_ID,
|
||||
lang = 'it',
|
||||
raceSlug = 'livorno',
|
||||
raceName = 'Livorno',
|
||||
raceSlug = 'isolotto',
|
||||
raceName = 'Festa sociale UP Isolotto',
|
||||
raceYear = '2026',
|
||||
raceMonthFolder = '04.APRILE',
|
||||
raceFolder = 'LIVORNO',
|
||||
raceFolder = 'ISOLOTTO',
|
||||
userId = '1',
|
||||
displayName = `Local Test User ${userId}`,
|
||||
email = `local-test-${userId}@example.invalid`,
|
||||
|
|
@ -166,6 +174,86 @@ function buildHandoffUrl({
|
|||
return url.toString();
|
||||
}
|
||||
|
||||
function buildLegacyLoginFormData() {
|
||||
if (!LEGACY_USERNAME || !LEGACY_PASSWORD) {
|
||||
throw new Error('FACEAI_E2E_LEGACY_USERNAME and FACEAI_E2E_LEGACY_PASSWORD must be set before running authenticated local E2E checks.');
|
||||
}
|
||||
|
||||
return {
|
||||
login: LEGACY_USERNAME,
|
||||
pwd: LEGACY_PASSWORD,
|
||||
cmdIU: 'check',
|
||||
act: '',
|
||||
thePage: ''
|
||||
};
|
||||
}
|
||||
|
||||
async function performLocalLoginRequest(requestContext) {
|
||||
const response = await requestContext.post(`${LEGACY_BASE_URL}/Logon.abl`, {
|
||||
form: buildLegacyLoginFormData(),
|
||||
failOnStatusCode: false
|
||||
});
|
||||
|
||||
const finalUrl = response.url();
|
||||
const bodyText = await response.text();
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Local login request failed with HTTP ${response.status()} at ${finalUrl}`);
|
||||
}
|
||||
|
||||
if (/login_clienti|Username \/ Email|Password/iu.test(bodyText) && !/user_logout|dettaglio_clienti|Il mio account/iu.test(bodyText)) {
|
||||
throw new Error(`Local login request appears to have remained on the login page at ${finalUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function expectLocalRacePageLoaded(page) {
|
||||
await page.waitForSelector('form[onsubmit="return searching()"]', { state: 'visible' });
|
||||
const raceId = await page.locator('#id_gara').inputValue();
|
||||
if (!/\d+/u.test(raceId)) {
|
||||
throw new Error(`Expected the local race page to expose a numeric race id, got: ${raceId}`);
|
||||
}
|
||||
|
||||
await page.waitForSelector('#faceaiLaunchButton', { state: 'visible' });
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelectorAll('a[data-faceai-photo-id] img.thumb').length > 0;
|
||||
}, null, {
|
||||
timeout: 30 * 1000
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureLocalAuthenticatedRacePage(page, options = {}) {
|
||||
await page.goto(LEGACY_LOGIN_URL, { waitUntil: 'domcontentloaded' });
|
||||
await page.locator('#login').fill(LEGACY_USERNAME);
|
||||
await page.locator('#pwd').fill(LEGACY_PASSWORD);
|
||||
|
||||
const submitLocator = page.locator('input[type="submit"], button[type="submit"], a.btn').filter({ hasText: /Accedi|Sign in/i }).first();
|
||||
if (await submitLocator.count()) {
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
|
||||
submitLocator.click()
|
||||
]);
|
||||
} else {
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
|
||||
page.evaluate(() => {
|
||||
const form = document.querySelector('form[action="Logon.abl"]');
|
||||
if (!form) {
|
||||
throw new Error('Local login page did not expose a Logon.abl form.');
|
||||
}
|
||||
|
||||
const cmdIUField = form.querySelector('[name="cmdIU"]');
|
||||
if (cmdIUField) {
|
||||
cmdIUField.value = 'check';
|
||||
}
|
||||
form.submit();
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
await page.goto(buildSimulatorUrl(options), { waitUntil: 'domcontentloaded' });
|
||||
await expectLocalRacePageLoaded(page);
|
||||
}
|
||||
|
||||
function getSearchArtifacts(searchId) {
|
||||
const searchRoot = path.join(SEARCH_LOG_ROOT, searchId);
|
||||
return {
|
||||
|
|
@ -188,15 +276,22 @@ module.exports = {
|
|||
FACEAI_BASE_URL,
|
||||
LEGACY_BASE_URL,
|
||||
LEGACY_HOME_URL,
|
||||
LEGACY_LOGIN_URL,
|
||||
LEGACY_PASSWORD,
|
||||
SIMULATOR_URL,
|
||||
SELFIE_NAME,
|
||||
EXPECTED_MATCH_COUNT,
|
||||
LEGACY_RACE_ID,
|
||||
LEGACY_USERNAME,
|
||||
buildHandoffUrl,
|
||||
buildLegacyLoginFormData,
|
||||
buildSimulatorUrl,
|
||||
dockerCompose,
|
||||
ensureLocalAuthenticatedRacePage,
|
||||
expectLocalRacePageLoaded,
|
||||
getSearchArtifacts,
|
||||
getSelfiePath,
|
||||
performLocalLoginRequest,
|
||||
prepareHostState,
|
||||
readUtf8,
|
||||
runCommand,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue