easyai-ai-gateway/apps/api/internal/identity/configuration_test.go
chengcheng a312ad880d fix(identity): 完善统一认证配对恢复与安全退役
修复 credentials_saved 状态无法恢复、配对与激活并发冲突,以及 SSF 和身份 Secret 生命周期不完整的问题。新增持久化协调器、取消与清理状态机、事务级并发门禁、受控 SSF 凭据交接、禁用后的延迟 Secret 清理,并对生产环境统一认证及 Discovery 端点强制 HTTPS。

验证:go test ./...;go test -race ./internal/auth ./internal/identity ./internal/identityruntime ./internal/securityevents ./internal/httpapi ./internal/store -count=1;go vet ./...;真实 PostgreSQL 并发及清理成功/冲突回滚测试;pnpm openapi。
2026-07-17 18:31:12 +08:00

128 lines
5.0 KiB
Go

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 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://localhost: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)
}
}
}