package identity import ( "encoding/json" "strings" "testing" ) func TestRevisionStateTransitions(t *testing.T) { tests := []struct { from, to RevisionState allowed bool }{ {RevisionDraft, RevisionValidated, true}, {RevisionDraft, RevisionFailed, true}, {RevisionValidated, RevisionActive, true}, {RevisionValidated, RevisionFailed, true}, {RevisionActive, RevisionSuperseded, true}, {RevisionSuperseded, RevisionValidated, true}, {RevisionSuperseded, RevisionActive, false}, {RevisionDraft, RevisionActive, false}, {RevisionFailed, RevisionActive, false}, {RevisionActive, RevisionValidated, false}, } for _, test := range tests { if got := CanTransition(test.from, test.to); got != test.allowed { t.Errorf("CanTransition(%q, %q)=%v, want %v", test.from, test.to, got, test.allowed) } } } func TestRevisionNeverSerializesSecretValues(t *testing.T) { type secretFields interface { MachineSecret() string } var _ = any((*Revision)(nil)) if _, exposesSecret := any((*Revision)(nil)).(secretFields); exposesSecret { t.Fatal("Revision unexpectedly exposes a machine secret value") } revision := Revision{MachineCredentialRef: "identity-machine-example", SessionEncryptionKeyRef: "identity-session-example"} if revision.MachineCredentialRef == "" || revision.SessionEncryptionKeyRef == "" { t.Fatal("Revision must retain SecretStore references") } } func TestValidatePairingInputDerivesExactGatewayURIs(t *testing.T) { input := PairingInput{ AuthCenterURL: "https://auth.example.com", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default", } metadata, err := input.ConsumerMetadata(true, "production") if err != nil { t.Fatal(err) } if metadata.RedirectURIs[0] != "https://api.example.com/api/v1/auth/oidc/callback" || metadata.LogoutURIs[0] != "https://gateway.example.com/" || metadata.ReceiverEndpoint != "https://api.example.com/api/v1/security-events/ssf" { t.Fatalf("unexpected derived metadata: %#v", metadata) } } func TestValidatePairingInputRejectsRemoteHTTPAndURLCredentials(t *testing.T) { for _, input := range []PairingInput{ {AuthCenterURL: "http://auth.example.com", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default"}, {AuthCenterURL: "https://user:password@auth.example.com", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default"}, } { if _, err := input.ConsumerMetadata(false, "production"); err == nil { t.Fatalf("unsafe pairing input accepted: %#v", input) } } } func TestValidatePairingInputRejectsCrossSiteBrowserEndpoints(t *testing.T) { for _, test := range []struct { name string appEnv string input PairingInput }{ { name: "different registrable domains", appEnv: "production", input: PairingInput{ AuthCenterURL: "https://auth.example.com", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.net", LocalTenantKey: "default", }, }, { name: "different loopback sites", appEnv: "test", input: PairingInput{ AuthCenterURL: "http://127.0.0.1:18000", PublicBaseURL: "http://127.0.0.1:18088", WebBaseURL: "http://localhost:15178", LocalTenantKey: "default", }, }, { name: "different schemes", appEnv: "test", input: PairingInput{ AuthCenterURL: "http://127.0.0.1:18000", PublicBaseURL: "http://localhost:18088", WebBaseURL: "https://localhost:15178", LocalTenantKey: "default", }, }, } { t.Run(test.name, func(t *testing.T) { if _, err := test.input.ConsumerMetadata(false, test.appEnv); err == nil { t.Fatalf("cross-site browser endpoints were accepted: %#v", test.input) } }) } } func TestValidateRevisionURLsRejectsCrossSiteBrowserEndpoints(t *testing.T) { revision := Revision{ AuthCenterURL: "https://auth.example.com", Issuer: "https://auth.example.com/issuer/shared", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.net", } if err := ValidateRevisionURLs(revision, "production"); err == nil { t.Fatal("cross-site persisted browser endpoints were accepted") } } func TestNewDraftAppliesSessionDefaultsWithoutPersistingOnboardingCode(t *testing.T) { draft, err := NewDraft(PairingInput{ AuthCenterURL: "https://auth.example.com", OnboardingCode: "onb1.must-never-be-persisted", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default", LegacyJWTEnabled: true, }, "production") if err != nil { t.Fatal(err) } if draft.State != RevisionDraft || draft.SessionIdleSeconds != 1800 || draft.SessionAbsoluteSeconds != 28800 || draft.SessionRefreshSeconds != 60 { t.Fatalf("unexpected draft defaults: %#v", draft) } payload, _ := json.Marshal(draft) if strings.Contains(string(payload), "must-never-be-persisted") || strings.Contains(string(payload), "onboardingCode") { t.Fatalf("draft exposed onboarding code: %s", payload) } } func TestNewDraftAllowsLoopbackHTTPOnlyInLocalEnvironments(t *testing.T) { localInput := PairingInput{ AuthCenterURL: "http://localhost:18000", PublicBaseURL: "http://127.0.0.1:18089", WebBaseURL: "http://127.0.0.1:5178", LocalTenantKey: "default", } secureInput := PairingInput{ AuthCenterURL: "https://auth.example.com", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default", } for _, test := range []struct { name string mutate func(*PairingInput) }{ {name: "auth center", mutate: func(input *PairingInput) { input.AuthCenterURL = localInput.AuthCenterURL }}, {name: "public base", mutate: func(input *PairingInput) { input.PublicBaseURL = localInput.PublicBaseURL }}, {name: "web base", mutate: func(input *PairingInput) { input.WebBaseURL = localInput.WebBaseURL }}, } { t.Run(test.name, func(t *testing.T) { input := secureInput test.mutate(&input) for _, appEnv := range []string{"", "production", "staging"} { if _, err := NewDraft(input, appEnv); err == nil { t.Fatalf("%s accepted loopback HTTP %s URL", appEnv, test.name) } } }) } for _, appEnv := range []string{"local", "development", "dev", "test"} { draft, err := NewDraft(localInput, appEnv) if err != nil { t.Fatalf("%s rejected loopback HTTP pairing URLs: %v", appEnv, err) } if draft.AuthCenterURL != localInput.AuthCenterURL || draft.PublicBaseURL != localInput.PublicBaseURL || draft.WebBaseURL != localInput.WebBaseURL { t.Fatalf("%s changed normalized loopback URLs: %#v", appEnv, draft) } } } func TestApplyManifestV2RemovesFixedTenantMappingWhileV1StillRequiresIt(t *testing.T) { draft, err := NewDraft(PairingInput{ AuthCenterURL: "https://auth.example.com", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", }, "production") if err != nil { t.Fatalf("multi-tenant pairing draft rejected: %v", err) } applied, err := ApplyManifest(draft, ManifestApplication{ AppEnv: "production", MachineCredentialRef: "identity-machine-example", SessionEncryptionKeyRef: "identity-session-example", Manifest: ManifestV2{ SchemaVersion: 2, TenantMode: "multi_tenant", Issuer: "https://auth.example.com/issuer/shared", ApplicationID: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", Audience: "urn:easyai:resource:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", Capabilities: []string{"oidc_login", "api_access", "machine_to_machine", "token_introspection", "session_revocation"}, Scopes: []string{"openid", "gateway.access"}, Clients: ManifestClients{ BrowserLogin: &ManifestClient{ClientID: "browser"}, MachineToMachine: &ManifestClient{ClientID: "service"}, }, TenantContext: &ManifestTenantContext{ Endpoint: "https://auth.example.com/api/v1/runtime/tenants/{tenantId}", Audience: "urn:easyai:auth-center:tenant-context", Scope: "tenant.context.read", }, SecurityEvents: &ManifestSecurityEvents{ TransmitterIssuer: "https://auth.example.com/ssf", ConfigurationEndpoint: "https://auth.example.com/.well-known/ssf-configuration/ssf", Audience: "urn:easyai:ssf:receiver:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", }, }, }) if err != nil { t.Fatal(err) } if applied.SchemaVersion != 2 || applied.TenantMode != "multi_tenant" || applied.TenantID != "" || applied.LocalTenantKey != "" || applied.TenantContextEndpoint == "" { t.Fatalf("unexpected multi-tenant revision: %#v", applied) } v1Draft := draft v1Draft.ID = "v1-draft" if _, err := ApplyManifest(v1Draft, ManifestApplication{AppEnv: "production", Manifest: ManifestV1{ SchemaVersion: 1, Issuer: "https://auth.example.com/issuer/shared", TenantID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", ApplicationID: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", Capabilities: []string{}, Scopes: []string{}, Clients: ManifestClients{}, }}); err == nil { t.Fatal("Manifest V1 without a fixed local tenant mapping was accepted") } }