Files
privatae-bastion/client-go/dcn_plan_test.go
T
Privatae e4c9b69b46
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s
bastion 0.53.2
2026-10-20 04:42:14 +00:00

217 lines
8.4 KiB
Go

package main
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// The move-plan lane: signed plan, sandboxed moves, dry-run then
// apply with rollback and verify, archive-only — no delete, no overwrite.
func TestDcnPlanCheckApplyVerify(t *testing.T) {
root := t.TempDir()
mk := func(rel, body string) {
p := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
mk("Downloads/setup.exe", "xxxxxxxx")
mk("Downloads/photo.jpg", "img")
mk("Documents/notes.txt", "n")
mk("keep/.env", "secret")
if err := os.MkdirAll(filepath.Join(root, "Existing"), 0o755); err != nil {
t.Fatal(err)
}
size8 := int64(8)
excludes := []string{".env*"}
// dry run finds every kind of problem and touches nothing
bad := []dcnMove{
{From: "Downloads/setup.exe", To: "Existing"}, // target exists
{From: "../outside", To: "Archive/x"}, // escape
{From: "keep/.env", To: "Archive/env"}, // excluded
{From: "nope.txt", To: "Archive/nope.txt"}, // missing source
{From: "Documents", To: "Documents/inner"}, // into itself
{From: "Downloads/photo.jpg", To: "Downloads/photo.jpg"}, // same
}
pr := dcnCheckPlan(root, excludes, bad)
if len(pr) != len(bad) {
t.Fatalf("every bad move must be a problem: %+v", pr)
}
if _, err := os.Stat(filepath.Join(root, "Downloads", "setup.exe")); err != nil {
t.Fatal("dry run must not touch files")
}
// a good plan: reorganize + archive
good := &dcnPlanTerms{Kind: "dcn_move_plan", StoreID: 1, PlanID: 77, Root: root, Archive: "Archive",
Moves: []dcnMove{
{From: "Downloads/setup.exe", To: "Archive/installers/setup.exe", SizeBytes: &size8},
{From: "Downloads/photo.jpg", To: "Pictures/photo.jpg"},
{From: "Documents/notes.txt", To: "Documents/Notes/notes.txt"},
}}
if pr := dcnCheckPlan(root, excludes, good.Moves); len(pr) != 0 {
t.Fatalf("good plan must pass the check: %+v", pr)
}
rep := dcnApplyPlan(root, excludes, good)
if rep["state"] != "ok" || rep["applied"] != 3 || rep["verified"] != 3 {
t.Fatalf("apply+verify: %+v", rep)
}
for _, p := range []string{"Archive/installers/setup.exe", "Pictures/photo.jpg", "Documents/Notes/notes.txt"} {
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(p))); err != nil {
t.Fatalf("%s must exist after apply", p)
}
}
if _, err := os.Stat(filepath.Join(root, "Downloads", "setup.exe")); err == nil {
t.Fatal("source must be gone after a move")
}
if _, err := os.Stat(filepath.Join(root, "keep", ".env")); err != nil {
t.Fatal("untouched files stay untouched")
}
// a plan that fails mid-way rolls back what it did
mk("A/one.txt", "1")
mk("A/two.txt", "2")
if err := os.MkdirAll(filepath.Join(root, "B", "two.txt"), 0o755); err != nil { // a DIR where a file wants to go
t.Fatal(err)
}
// pre-check catches the collision (target exists) → refused, nothing moved
rb := &dcnPlanTerms{Kind: "dcn_move_plan", StoreID: 1, PlanID: 78, Root: root,
Moves: []dcnMove{{From: "A/one.txt", To: "B/one.txt"}, {From: "A/two.txt", To: "B/two.txt"}}}
rep = dcnApplyPlan(root, excludes, rb)
if rep["state"] != "refused" {
t.Fatalf("a colliding plan must be refused whole: %+v", rep)
}
if _, err := os.Stat(filepath.Join(root, "A", "one.txt")); err != nil {
t.Fatal("a refused plan must change nothing")
}
// signature wall: a plan must be signed by the pinned key and parse as a plan
pub, priv, _ := ed25519.GenerateKey(nil)
pubB64 := base64.StdEncoding.EncodeToString(pub)
terms := map[string]any{"kind": "dcn_move_plan", "store_id": 1, "plan_id": 79, "server": "box", "root": root,
"archive": "Archive", "moves": []map[string]any{{"from": "a", "to": "b"}},
"expires": time.Now().Add(time.Hour).UTC().Format(time.RFC3339)}
rec := signedRec(t, priv, terms)
if p, err := dcnVerifyPlan(pubB64, rec.TermsCanonical, rec.Signature); err != nil || p.PlanID != 79 {
t.Fatalf("a signed plan must verify: %v", err)
}
other, _, _ := ed25519.GenerateKey(nil)
if _, err := dcnVerifyPlan(base64.StdEncoding.EncodeToString(other), rec.TermsCanonical, rec.Signature); err == nil {
t.Fatal("a plan signed by another key must not verify")
}
terms["kind"] = "dcn_store_grant"
rec2 := signedRec(t, priv, terms)
if _, err := dcnVerifyPlan(pubB64, rec2.TermsCanonical, rec2.Signature); err == nil {
t.Fatal("a store grant must not pass as a plan")
}
// the vocabulary carries the op, and there is still no delete
ops := mgmtOpVocabulary()
hasPlan := false
for _, o := range ops {
if o == "dcn_plan_apply" {
hasPlan = true
}
if o == "dcn_delete" || o == "dcn_remove" || o == "stepup_verify" {
t.Fatal("no delete op may exist")
}
}
if !hasPlan {
t.Fatal("dcn_plan_apply must be in the reported vocabulary")
}
_ = json.Marshal
}
// Walls the first cut missed: paths that clean to the same target are one target;
// a stale size refuses BEFORE anything moves; a failure mid-plan really rolls
// back; a plan needs an expiry; the report never leaves the root.
func TestDcnPlanReviewWalls(t *testing.T) {
root := t.TempDir()
mk := func(rel, body string) {
p := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
mk("one.txt", "ONE")
mk("two.txt", "TWO")
// two targets that normalize to the same file — the second must be a problem
pr := dcnCheckPlan(root, nil, []dcnMove{
{From: "one.txt", To: "Archive/x.txt"}, {From: "two.txt", To: "./Archive//x.txt"}})
if len(pr) != 1 || pr[0].Index != 1 {
t.Fatalf("normalized duplicate target must be refused: %+v", pr)
}
pr = dcnCheckPlan(root, nil, []dcnMove{
{From: "one.txt", To: "Archive/x.txt"}, {From: "two.txt", To: "archive/X.TXT"}})
if len(pr) != 1 {
t.Fatalf("case-folded duplicate target must be refused: %+v", pr)
}
// the apply path refuses the whole plan and touches nothing
rep := dcnApplyPlan(root, nil, &dcnPlanTerms{PlanID: 1, Moves: []dcnMove{
{From: "one.txt", To: "Archive/x.txt"}, {From: "two.txt", To: "./Archive//x.txt"}}})
if rep["state"] != "refused" {
t.Fatalf("apply must refuse a colliding plan: %+v", rep)
}
if b, _ := os.ReadFile(filepath.Join(root, "one.txt")); string(b) != "ONE" {
t.Fatal("nothing may move when a plan is refused")
}
// size in the plan ≠ size on disk → refused before moving
wrong := int64(99)
pr = dcnCheckPlan(root, nil, []dcnMove{{From: "one.txt", To: "Archive/one.txt", SizeBytes: &wrong}})
if len(pr) != 1 || !strings.Contains(pr[0].Why, "size") {
t.Fatalf("a stale catalog size must refuse before the move: %+v", pr)
}
// a real mid-plan failure rolls the first move back. Move 2's target
// parent is a FILE ("blocker"), so MkdirAll fails after move 1 succeeded.
mk("blocker", "i am a file")
rep = dcnApplyPlan(root, nil, &dcnPlanTerms{PlanID: 2, Moves: []dcnMove{
{From: "one.txt", To: "Moved/one.txt"}, {From: "two.txt", To: "blocker/two.txt"}}})
if rep["state"] != "failed" || rep["failed_at"] != 1 || rep["rolled_back"] != 1 {
t.Fatalf("mid-plan failure must roll back: %+v", rep)
}
if b, _ := os.ReadFile(filepath.Join(root, "one.txt")); string(b) != "ONE" {
t.Fatal("rolled-back source must be back in place with its content")
}
if _, err := os.Stat(filepath.Join(root, "Moved", "one.txt")); err == nil {
t.Fatal("rolled-back target must be gone")
}
// a plan without an expiry never verifies
pub, priv, _ := ed25519.GenerateKey(nil)
pubB64 := base64.StdEncoding.EncodeToString(pub)
terms := map[string]any{"kind": "dcn_move_plan", "store_id": 1, "plan_id": 3, "root": root,
"archive": "Archive", "moves": []map[string]any{{"from": "a", "to": "b"}}}
rec := signedRec(t, priv, terms)
if _, err := dcnVerifyPlan(pubB64, rec.TermsCanonical, rec.Signature); err == nil {
t.Fatal("a plan with no expiry must not verify")
}
// a symlinked .agent pointing outside the root → no report file written
outside := t.TempDir()
root2 := t.TempDir()
if err := os.Symlink(outside, filepath.Join(root2, ".agent")); err != nil {
t.Skip("symlinks unavailable")
}
if p := dcnReportPath(root2, 7); p != "" {
t.Fatalf("report path must not resolve outside the root: %q", p)
}
if p := dcnReportPath(root, 7); p == "" || !strings.HasPrefix(p, root) {
t.Fatalf("report path inside the root must resolve: %q", p)
}
}