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:
nilesh.p
2026-09-13 11:38:01 +05:30
co-authored by Claude Opus 4.8
commit 4d3eb18a6c
129 changed files with 25424 additions and 0 deletions
+153
View File
@@ -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();
});
});