353 lines
13 KiB
Go
353 lines
13 KiB
Go
/*
|
|
Copyright 2026.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package instance_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
|
)
|
|
|
|
const testServerVersion = "17.6"
|
|
|
|
func completeChecks() instance.ManagementChecks {
|
|
return instance.ManagementChecks{
|
|
Connection: instance.CheckPassed, Metadata: instance.CheckPassed,
|
|
Roles: instance.CheckPassed, Databases: instance.CheckPassed,
|
|
Grants: instance.CheckPassed, Extensions: instance.CheckPassed,
|
|
}
|
|
}
|
|
|
|
func capability(t *testing.T, value *instance.Instance, checks instance.ManagementChecks,
|
|
registry instance.RegistryState,
|
|
) instance.CapabilityObservation {
|
|
t.Helper()
|
|
o, err := instance.NewCapabilityObservation(value.Target(), testServerVersion, checks, registry)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return o
|
|
}
|
|
|
|
func readyInstance(t *testing.T) *instance.Instance {
|
|
t.Helper()
|
|
i := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseInitializingRegistry}, false)
|
|
if err := i.AssessRegistryResult(instance.RegistryReadBack(capability(t, i, completeChecks(), instance.RegistryUsable))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := i.RequireProvisioningReady(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return i
|
|
}
|
|
|
|
func TestReadinessRequiresCompleteReadBack(t *testing.T) {
|
|
i := lifecycleInstance(t, instance.Snapshot{}, false)
|
|
if err := i.BeginValidation(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
absent := capability(t, i, completeChecks(), instance.RegistryAbsent)
|
|
if err := i.AssessManagement(absent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := i.Snapshot(); s.Phase != instance.PhaseInitializingRegistry || s.ObservedRevision != 0 || s.Readiness != instance.Unknown {
|
|
t.Fatalf("management observation prematurely concluded readiness: %+v", s)
|
|
}
|
|
for range 2 {
|
|
decision, err := i.PlanRegistryPreparation(absent)
|
|
if err != nil || decision != instance.PreparationAllowed {
|
|
t.Fatalf("preparation: %v, %v", decision, err)
|
|
}
|
|
if i.RequireProvisioningReady() == nil {
|
|
t.Fatal("preparation authorized provisioning")
|
|
}
|
|
}
|
|
if err := i.AssessRegistryResult(instance.RegistryReadBack(absent)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if i.Snapshot().Failure != instance.RegistryNotUsable || i.RequireProvisioningReady() == nil {
|
|
t.Fatal("absent registry accepted as ready")
|
|
}
|
|
usable := capability(t, i, completeChecks(), instance.RegistryUsable)
|
|
decision, err := i.PlanRegistryPreparation(usable)
|
|
if err != nil || decision != instance.AlreadyUsable {
|
|
t.Fatalf("retry after external preparation: %v, %v", decision, err)
|
|
}
|
|
if err := i.AssessRegistryResult(instance.RegistryReadBack(usable)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := i.Snapshot(); s.Readiness != instance.Ready || s.ReportedVersion != testServerVersion || s.ObservedRevision != i.Target().Revision().Value() {
|
|
t.Fatalf("complete observation not accepted: %+v", s)
|
|
}
|
|
if err := i.RequireProvisioningReady(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// 重启只恢复 checkpoint;依赖稍后恢复时必须重新取得完整事实。
|
|
func TestReadinessRecoveryAndInvalidation(t *testing.T) {
|
|
i := readyInstance(t)
|
|
restored, err := instance.Reconstitute(i.Target(), i.Snapshot(), false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if restored.RequireProvisioningReady() == nil {
|
|
t.Fatal("persisted Ready fabricated fresh evidence")
|
|
}
|
|
for range 2 {
|
|
if err := restored.AssessReadiness(capability(t, restored, completeChecks(), instance.RegistryUsable)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := restored.RequireProvisioningReady(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := restored.BeginValidation(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if restored.RequireProvisioningReady() == nil {
|
|
t.Fatal("validation retained evidence")
|
|
}
|
|
deleted, err := instance.Reconstitute(i.Target(), i.Snapshot(), true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if deleted.RequireProvisioningReady() == nil {
|
|
t.Fatal("deletion allowed provisioning")
|
|
}
|
|
if err := deleted.BeginDeletion(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if deleted.RequireProvisioningReady() == nil {
|
|
t.Fatal("deleting checkpoint allowed provisioning")
|
|
}
|
|
}
|
|
|
|
func TestEachManagementCheckIsRequired(t *testing.T) {
|
|
for field := range 6 {
|
|
for _, result := range []instance.CheckResult{instance.CheckUnobserved, instance.CheckUnavailable,
|
|
instance.CheckAuthenticationFailed, instance.CheckInsufficientPrivileges, 255} {
|
|
checks := completeChecks()
|
|
fields := []*instance.CheckResult{&checks.Connection, &checks.Metadata, &checks.Roles,
|
|
&checks.Databases, &checks.Grants, &checks.Extensions}
|
|
*fields[field] = result
|
|
i := readyInstance(t)
|
|
if err := i.AssessReadiness(capability(t, i, checks, instance.RegistryUsable)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := i.Snapshot(); s.Phase != instance.PhaseValidating || s.Readiness != instance.NotReady ||
|
|
s.Failure == instance.NoFailure || i.RequireProvisioningReady() == nil {
|
|
t.Fatalf("check %d result %d accepted: %+v", field, result, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegistryDecisionsAndReadinessLoss(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
state instance.RegistryState
|
|
decision instance.PreparationDecision
|
|
failure instance.Failure
|
|
}{
|
|
{instance.RegistryUsable, instance.AlreadyUsable, instance.NoFailure},
|
|
{instance.RegistryAbsent, instance.PreparationAllowed, instance.RegistryNotUsable},
|
|
{instance.RegistryNeedsMigration, instance.PreparationAllowed, instance.RegistryNotUsable},
|
|
{instance.RegistryUnsupported, instance.PreparationDenied, instance.RegistryIncompatible},
|
|
{instance.RegistryUnavailable, instance.PreparationDenied, instance.DependencyUnavailable},
|
|
{instance.RegistryUnobserved, instance.PreparationDenied, instance.ObservationIncomplete},
|
|
{255, instance.PreparationDenied, instance.ObservationIncomplete},
|
|
} {
|
|
i := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseInitializingRegistry}, false)
|
|
o := capability(t, i, completeChecks(), tc.state)
|
|
decision, err := i.PlanRegistryPreparation(o)
|
|
if err != nil || decision != tc.decision {
|
|
t.Fatalf("registry %d: %v, %v", tc.state, decision, err)
|
|
}
|
|
i = readyInstance(t)
|
|
if err := i.AssessReadiness(o); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if i.Snapshot().Failure != tc.failure {
|
|
t.Fatalf("registry %d: %+v", tc.state, i.Snapshot())
|
|
}
|
|
if tc.state != instance.RegistryUsable {
|
|
wantPhase := instance.PhaseInitializingRegistry
|
|
if tc.state == instance.RegistryUnavailable {
|
|
wantPhase = instance.PhaseValidating
|
|
}
|
|
if i.Snapshot().Phase != wantPhase || i.RequireProvisioningReady() == nil {
|
|
t.Fatal("registry drift retained readiness")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInitializationRejectsIncompleteOrFailedManagement(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
checks instance.ManagementChecks
|
|
registry instance.RegistryState
|
|
failure instance.Failure
|
|
}{
|
|
{instance.ManagementChecks{}, instance.RegistryUsable, instance.ObservationIncomplete},
|
|
{completeChecks(), instance.RegistryUnsupported, instance.RegistryIncompatible},
|
|
{completeChecks(), instance.RegistryUnavailable, instance.DependencyUnavailable},
|
|
} {
|
|
i := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseValidating}, false)
|
|
if err := i.AssessManagement(capability(t, i, tc.checks, tc.registry)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := i.Snapshot(); s.Phase != instance.PhaseValidating || s.Failure != tc.failure ||
|
|
s.ObservedRevision != i.Target().Revision().Value() || s.Readiness != instance.NotReady {
|
|
t.Fatalf("invalid management accepted: %+v", s)
|
|
}
|
|
}
|
|
i := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseInitializingRegistry}, false)
|
|
decision, err := i.PlanRegistryPreparation(capability(t, i, instance.ManagementChecks{}, instance.RegistryAbsent))
|
|
if err != nil || decision != instance.PreparationDenied || i.Snapshot().Failure != instance.ObservationIncomplete {
|
|
t.Fatal("incomplete management allowed registry writes")
|
|
}
|
|
if err := i.AssessRegistryResult(instance.RegistryReadBack(capability(t, i, completeChecks(), instance.RegistryUsable))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := i.RequireProvisioningReady(); err != nil {
|
|
t.Fatal("dependency recovery did not restore readiness", err)
|
|
}
|
|
}
|
|
|
|
func TestReadinessMethodsRejectWrongPhaseAndDeletion(t *testing.T) {
|
|
for _, deleting := range []bool{false, true} {
|
|
for _, phase := range []instance.Phase{instance.PhasePending, instance.PhaseValidating,
|
|
instance.PhaseInitializingRegistry, instance.PhaseReady, instance.PhaseDeleting} {
|
|
for _, operation := range []struct {
|
|
phase instance.Phase
|
|
apply func(*instance.Instance, instance.CapabilityObservation) error
|
|
}{
|
|
{instance.PhaseValidating, (*instance.Instance).AssessManagement},
|
|
{instance.PhaseReady, (*instance.Instance).AssessReadiness},
|
|
{instance.PhaseInitializingRegistry, func(i *instance.Instance, o instance.CapabilityObservation) error {
|
|
_, err := i.PlanRegistryPreparation(o)
|
|
return err
|
|
}},
|
|
{instance.PhaseInitializingRegistry, func(i *instance.Instance, o instance.CapabilityObservation) error {
|
|
return i.AssessRegistryResult(instance.RegistryReadBack(o))
|
|
}},
|
|
} {
|
|
if !deleting && operation.phase == phase {
|
|
continue
|
|
}
|
|
i := lifecycleInstance(t, instance.Snapshot{Phase: phase}, deleting)
|
|
before := i.Snapshot()
|
|
if err := operation.apply(i, capability(t, i, completeChecks(), instance.RegistryUsable)); err == nil {
|
|
t.Fatalf("phase %s deleting=%t accepted operation for %s", phase, deleting, operation.phase)
|
|
}
|
|
if i.Snapshot() != before {
|
|
t.Fatal("rejected operation mutated snapshot")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOldGenerationObservationDoesNotReplaceEvidence(t *testing.T) {
|
|
i := readyInstance(t)
|
|
target := i.Target()
|
|
revision, err := instance.NewRevision(target.Revision().Value() + 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := instance.NewObservationTarget(target.Identity(), revision, target.Definition())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
o, err := instance.NewCapabilityObservation(other, testServerVersion, completeChecks(), instance.RegistryUsable)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := i.Snapshot()
|
|
if err := i.AssessReadiness(o); err == nil || i.Snapshot() != before {
|
|
t.Fatal("mismatched generation observation was accepted")
|
|
}
|
|
if err := i.RequireProvisioningReady(); err != nil {
|
|
t.Fatal("rejected unrelated input changed previously accepted evidence", err)
|
|
}
|
|
}
|
|
|
|
func TestPreparationFailureCannotEstablishReadiness(t *testing.T) {
|
|
i := lifecycleInstance(t, instance.Snapshot{Phase: instance.PhaseInitializingRegistry}, false)
|
|
for _, failure := range []instance.Failure{instance.DependencyUnavailable, instance.AuthenticationFailed,
|
|
instance.InsufficientPrivileges, instance.RegistryIncompatible} {
|
|
result, err := instance.RegistryPreparationFailed(i.Target(), failure)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := i.AssessRegistryResult(result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := i.Snapshot(); s.Failure != failure || s.Readiness != instance.NotReady ||
|
|
s.Phase != instance.PhaseInitializingRegistry || i.RequireProvisioningReady() == nil {
|
|
t.Fatalf("failed operation accepted: %+v", s)
|
|
}
|
|
}
|
|
for _, failure := range []instance.Failure{instance.NoFailure, 255} {
|
|
if _, err := instance.RegistryPreparationFailed(i.Target(), failure); err == nil {
|
|
t.Fatal("invalid failure accepted")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCapabilityInputsAndLifecycleGuards(t *testing.T) {
|
|
i := readyInstance(t)
|
|
if _, err := instance.NewCapabilityObservation(instance.ObservationTarget{}, testServerVersion,
|
|
completeChecks(), instance.RegistryUsable); err == nil {
|
|
t.Fatal("invalid target accepted")
|
|
}
|
|
if _, err := instance.RegistryPreparationFailed(instance.ObservationTarget{}, instance.DependencyUnavailable); err == nil {
|
|
t.Fatal("invalid failure target accepted")
|
|
}
|
|
for _, method := range []func(instance.CapabilityObservation) error{
|
|
i.AssessManagement, i.AssessReadiness,
|
|
func(o instance.CapabilityObservation) error { _, err := i.PlanRegistryPreparation(o); return err },
|
|
func(o instance.CapabilityObservation) error {
|
|
return i.AssessRegistryResult(instance.RegistryReadBack(o))
|
|
},
|
|
} {
|
|
before := i.Snapshot()
|
|
if err := method(instance.CapabilityObservation{}); err == nil || i.Snapshot() != before {
|
|
t.Fatal("mismatched observation accepted or mutated state")
|
|
}
|
|
}
|
|
old := i.Snapshot()
|
|
old.ObservedRevision = 0
|
|
changed := lifecycleInstance(t, old, false)
|
|
if err := changed.AssessReadiness(capability(t, changed, completeChecks(), instance.RegistryUsable)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := changed.Snapshot(); s.Phase != instance.PhaseValidating || s.ObservedRevision != 0 || s.Readiness != instance.Unknown {
|
|
t.Fatalf("changed generation accepted old checkpoint: %+v", s)
|
|
}
|
|
o, err := instance.NewCapabilityObservation(i.Target(), "", completeChecks(), instance.RegistryUsable)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := i.AssessReadiness(o); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if i.Snapshot().Failure != instance.ObservationIncomplete {
|
|
t.Fatal("missing version accepted")
|
|
}
|
|
}
|