feat: 接通 Database 凭据准备闭环
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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("未确认创建重入时不得生成替代密码")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user