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
+498
View File
@@ -0,0 +1,498 @@
# TravelHub test catalogue
> Generated by `npm run docs:tests` from the test sources.
> Do not edit by hand — regenerate instead.
**277 test cases** across 25 files.
| Suite | Files | Cases | Command |
| ----- | ----: | ----: | ------- |
| [Go](#go-backend) | 6 | 98 | `npm run test:go` |
| [Angular](#angular-frontend) | 12 | 114 | `npm run test:web` |
| [Playwright](#playwright-end-to-end) | 7 | 65 | `npm run test:e2e` |
| **Total** | **25** | **277** | `npm test` |
### Why the runners report different numbers
A *case* below is one thing being tested. The runners count differently, so
both figures are given rather than one being quietly preferred:
- **Go** reports 74 test functions plus each table-driven subtest
separately, so `go test -v` prints more `PASS` lines than there are cases
here. A few tables build their subtest names at runtime and cannot be
listed statically; those appear as a single case.
- **Playwright** re-runs `auth` and `responsive` (17 tests) under a second,
mobile viewport, so a full run executes 82 tests from 65 declared.
- **Angular** counts exactly as listed.
Running a single file:
```bash
cd backend && go test ./internal/repository/ -run TestCreateBookingSellsSeats -v
cd frontend && npx ng test --no-watch # all specs
cd e2e && npx playwright test tests/booking.spec.ts
```
---
## Go (backend)
74 test functions covering 98 cases. These run against a real
PostgreSQL, with a fresh tenant created and dropped per test.
### `backend/internal/config/config_test.go`
7 tests, 7 cases.
- **Load defaults** — `TestLoadDefaults`
- **Load requires a password** — `TestLoadRequiresAPassword`
- **Load escapes awkward passwords** — `TestLoadEscapesAwkwardPasswords`
- **Database url wins over parts** — `TestDatabaseURLWinsOverParts`
- **Load reads dot env** — `TestLoadReadsDotEnv`
- **Environment beats dot env** — `TestEnvironmentBeatsDotEnv`
- **Load finds dot env in a parent directory** — `TestLoadFindsDotEnvInAParentDirectory`
### `backend/internal/handlers/handlers_test.go`
22 tests, 33 cases.
- **Health needs no credentials** — `TestHealthNeedsNoCredentials`
- **Tenant list is public but thin** — `TestTenantListIsPublicButThin`
- **Protected routes reject anonymous** — `TestProtectedRoutesRejectAnonymous`
- **Session describes the caller** — `TestSessionDescribesTheCaller`
- **Session never returns the password hash** — `TestSessionNeverReturnsThePasswordHash`
- **Search flights returns a page** — `TestSearchFlightsReturnsAPage`
- **Search flights validates input** — `TestSearchFlightsValidatesInput`
- bad date
- bad cabin
- good cabin
- absurd limit is clamped
- negative offset falls back
- non-numeric passengers falls back
- **Get flight rejects a malformed id** — `TestGetFlightRejectsAMalformedID`
- **Get flight across tenants is four oh four** — `TestGetFlightAcrossTenantsIsFourOhFour`
- **Create booking returns created** — `TestCreateBookingReturnsCreated`
- **Create booking validation** — `TestCreateBookingValidation`
- missing flight id
- missing contact email
- malformed contact email
- no passengers
- blank passenger name
- more than nine passengers
- **Create booking rejects unknown fields** — `TestCreateBookingRejectsUnknownFields`
- **Create booking rejects malformed json** — `TestCreateBookingRejectsMalformedJSON`
- **Create booking over capacity is conflict** — `TestCreateBookingOverCapacityIsConflict`
- **Create booking on another tenants flight is four oh four** — `TestCreateBookingOnAnotherTenantsFlightIsFourOhFour`
- **Booking lifecycle over http** — `TestBookingLifecycleOverHTTP`
- **List bookings rejects an unknown status** — `TestListBookingsRejectsAnUnknownStatus`
- **Bookings are invisible to other tenants** — `TestBookingsAreInvisibleToOtherTenants`
- **Dashboard summarises the tenant** — `TestDashboardSummarisesTheTenant`
- **Reference lists are scoped to the tenant network** — `TestReferenceListsAreScopedToTheTenantNetwork`
- **Unknown route and wrong method** — `TestUnknownRouteAndWrongMethod`
- unknown path
- wrong method
- **Every json response carries its content type** — `TestEveryJSONResponseCarriesItsContentType`
### `backend/internal/middleware/middleware_test.go`
10 tests, 20 cases.
- **Basic auth accepts valid credentials** — `TestBasicAuthAcceptsValidCredentials`
- **Basic auth is case insensitive on email** — `TestBasicAuthIsCaseInsensitiveOnEmail`
- **Basic auth rejections** — `TestBasicAuthRejections`
- no credentials at all
- wrong password
- unknown user
- unknown tenant
- valid credentials against another tenant
- missing tenant header
- malformed authorization header
- **Basic auth refuses suspended tenant** — `TestBasicAuthRefusesSuspendedTenant`
- **Unauthorized does not trigger the browser dialog** — `TestUnauthorizedDoesNotTriggerTheBrowserDialog`
- **Tenant falls back to subdomain** — `TestTenantFallsBackToSubdomain`
- **Tenant header beats subdomain** — `TestTenantHeaderBeatsSubdomain`
- **Require admin** — `TestRequireAdmin`
- admin passes
- agent is refused
- **Cors** — `TestCORS`
- echoes an allowed origin
- tolerates a trailing slash in configuration
- ignores an origin that is not on the list
- answers preflight without reaching the handler
- **Recoverer turns panic into five hundred** — `TestRecovererTurnsPanicIntoFiveHundred`
### `backend/internal/repository/booking_repo_test.go`
15 tests, 15 cases.
- **Create booking sells seats** — `TestCreateBookingSellsSeats`
- **Create booking stores passenger detail** — `TestCreateBookingStoresPassengerDetail`
- **Create booking rejects blank date of birth** — `TestCreateBookingRejectsBlankDateOfBirth`
- **Create booking refuses more seats than remain** — `TestCreateBookingRefusesMoreSeatsThanRemain`
- **Create booking requires a passenger** — `TestCreateBookingRequiresAPassenger`
- **Create booking cannot reach another tenants flight** — `TestCreateBookingCannotReachAnotherTenantsFlight`
- **Create booking refuses cancelled flight** — `TestCreateBookingRefusesCancelledFlight`
- **Create booking does not oversell under concurrency** — `TestCreateBookingDoesNotOversellUnderConcurrency`
- **Cancel booking returns seats** — `TestCancelBookingReturnsSeats`
- **Cancel booking is not repeatable** — `TestCancelBookingIsNotRepeatable`
- **Cancel booking across tenants is not found** — `TestCancelBookingAcrossTenantsIsNotFound`
- **List bookings isolates tenants and filters status** — `TestListBookingsIsolatesTenantsAndFiltersStatus`
- **Booking by id across tenants is not found** — `TestBookingByIDAcrossTenantsIsNotFound`
- **Booking references are unique within a tenant** — `TestBookingReferencesAreUniqueWithinATenant`
- **Dashboard stats count only this tenant** — `TestDashboardStatsCountOnlyThisTenant`
### `backend/internal/repository/flight_repo_test.go`
13 tests, 16 cases.
- **Search flights filters by route** — `TestSearchFlightsFiltersByRoute`
- **Search flights lowercase iata still matches** — `TestSearchFlightsLowercaseIATAStillMatches`
- **Search flights isolates tenants** — `TestSearchFlightsIsolatesTenants`
- **Flight by id across tenants is not found** — `TestFlightByIDAcrossTenantsIsNotFound`
- **Flight by id unknown is not found** — `TestFlightByIDUnknownIsNotFound`
- **Search flights hides departed and cancelled** — `TestSearchFlightsHidesDepartedAndCancelled`
- **Search flights respects party size** — `TestSearchFlightsRespectsPartySize`
- **Search flights filters by cabin** — `TestSearchFlightsFiltersByCabin`
- **Search flights filters by departure day** — `TestSearchFlightsFiltersByDepartureDay`
- **Search flights sorting** — `TestSearchFlightsSorting`
- price
- duration
- departure
- nonsense-falls-back-to-price
- **Search flights paginates** — `TestSearchFlightsPaginates`
- **Search flights computes duration** — `TestSearchFlightsComputesDuration`
- **List airports covers only this tenants network** — `TestListAirportsCoversOnlyThisTenantsNetwork`
### `backend/internal/repository/identity_repo_test.go`
7 tests, 7 cases.
- **Tenant by slug** — `TestTenantBySlug`
- **Tenant by slug unknown is not found** — `TestTenantBySlugUnknownIsNotFound`
- **Tenant by slug suspended is not found** — `TestTenantBySlugSuspendedIsNotFound`
- **User by email is case insensitive** — `TestUserByEmailIsCaseInsensitive`
- **User by email is scoped to tenant** — `TestUserByEmailIsScopedToTenant`
- **Same email can exist in two tenants** — `TestSameEmailCanExistInTwoTenants`
- **List tenants excludes suspended** — `TestListTenantsExcludesSuspended`
---
## Angular (frontend)
114 tests. Vitest with jsdom, via `ng test`.
### `frontend/src/app/core/api.service.spec.ts`
10 tests.
- **ApiService › searchFlights**
- sends only the filters that were actually set
- drops empty strings so a cleared filter does not narrow the search
- keeps a zero offset, which is a real value rather than an absent one
- **ApiService**
- requests one flight by id
- **ApiService › listBookings**
- defaults to the first page of every status
- passes a status filter through when given
- **ApiService**
- posts a booking
- cancels a booking with an empty body
- reads the dashboard
- reads the tenant reference lists
### `frontend/src/app/core/auth.guard.spec.ts`
5 tests.
- **authGuard**
- lets a signed-in user through
- redirects to sign-in and remembers where the user was heading
- omits returnTo when the destination was the root
- **guestGuard**
- lets a signed-out visitor reach the sign-in screen
- sends an already signed-in user to the dashboard
### `frontend/src/app/core/auth.interceptor.spec.ts`
8 tests.
- **authInterceptor**
- stamps API calls with the credentials and tenant
- leaves non-API requests alone
- does not overwrite credentials a caller set explicitly
- adds nothing when signed out
- signs out and redirects when stored credentials go stale
- does not sign out on a 401 from an unstamped request
- leaves the session intact for other error statuses
- passes the error through to the caller
### `frontend/src/app/core/auth.service.spec.ts`
12 tests.
- **AuthService**
- starts signed out with nothing in storage
- fetches the public tenant list without credentials
- verifies credentials against the chosen tenant before storing them
- leaves no trace when the server rejects the credentials
- reports the admin role
- revalidates stored credentials on restore
- discards stored credentials the server no longer accepts
- restores without a second round trip once the session is known
- short-circuits restore when there are no credentials
- clears everything on sign out
- treats unreadable storage as signed out rather than crashing
- ignores a stored value of the wrong shape
### `frontend/src/app/features/bookings/booking-detail.spec.ts`
8 tests.
- **BookingDetailPage**
- loads the booking with its travellers
- shows the confirmation banner only when arriving from checkout
- asks for confirmation before cancelling
- lets the user back out
- cancels on confirmation and reflects the new status
- reports a cancel that failed and leaves the booking alone
- offers no cancel control on an already cancelled booking
- shows a not-found state for a booking in another agency
### `frontend/src/app/features/bookings/bookings.spec.ts`
6 tests.
- **BookingsPage**
- loads every booking on open
- filters by status and starts again from page one
- pages through a long list
- shows an empty state with a way forward
- surfaces a load failure
- renders a row with the reference, route and status
### `frontend/src/app/features/checkout/checkout.spec.ts`
16 tests.
- **CheckoutPage**
- loads the flight and prefills the agent as the contact
- opens with one traveller form by default
- honours the traveller count carried over from search
- clamps a silly traveller count to what the cabin allows
- never asks for more travellers than there are seats
- ignores a non-numeric traveller count
- adds and removes travellers, repricing as it goes
- will not go below one traveller or above nine
- warns when the party outgrows the remaining seats
- blocks submission until every required field is filled
- submits the booking and moves to the confirmation
- explains a seat sold out from under the form
- explains a flight that disappeared
- relays the server`s own validation message
- shows a not-found state when the flight cannot be loaded
- renders cabin codes as words
### `frontend/src/app/features/dashboard/dashboard.spec.ts`
7 tests.
- **DashboardPage**
- greets the user by first name and names the agency
- renders the headline figures
- scales the route bars against the biggest earner
- keeps a tiny route visible rather than drawing nothing
- copes with no sales at all
- treats a null topRoutes as an empty list
- surfaces a failure to load
### `frontend/src/app/features/login/login.spec.ts`
14 tests.
- **LoginPage**
- loads the agency list on open
- preselects the only agency in a single-tenant deployment
- does not preselect when there is a real choice to make
- explains a backend that is not running
- fills the form from a demo chip
- refuses to submit an incomplete form
- rejects a malformed email before calling the server
- signs in and moves to the dashboard
- returns the user to the page they were trying to reach
- reports wrong credentials in the agency`s terms
- reports an unreachable API distinctly from a bad password
- falls back to a generic message for unexpected failures
- renders one option per agency plus the placeholder
- shows four demo account shortcuts
### `frontend/src/app/features/search/search.spec.ts`
16 tests.
- **SearchPage**
- searches once on open and fills the airport pickers
- asks for the first page with the default page size
- passes the filter panel through to the API
- returns to page one when the filters change
- clears the filters on reset
- **SearchPage › pagination**
- describes the visible slice
- moves forward and back
- never steps past the ends
- **SearchPage**
- reports an empty result set rather than an error
- surfaces a failed search
- carries the traveller count into checkout
- **SearchPage › presentation helpers**
- marks an arrival on a later calendar day
- shows no offset for a same-day arrival
- flags scarce inventory as urgent
- renders cabin codes as words
- **SearchPage**
- renders a result card with the fare and route
### `frontend/src/app/layout/shell.spec.ts`
8 tests.
- **Shell**
- keeps the agency permanently on screen
- shows who is signed in
- **Shell › initials**
- takes the first letter of the first two names
- copes with a single name
- copes with extra spacing
- falls back when there is no name yet
- **Shell**
- offers the three main destinations
- signs out and returns to the sign-in screen
### `frontend/src/app/shared/duration.pipe.spec.ts`
4 tests.
- **DurationPipe**
- renders hours and zero-padded minutes
- drops the hour part under an hour
- handles long-haul durations past a day
- renders a placeholder for missing or nonsensical values
---
## Playwright (end to end)
65 tests, each run against a real browser and the real stack.
### `e2e/tests/auth.spec.ts`
13 tests.
- **Sign in**
- offers every active agency in the picker
- a demo shortcut fills the whole form
- signs an agent into their own agency
- signs an admin in and shows the admin role
- rejects a wrong password
- rejects valid credentials aimed at the wrong agency
- will not submit without an agency
- rejects a malformed email before contacting the server
- **Session**
- survives a page reload
- sends an anonymous visitor to sign-in and back again
- signing out clears the session for good
- an already signed-in user is moved off the sign-in screen
- stale stored credentials are discarded
### `e2e/tests/booking.spec.ts`
7 tests.
- **Booking a flight**
- carries the fare and traveller count into checkout
- reprices as travellers are added and removed
- refuses to submit with missing traveller names
- books, confirms, lists and cancels, returning the seats
- a cancel can be backed out of
- the bookings list filters by status
- a booking opened directly shows no stale confirmation banner
### `e2e/tests/dashboard.spec.ts`
6 tests.
- **Dashboard**
- greets the agent and names the agency
- shows the four headline figures
- lists recent bookings and best routes from the seed data
- a new sale moves the figures, and cancelling moves them back
- a recent booking row opens the booking
- the new booking shortcut goes to search
### `e2e/tests/responsive.spec.ts`
4 tests.
- **Layout**
- the sign-in screen fits without sideways scrolling
- the console screens fit at phone width
- wide tables scroll inside their own container
- the sidebar collapses into a top bar on narrow screens
### `e2e/tests/search.spec.ts`
11 tests.
- **Flight search**
- opens with the agency inventory already listed
- offers only airports this agency flies
- narrows results to a route
- a route with no inventory shows an empty state
- resets back to the full list
- sorts by lowest fare
- sorts by shortest trip
- filters by cabin
- hides flights without room for the whole party
- pages through results without repeating them
- a result card carries everything needed to choose
### `e2e/tests/simulation.spec.ts`
19 tests.
- **When the API fails**
- search shows an error instead of an empty page
- the dashboard explains itself rather than showing zeroes
- the bookings list reports a failure
- a missing backend is named on the sign-in screen
- going offline mid-session surfaces a failure, not a hang
- **When the API is slow**
- search shows skeletons while it waits
- the sign-in button reports that it is working
- the dashboard shows skeleton tiles before its figures
- **When the session expires**
- a 401 mid-session returns the agent to sign-in
- **Edge cases in the data**
- an agency with no inventory sees an empty state, not an error
- a brand new agency sees zeroes without a broken dashboard
- the last seat is flagged as urgent
- a seat sold during checkout is explained clearly
- a party larger than the cabin is blocked before submitting
- a huge result set still paginates sensibly
- **Awkward content**
- very long names do not break the layout or escape the page
- markup in data is rendered as text, never as HTML
- **Keyboard operation**
- the sign-in form can be completed without a mouse
- every interactive control on search is reachable by tab
### `e2e/tests/tenant-isolation.spec.ts`
5 tests.
- **Tenant isolation**
- the same route is priced per agency
- the sidebar always names the agency you are working in
- one agency never sees another agency booking
- revenue figures are per agency
- a deep link to another agency flight is refused