/** * 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(); }); });