easyai-ai-gateway/apps/api/internal/identityruntime/manager_test.go
chengcheng a9e23cb237 feat(identity): 实现统一认证运行时热切换
从 Active Revision 构造并验证 OIDC、BFF Session、Introspection 与 SSF Runtime,在数据库激活成功后原子替换内存引用。请求链路使用不可变快照,失败保留当前运行时,本地管理登录不受影响。\n\n验证:go test ./apps/api/...;go vet ./apps/api/...
2026-07-17 12:05:00 +08:00

121 lines
4.7 KiB
Go

package identityruntime
import (
"context"
"errors"
"testing"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
)
type runtimeRepositoryFake struct {
revisions map[string]identity.Revision
active identity.Revision
activateCalled bool
}
func (f *runtimeRepositoryFake) IdentityConfigurationRevision(_ context.Context, id string) (identity.Revision, error) {
revision, ok := f.revisions[id]
if !ok {
return identity.Revision{}, identity.ErrRevisionNotFound
}
return revision, nil
}
func (f *runtimeRepositoryFake) ActiveIdentityConfigurationRevision(context.Context) (identity.Revision, error) {
if f.active.ID == "" {
return identity.Revision{}, identity.ErrRevisionNotFound
}
return f.active, nil
}
func (f *runtimeRepositoryFake) MarkIdentityRevisionValidated(_ context.Context, id string, expected int64, _, _ string) (identity.Revision, error) {
revision := f.revisions[id]
if revision.Version != expected {
return identity.Revision{}, identity.ErrRevisionConflict
}
revision.State, revision.Version = identity.RevisionValidated, revision.Version+1
f.revisions[id] = revision
return revision, nil
}
func (f *runtimeRepositoryFake) MarkIdentityRevisionFailed(_ context.Context, id string, expected int64, category, _, _ string) (identity.Revision, error) {
revision := f.revisions[id]
if revision.Version != expected {
return identity.Revision{}, identity.ErrRevisionConflict
}
revision.State, revision.Version, revision.LastErrorCategory = identity.RevisionFailed, revision.Version+1, category
f.revisions[id] = revision
return revision, nil
}
func (f *runtimeRepositoryFake) ActivateIdentityRevision(_ context.Context, id string, expected int64) (identity.Revision, bool, error) {
f.activateCalled = true
revision := f.revisions[id]
if revision.Version != expected || revision.State != identity.RevisionValidated {
return identity.Revision{}, false, identity.ErrRevisionConflict
}
if f.active.ID != "" {
old := f.active
old.State = identity.RevisionSuperseded
f.revisions[old.ID] = old
}
revision.State, revision.Version = identity.RevisionActive, revision.Version+1
f.active, f.revisions[id] = revision, revision
return revision, true, nil
}
func (f *runtimeRepositoryFake) DisableActiveIdentityRevision(_ context.Context, expected int64) (identity.Revision, error) {
if f.active.Version != expected {
return identity.Revision{}, identity.ErrRevisionConflict
}
disabled := f.active
disabled.State, disabled.Version = identity.RevisionSuperseded, disabled.Version+1
f.revisions[disabled.ID] = disabled
f.active = identity.Revision{}
return disabled, nil
}
type runtimeBuilderFake struct {
err error
builtIDs []string
}
func (f *runtimeBuilderFake) Build(_ context.Context, revision identity.Revision) (*Runtime, error) {
f.builtIDs = append(f.builtIDs, revision.ID)
if f.err != nil {
return nil, f.err
}
return &Runtime{Revision: revision}, nil
}
func TestValidationFailureKeepsCurrentRuntimeAndDoesNotActivate(t *testing.T) {
active := identity.Revision{ID: "active", State: identity.RevisionActive, Version: 4}
draft := identity.Revision{ID: "draft", State: identity.RevisionDraft, Version: 1}
repository := &runtimeRepositoryFake{revisions: map[string]identity.Revision{"active": active, "draft": draft}, active: active}
builder := &runtimeBuilderFake{err: errors.New("discovery failed")}
manager := NewManager(repository, builder)
manager.current.Store(&Runtime{Revision: active})
if _, err := manager.Validate(context.Background(), draft.ID, draft.Version, "trace", "audit"); err == nil {
t.Fatal("validation failure was ignored")
}
if manager.Current().Revision.ID != active.ID || repository.activateCalled {
t.Fatal("validation failure changed current runtime or activated the draft")
}
if repository.revisions[draft.ID].State != identity.RevisionFailed {
t.Fatal("failed draft was not marked failed")
}
}
func TestActivationSwapsRuntimeOnlyAfterRepositoryActivation(t *testing.T) {
active := identity.Revision{ID: "old", State: identity.RevisionActive, Version: 2}
candidate := identity.Revision{ID: "new", State: identity.RevisionValidated, Version: 3}
repository := &runtimeRepositoryFake{revisions: map[string]identity.Revision{"old": active, "new": candidate}, active: active}
manager := NewManager(repository, &runtimeBuilderFake{})
manager.current.Store(&Runtime{Revision: active})
activated, err := manager.Activate(context.Background(), candidate.ID, candidate.Version)
if err != nil {
t.Fatal(err)
}
if !repository.activateCalled || activated.State != identity.RevisionActive || manager.Current().Revision.ID != candidate.ID {
t.Fatalf("activation order failed: activated=%#v current=%#v", activated, manager.Current())
}
}