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,153 @@
|
||||
import { expect, test } from '../fixtures/test';
|
||||
import { SKYLINE_AGENT } from '../fixtures/accounts';
|
||||
|
||||
test.beforeEach(async ({ signInAs, shell, search }) => {
|
||||
await signInAs(SKYLINE_AGENT);
|
||||
await shell.goToSearch();
|
||||
await search.settled();
|
||||
});
|
||||
|
||||
test.describe('Flight search', () => {
|
||||
test('opens with the agency inventory already listed', async ({ search }) => {
|
||||
await expect(search.results.first()).toBeVisible();
|
||||
await expect(search.resultSummary).toContainText('flights');
|
||||
expect(await search.totalResults()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('offers only airports this agency flies', async ({ search }) => {
|
||||
const codes = await search.origin.locator('option').allInnerTexts();
|
||||
|
||||
expect(codes.join(' ')).toContain('JFK');
|
||||
expect(codes.join(' ')).toContain('LHR');
|
||||
expect(codes[0]).toContain('Any airport');
|
||||
});
|
||||
|
||||
test('narrows results to a route', async ({ search, evidence }) => {
|
||||
const before = await search.totalResults();
|
||||
await evidence.step('Full inventory before filtering', { flightsVisible: before });
|
||||
|
||||
await search.searchRoute('JFK', 'LHR');
|
||||
const after = await search.totalResults();
|
||||
|
||||
await evidence.step('Filtered to JFK to LHR', {
|
||||
route: 'JFK to LHR',
|
||||
flightsBefore: before,
|
||||
flightsAfter: after,
|
||||
cheapestFare: `$${(await search.card(0).fare()).toFixed(2)}`,
|
||||
firstFlight: await search.card(0).flightNumber(),
|
||||
});
|
||||
|
||||
expect(after).toBeGreaterThan(0);
|
||||
expect(after).toBeLessThan(before);
|
||||
|
||||
// Every visible card should be on the requested route.
|
||||
const cards = await search.results.all();
|
||||
for (const card of cards) {
|
||||
await expect(card.locator('.iata').first()).toHaveText('JFK');
|
||||
await expect(card.locator('.iata').last()).toHaveText('LHR');
|
||||
}
|
||||
});
|
||||
|
||||
test('a route with no inventory shows an empty state', async ({ search }) => {
|
||||
// Nothing in the seed data flies Heathrow to Heathrow.
|
||||
await search.searchRoute('LHR', 'LHR');
|
||||
|
||||
await expect(search.emptyState).toBeVisible();
|
||||
await expect(search.emptyState).toContainText('Nothing on this route yet');
|
||||
await expect(search.results).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('resets back to the full list', async ({ search }) => {
|
||||
await search.searchRoute('JFK', 'LHR');
|
||||
const narrowed = await search.totalResults();
|
||||
|
||||
await search.resetButton.click();
|
||||
await search.settled();
|
||||
|
||||
expect(await search.totalResults()).toBeGreaterThan(narrowed);
|
||||
await expect(search.origin).toHaveValue('');
|
||||
});
|
||||
|
||||
test('sorts by lowest fare', async ({ search, evidence }) => {
|
||||
await search.searchRoute('JFK', 'LHR');
|
||||
await search.sortBy('price');
|
||||
|
||||
const fares: number[] = [];
|
||||
for (let i = 0; i < (await search.results.count()); i++) {
|
||||
fares.push(await search.card(i).fare());
|
||||
}
|
||||
|
||||
expect(fares.length).toBeGreaterThan(1);
|
||||
expect([...fares]).toEqual([...fares].sort((a, b) => a - b));
|
||||
|
||||
await evidence.step('Results ordered by lowest fare', {
|
||||
sortedBy: 'price',
|
||||
faresOnPage: fares.map((f) => `$${f.toFixed(2)}`).join(', '),
|
||||
lowest: `$${fares[0].toFixed(2)}`,
|
||||
highest: `$${fares[fares.length - 1].toFixed(2)}`,
|
||||
ascending: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('sorts by shortest trip', async ({ search }) => {
|
||||
await search.sortBy('duration');
|
||||
|
||||
const durations: number[] = [];
|
||||
for (let i = 0; i < Math.min(5, await search.results.count()); i++) {
|
||||
durations.push(await search.card(i).durationMinutes());
|
||||
}
|
||||
|
||||
expect([...durations]).toEqual([...durations].sort((a, b) => a - b));
|
||||
});
|
||||
|
||||
test('filters by cabin', async ({ search }) => {
|
||||
await search.cabin.selectOption('business');
|
||||
await search.searchButton.click();
|
||||
await search.settled();
|
||||
|
||||
const count = await search.results.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
for (let i = 0; i < count; i++) {
|
||||
await expect(search.results.nth(i).locator('.fare-tags .badge').first()).toHaveText(
|
||||
/business/i,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('hides flights without room for the whole party', async ({ search }) => {
|
||||
await search.setTravellers(9);
|
||||
|
||||
const count = await search.results.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
expect(await search.card(i).seatsLeft()).toBeGreaterThanOrEqual(9);
|
||||
}
|
||||
});
|
||||
|
||||
test('pages through results without repeating them', async ({ search }) => {
|
||||
await expect(search.prevPage).toBeDisabled();
|
||||
|
||||
const firstPage = await search.results.first().innerText();
|
||||
|
||||
await search.nextPage.click();
|
||||
await search.settled();
|
||||
|
||||
await expect(search.prevPage).toBeEnabled();
|
||||
await expect(search.resultSummary).toContainText('Showing 11');
|
||||
expect(await search.results.first().innerText()).not.toBe(firstPage);
|
||||
|
||||
await search.prevPage.click();
|
||||
await search.settled();
|
||||
await expect(search.resultSummary).toContainText('Showing 1–10');
|
||||
});
|
||||
|
||||
test('a result card carries everything needed to choose', async ({ search }) => {
|
||||
const card = search.card(0);
|
||||
|
||||
await expect(card.root.locator('.carrier-name')).not.toBeEmpty();
|
||||
await expect(card.root.locator('.leg-duration')).toContainText(/\d+h|\d+m/);
|
||||
await expect(card.price).toContainText('$');
|
||||
await expect(card.seatsBadge).toContainText('left');
|
||||
await expect(card.root.locator('.departure-date')).not.toBeEmpty();
|
||||
await expect(card.selectButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user