package accessauth import ( "context" "database/sql" "fmt" "os" "strings" "testing" "time" _ "github.com/jackc/pgx/v5/stdlib" "github.com/sarathsp06/sparrow/pkg/access" "github.com/sarathsp06/sparrow/pkg/access/memstore" "github.com/sarathsp06/sparrow/pkg/access/pgstore" "github.com/sarathsp06/sparrow/pkg/access/storetest" ) // The migrations must ship exactly the library's schema, one part each, or // pgstore queries could drift from the tables Sparrow creates. func TestMigrationsMatchPgstoreSchema(t *testing.T) { migrations := []string{"000027_access_tokens.up.sql", "000028_access_token_idempotency.up.sql", "000029_access_token_external_id.up.sql"} if len(migrations) != len(pgstore.SchemaParts()) { t.Fatalf("pgstore has %d schema parts but %d migrations are checked; add the new migration here", len(pgstore.SchemaParts()), len(migrations)) } for i, name := range migrations { b, err := os.ReadFile("../../db/migrations/" + name) if err != nil { t.Fatal(err) } if !strings.Contains(string(b), pgstore.SchemaParts()[i]) { t.Fatalf("db/migrations/%s no longer contains pgstore schema part %d verbatim; copy it over", name, i+1) } } } func TestTTLRules(t *testing.T) { acme := "acme" cases := []struct { consumer *string requested time.Duration never bool def time.Duration want time.Duration wantErr bool }{ {nil, 0, false, TenantTokenDefaultTTL, TenantTokenDefaultTTL, false}, // tenant-wide: operator default {nil, 0, false, 0, 0, false}, // default 0 = never (opt-out) {nil, 0, true, TenantTokenDefaultTTL, 0, false}, // explicit never {nil, time.Hour, true, TenantTokenDefaultTTL, 0, true}, // both: rejected {nil, 10 * 365 * 24 * time.Hour, false, TenantTokenDefaultTTL, 10 * 365 * 24 * time.Hour, false}, {&acme, 0, false, TenantTokenDefaultTTL, ConsumerTokenDefaultTTL, false}, {&acme, time.Hour, false, 0, time.Hour, false}, {&acme, 0, true, 0, 0, true}, // consumer tokens always expire {&acme, ConsumerTokenMaxTTL + time.Second, false, 0, 0, true}, } for _, c := range cases { got, err := TokenTTL(c.consumer, c.requested, c.never, c.def) if got != c.want || (err != nil) != c.wantErr { t.Errorf("TokenTTL(%v, %v) = %v, %v", c.consumer, c.requested, got, err) } } if got, _ := InviteTTL(0); got != InviteDefaultTTL { t.Errorf("InviteTTL(0) = %v", got) } if _, err := InviteTTL(InviteMaxTTL + time.Second); err == nil || err.Error() != "invites can live at most 7 days" { t.Errorf("InviteTTL above max: %v", err) } if _, err := TokenTTL(&acme, ConsumerTokenMaxTTL+time.Second, false, 0); err == nil || err.Error() != "consumer tokens can live at most 30 days" { t.Errorf("TokenTTL above max: %v", err) } } func TestMasterKeyIsRootInDefaultTenant(t *testing.T) { svc, err := NewWithStore(memstore.New(), "master", nil) if err != nil { t.Fatal(err) } p, err := svc.Authenticate(context.Background(), "master") if err != nil || !p.Root || p.Realm != RootRealm() || p.Name != MasterKeyName { t.Fatalf("master key principal = %+v, %v", p, err) } open, _ := NewWithStore(memstore.New(), "", nil) if _, err := open.Authenticate(context.Background(), ""); err == nil { t.Fatal("empty credential authenticated without a master key") } _, secret, err := svc.CreateToken(context.Background(), access.CreateTokenRequest{Realm: RootRealm(), Name: "x", CreatedBy: MasterKeyName}) if err != nil || !strings.HasPrefix(secret, TokenPrefix) { t.Fatalf("token secret %q, %v", secret, err) } } // TestPgstoreConformance runs the library's store contract against real // Postgres, in a throwaway schema. Needs DATABASE_URL (CI provides one). func TestPgstoreConformance(t *testing.T) { url := os.Getenv("DATABASE_URL") if url == "" { t.Skip("DATABASE_URL not set") } admin, err := sql.Open("pgx", url) if err != nil { t.Fatal(err) } defer admin.Close() //nolint:errcheck if err := admin.Ping(); err != nil { t.Skipf("postgres unreachable: %v", err) } n := 0 storetest.Run(t, func(t *testing.T) access.Store { n++ schema := fmt.Sprintf("access_conformance_%d_%d", time.Now().UnixNano(), n) if _, err := admin.Exec(`CREATE SCHEMA ` + schema); err != nil { t.Fatal(err) } t.Cleanup(func() { _, _ = admin.Exec(`DROP SCHEMA ` + schema + ` CASCADE`) }) sep := "?" if strings.Contains(url, "?") { sep = "&" } db, err := sql.Open("pgx", url+sep+"search_path="+schema) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.Close() }) if _, err := db.Exec(pgstore.Schema()); err != nil { t.Fatal(err) } return pgstore.New(db) }) } func TestValidateConsumer(t *testing.T) { for _, ok := range []string{"acme", "team-a", "Team_B.2", "ünïcode", strings.Repeat("x", 255)} { if err := ValidateConsumer(ok); err != nil { t.Errorf("ValidateConsumer(%q) = %v", ok, err) } } for _, bad := range []string{"", " acme", "acme ", ".", "..", "../../v1", "a/b", `a\b`, "a?b", "a#b", "a%2fb", "a\nb", "_sparrow", strings.Repeat("x", 256)} { if err := ValidateConsumer(bad); err == nil { t.Errorf("ValidateConsumer(%q) accepted", bad) } } }