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,117 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/travelswitch/travelhub/backend/internal/repository"
|
||||
"github.com/travelswitch/travelhub/backend/internal/testsupport"
|
||||
)
|
||||
|
||||
func TestTenantBySlug(t *testing.T) {
|
||||
f := testsupport.Setup(t)
|
||||
|
||||
got, err := f.Repo.TenantBySlug(context.Background(), f.Tenant.Slug)
|
||||
if err != nil {
|
||||
t.Fatalf("lookup: %v", err)
|
||||
}
|
||||
if got.ID != f.Tenant.ID {
|
||||
t.Errorf("got tenant %s, want %s", got.ID, f.Tenant.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantBySlugUnknownIsNotFound(t *testing.T) {
|
||||
f := testsupport.Setup(t)
|
||||
|
||||
if _, err := f.Repo.TenantBySlug(context.Background(), "no-such-agency"); err != repository.ErrNotFound {
|
||||
t.Fatalf("want ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A suspended tenant must be indistinguishable from a missing one, so its
|
||||
// users cannot sign in.
|
||||
func TestTenantBySlugSuspendedIsNotFound(t *testing.T) {
|
||||
f := testsupport.Setup(t)
|
||||
f.Suspend(t)
|
||||
|
||||
if _, err := f.Repo.TenantBySlug(context.Background(), f.Tenant.Slug); err != repository.ErrNotFound {
|
||||
t.Fatalf("want ErrNotFound for a suspended tenant, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserByEmailIsCaseInsensitive(t *testing.T) {
|
||||
f := testsupport.Setup(t)
|
||||
|
||||
got, err := f.Repo.UserByEmail(context.Background(), f.Tenant.ID, strings.ToUpper(f.Agent.Email))
|
||||
if err != nil {
|
||||
t.Fatalf("lookup: %v", err)
|
||||
}
|
||||
if got.ID != f.Agent.ID {
|
||||
t.Errorf("got user %s, want %s", got.ID, f.Agent.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// The tenant filter on the user lookup is what stops one agency's credentials
|
||||
// from authenticating at another.
|
||||
func TestUserByEmailIsScopedToTenant(t *testing.T) {
|
||||
mine := testsupport.Setup(t)
|
||||
theirs := testsupport.Setup(t)
|
||||
|
||||
_, err := mine.Repo.UserByEmail(context.Background(), mine.Tenant.ID, theirs.Agent.Email)
|
||||
if err != repository.ErrNotFound {
|
||||
t.Fatalf("want ErrNotFound looking up another tenant's user, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The same address may hold an account at more than one agency; each must
|
||||
// resolve to its own row.
|
||||
func TestSameEmailCanExistInTwoTenants(t *testing.T) {
|
||||
a := testsupport.Setup(t)
|
||||
b := testsupport.Setup(t)
|
||||
const shared = "traveller@example.com"
|
||||
|
||||
inA := a.NewUser(t, shared, "agent")
|
||||
inB := b.NewUser(t, shared, "admin")
|
||||
|
||||
gotA, err := a.Repo.UserByEmail(context.Background(), a.Tenant.ID, shared)
|
||||
if err != nil {
|
||||
t.Fatalf("lookup in A: %v", err)
|
||||
}
|
||||
gotB, err := b.Repo.UserByEmail(context.Background(), b.Tenant.ID, shared)
|
||||
if err != nil {
|
||||
t.Fatalf("lookup in B: %v", err)
|
||||
}
|
||||
|
||||
if gotA.ID != inA.ID || gotB.ID != inB.ID {
|
||||
t.Error("shared email resolved to the wrong tenant's user")
|
||||
}
|
||||
if gotA.ID == gotB.ID {
|
||||
t.Error("the two tenants collapsed onto one user row")
|
||||
}
|
||||
if gotA.Role != "agent" || gotB.Role != "admin" {
|
||||
t.Errorf("roles crossed over: A=%s B=%s", gotA.Role, gotB.Role)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListTenantsExcludesSuspended(t *testing.T) {
|
||||
live := testsupport.Setup(t)
|
||||
dead := testsupport.Setup(t)
|
||||
dead.Suspend(t)
|
||||
|
||||
tenants, err := live.Repo.ListTenants(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
for _, tn := range tenants {
|
||||
seen[tn.Slug] = true
|
||||
}
|
||||
if !seen[live.Tenant.Slug] {
|
||||
t.Error("active tenant missing from the sign-in picker")
|
||||
}
|
||||
if seen[dead.Tenant.Slug] {
|
||||
t.Error("suspended tenant offered in the sign-in picker")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user