374 lines
16 KiB
Go
374 lines
16 KiB
Go
package app_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"proxyctl/internal/app"
|
|
"proxyctl/internal/profile"
|
|
"proxyctl/internal/target"
|
|
)
|
|
|
|
func TestApplicationSnapshotStructuresContextualTargetActions(t *testing.T) {
|
|
states := []struct {
|
|
id string
|
|
state target.State
|
|
action app.TargetActionKind
|
|
}{
|
|
{id: "disabled", state: target.Disabled, action: app.EnableTargetAction},
|
|
{id: "enabled", state: target.Enabled, action: app.DisableTargetAction},
|
|
{id: "external", state: target.External, action: app.ApplyProfileTargetAction},
|
|
{id: "drifted", state: target.Drifted, action: app.ResolveDriftTargetAction},
|
|
}
|
|
adapters := make([]target.Target, 0, len(states))
|
|
for _, item := range states {
|
|
adapters = append(adapters, targetWithID{fakeTarget: &fakeTarget{available: true, state: item.state}, id: item.id})
|
|
}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), adapters)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
|
|
snapshot := application.Snapshot()
|
|
for i, want := range states {
|
|
got := snapshot.Targets[i]
|
|
if got.PrimaryAction.Kind != want.action || !got.PrimaryAction.Available || got.PrimaryAction.DisabledReason != "" {
|
|
t.Errorf("%s primary action = %#v, want available %v", want.state, got.PrimaryAction, want.action)
|
|
}
|
|
}
|
|
drift := snapshot.Targets[3]
|
|
if len(drift.DriftResolutionActions) != 2 || drift.DriftResolutionActions[0].Kind != app.ReapplyActiveProfileAction || drift.DriftResolutionActions[1].Kind != app.RestoreOriginalAction {
|
|
t.Fatalf("Drift Resolution actions = %#v", drift.DriftResolutionActions)
|
|
}
|
|
for _, action := range drift.DriftResolutionActions {
|
|
if !action.Available || action.DisabledReason != "" {
|
|
t.Errorf("Drift Resolution action = %#v, want available", action)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApplicationSnapshotMakesUnknownTargetStateStructurallyUnavailable(t *testing.T) {
|
|
adapter := &fakeTarget{available: true}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
|
|
got := application.Snapshot().Targets[0].PrimaryAction
|
|
const reason = "Target actions are unavailable because this Target state is unknown"
|
|
if got.Kind != 0 || got.Available || got.DisabledReason != reason {
|
|
t.Fatalf("unknown-state action = %#v, want structurally unavailable with reason %q", got, reason)
|
|
}
|
|
}
|
|
|
|
func TestApplicationDispatchesExplicitContextualTargetIntents(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
state target.State
|
|
intent app.IntentKind
|
|
enabled bool
|
|
}{
|
|
{name: "Enable", state: target.Disabled, intent: app.EnableTarget, enabled: true},
|
|
{name: "Disable", state: target.Enabled, intent: app.DisableTarget, enabled: false},
|
|
{name: "Apply Profile", state: target.External, intent: app.ApplyProfileToTarget, enabled: true},
|
|
{name: "Reapply Active Profile", state: target.Drifted, intent: app.ReapplyActiveProfile, enabled: true},
|
|
{name: "Restore Original", state: target.Drifted, intent: app.RestoreOriginal, enabled: false},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
adapter := &fakeTarget{available: true, state: target.Disabled}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
adapter.state = test.state
|
|
adapter.calls = nil
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
|
|
result := application.Dispatch(app.Intent{Kind: test.intent, TargetID: adapter.ID()})
|
|
if result.Err != nil || len(adapter.calls) != 1 || adapter.calls[0] != test.enabled {
|
|
t.Fatalf("result = %#v, SetDesired calls = %#v", result, adapter.calls)
|
|
}
|
|
wantState := target.Disabled
|
|
if test.enabled {
|
|
wantState = target.Enabled
|
|
}
|
|
if result.Snapshot.Targets[0].State != wantState {
|
|
t.Fatalf("final state = %s, want %s", result.Snapshot.Targets[0].State, wantState)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplicationRejectsContextualIntentWhenTargetStateChanged(t *testing.T) {
|
|
adapter := &fakeTarget{available: true, state: target.External}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
|
|
result := application.Dispatch(app.Intent{Kind: app.EnableTarget, TargetID: adapter.ID()})
|
|
if result.Err == nil || !strings.Contains(result.Err.Error(), "External") || len(adapter.calls) != 0 {
|
|
t.Fatalf("stale Enable intent = %#v, calls = %#v", result, adapter.calls)
|
|
}
|
|
}
|
|
|
|
func TestApplicationReapplyPublishesTruthfulDriftWhenAdapterCannotConverge(t *testing.T) {
|
|
adapter := &fakeTarget{available: true, state: target.Disabled}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
adapter.state = target.Drifted
|
|
adapter.setState = target.Drifted
|
|
adapter.calls = nil
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
|
|
result := application.Dispatch(app.Intent{Kind: app.ReapplyActiveProfile, TargetID: adapter.ID()})
|
|
if result.Err != nil || result.Snapshot.Targets[0].State != target.Drifted || result.Progress[0].State != target.Drifted {
|
|
t.Fatalf("truthful non-converged reapply = %#v", result)
|
|
}
|
|
}
|
|
|
|
type changingAvailabilityTarget struct {
|
|
*fakeTarget
|
|
detections int
|
|
}
|
|
|
|
func (f *changingAvailabilityTarget) Detect(context.Context, *profile.Profile) target.Detection {
|
|
f.detections++
|
|
available := f.detections == 1
|
|
return target.Detection{
|
|
Availability: target.Availability{Available: available, Evidence: map[bool]string{true: "available", false: "no longer available"}[available]},
|
|
Inspection: target.Inspection{State: target.External, Evidence: "actual state"},
|
|
}
|
|
}
|
|
|
|
func TestApplicationRechecksAvailabilityImmediatelyBeforeTargetMutation(t *testing.T) {
|
|
base := &fakeTarget{available: true, state: target.External}
|
|
adapter := &changingAvailabilityTarget{fakeTarget: base}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
if before := application.Snapshot().Targets[0]; !before.Available || !before.PrimaryAction.Available {
|
|
t.Fatalf("initial detection = %#v, want available action", before)
|
|
}
|
|
|
|
result := application.Dispatch(app.Intent{Kind: app.ApplyProfileToTarget, TargetID: adapter.ID()})
|
|
const reason = "Target actions are unavailable because this Target is unavailable"
|
|
if result.Err == nil || result.Err.Error() != reason || len(base.calls) != 0 {
|
|
t.Fatalf("available-to-unavailable result = %#v, SetDesired calls = %#v", result, base.calls)
|
|
}
|
|
if got := result.Snapshot.Targets[0]; got.Available || got.AvailabilityEvidence != "no longer available" || got.PrimaryAction.Available || got.PrimaryAction.DisabledReason != reason {
|
|
t.Fatalf("published targeted detection = %#v", got)
|
|
}
|
|
}
|
|
|
|
type failedDetectionTarget struct {
|
|
*fakeTarget
|
|
}
|
|
|
|
func (f failedDetectionTarget) Detect(context.Context, *profile.Profile) target.Detection {
|
|
return target.Detection{
|
|
Availability: target.Availability{Available: true, Evidence: "detected"},
|
|
Inspection: target.Inspection{State: f.state, Evidence: "actual state"},
|
|
Err: errors.New("detection failed"),
|
|
}
|
|
}
|
|
|
|
func TestApplicationRejectsStructuredUnavailableActionWithoutAdapterMutation(t *testing.T) {
|
|
adapter := &fakeTarget{available: false, state: target.External}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
adapter.calls = nil
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
if action := application.Snapshot().Targets[0].PrimaryAction; action.Available || !strings.Contains(action.DisabledReason, "unavailable") {
|
|
t.Fatalf("unavailable action = %#v", action)
|
|
}
|
|
|
|
result := application.Dispatch(app.Intent{Kind: app.ApplyProfileToTarget, TargetID: adapter.ID()})
|
|
if result.Err == nil || !strings.Contains(result.Err.Error(), "unavailable") || len(adapter.calls) != 0 {
|
|
t.Fatalf("unavailable action result = %#v, calls=%#v", result, adapter.calls)
|
|
}
|
|
}
|
|
|
|
func TestApplicationValidatesEveryStructuredDisabledReasonBeforeAdapterMutation(t *testing.T) {
|
|
assertRejected := func(t *testing.T, application *app.Application, adapter *fakeTarget, intent app.Intent, reason string) {
|
|
t.Helper()
|
|
adapter.calls = nil
|
|
result := application.Dispatch(intent)
|
|
if result.Err == nil || !strings.Contains(result.Err.Error(), reason) || len(adapter.calls) != 0 {
|
|
t.Fatalf("disabled action result = %#v, calls = %#v, want reason containing %q", result, adapter.calls, reason)
|
|
}
|
|
}
|
|
|
|
t.Run("failed check", func(t *testing.T) {
|
|
adapter := &fakeTarget{available: true, state: target.Disabled}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{failedDetectionTarget{fakeTarget: adapter}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err == nil {
|
|
t.Fatal("Target check unexpectedly succeeded")
|
|
}
|
|
assertRejected(t, application, adapter, app.Intent{Kind: app.EnableTarget, TargetID: adapter.ID()}, "check failed")
|
|
})
|
|
|
|
t.Run("unavailable Drift Resolution", func(t *testing.T) {
|
|
adapter := &fakeTarget{available: false, state: target.Drifted}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
assertRejected(t, application, adapter, app.Intent{Kind: app.RestoreOriginal, TargetID: adapter.ID()}, "unavailable")
|
|
})
|
|
}
|
|
|
|
func TestApplicationRejectsProfileDependentIntentsWithoutActiveProfile(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
state target.State
|
|
intent app.IntentKind
|
|
}{
|
|
{name: "Apply Profile", state: target.External, intent: app.ApplyProfileToTarget},
|
|
{name: "Reapply Active Profile", state: target.Drifted, intent: app.ReapplyActiveProfile},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
adapter := &fakeTarget{available: true, state: test.state}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
result := application.Dispatch(app.Intent{Kind: test.intent, TargetID: adapter.ID()})
|
|
if result.Err == nil || !strings.Contains(result.Err.Error(), "no Active Profile") || len(adapter.calls) != 0 {
|
|
t.Fatalf("missing Active Profile result = %#v, calls=%#v", result, adapter.calls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplicationRevalidatesRestoreAfterOwnershipSensitiveStateChange(t *testing.T) {
|
|
adapter := &fakeTarget{available: true, state: target.Drifted}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
|
|
// Model ownership loss as a fresh inspection becoming External between the
|
|
// displayed Drift Resolution and dispatch. The stale Restore intent must not
|
|
// reach SetDesired, where it could otherwise overwrite external state.
|
|
adapter.state = target.External
|
|
adapter.calls = nil
|
|
result := application.Dispatch(app.Intent{Kind: app.RestoreOriginal, TargetID: adapter.ID()})
|
|
if result.Err == nil || !strings.Contains(result.Err.Error(), "External") || len(adapter.calls) != 0 || result.Snapshot.Targets[0].State != target.External {
|
|
t.Fatalf("stale Restore result = %#v, calls = %#v", result, adapter.calls)
|
|
}
|
|
}
|
|
|
|
func TestApplicationReapplyAndRestoreFailuresPublishFreshDriftedState(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
intent app.IntentKind
|
|
err string
|
|
}{
|
|
{name: "reapply", intent: app.ReapplyActiveProfile, err: "write failed"},
|
|
{name: "restore ownership loss", intent: app.RestoreOriginal, err: "restoration evidence is insufficient; refusing to overwrite external changes"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
adapter := &fakeTarget{available: true, state: target.Drifted, setErr: errors.New(test.err)}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createAndActivate(t, application)
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
|
|
result := application.Dispatch(app.Intent{Kind: test.intent, TargetID: adapter.ID()})
|
|
if result.Err == nil || !strings.Contains(result.Err.Error(), test.err) || result.Snapshot.Targets[0].State != target.Drifted || result.Snapshot.Targets[0].Evidence != "actual state" {
|
|
t.Fatalf("failure = %#v", result)
|
|
}
|
|
if len(adapter.calls) != 1 || adapter.calls[0] != (test.intent == app.ReapplyActiveProfile) {
|
|
t.Fatalf("SetDesired calls = %#v", adapter.calls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplicationSnapshotDisablesProfileDependentActionsWithoutActiveProfile(t *testing.T) {
|
|
adapters := []target.Target{
|
|
targetWithID{fakeTarget: &fakeTarget{available: true, state: target.Disabled}, id: "disabled"},
|
|
targetWithID{fakeTarget: &fakeTarget{available: true, state: target.External}, id: "external"},
|
|
targetWithID{fakeTarget: &fakeTarget{available: true, state: target.Drifted}, id: "drifted"},
|
|
}
|
|
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), adapters)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result := application.Dispatch(app.Intent{Kind: app.RefreshTargets}); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
|
|
snapshot := application.Snapshot()
|
|
for _, index := range []int{0, 1} {
|
|
action := snapshot.Targets[index].PrimaryAction
|
|
if action.Available || !strings.Contains(action.DisabledReason, "no Active Profile") {
|
|
t.Errorf("profile-dependent primary action = %#v", action)
|
|
}
|
|
}
|
|
drift := snapshot.Targets[2]
|
|
if !drift.PrimaryAction.Available {
|
|
t.Fatalf("Restore Original should keep Resolve available: %#v", drift.PrimaryAction)
|
|
}
|
|
if got := drift.DriftResolutionActions[0]; got.Available || !strings.Contains(got.DisabledReason, "no Active Profile") {
|
|
t.Fatalf("Reapply action without Active Profile = %#v", got)
|
|
}
|
|
if got := drift.DriftResolutionActions[1]; !got.Available {
|
|
t.Fatalf("Restore Original action without Active Profile = %#v", got)
|
|
}
|
|
}
|