mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-04 21:33:23 +00:00
Add purely-additive branch-coverage tests for freshly-landed, previously- untested backend surface: - internal/agentexec: docker container update codec (decode/validate/bind/ digest/result-cross-validation), residual host apt codec validation arms (host update + storage cleanup decode/bind/result/operation-query identity), and NormalizeDeployMaxParallel clamping. - pkg/licensing: service helpers (unionFeatures, safeIntFromInt64, remainingDaysCeil, ensureGracePeriodEnd, Set/Get Evaluator+StateMachine, IsValid, CurrentUnsafeForTesting, dev-mode) and activation GrantEnvelope ParseExpiresAt + uncapped-core-monitoring tier classification. No source changed.
748 lines
28 KiB
Go
748 lines
28 KiB
Go
package agentexec
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/operationreceipt"
|
|
)
|
|
|
|
// --- JSON / identity helpers for coverage tests ---
|
|
|
|
func covHostUpdateDigest(actionID, operation string, opVersion int, hash string) string {
|
|
d, err := hostUpdateRequestDigest(HostUpdatePayload{
|
|
ActionID: actionID,
|
|
Operation: operation,
|
|
OperationVersion: opVersion,
|
|
ExpectedInventoryHash: hash,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return d
|
|
}
|
|
|
|
func covHostStorageCleanupDigest(actionID, operation string, opVersion int, fingerprint string) string {
|
|
d, err := hostStorageCleanupRequestDigest(HostStorageCleanupPayload{
|
|
ActionID: actionID,
|
|
Operation: operation,
|
|
OperationVersion: opVersion,
|
|
ExpectedFingerprint: fingerprint,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return d
|
|
}
|
|
|
|
func covBuildHostUpdatePayloadJSON(t *testing.T, requestID, actionID, operation string, opVersion int, hash string, timeout int) []byte {
|
|
t.Helper()
|
|
raw, err := json.Marshal(map[string]any{
|
|
"request_id": requestID,
|
|
"action_id": actionID,
|
|
"operation": operation,
|
|
"operation_version": opVersion,
|
|
"request_digest": covHostUpdateDigest(actionID, operation, opVersion, hash),
|
|
"expected_inventory_hash": hash,
|
|
"timeout": timeout,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func covBuildHostStorageCleanupPayloadJSON(t *testing.T, requestID, actionID, operation string, opVersion int, fingerprint string, timeout int) []byte {
|
|
t.Helper()
|
|
raw, err := json.Marshal(map[string]any{
|
|
"request_id": requestID,
|
|
"action_id": actionID,
|
|
"operation": operation,
|
|
"operation_version": opVersion,
|
|
"request_digest": covHostStorageCleanupDigest(actionID, operation, opVersion, fingerprint),
|
|
"expected_fingerprint": fingerprint,
|
|
"timeout": timeout,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func covWithJSONField(t *testing.T, raw []byte, key string, value any) []byte {
|
|
t.Helper()
|
|
var m map[string]any
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m[key] = value
|
|
out, err := json.Marshal(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func covHostUpdateIdentity(agentID string) operationreceipt.Identity {
|
|
req := HostUpdatePayload{
|
|
RequestID: "cov.dispatch.1",
|
|
ActionID: "cov-action",
|
|
Operation: HostUpdateOperationInstall,
|
|
ExpectedInventoryHash: "sha256:" + strings.Repeat("a", 64),
|
|
}
|
|
if err := BindHostUpdatePayload(&req); err != nil {
|
|
panic(err)
|
|
}
|
|
return HostUpdateOperationIdentity(agentID, req)
|
|
}
|
|
|
|
func covHostCleanupIdentity(agentID string) operationreceipt.Identity {
|
|
req := HostStorageCleanupPayload{
|
|
RequestID: "cov.dispatch.1",
|
|
ActionID: "cov-action",
|
|
Operation: HostStorageCleanupOperationPackageCache,
|
|
ExpectedFingerprint: "sha256:" + strings.Repeat("c", 64),
|
|
}
|
|
if err := BindHostStorageCleanupPayload(&req); err != nil {
|
|
panic(err)
|
|
}
|
|
return HostStorageCleanupOperationIdentity(agentID, req)
|
|
}
|
|
|
|
func covTerminalRecord(identity operationreceipt.Identity, resultKind string, resultVersion int, result json.RawMessage, terminalAt time.Time) *operationreceipt.Record {
|
|
return &operationreceipt.Record{
|
|
Identity: identity,
|
|
State: operationreceipt.StateTerminal,
|
|
AcceptedAt: terminalAt.Add(-10 * time.Minute),
|
|
StartedAt: terminalAt.Add(-9 * time.Minute),
|
|
TerminalAt: terminalAt,
|
|
ResultKind: resultKind,
|
|
ResultVersion: resultVersion,
|
|
Result: result,
|
|
}
|
|
}
|
|
|
|
func covAcceptedRecord(identity operationreceipt.Identity, acceptedAt time.Time) *operationreceipt.Record {
|
|
return &operationreceipt.Record{
|
|
Identity: identity,
|
|
State: operationreceipt.StateAccepted,
|
|
AcceptedAt: acceptedAt,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DecodeHostUpdatePayload — apt_codec.go:32
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCoverageDecodeHostUpdatePayloadRejectionArms(t *testing.T) {
|
|
hash := "sha256:" + strings.Repeat("a", 64)
|
|
valid := covBuildHostUpdatePayloadJSON(t, "r1", "a1", HostUpdateOperationInstall, HostAPTOperationVersion, hash, 30)
|
|
|
|
// Success case.
|
|
t.Run("valid payload decodes", func(t *testing.T) {
|
|
got, err := DecodeHostUpdatePayload(valid)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got.RequestID != "r1" || got.ActionID != "a1" || got.Operation != HostUpdateOperationInstall {
|
|
t.Fatalf("decoded payload mismatch: %#v", got)
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
body []byte
|
|
wantSub string
|
|
}{
|
|
{"empty payload", []byte(""), "empty"},
|
|
{"whitespace only", []byte(" \n\t "), "empty"},
|
|
{"malformed JSON", []byte(`{"request_id":"incomplete`), ""},
|
|
{"unknown field rejected", covWithJSONField(t, valid, "rogue_field", true), "unknown field"},
|
|
{"trailing JSON", append(append([]byte{}, valid...), '{', '}'), "trailing"},
|
|
{"missing request id", covBuildHostUpdatePayloadJSON(t, "", "a1", HostUpdateOperationInstall, HostAPTOperationVersion, hash, 30), "request id is required"},
|
|
{"empty action id", covBuildHostUpdatePayloadJSON(t, "r1", "", HostUpdateOperationInstall, HostAPTOperationVersion, hash, 30), "action id is required"},
|
|
{"unsupported operation", covBuildHostUpdatePayloadJSON(t, "r1", "a1", "bogus_op", HostAPTOperationVersion, hash, 30), "unsupported host update operation"},
|
|
{"unsupported operation version", covBuildHostUpdatePayloadJSON(t, "r1", "a1", HostUpdateOperationInstall, 99, hash, 30), "unsupported host update operation version"},
|
|
{"request digest mismatch", covWithJSONField(t, valid, "request_digest", "sha256:"+strings.Repeat("0", 64)), "digest mismatch"},
|
|
{"invalid inventory hash", covBuildHostUpdatePayloadJSON(t, "r1", "a1", HostUpdateOperationInstall, HostAPTOperationVersion, "not-a-hash", 30), "expected inventory hash"},
|
|
{"negative timeout", covBuildHostUpdatePayloadJSON(t, "r1", "a1", HostUpdateOperationInstall, HostAPTOperationVersion, hash, -1), "timeout"},
|
|
{"timeout exceeds maximum", covBuildHostUpdatePayloadJSON(t, "r1", "a1", HostUpdateOperationInstall, HostAPTOperationVersion, hash, 9999), "timeout"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := DecodeHostUpdatePayload(tc.body)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if tc.wantSub != "" && !strings.Contains(err.Error(), tc.wantSub) {
|
|
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DecodeHostStorageCleanupPayload — apt_codec.go:54
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCoverageDecodeHostStorageCleanupPayloadRejectionArms(t *testing.T) {
|
|
fp := "sha256:" + strings.Repeat("c", 64)
|
|
valid := covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp, 30)
|
|
|
|
t.Run("valid payload decodes", func(t *testing.T) {
|
|
got, err := DecodeHostStorageCleanupPayload(valid)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got.RequestID != "r1" || got.ActionID != "a1" || got.Operation != HostStorageCleanupOperationPackageCache {
|
|
t.Fatalf("decoded payload mismatch: %#v", got)
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
body []byte
|
|
wantSub string
|
|
}{
|
|
{"empty payload", []byte(""), "empty"},
|
|
{"whitespace only", []byte("\t\n "), "empty"},
|
|
{"malformed JSON", []byte(`{"request_id": invalid`), ""},
|
|
{"unknown field rejected", covWithJSONField(t, valid, "rogue_field", 42), "unknown field"},
|
|
{"trailing JSON", append(append([]byte{}, valid...), '{', '}'), "trailing"},
|
|
{"missing request id", covBuildHostStorageCleanupPayloadJSON(t, "", "a1", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp, 30), "invalid request id"},
|
|
{"empty action id", covBuildHostStorageCleanupPayloadJSON(t, "r1", "", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp, 30), "invalid action id"},
|
|
{"unsupported operation", covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", "bogus_cleanup", HostAPTOperationVersion, fp, 30), "unsupported host storage cleanup operation"},
|
|
{"unsupported operation version", covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", HostStorageCleanupOperationPackageCache, 77, fp, 30), "unsupported host storage cleanup operation version"},
|
|
{"request digest mismatch", covWithJSONField(t, valid, "request_digest", "sha256:"+strings.Repeat("9", 64)), "digest mismatch"},
|
|
{"invalid fingerprint", covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, "garbage-fp", 30), "expected cleanup fingerprint"},
|
|
{"negative timeout", covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp, -1), "timeout"},
|
|
{"timeout exceeds maximum", covBuildHostStorageCleanupPayloadJSON(t, "r1", "a1", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp, 9999), "timeout"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := DecodeHostStorageCleanupPayload(tc.body)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if tc.wantSub != "" && !strings.Contains(err.Error(), tc.wantSub) {
|
|
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ValidateOperationQueryResultForIdentity — apt_codec.go:98
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCoverageValidateOperationQueryResultForIdentity(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
updateIdentity := covHostUpdateIdentity("agent-1")
|
|
cleanupIdentity := covHostCleanupIdentity("agent-1")
|
|
|
|
// A mismatched identity for the identity-mismatch arm.
|
|
otherIdentity := updateIdentity
|
|
otherIdentity.ActionID = "different-action"
|
|
|
|
// A bare identity with an unsupported operation kind (still normalisable).
|
|
bogusIdentity := operationreceipt.Identity{
|
|
AttemptID: "cov.dispatch.1",
|
|
ActionID: "cov-action",
|
|
OperationKind: "bogus_operation",
|
|
OperationVersion: 1,
|
|
RequestDigest: "sha256:" + strings.Repeat("a", 64),
|
|
AgentID: "agent-1",
|
|
}
|
|
|
|
// Valid not-found result (success path: nil record, no error).
|
|
notFoundOK := operationreceipt.QueryResult{Version: operationreceipt.ProtocolVersion, Status: operationreceipt.QueryNotFound}
|
|
|
|
// Not-found with a stray record.
|
|
notFoundWithRecord := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryNotFound,
|
|
Record: covAcceptedRecord(updateIdentity, now),
|
|
}
|
|
|
|
// Unsupported version.
|
|
badVersion := operationreceipt.QueryResult{Version: 99, Status: operationreceipt.QueryNotFound}
|
|
|
|
// Found-terminal but nil record.
|
|
nilRecord := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundTerminal,
|
|
}
|
|
|
|
// Identity mismatch (record identity differs).
|
|
mismatchResult := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundInterrupted,
|
|
Record: covAcceptedRecord(otherIdentity, now),
|
|
}
|
|
|
|
// ValidateRecord failure (zero AcceptedAt).
|
|
badRecord := &operationreceipt.Record{
|
|
Identity: updateIdentity,
|
|
State: operationreceipt.StateAccepted,
|
|
AcceptedAt: time.Time{},
|
|
}
|
|
validateRecordFail := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundInterrupted,
|
|
Record: badRecord,
|
|
}
|
|
|
|
// Unknown status (neither terminal nor interrupted).
|
|
unknownStatus := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: "bogus_status",
|
|
Record: covAcceptedRecord(updateIdentity, now),
|
|
}
|
|
|
|
// Interrupted status with a terminal-state record (state mismatch).
|
|
interruptedTerminal := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundInterrupted,
|
|
Record: covTerminalRecord(updateIdentity, "any", 1, json.RawMessage(`{}`), now),
|
|
}
|
|
|
|
// Terminal status with a non-terminal-state record.
|
|
terminalNonTerminalRecord := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundTerminal,
|
|
Record: covAcceptedRecord(updateIdentity, now),
|
|
}
|
|
|
|
// Valid interrupted result (success path).
|
|
validInterrupted := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundInterrupted,
|
|
Record: covAcceptedRecord(updateIdentity, now),
|
|
}
|
|
|
|
// Terminal but receivedAt is zero.
|
|
zeroReceivedAt := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundTerminal,
|
|
Record: covTerminalRecord(updateIdentity, HostUpdateReceiptKind, HostAPTReceiptVersion, json.RawMessage(`{}`), now),
|
|
}
|
|
|
|
// Host update envelope mismatch (wrong ResultKind).
|
|
updateEnvelopeMismatch := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundTerminal,
|
|
Record: covTerminalRecord(updateIdentity, "wrong.kind", HostAPTReceiptVersion, json.RawMessage(`{}`), now),
|
|
}
|
|
|
|
// Host update decode error (correct envelope, malformed payload).
|
|
updateDecodeError := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundTerminal,
|
|
Record: covTerminalRecord(updateIdentity, HostUpdateReceiptKind, HostAPTReceiptVersion, json.RawMessage(`{"rogue":true}`), now),
|
|
}
|
|
|
|
// Host storage cleanup envelope mismatch.
|
|
cleanupEnvelopeMismatch := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundTerminal,
|
|
Record: covTerminalRecord(cleanupIdentity, "wrong.kind", HostAPTReceiptVersion, json.RawMessage(`{}`), now),
|
|
}
|
|
|
|
// Host storage cleanup decode error.
|
|
cleanupDecodeError := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundTerminal,
|
|
Record: covTerminalRecord(cleanupIdentity, HostStorageCleanupReceiptKind, HostAPTReceiptVersion, json.RawMessage(`{"rogue":true}`), now),
|
|
}
|
|
|
|
// Unsupported operation kind (default switch arm).
|
|
unsupportedKind := operationreceipt.QueryResult{
|
|
Version: operationreceipt.ProtocolVersion,
|
|
Status: operationreceipt.QueryFoundTerminal,
|
|
Record: covTerminalRecord(bogusIdentity, "any.kind", 1, json.RawMessage(`{}`), now),
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
result operationreceipt.QueryResult
|
|
identity operationreceipt.Identity
|
|
receivedAt time.Time
|
|
wantSub string
|
|
wantOK bool
|
|
}{
|
|
{"valid not-found returns nil", notFoundOK, updateIdentity, now, "", true},
|
|
{"valid interrupted returns nil", validInterrupted, updateIdentity, now, "", true},
|
|
{"unsupported version", badVersion, updateIdentity, now, "unsupported operation query result version", false},
|
|
{"not-found with record", notFoundWithRecord, updateIdentity, now, "not-found operation query result contains a record", false},
|
|
{"nil record identity mismatch", nilRecord, updateIdentity, now, "identity mismatch", false},
|
|
{"record identity mismatch", mismatchResult, updateIdentity, now, "identity mismatch", false},
|
|
{"validate record failure", validateRecordFail, updateIdentity, now, "invalid operation receipt bounds", false},
|
|
{"unknown status mismatch", unknownStatus, updateIdentity, now, "status and record state mismatch", false},
|
|
{"interrupted with terminal state", interruptedTerminal, updateIdentity, now, "status and record state mismatch", false},
|
|
{"terminal with non-terminal record", terminalNonTerminalRecord, updateIdentity, now, "terminal operation query result requires terminal record", false},
|
|
{"zero receivedAt rejected", zeroReceivedAt, updateIdentity, time.Time{}, "invalid or implausibly future", false},
|
|
{"host update envelope mismatch", updateEnvelopeMismatch, updateIdentity, now, "host update query result envelope mismatch", false},
|
|
{"host update decode error", updateDecodeError, updateIdentity, now, "", false},
|
|
{"host cleanup envelope mismatch", cleanupEnvelopeMismatch, cleanupIdentity, now, "host cleanup query result envelope mismatch", false},
|
|
{"host cleanup decode error", cleanupDecodeError, cleanupIdentity, now, "", false},
|
|
{"unsupported operation kind", unsupportedKind, bogusIdentity, now, "unsupported operation query kind", false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := ValidateOperationQueryResultForIdentity(tc.result, tc.identity, tc.receivedAt)
|
|
if tc.wantOK {
|
|
if err != nil {
|
|
t.Fatalf("expected nil error, got: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if tc.wantSub != "" && !strings.Contains(err.Error(), tc.wantSub) {
|
|
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BindHostUpdatePayload — apt_codec.go:193
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCoverageBindHostUpdatePayload(t *testing.T) {
|
|
t.Run("nil payload rejected", func(t *testing.T) {
|
|
if err := BindHostUpdatePayload(nil); err == nil || !strings.Contains(err.Error(), "host update payload is required") {
|
|
t.Fatalf("nil payload error=%v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("valid payload binds version and digest", func(t *testing.T) {
|
|
hash := "sha256:" + strings.Repeat("a", 64)
|
|
req := &HostUpdatePayload{
|
|
RequestID: "bind-1",
|
|
ActionID: "act-1",
|
|
Operation: HostUpdateOperationInstall,
|
|
ExpectedInventoryHash: hash,
|
|
}
|
|
if err := BindHostUpdatePayload(req); err != nil {
|
|
t.Fatalf("bind error: %v", err)
|
|
}
|
|
if req.OperationVersion != HostAPTOperationVersion {
|
|
t.Fatalf("operation version = %d, want %d", req.OperationVersion, HostAPTOperationVersion)
|
|
}
|
|
wantDigest := covHostUpdateDigest("act-1", HostUpdateOperationInstall, HostAPTOperationVersion, hash)
|
|
if req.RequestDigest != wantDigest {
|
|
t.Fatalf("request digest = %q, want %q", req.RequestDigest, wantDigest)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BindHostStorageCleanupPayload — apt_codec.go:215
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCoverageBindHostStorageCleanupPayload(t *testing.T) {
|
|
t.Run("nil payload rejected", func(t *testing.T) {
|
|
if err := BindHostStorageCleanupPayload(nil); err == nil || !strings.Contains(err.Error(), "host storage cleanup payload is required") {
|
|
t.Fatalf("nil payload error=%v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("valid payload binds version and digest", func(t *testing.T) {
|
|
fp := "sha256:" + strings.Repeat("c", 64)
|
|
req := &HostStorageCleanupPayload{
|
|
RequestID: "bind-2",
|
|
ActionID: "act-2",
|
|
Operation: HostStorageCleanupOperationPackageCache,
|
|
ExpectedFingerprint: fp,
|
|
}
|
|
if err := BindHostStorageCleanupPayload(req); err != nil {
|
|
t.Fatalf("bind error: %v", err)
|
|
}
|
|
if req.OperationVersion != HostAPTOperationVersion {
|
|
t.Fatalf("operation version = %d, want %d", req.OperationVersion, HostAPTOperationVersion)
|
|
}
|
|
wantDigest := covHostStorageCleanupDigest("act-2", HostStorageCleanupOperationPackageCache, HostAPTOperationVersion, fp)
|
|
if req.RequestDigest != wantDigest {
|
|
t.Fatalf("request digest = %q, want %q", req.RequestDigest, wantDigest)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ValidateHostUpdateResultPayload — apt_codec.go:245
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCoverageValidateHostUpdateResultPayload(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
hashA := "sha256:" + strings.Repeat("a", 64)
|
|
hashB := "sha256:" + strings.Repeat("b", 64)
|
|
|
|
// inconclusiveBase passes the internal validator and reaches every public
|
|
// arm when specific fields are tweaked.
|
|
inconclusiveBase := HostUpdateResultPayload{
|
|
RequestID: "r1",
|
|
ActionID: "a1",
|
|
ExecutionPhase: HostUpdatePhaseVerify,
|
|
Verification: HostUpdateVerificationInconclusive,
|
|
}
|
|
|
|
// A fully valid verified+complete result for the success arm.
|
|
validVerified := HostUpdateResultPayload{
|
|
RequestID: "r1",
|
|
ActionID: "a1",
|
|
Success: true,
|
|
ExecutionPhase: HostUpdatePhaseComplete,
|
|
MutationStarted: true,
|
|
HealthChecked: true,
|
|
PackageManagerHealthy: true,
|
|
Verification: HostUpdateVerificationVerified,
|
|
Before: HostPackageUpdateSnapshot{Supported: true, Manager: "apt", InventoryHash: hashA, PendingCount: 1, CheckedAt: now.Add(-2 * time.Minute)},
|
|
After: HostPackageUpdateSnapshot{Supported: true, Manager: "apt", InventoryHash: hashB, PendingCount: 0, CheckedAt: now.Add(-time.Minute)},
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
result HostUpdateResultPayload
|
|
wantSub string
|
|
wantOK bool
|
|
}{
|
|
{
|
|
name: "valid verified complete accepted",
|
|
result: validVerified,
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "empty action id",
|
|
result: func() HostUpdateResultPayload { r := inconclusiveBase; r.ActionID = ""; return r }(),
|
|
wantSub: "invalid action id",
|
|
},
|
|
{
|
|
name: "unsupported execution phase",
|
|
result: func() HostUpdateResultPayload {
|
|
r := inconclusiveBase
|
|
r.ExecutionPhase = "bogus_phase"
|
|
return r
|
|
}(),
|
|
wantSub: "unsupported host update execution phase",
|
|
},
|
|
{
|
|
name: "success in wrong phase",
|
|
result: func() HostUpdateResultPayload {
|
|
r := inconclusiveBase
|
|
r.Success = true
|
|
r.ExecutionPhase = HostUpdatePhasePreflight
|
|
return r
|
|
}(),
|
|
wantSub: "successful host update mutation must be in verify or complete phase",
|
|
},
|
|
{
|
|
name: "evidence timestamps invalid chronology",
|
|
result: func() HostUpdateResultPayload {
|
|
r := inconclusiveBase
|
|
r.Verification = HostUpdateVerificationFailed
|
|
r.Before = HostPackageUpdateSnapshot{CheckedAt: now}
|
|
r.After = HostPackageUpdateSnapshot{CheckedAt: time.Time{}}
|
|
return r
|
|
}(),
|
|
wantSub: "evidence-bearing host update observation timestamps are invalid",
|
|
},
|
|
{
|
|
name: "mutation state conflicts with phase",
|
|
result: func() HostUpdateResultPayload {
|
|
r := inconclusiveBase
|
|
r.MutationStarted = true
|
|
r.ExecutionPhase = HostUpdatePhasePreflight
|
|
return r
|
|
}(),
|
|
wantSub: "host update mutation state conflicts with execution phase",
|
|
},
|
|
{
|
|
name: "recovery required without mutation",
|
|
result: func() HostUpdateResultPayload {
|
|
r := inconclusiveBase
|
|
r.RecoveryRequired = true
|
|
return r
|
|
}(),
|
|
wantSub: "host update recovery requirement conflicts with mutation state",
|
|
},
|
|
{
|
|
name: "healthy package manager without health check",
|
|
result: func() HostUpdateResultPayload {
|
|
r := inconclusiveBase
|
|
r.PackageManagerHealthy = true
|
|
return r
|
|
}(),
|
|
wantSub: "healthy package manager claim requires a completed health check",
|
|
},
|
|
{
|
|
name: "unhealthy package manager after mutation without recovery",
|
|
result: func() HostUpdateResultPayload {
|
|
r := inconclusiveBase
|
|
r.HealthChecked = true
|
|
r.MutationStarted = true
|
|
return r
|
|
}(),
|
|
wantSub: "unhealthy package manager after mutation requires recovery",
|
|
},
|
|
{
|
|
name: "partial install without recovery",
|
|
result: func() HostUpdateResultPayload {
|
|
r := inconclusiveBase
|
|
r.ExecutionPhase = HostUpdatePhaseInstall
|
|
r.MutationStarted = true
|
|
return r
|
|
}(),
|
|
wantSub: "partial host update install requires recovery",
|
|
},
|
|
{
|
|
name: "successful completion cannot require recovery",
|
|
result: func() HostUpdateResultPayload {
|
|
r := inconclusiveBase
|
|
r.Success = true
|
|
r.ExecutionPhase = HostUpdatePhaseComplete
|
|
r.MutationStarted = true
|
|
r.RecoveryRequired = true
|
|
return r
|
|
}(),
|
|
wantSub: "successful host update completion cannot require recovery",
|
|
},
|
|
{
|
|
name: "verified but not complete",
|
|
result: func() HostUpdateResultPayload {
|
|
r := validVerified
|
|
r.ExecutionPhase = HostUpdatePhaseVerify
|
|
return r
|
|
}(),
|
|
wantSub: "verified host update must be complete",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := ValidateHostUpdateResultPayload(&tc.result)
|
|
if tc.wantOK {
|
|
if err != nil {
|
|
t.Fatalf("expected nil error, got: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantSub) {
|
|
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ValidateHostStorageCleanupResultPayload — apt_codec.go:289
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCoverageValidateHostStorageCleanupResultPayload(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
fpA := "sha256:" + strings.Repeat("a", 64)
|
|
fpB := "sha256:" + strings.Repeat("b", 64)
|
|
|
|
inconclusiveBase := HostStorageCleanupResultPayload{
|
|
RequestID: "r1",
|
|
ActionID: "a1",
|
|
ExecutionPhase: HostStorageCleanupPhaseVerify,
|
|
Verification: HostStorageCleanupVerificationInconclusive,
|
|
}
|
|
|
|
validVerified := HostStorageCleanupResultPayload{
|
|
RequestID: "r1",
|
|
ActionID: "a1",
|
|
Success: true,
|
|
ExecutionPhase: HostStorageCleanupPhaseComplete,
|
|
MutationStarted: true,
|
|
Verification: HostStorageCleanupVerificationVerified,
|
|
Before: HostStorageCleanupSnapshot{Supported: true, Provider: "apt-package-cache", Fingerprint: fpA, ReclaimableBytes: 100, CheckedAt: now.Add(-2 * time.Minute)},
|
|
After: HostStorageCleanupSnapshot{Supported: true, Provider: "apt-package-cache", Fingerprint: fpB, ReclaimableBytes: 10, CheckedAt: now.Add(-time.Minute)},
|
|
ReclaimedBytes: 90,
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
result HostStorageCleanupResultPayload
|
|
wantSub string
|
|
wantOK bool
|
|
}{
|
|
{
|
|
name: "valid verified complete accepted",
|
|
result: validVerified,
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "empty action id",
|
|
result: func() HostStorageCleanupResultPayload { r := inconclusiveBase; r.ActionID = ""; return r }(),
|
|
wantSub: "invalid action id",
|
|
},
|
|
{
|
|
name: "unsupported execution phase",
|
|
result: func() HostStorageCleanupResultPayload {
|
|
r := inconclusiveBase
|
|
r.ExecutionPhase = "bogus_phase"
|
|
return r
|
|
}(),
|
|
wantSub: "unsupported host storage cleanup execution phase",
|
|
},
|
|
{
|
|
name: "success in wrong phase",
|
|
result: func() HostStorageCleanupResultPayload {
|
|
r := inconclusiveBase
|
|
r.Success = true
|
|
r.ExecutionPhase = HostStorageCleanupPhasePreflight
|
|
return r
|
|
}(),
|
|
wantSub: "successful host storage cleanup mutation must be in verify or complete phase",
|
|
},
|
|
{
|
|
name: "verified but not complete",
|
|
result: func() HostStorageCleanupResultPayload {
|
|
r := validVerified
|
|
r.ExecutionPhase = HostStorageCleanupPhaseVerify
|
|
return r
|
|
}(),
|
|
wantSub: "verified host storage cleanup must be complete",
|
|
},
|
|
{
|
|
name: "evidence timestamps invalid chronology",
|
|
result: func() HostStorageCleanupResultPayload {
|
|
r := inconclusiveBase
|
|
r.Verification = HostStorageCleanupVerificationFailed
|
|
r.Before = HostStorageCleanupSnapshot{CheckedAt: now}
|
|
r.After = HostStorageCleanupSnapshot{CheckedAt: time.Time{}}
|
|
return r
|
|
}(),
|
|
wantSub: "evidence-bearing host storage cleanup observation timestamps are invalid",
|
|
},
|
|
{
|
|
name: "mutation state conflicts with phase",
|
|
result: func() HostStorageCleanupResultPayload {
|
|
r := inconclusiveBase
|
|
r.MutationStarted = true
|
|
r.ExecutionPhase = HostStorageCleanupPhasePreflight
|
|
return r
|
|
}(),
|
|
wantSub: "host storage cleanup mutation state conflicts with execution phase",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := ValidateHostStorageCleanupResultPayload(&tc.result)
|
|
if tc.wantOK {
|
|
if err != nil {
|
|
t.Fatalf("expected nil error, got: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantSub) {
|
|
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
|
|
}
|
|
})
|
|
}
|
|
}
|