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:
+498
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user