Initial commit: TravelHub multi-tenant flight booking platform
Angular + Go + PostgreSQL sample with strict per-tenant isolation. - Backend: Go API (basic auth, tenant-scoped repository layer, pgx pool) - Frontend: Angular 22 SPA (search, checkout, bookings, dashboard) - Database: schema, seed data and demo bookings migrations - Tests: 311 across Go (integration), Angular (unit) and Playwright (e2e), plus a generated evidence report with PDF export - Security: whitebox VAPT report under security/ Secrets (.env, tokens, DB credentials) are git-ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { expect, test } from '../fixtures/test';
|
||||
import { GLOBETROTTER_AGENT, SKYLINE_ADMIN, SKYLINE_AGENT } from '../fixtures/accounts';
|
||||
|
||||
test.describe('Sign in', () => {
|
||||
test('offers every active agency in the picker', async ({ login }) => {
|
||||
await login.goto();
|
||||
|
||||
// One placeholder option plus the seeded agencies.
|
||||
await expect(login.tenantSelect.locator('option')).toHaveCount(3);
|
||||
await expect(login.tenantSelect.locator('option')).toContainText([
|
||||
'Select an agency',
|
||||
'Globetrotter Tours',
|
||||
'Skyline Travel',
|
||||
]);
|
||||
});
|
||||
|
||||
test('a demo shortcut fills the whole form', async ({ login }) => {
|
||||
await login.goto();
|
||||
await login.useDemoChip(SKYLINE_AGENT);
|
||||
|
||||
await expect(login.tenantSelect).toHaveValue('skyline');
|
||||
await expect(login.email).toHaveValue(SKYLINE_AGENT.email);
|
||||
await expect(login.password).toHaveValue(SKYLINE_AGENT.password);
|
||||
});
|
||||
|
||||
test('signs an agent into their own agency', async ({ login, shell, page, evidence }) => {
|
||||
await login.goto();
|
||||
await evidence.step('Sign-in screen with the agency picker', {
|
||||
agenciesOffered: (await login.tenantSelect.locator('option').count()) - 1,
|
||||
demoShortcuts: await login.demoChips.count(),
|
||||
});
|
||||
|
||||
await login.signIn(SKYLINE_AGENT);
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
await expect(shell.agencyName).toHaveText(SKYLINE_AGENT.tenantName);
|
||||
await expect(shell.accountName).toHaveText(SKYLINE_AGENT.fullName);
|
||||
await expect(shell.accountMeta).toContainText(SKYLINE_AGENT.email);
|
||||
|
||||
await evidence.step('Signed in to the agency console', {
|
||||
agency: SKYLINE_AGENT.tenantName,
|
||||
user: SKYLINE_AGENT.fullName,
|
||||
email: SKYLINE_AGENT.email,
|
||||
role: SKYLINE_AGENT.role,
|
||||
landedOn: '/dashboard',
|
||||
});
|
||||
});
|
||||
|
||||
test('signs an admin in and shows the admin role', async ({ login, shell }) => {
|
||||
await login.signIn(SKYLINE_ADMIN);
|
||||
|
||||
await expect(shell.accountName).toHaveText(SKYLINE_ADMIN.fullName);
|
||||
await expect(shell.accountMeta).toContainText('admin');
|
||||
});
|
||||
|
||||
test('rejects a wrong password', async ({ login }) => {
|
||||
await login.goto();
|
||||
|
||||
const message = await login.signInExpectingFailure(SKYLINE_AGENT, 'definitely-wrong');
|
||||
|
||||
expect(message).toContain('not valid for this agency');
|
||||
await expect(login.submit).toBeVisible();
|
||||
});
|
||||
|
||||
// The central multi-tenant rule, exercised through the UI.
|
||||
test('rejects valid credentials aimed at the wrong agency', async ({ login }) => {
|
||||
await login.goto();
|
||||
await login.tenantSelect.selectOption(GLOBETROTTER_AGENT.tenant);
|
||||
await login.email.fill(SKYLINE_AGENT.email);
|
||||
await login.password.fill(SKYLINE_AGENT.password);
|
||||
await login.submit.click();
|
||||
|
||||
await expect(login.error).toContainText('not valid for this agency');
|
||||
await expect(login.submit).toBeVisible();
|
||||
});
|
||||
|
||||
test('will not submit without an agency', async ({ login, page }) => {
|
||||
await login.goto();
|
||||
await login.email.fill(SKYLINE_AGENT.email);
|
||||
await login.password.fill(SKYLINE_AGENT.password);
|
||||
await login.submit.click();
|
||||
|
||||
await expect(page.locator('.field .error')).toContainText('choose an agency');
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
|
||||
test('rejects a malformed email before contacting the server', async ({ login, page }) => {
|
||||
await login.goto();
|
||||
await login.tenantSelect.selectOption(SKYLINE_AGENT.tenant);
|
||||
await login.email.fill('not-an-email');
|
||||
await login.password.fill('whatever');
|
||||
await login.submit.click();
|
||||
|
||||
await expect(page.locator('.field .error')).toContainText('valid email');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Session', () => {
|
||||
test('survives a page reload', async ({ login, shell, page }) => {
|
||||
await login.signIn(SKYLINE_AGENT);
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
await expect(shell.agencyName).toHaveText(SKYLINE_AGENT.tenantName);
|
||||
});
|
||||
|
||||
test('sends an anonymous visitor to sign-in and back again', async ({ login, page }) => {
|
||||
await page.goto('/bookings');
|
||||
|
||||
// The guard should bounce to the sign-in screen and remember the target.
|
||||
await expect(page).toHaveURL(/\/login\?returnTo=%2Fbookings/);
|
||||
|
||||
await login.useDemoChip(SKYLINE_AGENT);
|
||||
await login.submit.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/bookings$/);
|
||||
});
|
||||
|
||||
test('signing out clears the session for good', async ({ login, shell, page }) => {
|
||||
await login.signIn(SKYLINE_AGENT);
|
||||
await shell.signOutAndWait();
|
||||
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
|
||||
// Going back must not resurrect the console.
|
||||
await page.goto('/dashboard');
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
|
||||
test('an already signed-in user is moved off the sign-in screen', async ({ login, page }) => {
|
||||
await login.signIn(SKYLINE_AGENT);
|
||||
|
||||
await page.goto('/login');
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
});
|
||||
|
||||
test('stale stored credentials are discarded', async ({ login, page }) => {
|
||||
await login.signIn(SKYLINE_AGENT);
|
||||
|
||||
// Corrupt the stored password the way a changed account would.
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem(
|
||||
'travelhub.credentials',
|
||||
JSON.stringify({ tenant: 'skyline', basic: btoa('agent@skyline.test:stale') }),
|
||||
);
|
||||
});
|
||||
await page.goto('/dashboard');
|
||||
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
const stored = await page.evaluate(() => localStorage.getItem('travelhub.credentials'));
|
||||
expect(stored).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user