Files
OSIT-AE-App-Svelte/tests/leads_auth.test.ts
Scott Idem f95243a9c7 fix(leads): disable sign-in submit until exhibit loads; add licensed-user auth tests
Prevents silent no-op when user clicks submit before lq__exhibit_obj is ready
(exhibit not yet written to Dexie). Button now shows 'Loading...' spinner while
the exhibit record is resolving, eliminating the two-tap workaround needed on
first page load.

Also adds 7 Playwright tests for licensed user sign-in (leads_licensed_signin.test.ts)
covering success path, wrong credentials, email/identity tagging on captured leads,
identity isolation between staff members, and returning-session bypass.

Helpers: attach_leads_routes/setup_leads_test_page now accept exhibit_overrides
(e.g. license_li_json) to inject licensed users into mocked API responses.
seed_leads_loc import added to leads_auth.test.ts multi-exhibit test.

Total leads test coverage: 29 tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 17:04:57 -04:00

304 lines
14 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { testing_event_id } from './_helpers/env';
import {
testing_exhibit_id,
testing_exhibit_id_b,
exhibit_staff_passcode,
setup_leads_test_page,
attach_leads_routes,
seed_ae_loc,
seed_leads_loc,
seed_events_loc,
minimal_exhibit,
} from './_helpers/leads_helpers';
const event_id = testing_event_id;
const exhibit_id = testing_exhibit_id;
const exhibit_url = `/events/${event_id}/leads/exhibit/${exhibit_id}`;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Locator for the sign-in form card. */
const signin_form = (page: Parameters<typeof expect>[0]) =>
(page as import('@playwright/test').Page).locator('.exhibit-signin');
/** Locator for the shared-passcode input field. */
const passcode_input = (page: Parameters<typeof expect>[0]) =>
(page as import('@playwright/test').Page).locator(
'input[placeholder="Enter shared code..."]'
);
/** Locator for the "Add Lead" / "Lead List" toggle button — only visible when signed in. */
const header_action_btn = (page: Parameters<typeof expect>[0]) =>
(page as import('@playwright/test').Page).locator(
'header button.preset-filled-primary'
);
// ---------------------------------------------------------------------------
// Test suite
// ---------------------------------------------------------------------------
test.describe('Leads — Auth Gate', () => {
// -----------------------------------------------------------------------
// 1. Unauthenticated user sees sign-in form
// -----------------------------------------------------------------------
test('unauthenticated user sees sign-in form, not the lead list', async ({ page }) => {
// No auth_kv → no exhibit in auth_exhibit_kv → is_signed_in = false
await setup_leads_test_page(page, event_id, exhibit_id);
await page.goto(exhibit_url);
// Sign-in form must appear within a reasonable time
await expect(signin_form(page)).toBeVisible({ timeout: 10_000 });
// Header action buttons (Add Lead / Lead List) must NOT be visible
await expect(header_action_btn(page)).not.toBeVisible();
});
// -----------------------------------------------------------------------
// 2. manager_access bypasses sign-in entirely
// -----------------------------------------------------------------------
test('manager_access bypasses sign-in — list tab shown directly', async ({ page }) => {
await setup_leads_test_page(page, event_id, exhibit_id, {
// manager_access=true → is_signed_in = true without any exhibit auth
access: {
allow_access: true,
authenticated_access: true,
trusted_access: true,
manager_access: true,
},
});
await page.goto(exhibit_url);
// Sign-in form must NOT appear
await expect(signin_form(page)).not.toBeVisible({ timeout: 10_000 });
// Header action button (Add Lead / Lead List) must be visible
await expect(header_action_btn(page)).toBeVisible({ timeout: 10_000 });
});
// -----------------------------------------------------------------------
// 3. Pre-authenticated user skips sign-in (already in auth_exhibit_kv)
// -----------------------------------------------------------------------
test('already-signed-in user sees lead list, not sign-in form', async ({ page }) => {
await setup_leads_test_page(page, event_id, exhibit_id, {
// Seed auth_exhibit_kv — simulates a returning user whose session survived
auth_kv: {
[exhibit_id]: { key: exhibit_staff_passcode, type: 'shared' },
},
});
await page.goto(exhibit_url);
await expect(signin_form(page)).not.toBeVisible({ timeout: 10_000 });
await expect(header_action_btn(page)).toBeVisible({ timeout: 10_000 });
});
// -----------------------------------------------------------------------
// 4. Shared passcode sign-in — success path
//
// trusted_access = true causes the sign-in component to auto-fill the
// passcode field once $lq__exhibit_obj loads into Dexie via the mocked API.
// Waiting for the input to have a value gives us a reliable timing signal
// (exhibit is in Dexie, $lq__exhibit_obj is live, sign-in will process).
// -----------------------------------------------------------------------
test('shared passcode sign-in — correct passcode signs in successfully', async ({ page }) => {
await setup_leads_test_page(page, event_id, exhibit_id, {
// trusted_access triggers auto-fill once exhibit loads; no manager bypass
access: { allow_access: true, trusted_access: true },
});
await page.goto(exhibit_url);
// Sign-in form visible (trusted_access does NOT bypass the auth gate)
await expect(signin_form(page)).toBeVisible({ timeout: 10_000 });
// Wait for exhibit to load into Dexie — auto-fill kicks in for trusted users.
// Input having the correct value means $lq__exhibit_obj is live.
await expect(passcode_input(page)).toHaveValue(exhibit_staff_passcode, {
timeout: 10_000,
});
// Submit
await page.locator('button[type="submit"]').click();
// After 800 ms UX delay + Svelte reactivity, form should disappear
await expect(signin_form(page)).not.toBeVisible({ timeout: 5_000 });
await expect(header_action_btn(page)).toBeVisible({ timeout: 5_000 });
});
// -----------------------------------------------------------------------
// 5. Wrong passcode shows error and keeps the form open
// -----------------------------------------------------------------------
test('wrong passcode shows error message, form stays visible', async ({ page }) => {
await setup_leads_test_page(page, event_id, exhibit_id, {
access: { allow_access: true, trusted_access: true },
});
await page.goto(exhibit_url);
// Wait for exhibit to be ready (auto-fill is the readiness signal)
await expect(passcode_input(page)).toHaveValue(exhibit_staff_passcode, {
timeout: 10_000,
});
// Override with an incorrect passcode
await passcode_input(page).fill('WRONGCODE');
await page.locator('button[type="submit"]').click();
// Error text must appear
await expect(
page.locator('text=Invalid shared passcode')
).toBeVisible({ timeout: 5_000 });
// Sign-in form must still be visible
await expect(signin_form(page)).toBeVisible({ timeout: 3_000 });
// Header action button must NOT appear (not signed in)
await expect(header_action_btn(page)).not.toBeVisible();
});
// -----------------------------------------------------------------------
// 6. Simultaneous multi-exhibit auth
//
// auth_exhibit_kv is keyed by exhibit_id, so Booth A and Booth B auth are
// completely independent entries. A user can be signed into both at the
// same time — this mirrors real-world use where staff manages adjacent booths.
//
// We pre-seed both exhibits in auth_exhibit_kv and verify that navigating
// between them never triggers the sign-in form.
// -----------------------------------------------------------------------
test('multi-exhibit: both booths authorized simultaneously, neither shows sign-in', async ({ page }) => {
const exhibit_id_b = testing_exhibit_id_b;
const exhibit_url_b = `/events/${event_id}/leads/exhibit/${exhibit_id_b}`;
page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`));
// Route mock covering both exhibit IDs
await page.route('**/v3/**', async (route) => {
const req = route.request();
const url = req.url();
const method = req.method();
if (url.includes('site_domain/search')) {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: [
{
id: '_6jcTbnJk-o', site_id: '92vkYC4fVEl',
site_domain_id: '_6jcTbnJk-o', account_id: '_XY7DXtc9MY',
account_id_random: '_XY7DXtc9MY', account_code: 'OSIT_DEMO',
account_name: 'One Sky IT Demo', fqdn: 'demo.localhost:5173',
enable: '1', cfg_json: {}, style_href: '', header_image_path: '',
}
] }),
});
}
if (url.includes(`/v3/crud/event_exhibit/${exhibit_id}`) && method === 'GET') {
return route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify(minimal_exhibit(exhibit_id)) });
}
if (url.includes(`/v3/crud/event_exhibit/${exhibit_id_b}`) && method === 'GET') {
return route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify(minimal_exhibit(exhibit_id_b, {
name: 'Test Booth — Beta Corp', code: 'BETA', staff_passcode: 'BOOTHB99',
})) });
}
if (url.includes(`/v3/crud/event/${event_id}`) && method === 'GET') {
return route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify({ data: { id: event_id, event_id,
name: 'Test Event', mod_exhibits_json: { leads_require_payment: false } } }) });
}
return route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify({ data: [] }) });
});
// Seed ae_loc (regular shared-passcode user — no manager bypass)
await seed_ae_loc(page, { allow_access: true });
// Pre-seed BOTH exhibits as authorized — independent KV entries.
// Must seed ae_leads_loc (new PersistedState store) in addition to ae_events_loc.
await seed_leads_loc(page, {
auth_exhibit_kv: {
[exhibit_id]: { key: exhibit_staff_passcode, type: 'shared', updated_on: new Date().toISOString() },
[exhibit_id_b]: { key: 'BOOTHB99', type: 'shared', updated_on: new Date().toISOString() },
},
});
await seed_events_loc(page, {
[exhibit_id]: { key: exhibit_staff_passcode, type: 'shared' },
[exhibit_id_b]: { key: 'BOOTHB99', type: 'shared' },
});
// Booth A → no sign-in form
await page.goto(exhibit_url);
await expect(signin_form(page)).not.toBeVisible({ timeout: 10_000 });
await expect(header_action_btn(page)).toBeVisible({ timeout: 10_000 });
// Booth B → no sign-in form (independent KV entry, unaffected by Booth A)
await page.goto(exhibit_url_b);
await expect(signin_form(page)).not.toBeVisible({ timeout: 10_000 });
await expect(header_action_btn(page)).toBeVisible({ timeout: 10_000 });
// Back to Booth A → still authorized
await page.goto(exhibit_url);
await expect(signin_form(page)).not.toBeVisible({ timeout: 10_000 });
await expect(header_action_btn(page)).toBeVisible({ timeout: 10_000 });
});
// -----------------------------------------------------------------------
// 7. Unauthorized exhibit shows sign-in even when another booth is auth'd
//
// Verifies that auth_exhibit_kv[booth_A] does not bleed into booth_B.
// -----------------------------------------------------------------------
test('multi-exhibit: authorized for Booth A only → Booth B still shows sign-in', async ({ page }) => {
const exhibit_id_b = testing_exhibit_id_b;
const exhibit_url_b = `/events/${event_id}/leads/exhibit/${exhibit_id_b}`;
page.on('pageerror', (err) => console.error(`BROWSER ERROR: ${err.message}`));
await page.route('**/v3/**', async (route) => {
const url = route.request().url();
const method = route.request().method();
if (url.includes('site_domain/search')) {
return route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify({ data: [
{ id: '_6jcTbnJk-o', site_id: '92vkYC4fVEl',
site_domain_id: '_6jcTbnJk-o', account_id: '_XY7DXtc9MY',
account_id_random: '_XY7DXtc9MY', account_code: 'OSIT_DEMO',
account_name: 'One Sky IT Demo', fqdn: 'demo.localhost:5173',
enable: '1', cfg_json: {}, style_href: '', header_image_path: '' }
] }) });
}
if (url.includes(`/v3/crud/event_exhibit/${exhibit_id}`) && method === 'GET') {
return route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify(minimal_exhibit(exhibit_id)) });
}
if (url.includes(`/v3/crud/event_exhibit/${exhibit_id_b}`) && method === 'GET') {
return route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify(minimal_exhibit(exhibit_id_b, { name: 'Beta Corp', code: 'BETA' })) });
}
return route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify({ data: [] }) });
});
await seed_ae_loc(page, { allow_access: true });
// Only Booth A authorized — Booth B not in auth_kv
await seed_events_loc(page, {
[exhibit_id]: { key: exhibit_staff_passcode, type: 'shared' },
});
// Booth A → no sign-in form
await page.goto(exhibit_url);
await expect(signin_form(page)).not.toBeVisible({ timeout: 10_000 });
// Booth B → must show sign-in form (auth does not cross exhibit boundaries)
await page.goto(exhibit_url_b);
await expect(signin_form(page)).toBeVisible({ timeout: 10_000 });
await expect(header_action_btn(page)).not.toBeVisible();
});
});