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,130 @@
|
||||
import { expect, test, uniqueSurname } from '../fixtures/test';
|
||||
import { SKYLINE_AGENT } from '../fixtures/accounts';
|
||||
|
||||
test.beforeEach(async ({ signInAs }) => {
|
||||
await signInAs(SKYLINE_AGENT);
|
||||
});
|
||||
|
||||
test.describe('Dashboard', () => {
|
||||
test('greets the agent and names the agency', async ({ dashboard }) => {
|
||||
await dashboard.settled();
|
||||
|
||||
await expect(dashboard.greeting).toContainText('Marcus');
|
||||
await expect(dashboard.greeting.locator('..')).toContainText('Skyline Travel');
|
||||
});
|
||||
|
||||
test('shows the four headline figures', async ({ dashboard, page }) => {
|
||||
await dashboard.settled();
|
||||
|
||||
await expect(dashboard.tiles).toHaveCount(4);
|
||||
await expect(page.locator('.tile-label')).toContainText([
|
||||
'Revenue',
|
||||
'Bookings',
|
||||
'Seats sold',
|
||||
'Flights in stock',
|
||||
]);
|
||||
expect(await dashboard.tileValue('Flights in stock')).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('lists recent bookings and best routes from the seed data', async ({ dashboard }) => {
|
||||
await dashboard.settled();
|
||||
|
||||
expect(await dashboard.recentRows.count()).toBeGreaterThan(0);
|
||||
expect(await dashboard.topRoutes.count()).toBeGreaterThan(0);
|
||||
await expect(dashboard.topRoutes.first()).toContainText(' to ');
|
||||
});
|
||||
|
||||
// Delta-based, so the test does not care what the database already held.
|
||||
test('a new sale moves the figures, and cancelling moves them back', async ({
|
||||
dashboard,
|
||||
shell,
|
||||
search,
|
||||
checkout,
|
||||
bookings,
|
||||
bookingDetail,
|
||||
page,
|
||||
evidence,
|
||||
}) => {
|
||||
await dashboard.settled();
|
||||
const revenueBefore = await dashboard.tileValue('Revenue');
|
||||
const seatsBefore = await dashboard.tileValue('Seats sold');
|
||||
const bookingsBefore = await dashboard.tileValue('Bookings');
|
||||
|
||||
await evidence.step('Dashboard before the sale', {
|
||||
agency: 'Skyline Travel',
|
||||
revenue: `$${revenueBefore.toLocaleString()}`,
|
||||
seatsSold: seatsBefore,
|
||||
bookings: bookingsBefore,
|
||||
});
|
||||
|
||||
await shell.goToSearch();
|
||||
const fare = await search.card(0).fare();
|
||||
await search.card(0).selectButton.click();
|
||||
await page.waitForURL(/\/book\//);
|
||||
await checkout.waitUntilReady();
|
||||
|
||||
const surname = uniqueSurname('DASH');
|
||||
await checkout.fillTravellers([
|
||||
{ firstName: 'Ana', lastName: surname },
|
||||
{ firstName: 'Bo', lastName: surname },
|
||||
]);
|
||||
await checkout.confirmBooking();
|
||||
const reference = await bookingDetail.bookingReference();
|
||||
|
||||
await shell.goToDashboard();
|
||||
await dashboard.settled();
|
||||
|
||||
// Revenue is rounded to whole units in the tile, so allow a unit of slack.
|
||||
expect(await dashboard.tileValue('Revenue')).toBeGreaterThanOrEqual(
|
||||
Math.floor(revenueBefore + fare * 2) - 1,
|
||||
);
|
||||
const revenueAfter = await dashboard.tileValue('Revenue');
|
||||
expect(await dashboard.tileValue('Seats sold')).toBe(seatsBefore + 2);
|
||||
expect(await dashboard.tileValue('Bookings')).toBe(bookingsBefore + 1);
|
||||
await expect(dashboard.recentRows.first()).toContainText(reference);
|
||||
|
||||
await evidence.step('Dashboard after the sale', {
|
||||
bookingReference: reference,
|
||||
revenueBefore: `$${revenueBefore.toLocaleString()}`,
|
||||
revenueAfter: `$${revenueAfter.toLocaleString()}`,
|
||||
farePerTraveller: `$${fare.toFixed(2)}`,
|
||||
seatsSold: `${seatsBefore} to ${seatsBefore + 2}`,
|
||||
bookings: `${bookingsBefore} to ${bookingsBefore + 1}`,
|
||||
});
|
||||
|
||||
// --- cancel and re-check ------------------------------------------------
|
||||
await shell.goToBookings();
|
||||
await bookings.open(reference);
|
||||
await bookingDetail.cancel(reference);
|
||||
|
||||
await shell.goToDashboard();
|
||||
await dashboard.settled();
|
||||
|
||||
expect(await dashboard.tileValue('Seats sold')).toBe(seatsBefore);
|
||||
// The booking still exists; it is just no longer confirmed.
|
||||
expect(await dashboard.tileValue('Bookings')).toBe(bookingsBefore + 1);
|
||||
|
||||
await evidence.step('Dashboard after cancelling', {
|
||||
bookingReference: reference,
|
||||
seatsSold: `back to ${seatsBefore}`,
|
||||
bookings: `${bookingsBefore + 1} (the cancelled booking is still counted)`,
|
||||
revenue: `$${(await dashboard.tileValue('Revenue')).toLocaleString()}`,
|
||||
});
|
||||
});
|
||||
|
||||
test('a recent booking row opens the booking', async ({ dashboard, page }) => {
|
||||
await dashboard.settled();
|
||||
|
||||
await dashboard.recentRows.first().click();
|
||||
|
||||
await expect(page).toHaveURL(/\/bookings\/[0-9a-f-]+/);
|
||||
});
|
||||
|
||||
test('the new booking shortcut goes to search', async ({ dashboard, page }) => {
|
||||
await dashboard.settled();
|
||||
|
||||
await page.getByRole('link', { name: '+ New booking' }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/search$/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user