Active Revision 重验证成功后更新验证元数据并原子替换同配置 Runtime;验证失败时不修改数据库状态或当前运行时。\n\n验证:go test ./apps/api/internal/identityruntime ./apps/api/internal/store ./apps/api/internal/httpapi
165 lines
7.1 KiB
Go
165 lines
7.1 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) RevalidateActiveIdentityRevision(_ context.Context, id string, expected int64, traceID, auditID string) (identity.Revision, error) {
|
|
revision := f.revisions[id]
|
|
if revision.Version != expected || revision.State != identity.RevisionActive {
|
|
return identity.Revision{}, identity.ErrRevisionConflict
|
|
}
|
|
revision.Version, revision.LastTraceID, revision.LastAuditID = revision.Version+1, traceID, auditID
|
|
f.revisions[id], f.active = revision, 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, traceID, auditID string) (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, revision.LastTraceID, revision.LastAuditID = identity.RevisionActive, revision.Version+1, traceID, auditID
|
|
f.active, f.revisions[id] = revision, revision
|
|
return revision, true, nil
|
|
}
|
|
func (f *runtimeRepositoryFake) DisableActiveIdentityRevision(_ context.Context, expected int64, traceID, auditID string) (identity.Revision, error) {
|
|
if f.active.Version != expected {
|
|
return identity.Revision{}, identity.ErrRevisionConflict
|
|
}
|
|
disabled := f.active
|
|
disabled.State, disabled.Version, disabled.LastTraceID, disabled.LastAuditID = identity.RevisionSuperseded, disabled.Version+1, traceID, auditID
|
|
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, "trace", "audit")
|
|
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())
|
|
}
|
|
}
|
|
|
|
func TestRollbackValidatesAndAtomicallyReplacesActiveRuntime(t *testing.T) {
|
|
active := identity.Revision{ID: "current", State: identity.RevisionActive, Version: 5}
|
|
previous := identity.Revision{ID: "previous", State: identity.RevisionSuperseded, Version: 3}
|
|
repository := &runtimeRepositoryFake{
|
|
revisions: map[string]identity.Revision{"current": active, "previous": previous}, active: active,
|
|
}
|
|
manager := NewManager(repository, &runtimeBuilderFake{})
|
|
manager.current.Store(&Runtime{Revision: active})
|
|
|
|
rolledBack, err := manager.Rollback(context.Background(), previous.ID, previous.Version, "trace", "audit")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rolledBack.State != identity.RevisionActive || manager.Current().Revision.ID != previous.ID || repository.active.ID != previous.ID {
|
|
t.Fatalf("rollback did not replace active runtime: revision=%#v current=%#v", rolledBack, manager.Current())
|
|
}
|
|
}
|
|
|
|
func TestActiveRevalidationSwapsOnlyAfterSuccessfulValidation(t *testing.T) {
|
|
active := identity.Revision{ID: "active", State: identity.RevisionActive, Version: 4}
|
|
repository := &runtimeRepositoryFake{revisions: map[string]identity.Revision{"active": active}, active: active}
|
|
builder := &runtimeBuilderFake{}
|
|
manager := NewManager(repository, builder)
|
|
original := &Runtime{Revision: active}
|
|
manager.current.Store(original)
|
|
|
|
revalidated, err := manager.Validate(context.Background(), active.ID, active.Version, "trace-new", "audit-new")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if revalidated.State != identity.RevisionActive || revalidated.Version != 5 || manager.Current() == original || manager.Current().Revision.LastAuditID != "audit-new" {
|
|
t.Fatalf("active runtime was not safely revalidated: %#v", revalidated)
|
|
}
|
|
}
|