| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "os" |
| 7 | "os/user" |
| 8 | "path/filepath" |
| 9 | "strings" |
| 10 | "syscall" |
| 11 | "testing" |
| 12 | "time" |
| 13 | |
| 14 | "reasonix/internal/pathidentity" |
| 15 | ) |
| 16 | |
| 17 | func TestInspectDoesNotReadContentOrCreateMissingTail(t *testing.T) { |
| 18 | dir := t.TempDir() |
| 19 | path := filepath.Join(dir, "existing.lock") |
| 20 | const secret = "sentinel-credential-must-not-appear" |
| 21 | if err := os.WriteFile(path, []byte(secret), 0o600); err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | before, err := os.Stat(path) |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | got := inspect(path) |
| 29 | if !got.Lstat.OK || !got.Eval.OK || !got.Resolve.OK { |
| 30 | t.Fatalf("ordinary file failed: %+v", got) |
| 31 | } |
| 32 | raw, err := json.Marshal(got) |
| 33 | if err != nil || strings.Contains(string(raw), secret) { |
| 34 | t.Fatalf("invalid or content-bearing report: %s, %v", raw, err) |
| 35 | } |
| 36 | after, err := os.Stat(path) |
| 37 | if err != nil || !after.ModTime().Equal(before.ModTime()) || after.Size() != before.Size() { |
| 38 | t.Fatalf("file metadata changed: %v", err) |
| 39 | } |
| 40 | content, err := os.ReadFile(path) |
| 41 | if err != nil || string(content) != secret { |
| 42 | t.Fatalf("file changed: %v", err) |
| 43 | } |
| 44 | missing := filepath.Join(dir, "absent", "child.lock") |
| 45 | result := inspect(missing) |
| 46 | if result.Lstat.OK || result.Eval.OK || !result.Resolve.OK { |
| 47 | t.Fatalf("missing-tail behavior: %+v", result) |
| 48 | } |
| 49 | if _, err := os.Lstat(filepath.Dir(missing)); !os.IsNotExist(err) { |
| 50 | t.Fatalf("probe created missing parent: %v", err) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | func TestFailureRetainsStagePathAndSystemCode(t *testing.T) { |
| 55 | err := &pathidentity.Error{Stage: "physical", Path: "missing.lock", Kind: pathidentity.ErrorUnavailable, |
| 56 | Err: &os.PathError{Op: "readlink", Path: "junction", Err: syscall.Errno(3)}} |
| 57 | got := failureOutcome(err) |
| 58 | if got.OK || len(got.Errors) != 3 || got.Errors[0].Stage != "physical" || |
| 59 | got.Errors[0].Path != "missing.lock" || got.Errors[1].Path != "junction" || got.Errors[2].Code != 3 { |
| 60 | t.Fatalf("lost structured failure: %+v", got) |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | func TestCollectionRedactsNestedErrorsAndDoesNotCreateLocks(t *testing.T) { |
| 65 | home := filepath.Join(t.TempDir(), "probe-private-user") |
| 66 | current := &user.User{HomeDir: home, Uid: "S-1-5-21-private-fixture", Username: "probe-private-user"} |
| 67 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| 68 | defer cancel() |
| 69 | r := collect(ctx, current, nil) |
| 70 | if r.TimedOut || len(r.Paths) == 0 { |
| 71 | t.Fatalf("no collection: %+v", r) |
| 72 | } |
| 73 | r.Notes = append(r.Notes, current.Uid) |
| 74 | r.Paths[0].Resolve = failureOutcome(&pathidentity.Error{Stage: "physical", Path: home, |
| 75 | Err: &os.PathError{Op: "open", Path: filepath.Join(home, "secret.lock"), Err: syscall.Errno(3)}}) |
| 76 | raw, err := encodeRedacted(r, current) |
| 77 | if err != nil || !json.Valid(raw) { |
| 78 | t.Fatalf("invalid report: %v", err) |
| 79 | } |
| 80 | if strings.Contains(string(raw), "probe-private-user") || strings.Contains(string(raw), current.Uid) { |
| 81 | t.Fatal("identity was not redacted in nested report") |
| 82 | } |
| 83 | if _, err := os.Lstat(home); !os.IsNotExist(err) { |
| 84 | t.Fatalf("collection created home/locks: %v", err) |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | func TestRunRefusesExistingReport(t *testing.T) { |
| 89 | path := filepath.Join(t.TempDir(), "report.json") |
| 90 | if err := os.WriteFile(path, []byte("preserve"), 0o600); err != nil { |
| 91 | t.Fatal(err) |
| 92 | } |
| 93 | if err := run(path, time.Second, nil); err == nil { |
| 94 | t.Fatal("overwrote existing report") |
| 95 | } |
| 96 | raw, err := os.ReadFile(path) |
| 97 | if err != nil || string(raw) != "preserve" { |
| 98 | t.Fatalf("changed report: %v", err) |
| 99 | } |
| 100 | } |
| 101 |