Regalamiunsorriso/faceai/tests/e2e/faceai-simulator.spec.js
MaddoScientisto 32db61c381
All checks were successful
Publish FaceAI Container / publish (push) Successful in 13m22s
feat(audit): implement audit logging for search requests and results
- Added configuration options for audit database path and retention days in backend and processor.
- Integrated audit logging in server and worker processes to track search requests, completions, and failures.
- Created utility functions for reading and parsing audit logs in end-to-end tests.
- Updated Docker Compose files to include audit database configuration.
- Added new tests to verify audit log entries for successful and no-results searches.
2026-05-19 23:29:38 +02:00

627 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { test, expect } = require('@playwright/test');
const {
ensureLocalAuthenticatedRacePage,
EXPECTED_MATCH_COUNT,
FACEAI_BASE_URL,
LEGACY_BASE_URL,
LEGACY_RACE_ID,
SELFIE_NAME,
buildHandoffUrl,
buildSimulatorUrl,
getSearchArtifacts,
getSelfiePath,
readAuditArtifacts,
readUtf8
} = require('./faceai-test-utils');
const FACEAI_HOME_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):3001\/?(?:\?.*)?$/;
const FACEAI_CALLBACK_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):3001\/auth\/callback\?token=/;
const LONG_TEST_TIMEOUT_MS = 3 * 60 * 1000;
const SHORT_UI_TIMEOUT_MS = 30 * 1000;
const SEARCH_COMPLETION_TIMEOUT_MS = 75 * 1000;
const LEGACY_RETURN_TIMEOUT_MS = 75 * 1000;
const FILE_CHOOSER_TIMEOUT_MS = 8 * 1000;
const FACEAI_CONSENT_HEADING_RE = /Prima di continuare|Before you continue/i;
const FACEAI_UPLOAD_HEADING_RE = /Carica il tuo selfie|Upload your selfie/i;
const LEGACY_BASE_URL_RE = new RegExp(`^${escapeRegExp(LEGACY_BASE_URL)}`);
const LEGACY_HOME_URL_RE = new RegExp(`^${escapeRegExp(`${LEGACY_BASE_URL}/index.jsp`)}$`);
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function buildLegacySimulatorReturnMatcher(raceId) {
return new RegExp(`^${escapeRegExp(`${LEGACY_BASE_URL}/Foto2.abl?id_gara=${raceId}`)}.*`);
}
function assertLogDoesNotContain(content, patterns, label) {
for (const pattern of patterns) {
expect(content, `${label} should not contain ${pattern}`).not.toMatch(pattern);
}
}
async function waitForFaceAiHome(page) {
await page.waitForURL((url) => FACEAI_HOME_URL_RE.test(url.toString()), {
timeout: SHORT_UI_TIMEOUT_MS
});
const consentHeading = page.getByRole('heading', { name: FACEAI_CONSENT_HEADING_RE });
if (await consentHeading.isVisible().catch(() => false)) {
await page.getByRole('checkbox', {
name: /Confermo di aver letto linformativa sul trattamento dei dati biometrici\.|I confirm that I have read the biometric data processing notice\./i
}).check();
await page.getByRole('button', { name: /Accetto e continuo|I agree and continue/i }).click();
}
await expect(page.getByRole('heading', { name: FACEAI_UPLOAD_HEADING_RE })).toBeVisible();
await expect(page.getByRole('button', { name: /Scegli immagine|Choose image/i })).toBeVisible();
}
async function launchFromSimulator(page, options = {}) {
const simulatorUrl = buildSimulatorUrl(options);
await page.goto(simulatorUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('#faceaiLaunchButton')).toBeVisible();
await expect(page.locator('select#tipoPuntoFoto')).toHaveCount(0);
await page.locator('#faceaiLaunchButton').click();
await waitForFaceAiHome(page);
return simulatorUrl;
}
async function enterViaHandoff(page, options = {}) {
await page.goto(buildHandoffUrl(options), { waitUntil: 'domcontentloaded' });
await waitForFaceAiHome(page);
}
async function readLaunchUrlFromLegacyPage(page) {
const launchUrl = await page.evaluate(() => {
return typeof buildFaceAiLaunchUrl === 'function' ? buildFaceAiLaunchUrl() : '';
});
expect(launchUrl, 'Expected the legacy race page to expose a FaceAI handoff URL builder.').toBeTruthy();
return new URL(launchUrl, LEGACY_BASE_URL);
}
async function startSearch(page, selfieName) {
const createResponsePromise = page.waitForResponse((response) => {
return response.url().includes('/api/searches')
&& response.request().method() === 'POST'
&& response.status() === 201;
});
await page.locator('input[type="file"]').setInputFiles(getSelfiePath(selfieName));
const createResponse = await createResponsePromise;
return createResponse.json();
}
async function fetchSearchStatus(page, searchId) {
return page.evaluate(async ({ searchId }) => {
const response = await fetch(`/api/searches/${searchId}`, { credentials: 'include' });
const body = await response.json().catch(() => null);
return {
statusCode: response.status,
body
};
}, { searchId });
}
async function waitForSearchCondition(page, searchId, predicate, timeoutMs = 30 * 1000) {
const deadline = Date.now() + timeoutMs;
let lastPayload = null;
while (Date.now() < deadline) {
const payload = await fetchSearchStatus(page, searchId);
lastPayload = payload;
if (payload.statusCode === 200 && predicate(payload.body)) {
return payload.body;
}
await page.waitForTimeout(250);
}
throw new Error(`Timed out waiting for search ${searchId}. Last payload: ${JSON.stringify(lastPayload)}`);
}
async function waitForLegacyResult(page, raceId, expectedMatchCount = null) {
await page.waitForURL(buildLegacySimulatorReturnMatcher(raceId), {
timeout: LEGACY_RETURN_TIMEOUT_MS,
waitUntil: 'commit'
});
await expect.poll(() => page.url(), {
timeout: 15 * 1000,
message: 'Expected the legacy race return URL to include FaceAI direct-return parameters.'
}).toMatch(/faceaiResultId=|faceaiMatchStorageKey=|faceaiPhotoIds=/);
if (expectedMatchCount === null) {
return;
}
await expect(page.locator('#faceAiPhotoCountValue')).toHaveText(String(expectedMatchCount), {
timeout: SHORT_UI_TIMEOUT_MS
});
}
async function verifySearchLogs(searchId, { expectedMatchCount, expectedSelfieName }) {
const artifacts = getSearchArtifacts(searchId);
const [backendLog, processorLog, workerLog, matcherLog] = await Promise.all([
readUtf8(artifacts.backendLogPath),
readUtf8(artifacts.processorLogPath),
readUtf8(artifacts.workerLogPath),
readUtf8(artifacts.matcherLogPath)
]);
expect(workerLog).toContain('Completed FaceAI search');
if (expectedMatchCount !== undefined) {
expect(workerLog).toContain(`"matchCount":${expectedMatchCount}`);
}
if (expectedSelfieName) {
expect(matcherLog).toContain(expectedSelfieName);
}
assertLogDoesNotContain(backendLog, [/\bnpm error\b/i, /\berror:\b/i, /\bfailed\b/i], 'backend.log');
assertLogDoesNotContain(processorLog, [new RegExp(`Failed FaceAI search ${searchId}`, 'i'), /\bnpm error\b/i], 'processor.log');
assertLogDoesNotContain(workerLog, [/FaceAI search failed/i], 'worker.log');
assertLogDoesNotContain(matcherLog, [/\[ERROR\]/i, /Traceback/i], 'matcher.log');
return { backendLog, processorLog, workerLog, matcherLog };
}
async function verifyAuditLog(searchId, { expectedMatchCount, expectedRaceId, expectedSelfieName, expectedUserId }) {
const artifacts = getSearchArtifacts(searchId);
const audit = readAuditArtifacts(searchId);
expect(audit.searchRow, `Expected ${artifacts.auditDbPath} to contain an audit row for ${searchId}`).toBeTruthy();
expect(audit.searchRow.search_id).toBe(searchId);
expect(audit.searchRow.status).toBe('completed');
expect(audit.searchRow.match_count).toBe(expectedMatchCount);
expect(audit.searchRow.race_id).toBe(expectedRaceId);
expect(audit.searchRow.user_id).toBe(expectedUserId);
expect(audit.searchRow.selfie_name).toBe(expectedSelfieName);
expect(audit.searchRow.selfie_sha256).toMatch(/^[a-f0-9]{64}$/i);
expect(audit.searchRow.selfie_size_bytes).toBeGreaterThan(0);
expect(audit.searchRow.result_id).toBeTruthy();
expect(audit.searchRow.requested_at).toBeGreaterThan(0);
expect(audit.searchRow.completed_at).toBeGreaterThan(0);
expect(audit.searchRow.redirect_issued_at).toBeGreaterThan(0);
expect(audit.searchRow.matches).toHaveLength(expectedMatchCount);
const eventTypes = audit.events.map((event) => event.event_type);
expect(eventTypes).toContain('search_requested');
expect(eventTypes).toContain('search_completed');
expect(eventTypes).toContain('search_redirect_issued');
const requestedEvent = audit.events.find((event) => event.event_type === 'search_requested');
expect(requestedEvent?.selfie_sha256).toBe(audit.searchRow.selfie_sha256);
const fingerprintMatch = audit.fingerprintMatches.find((entry) => entry.search_id === searchId);
expect(fingerprintMatch, 'Expected fingerprint lookup to find the original search row').toBeTruthy();
expect(fingerprintMatch.match_count).toBe(expectedMatchCount);
expect(fingerprintMatch.result_id).toBe(audit.searchRow.result_id);
expect(fingerprintMatch.status).toBe('completed');
return audit;
}
async function verifyNoResultsAuditLog(searchId, { expectedRaceId, expectedSelfieName, expectedUserId }) {
const artifacts = getSearchArtifacts(searchId);
const audit = readAuditArtifacts(searchId);
expect(audit.searchRow, `Expected ${artifacts.auditDbPath} to contain an audit row for ${searchId}`).toBeTruthy();
expect(audit.searchRow.search_id).toBe(searchId);
expect(audit.searchRow.status).toBe('completed');
expect(audit.searchRow.completion_code).toBe('NO_FACES_FOUND');
expect(audit.searchRow.match_count).toBe(0);
expect(audit.searchRow.race_id).toBe(expectedRaceId);
expect(audit.searchRow.user_id).toBe(expectedUserId);
expect(audit.searchRow.selfie_name).toBe(expectedSelfieName);
expect(audit.searchRow.selfie_sha256).toMatch(/^[a-f0-9]{64}$/i);
expect(audit.searchRow.selfie_size_bytes).toBeGreaterThan(0);
expect(audit.searchRow.result_id).toBeTruthy();
expect(audit.searchRow.requested_at).toBeGreaterThan(0);
expect(audit.searchRow.completed_at).toBeGreaterThan(0);
expect(audit.searchRow.redirect_issued_at).toBeNull();
expect(audit.searchRow.matches).toHaveLength(0);
const eventTypes = audit.events.map((event) => event.event_type);
expect(eventTypes).toContain('search_requested');
expect(eventTypes).toContain('search_completed');
expect(eventTypes).not.toContain('search_redirect_issued');
const requestedEvent = audit.events.find((event) => event.event_type === 'search_requested');
expect(requestedEvent?.selfie_sha256).toBe(audit.searchRow.selfie_sha256);
const fingerprintMatch = audit.fingerprintMatches.find((entry) => entry.search_id === searchId);
expect(fingerprintMatch, 'Expected fingerprint lookup to find the original search row').toBeTruthy();
expect(fingerprintMatch.match_count).toBe(0);
expect(fingerprintMatch.result_id).toBe(audit.searchRow.result_id);
expect(fingerprintMatch.status).toBe('completed');
return audit;
}
async function closeContexts(contexts) {
await Promise.all(contexts.map(async (context) => {
try {
await context.close();
} catch (error) {
if (!/ENOENT|Target page, context or browser has been closed/i.test(String(error))) {
throw error;
}
}
}));
}
test('runs the legacy Tomcat flow through FaceAI and returns to the filtered legacy result', async ({ page }) => {
test.slow();
await launchFromSimulator(page, {
raceId: LEGACY_RACE_ID,
raceSlug: 'isolotto',
raceName: 'Festa sociale UP Isolotto',
raceFolder: 'ISOLOTTO'
});
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: SELFIE_NAME.split(/[\\/]+/u).pop()
});
await verifyAuditLog(search.id, {
expectedMatchCount: EXPECTED_MATCH_COUNT,
expectedRaceId: LEGACY_RACE_ID,
expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop(),
expectedUserId: 'legacy-user-1'
});
});
test('records structured logs for a completed no-results FaceAI search in the dev compose stack', async ({ page }) => {
test.slow();
await launchFromSimulator(page, {
raceId: LEGACY_RACE_ID,
raceSlug: 'isolotto',
raceName: 'Festa sociale UP Isolotto',
raceFolder: 'ISOLOTTO'
});
const search = await startSearch(page, SELFIE_NAME);
await waitForSearchCondition(page, search.id, (payload) => {
return payload.status === 'completed' && payload.completionCode === 'NO_FACES_FOUND';
}, SEARCH_COMPLETION_TIMEOUT_MS);
await verifySearchLogs(search.id, {
expectedMatchCount: 0,
expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop()
});
await verifyNoResultsAuditLog(search.id, {
expectedRaceId: LEGACY_RACE_ID,
expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop(),
expectedUserId: 'legacy-user-1'
});
});
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: 'isolotto',
raceName: 'Festa sociale UP Isolotto',
raceYear: '2026',
raceMonthFolder: '04.APRILE',
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('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('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 }) => {
const consoleErrors = [];
page.on('console', (message) => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
}
});
await launchFromSimulator(page, {
raceId: '404',
raceSlug: 'corsa-di-livorno',
raceName: 'Corsa di Livorno',
raceFolder: 'LIVORNO'
});
await expect(page.locator('.faceai-feedback')).toContainText('FaceAI non è disponibile per questa gara.');
await expect(page.locator('input[type="file"]')).toBeDisabled();
await expect(page.getByRole('button', { name: 'Scegli immagine' })).toBeDisabled();
await expect.poll(() => {
return consoleErrors.find((entry) => entry.includes('[FaceAI] Invalid race data:')) || null;
}).toBeNull();
await page.waitForTimeout(2000);
await expect(page).toHaveURL(FACEAI_HOME_URL_RE);
await page.getByRole('button', { name: 'Torna alla pagina gara' }).click();
await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('404'));
});
test('shows a localized invalid-race error when the handoff omits race storage metadata', async ({ page }) => {
const consoleErrors = [];
page.on('console', (message) => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
}
});
const handoffUrl = new URL(buildHandoffUrl({
raceId: '405',
lang: 'en',
raceSlug: 'ghost-race',
raceName: 'Ghost Race',
raceFolder: 'THIS RACE DOES NOT EXIST'
}));
handoffUrl.searchParams.delete('raceYear');
handoffUrl.searchParams.delete('raceMonthFolder');
handoffUrl.searchParams.delete('raceFolder');
await page.goto(handoffUrl.toString(), { waitUntil: 'domcontentloaded' });
await page.waitForURL(FACEAI_HOME_URL_RE, {
timeout: SHORT_UI_TIMEOUT_MS
});
await expect(page.getByRole('heading', { name: 'Find your photos with a selfie' })).toBeVisible();
await expect(page.locator('.faceai-feedback')).toContainText('The race data received for this session is invalid. Go back to the race page and reopen Face ID from the correct race.');
await expect(page.locator('input[type="file"]')).toBeDisabled();
await expect(page.getByRole('button', { name: 'Choose image' })).toBeDisabled();
await expect(page.getByRole('button', { name: 'Start Face ID search' })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Back to the race page' })).toBeVisible();
await expect.poll(() => {
return consoleErrors.find((entry) => entry.includes('[FaceAI] Invalid race data:')) || null;
}).toContain('MISSING_RACE_STORAGE');
await expect.poll(() => {
return consoleErrors.find((entry) => entry.includes('[FaceAI] Invalid race data:')) || null;
}).toContain('MISSING_RACE_STORAGE');
await page.getByRole('button', { name: 'Back to the race page' }).click();
await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('405'));
});
test('rejects a not-logged-in user after clicking the Face ID button and sends them back to the original race page', async ({ page }) => {
const simulatorUrl = buildSimulatorUrl({
raceId: '202',
raceSlug: 'mezza-di-pisa',
raceName: 'Mezza di Pisa',
raceFolder: 'PISA'
});
await page.goto(simulatorUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('#faceaiLaunchButton')).toBeVisible();
await page.evaluate(() => {
if (window.faceAiSimulator) {
delete window.faceAiSimulator.devUserId;
delete window.faceAiSimulator.devDisplayName;
delete window.faceAiSimulator.devEmail;
delete window.faceAiSimulator.devMembershipStatus;
}
});
await page.locator('#faceaiLaunchButton').click();
await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('202'));
await expect(page).not.toHaveURL(FACEAI_HOME_URL_RE);
await expect(page.locator('#faceAiErrorModal')).toBeVisible();
await expect(page.locator('#faceAiErrorModalLabel')).toContainText('Face ID non disponibile');
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();
await launchFromSimulator(page, {
raceId: '202',
raceSlug: 'mezza-di-pisa',
raceName: 'Mezza di Pisa',
raceFolder: 'PISA'
});
const search = await startSearch(page, 'DSC_1994.JPG');
await waitForSearchCondition(page, search.id, (payload) => {
return payload.status === 'completed' && payload.completionCode === 'NO_FACES_FOUND';
}, SEARCH_COMPLETION_TIMEOUT_MS);
await expect(page.locator('.faceai-feedback')).toContainText('Nessun volto rilevato nella foto caricata');
await page.waitForTimeout(2000);
await expect(page).toHaveURL(FACEAI_HOME_URL_RE);
await verifySearchLogs(search.id, {
expectedMatchCount: 0,
expectedSelfieName: 'DSC_1994.JPG'
});
await page.getByRole('button', { name: 'Torna alla pagina gara' }).click();
await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('202'));
});
test('opens the file chooser when the user clicks the upload button', async ({ page }) => {
await launchFromSimulator(page, {
raceId: '202',
raceSlug: 'mezza-di-pisa',
raceName: 'Mezza di Pisa',
raceFolder: 'PISA'
});
const fileChooserPromise = page.waitForEvent('filechooser', { timeout: FILE_CHOOSER_TIMEOUT_MS });
await page.getByRole('button', { name: 'Scegli immagine' }).click();
const fileChooser = await fileChooserPromise;
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();
});
test('lets the user retry with a valid photo after a no-face upload and then returns to the filtered legacy result', async ({ page }) => {
test.slow();
await launchFromSimulator(page, {
raceId: '202',
raceSlug: 'mezza-di-pisa',
raceName: 'Mezza di Pisa',
raceFolder: 'PISA'
});
const noFaceSearch = await startSearch(page, 'DSC_1994.JPG');
await waitForSearchCondition(page, noFaceSearch.id, (payload) => {
return payload.status === 'completed' && payload.completionCode === 'NO_FACES_FOUND';
}, SEARCH_COMPLETION_TIMEOUT_MS);
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, SELFIE_NAME);
await waitForLegacyResult(page, '202', EXPECTED_MATCH_COUNT);
await verifySearchLogs(noFaceSearch.id, {
expectedMatchCount: 0,
expectedSelfieName: 'DSC_1994.JPG'
});
await verifySearchLogs(retrySearch.id, {
expectedMatchCount: EXPECTED_MATCH_COUNT,
expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop()
});
});
test('redirects direct-entry users without FaceAI session data back to the legacy site', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto(`${FACEAI_BASE_URL}/`, { waitUntil: 'domcontentloaded' });
await page.waitForURL(LEGACY_HOME_URL_RE, { timeout: SHORT_UI_TIMEOUT_MS });
} finally {
await context.close();
}
});
test('allows two users to process different photos at the same time', async ({ browser }) => {
test.slow();
test.setTimeout(LONG_TEST_TIMEOUT_MS);
const contexts = [await browser.newContext(), await browser.newContext()];
const pages = await Promise.all(contexts.map((context) => context.newPage()));
try {
await Promise.all([
enterViaHandoff(pages[0], { userId: 'concurrency-user-1' }),
enterViaHandoff(pages[1], { userId: 'concurrency-user-2' })
]);
const [searchOne, searchTwo] = await Promise.all([
startSearch(pages[0], SELFIE_NAME),
startSearch(pages[1], 'DSC_1987.JPG')
]);
await Promise.all([
waitForLegacyResult(pages[0], '202'),
waitForLegacyResult(pages[1], '202')
]);
await Promise.all([
verifySearchLogs(searchOne.id, { expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }),
verifySearchLogs(searchTwo.id, { expectedSelfieName: 'DSC_1987.JPG' })
]);
} finally {
await closeContexts(contexts);
}
});
test('queues the third user until a worker is free and then completes all three searches normally', async ({ browser }) => {
test.slow();
test.setTimeout(LONG_TEST_TIMEOUT_MS);
const contexts = [await browser.newContext(), await browser.newContext(), await browser.newContext()];
const pages = await Promise.all(contexts.map((context) => context.newPage()));
try {
await Promise.all([
enterViaHandoff(pages[0], { userId: 'queue-user-1' }),
enterViaHandoff(pages[1], { userId: 'queue-user-2' }),
enterViaHandoff(pages[2], { userId: 'queue-user-3' })
]);
const [searchOne, searchTwo, searchThree] = await Promise.all([
startSearch(pages[0], SELFIE_NAME),
startSearch(pages[1], 'DSC_1987.JPG'),
startSearch(pages[2], 'DSC_2058.JPG')
]);
const searchSessions = [
{ page: pages[0], searchId: searchOne.id },
{ page: pages[1], searchId: searchTwo.id },
{ page: pages[2], searchId: searchThree.id }
];
let queuedSearch = null;
const deadline = Date.now() + 30 * 1000;
while (Date.now() < deadline && !queuedSearch) {
const statuses = await Promise.all(searchSessions.map(async (session) => {
const payload = await fetchSearchStatus(session.page, session.searchId);
return {
...session,
search: payload.body
};
}));
const processingCount = statuses.filter((item) => item.search?.status === 'processing').length;
queuedSearch = processingCount >= 2
? statuses.find((item) => item.search?.status === 'queued') || null
: null;
if (!queuedSearch) {
await pages[0].waitForTimeout(250);
}
}
expect(queuedSearch, 'one search should remain queued while two worker slots are busy').toBeTruthy();
await waitForSearchCondition(queuedSearch.page, queuedSearch.searchId, (payload) => {
return payload.status === 'processing' || payload.status === 'completed';
}, SEARCH_COMPLETION_TIMEOUT_MS);
await Promise.all(pages.map((page) => waitForLegacyResult(page, '202')));
await Promise.all([
verifySearchLogs(searchOne.id, { expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }),
verifySearchLogs(searchTwo.id, { expectedSelfieName: 'DSC_1987.JPG' }),
verifySearchLogs(searchThree.id, { expectedSelfieName: 'DSC_2058.JPG' })
]);
} finally {
await closeContexts(contexts);
}
});