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>
499 lines
19 KiB
Markdown
499 lines
19 KiB
Markdown
# 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
|
||
|