package application import ( "context" "errors" "testing" "git.ddupan.top/panxiao81/ayatori/internal/database/domain/binding" "git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance" ) const ( preparationInstanceName = "credential-instance" preparationInstanceUID = "credential-instance-uid" ) func preparationRecord(t *testing.T) *CredentialRecord { t.Helper() tenant := binding.TenantIdentity{Namespace: bindingTestNamespace, Name: bindingTestName, UID: "credential-tenant-uid"} database := binding.Identity{Name: binding.DynamicDatabaseName(tenant.UID), UID: "credential-database-uid"} endpoint, err := instance.NewEndpoint(instance.EndpointValues{ Host: "postgres.example", HostAddr: "192.0.2.1", Port: 5432, ManagementDatabase: "management", TLSMode: instance.TLSVerifyFull, }) if err != nil { t.Fatal(err) } return &CredentialRecord{ Database: BindingDatabase{Database: binding.Database{ Identity: database, Instance: preparationInstanceName, InstanceUID: preparationInstanceUID, Name: bindingTestName, LoginRole: bindingTestName, Source: "Provision", Tenant: &tenant, }}, Tenant: &BindingTenant{ Identity: tenant, Phase: binding.Bound, Database: &database, Request: binding.Request{Provision: &binding.ProvisionRequest{Instance: preparationInstanceName}}, }, Instance: &CredentialInstance{Identity: binding.Identity{Name: preparationInstanceName, UID: preparationInstanceUID}, Ready: true, Endpoint: endpoint}, DatabaseProtected: true, TenantProtected: true, } } type memoryCredentialResources struct { record *CredentialRecord saveError error checkError error } func (r *memoryCredentialResources) Load(context.Context, string) (*CredentialRecord, error) { copy := *r.record return ©, nil } func (r *memoryCredentialResources) Save(_ context.Context, record *CredentialRecord, status CredentialStatus) (*CredentialRecord, error) { if r.saveError != nil { return nil, r.saveError } copy := *record copy.Status = status r.record = © return ©, nil } func (r *memoryCredentialResources) CheckCurrent(context.Context, *CredentialRecord) error { return r.checkError } type preparationStore struct { reads int creates int readError error createError error } func (*preparationStore) ProvisionLocation(uid string) (CredentialLocation, error) { return CredentialLocation{Mount: "applications", Path: "database/" + uid}, nil } func (s *preparationStore) ReadCredential(context.Context, CredentialLocation, int64) (ApplicationCredential, error) { s.reads++ return ApplicationCredential{}, s.readError } func (s *preparationStore) CreateCredential(context.Context, CredentialLocation, ApplicationCredential) (int64, error) { s.creates++ if s.createError != nil { return 0, s.createError } return 1, nil } func TestCredentialPreparationGates(t *testing.T) { for _, test := range []struct { name string change func(*CredentialRecord) }{ {"导入不供应", func(r *CredentialRecord) { r.Database.Source = "Import" }}, {"删除中的资源", func(r *CredentialRecord) { r.Database.Deleting = true }}, {"已释放资源", func(r *CredentialRecord) { r.Database.Phase = "Released" }}, {"缺少资源保护", func(r *CredentialRecord) { r.DatabaseProtected = false }}, {"缺少申请保护", func(r *CredentialRecord) { r.TenantProtected = false }}, {"单向绑定", func(r *CredentialRecord) { r.Tenant.Database = nil }}, {"删除中的申请", func(r *CredentialRecord) { r.Tenant.Deleting = true }}, {"申请尚未Bound", func(r *CredentialRecord) { r.Tenant.Phase = binding.Binding }}, {"旧申请身份", func(r *CredentialRecord) { r.Tenant.Identity.UID = "new-tenant" }}, {"旧资源身份", func(r *CredentialRecord) { r.Tenant.Database.UID = "new-database" }}, {"Instance未出现", func(r *CredentialRecord) { r.Instance = nil }}, {"Instance正在删除", func(r *CredentialRecord) { r.Instance.Deleting = true }}, {"Instance未Ready", func(r *CredentialRecord) { r.Instance.Ready = false }}, {"Instance身份未记录", func(r *CredentialRecord) { r.Database.InstanceUID = "" }}, {"Instance同名重建", func(r *CredentialRecord) { r.Instance.Identity.UID = "new-instance" }}, {"目标不一致", func(r *CredentialRecord) { r.Database.LoginRole = "another_owner" }}, } { t.Run(test.name, func(t *testing.T) { record := preparationRecord(t) test.change(record) resources := &memoryCredentialResources{record: record} store := &preparationStore{readError: ErrCredentialNotFound} if err := (CredentialPreparation{Resources: resources, Store: store}).Reconcile(t.Context(), record.Database.Identity.Name); err != nil { t.Fatal(err) } if store.reads != 0 || store.creates != 0 || resources.record.Status.Version != 0 { t.Fatal("前置条件不满足时不得读取、创建或确认凭据") } }) } } func TestCredentialPreparationWriteBoundary(t *testing.T) { for _, test := range []struct { name string saveError bool stale bool createError error wantError bool wantCreates int wantVersion int64 }{ {name: "位置无法保存", saveError: true, wantError: true}, {name: "外部操作前快照变化", stale: true, wantError: true}, {name: "明确权限拒绝", createError: ErrCredentialUnavailable, wantCreates: 1}, {name: "创建结果不确定", createError: ErrCredentialUncertain, wantCreates: 1}, {name: "并发创建冲突", createError: ErrCredentialConflict, wantCreates: 1}, {name: "创建与回读成功", wantCreates: 1, wantVersion: 1}, } { t.Run(test.name, func(t *testing.T) { resources := &memoryCredentialResources{record: preparationRecord(t)} if test.saveError { resources.saveError = errors.New("fixture write failure") } if test.stale { resources.checkError = errors.New("fixture stale observation") } store := &preparationStore{readError: ErrCredentialNotFound, createError: test.createError} service := CredentialPreparation{Resources: resources, Store: store} err := service.Reconcile(t.Context(), resources.record.Database.Identity.Name) if (err != nil) != test.wantError || store.creates != test.wantCreates || resources.record.Status.Version != test.wantVersion { t.Fatal("外部写入边界或确认时机不符合预期") } if test.createError == ErrCredentialUncertain || test.stale { resources.checkError = nil if err := service.Reconcile(t.Context(), resources.record.Database.Identity.Name); err != nil { t.Fatal(err) } if store.creates != test.wantCreates || resources.record.Status.Reason != binding.Conflict { t.Fatal("未确认创建重入时不得生成替代密码") } } }) } }