easyai-ai-gateway/apps/api/internal/identityruntime/builder_security_events_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

82 lines
3.9 KiB
Go

package identityruntime
import (
"context"
"strings"
"testing"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
)
func TestRuntimeBuilderRejectsLoopbackHTTPOutsideLocalEnvironments(t *testing.T) {
revision := identity.Revision{
AuthCenterURL: "https://auth.example.com", Issuer: "https://auth.example.com/issuer/easyai",
PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com",
SecurityEventIssuer: "https://auth.example.com/ssf", SecurityEventConfigURL: "https://auth.example.com/.well-known/ssf-configuration/ssf",
SessionRevocation: true,
}
production := &RuntimeBuilder{config: RuntimeBuilderConfig{AppEnv: "production"}}
for _, test := range []struct {
name string
mutate func(*identity.Revision)
}{
{name: "auth center", mutate: func(value *identity.Revision) { value.AuthCenterURL = "http://localhost:18000" }},
{name: "OIDC issuer", mutate: func(value *identity.Revision) { value.Issuer = "http://localhost:18003/issuer/easyai" }},
{name: "public base", mutate: func(value *identity.Revision) { value.PublicBaseURL = "http://127.0.0.1:18089" }},
{name: "web base", mutate: func(value *identity.Revision) { value.WebBaseURL = "http://localhost:5178" }},
{name: "SSF issuer", mutate: func(value *identity.Revision) { value.SecurityEventIssuer = "http://localhost:18004/ssf" }},
{name: "SSF configuration", mutate: func(value *identity.Revision) {
value.SecurityEventConfigURL = "http://localhost:18004/.well-known/ssf-configuration/ssf"
}},
} {
t.Run(test.name, func(t *testing.T) {
candidate := revision
test.mutate(&candidate)
if _, err := production.Build(context.Background(), candidate); err == nil || !strings.Contains(err.Error(), "HTTPS") {
t.Fatalf("production runtime did not reject loopback HTTP %s URL: %v", test.name, err)
}
})
}
localRevision := revision
localRevision.AuthCenterURL = "http://localhost:18000"
localRevision.Issuer = "http://localhost:18003/issuer/easyai"
localRevision.PublicBaseURL = "http://127.0.0.1:18089"
localRevision.WebBaseURL = "http://localhost:5178"
localRevision.SecurityEventIssuer = "http://localhost:18004/ssf"
localRevision.SecurityEventConfigURL = "http://localhost:18004/.well-known/ssf-configuration/ssf"
development := &RuntimeBuilder{config: RuntimeBuilderConfig{AppEnv: "development"}}
if _, err := development.Build(context.Background(), localRevision); err == nil || strings.Contains(err.Error(), "HTTPS") {
t.Fatalf("development runtime did not pass URL policy before completeness checks: %v", err)
}
}
func TestSecurityEventManagerConfigBindsRuntimeToTargetRevision(t *testing.T) {
builder := &RuntimeBuilder{config: RuntimeBuilderConfig{AppEnv: "test"}}
revision := identity.Revision{
ID: "revision-target",
Issuer: "https://auth.example/issuer",
TenantID: "tenant-public-id",
MachineClientID: "gateway-machine",
PublicBaseURL: "https://gateway.example",
SecurityEventIssuer: "https://auth.example/ssf/",
SecurityEventAudience: "urn:easyai:ssf:receiver:target",
}
runtimeConfig := builder.securityEventManagerConfig(revision, true)
if !runtimeConfig.StrictRevisionBinding ||
runtimeConfig.ExpectedTransmitterIssuer != "https://auth.example/ssf" ||
runtimeConfig.ExpectedAudience != revision.SecurityEventAudience ||
runtimeConfig.ManagementClientID != revision.MachineClientID ||
runtimeConfig.ExpectedOwnerKey != "identity-pairing-ssf-revision-target" {
t.Fatalf("runtime SSF binding config=%#v", runtimeConfig)
}
recoveryConfig := builder.securityEventManagerConfig(revision, false)
if recoveryConfig.StrictRevisionBinding {
t.Fatal("conflict recovery was incorrectly blocked by strict Revision binding")
}
if recoveryConfig.ExpectedOwnerKey != runtimeConfig.ExpectedOwnerKey {
t.Fatal("recovery manager lost the target owner needed for explicit validation")
}
}