Files
TS_Proto/e2e/tests/tenant-isolation.spec.ts
T
nilesh.pandClaude Opus 4.8 4d3eb18a6c 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>
2026-09-13 11:38:01 +05:30

188 lines
6.1 KiB
TypeScript

/**
* The point of the product: two agencies share one deployment and one database,
* and neither can see the other's inventory, customers or revenue.
*
* These assertions are made through the UI, the way a customer would notice a
* leak, rather than against the API.
*/
import { expect, test, uniqueSurname } from '../fixtures/test';
import { GLOBETROTTER_AGENT, SKYLINE_AGENT } from '../fixtures/accounts';
test.describe('Tenant isolation', () => {
test('the same route is priced per agency', async ({
signInAs,
shell,
search,
login,
evidence,
}) => {
await signInAs(SKYLINE_AGENT);
await shell.goToSearch();
await search.searchRoute('JFK', 'LHR');
await search.sortBy('price');
const skylineFare = await search.card(0).fare();
await evidence.step('Skyline Travel: cheapest JFK to LHR', {
agency: 'Skyline Travel',
route: 'JFK to LHR',
cheapestFare: `$${skylineFare.toFixed(2)}`,
resultCount: await search.totalResults(),
});
await shell.signOutAndWait();
await login.signIn(GLOBETROTTER_AGENT);
await shell.goToSearch();
await search.searchRoute('JFK', 'LHR');
await search.sortBy('price');
const globetrotterFare = await search.card(0).fare();
await evidence.step('Globetrotter Tours: same route, its own fare', {
agency: 'Globetrotter Tours',
route: 'JFK to LHR',
cheapestFare: `$${globetrotterFare.toFixed(2)}`,
resultCount: await search.totalResults(),
});
await evidence.note('Per-tenant pricing confirmed', {
skyline: `$${skylineFare.toFixed(2)}`,
globetrotter: `$${globetrotterFare.toFixed(2)}`,
difference: `$${(globetrotterFare - skylineFare).toFixed(2)}`,
markup: `${(((globetrotterFare - skylineFare) / skylineFare) * 100).toFixed(1)}%`,
});
expect(skylineFare).toBeGreaterThan(0);
expect(globetrotterFare).toBeGreaterThan(0);
// Globetrotter sells the same seats at a negotiated markup.
expect(globetrotterFare).not.toBeCloseTo(skylineFare, 2);
expect(globetrotterFare).toBeGreaterThan(skylineFare);
});
test('the sidebar always names the agency you are working in', async ({
signInAs,
shell,
login,
}) => {
await signInAs(SKYLINE_AGENT);
await expect(shell.agencyName).toHaveText('Skyline Travel');
await expect(shell.agencySlug).toHaveText('skyline');
await shell.signOutAndWait();
await login.signIn(GLOBETROTTER_AGENT);
await expect(shell.agencyName).toHaveText('Globetrotter Tours');
await expect(shell.agencySlug).toHaveText('globetrotter');
});
test('one agency never sees another agency booking', async ({
signInAs,
login,
shell,
search,
checkout,
bookings,
bookingDetail,
page,
evidence,
}) => {
const surname = uniqueSurname('ISO');
// --- Skyline sells a ticket --------------------------------------------
await signInAs(SKYLINE_AGENT);
await shell.goToSearch();
await search.card(0).selectButton.click();
await page.waitForURL(/\/book\//);
await checkout.waitUntilReady();
await checkout.fillTravellers([{ firstName: 'Iris', lastName: surname }]);
await checkout.confirmBooking();
const reference = await bookingDetail.bookingReference();
const bookingUrl = page.url();
await evidence.step('Skyline sells a ticket', {
agency: 'Skyline Travel',
bookingReference: reference,
passenger: `Iris ${surname}`,
bookingUrl,
});
await shell.signOutAndWait();
// --- Globetrotter must not find it -------------------------------------
await login.signIn(GLOBETROTTER_AGENT);
await shell.goToBookings();
await expect(bookings.rowFor(reference)).toHaveCount(0);
expect(await page.content()).not.toContain(surname);
await evidence.step('Globetrotter cannot see it in their bookings', {
signedInAs: 'Globetrotter Tours',
searchedFor: reference,
rowsFound: 0,
passengerNameOnPage: false,
});
// Not even by deep-linking straight at the booking's own URL.
await page.goto(bookingUrl);
await expect(page.getByText('Booking not found')).toBeVisible();
expect(await page.content()).not.toContain(surname);
await evidence.step('Deep link to the booking URL is refused', {
signedInAs: 'Globetrotter Tours',
requestedUrl: bookingUrl,
result: 'Booking not found',
dataLeaked: false,
});
// --- clean up as the owning agency -------------------------------------
await shell.signOutAndWait();
await login.signIn(SKYLINE_AGENT);
await shell.goToBookings();
await bookings.open(reference);
await bookingDetail.cancel(reference);
});
test('revenue figures are per agency', async ({ signInAs, login, shell, dashboard }) => {
await signInAs(SKYLINE_AGENT);
await dashboard.settled();
const skylineRevenue = await dashboard.tileValue('Revenue');
const skylineBookings = await dashboard.tileValue('Bookings');
await shell.signOutAndWait();
await login.signIn(GLOBETROTTER_AGENT);
await dashboard.settled();
const globetrotterRevenue = await dashboard.tileValue('Revenue');
const globetrotterBookings = await dashboard.tileValue('Bookings');
expect(skylineRevenue).toBeGreaterThan(0);
expect(globetrotterRevenue).toBeGreaterThan(0);
// Separate books: the seeded agencies sold different volumes.
expect(skylineRevenue).not.toBe(globetrotterRevenue);
expect(skylineBookings).not.toBe(globetrotterBookings);
});
test('a deep link to another agency flight is refused', async ({
signInAs,
login,
shell,
search,
page,
}) => {
// Capture a flight id that belongs to Globetrotter.
await signInAs(GLOBETROTTER_AGENT);
await shell.goToSearch();
await search.card(0).selectButton.click();
await page.waitForURL(/\/book\//);
const globetrotterBookUrl = page.url();
await page.goto('/dashboard');
await shell.signOutAndWait();
// Skyline holds a valid id, and still cannot reach it.
await login.signIn(SKYLINE_AGENT);
await page.goto(globetrotterBookUrl);
await expect(page.getByText('Flight not available')).toBeVisible();
});
});