Files
easyai-ai-gateway/apps/api/internal/identityruntime/tenant_context_sync_test.go
T
chengcheng 5c679ff13f feat(identity): 接入认证中心多租户登录
支持 Manifest V2 动态 tid 验证、Tenant Context 同步和租户内 JIT 投影,并保留 Manifest V1 与旧 Session 兼容。\n\n增加 tenantHint、租户切换、普通注册关闭及 application/principal/tenant 两级 SSF 撤销;迁移、定向安全测试和本地双租户跨仓 E2E 已通过。\n\nrelease_required=true;未执行 Release、Staging 或真实链路。
2026-07-28 17:28:35 +08:00

114 lines
4.1 KiB
Go

package identityruntime
import (
"context"
"errors"
"testing"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type tenantSyncRepository struct {
targets []store.OIDCTenantBindingSyncTarget
synced []string
unchanged []string
rejected []string
failed []string
failureNext time.Time
}
func (repository *tenantSyncRepository) DueOIDCTenantBindingSyncs(context.Context, string, string, int) ([]store.OIDCTenantBindingSyncTarget, error) {
return repository.targets, nil
}
func (repository *tenantSyncRepository) ApplyOIDCTenantBindingSync(_ context.Context, id string, _ identity.TenantContext, unchanged bool, _ time.Time) error {
if unchanged {
repository.unchanged = append(repository.unchanged, id)
} else {
repository.synced = append(repository.synced, id)
}
return nil
}
func (repository *tenantSyncRepository) RejectOIDCTenantBinding(_ context.Context, id, _ string, _ time.Time) error {
repository.rejected = append(repository.rejected, id)
return nil
}
func (repository *tenantSyncRepository) FailOIDCTenantBindingSync(_ context.Context, id, _ string, next time.Time) error {
repository.failed = append(repository.failed, id)
repository.failureNext = next
return nil
}
type tenantSyncReader struct {
tenants map[string]identity.TenantContext
unchanged map[string]bool
errors map[string]error
}
func (reader tenantSyncReader) Get(_ context.Context, tenantID, _ string) (identity.TenantContext, bool, error) {
return reader.tenants[tenantID], reader.unchanged[tenantID], reader.errors[tenantID]
}
func TestSynchronizeDueTenantContextsAppliesETagRejectsAndBacksOff(t *testing.T) {
now := time.Unix(1_780_000_000, 0).UTC()
repository := &tenantSyncRepository{targets: []store.OIDCTenantBindingSyncTarget{
{ID: "binding-a", ExternalTenantID: "tenant-a", ETag: `"a"`, FailureCount: 0},
{ID: "binding-b", ExternalTenantID: "tenant-b", ETag: `"b"`, FailureCount: 0},
{ID: "binding-c", ExternalTenantID: "tenant-c", FailureCount: 3},
{ID: "binding-d", ExternalTenantID: "tenant-d", FailureCount: 0},
}}
reader := tenantSyncReader{
tenants: map[string]identity.TenantContext{
"tenant-a": {TenantStatus: "active", TenantApplicationStatus: "active"},
"tenant-d": {TenantStatus: "suspended", TenantApplicationStatus: "active"},
},
unchanged: map[string]bool{"tenant-b": true},
errors: map[string]error{
"tenant-c": identity.ErrTenantContextUnavailable,
},
}
if err := synchronizeDueTenantContexts(context.Background(), repository, reader, "issuer", "application", now); err != nil {
t.Fatal(err)
}
if len(repository.synced) != 1 || repository.synced[0] != "binding-a" ||
len(repository.unchanged) != 1 || repository.unchanged[0] != "binding-b" ||
len(repository.rejected) != 1 || repository.rejected[0] != "binding-d" ||
len(repository.failed) != 1 || repository.failed[0] != "binding-c" {
t.Fatalf("sync outcomes=%#v", repository)
}
if repository.failureNext.Sub(now) != 4*time.Minute {
t.Fatalf("retry delay=%s", repository.failureNext.Sub(now))
}
}
func TestSynchronizeDueTenantContextsTreatsNotFoundAsRevocation(t *testing.T) {
repository := &tenantSyncRepository{targets: []store.OIDCTenantBindingSyncTarget{
{ID: "binding-a", ExternalTenantID: "tenant-a"},
}}
reader := tenantSyncReader{
tenants: map[string]identity.TenantContext{}, unchanged: map[string]bool{},
errors: map[string]error{"tenant-a": identity.ErrTenantContextNotFound},
}
if err := synchronizeDueTenantContexts(
context.Background(), repository, reader, "issuer", "application", time.Now(),
); err != nil {
t.Fatal(err)
}
if len(repository.rejected) != 1 || len(repository.failed) != 0 {
t.Fatalf("not-found outcome=%#v", repository)
}
}
func TestTenantContextRetryDelayIsBounded(t *testing.T) {
if got := tenantContextRetryDelay(0); got != 30*time.Second {
t.Fatalf("first retry=%s", got)
}
if got := tenantContextRetryDelay(100); got != 5*time.Minute {
t.Fatalf("bounded retry=%s", got)
}
if !errors.Is(identity.ErrTenantContextUnavailable, identity.ErrTenantContextUnavailable) {
t.Fatal("sentinel error changed unexpectedly")
}
}