mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-18 20:07:24 +00:00
291 lines
8.5 KiB
Go
291 lines
8.5 KiB
Go
package audit
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"hauler.dev/go/hauler/v2/internal/flags"
|
|
"hauler.dev/go/hauler/v2/pkg/consts"
|
|
)
|
|
|
|
func TestAppend(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
e := Entry{
|
|
Command: "store add image",
|
|
Args: []string{"busybox:latest"},
|
|
Flags: map[string]any{"platform": "linux/amd64"},
|
|
Store: filepath.Join(dir, "store"),
|
|
}
|
|
|
|
if err := Append(dir, e); err != nil {
|
|
t.Fatalf("Append: %v", err)
|
|
}
|
|
|
|
path := filepath.Join(dir, "audit.log")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
|
|
var got Entry
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatalf("unmarshal audit line: %v\nraw: %s", err, data)
|
|
}
|
|
|
|
if got.Command != e.Command {
|
|
t.Errorf("Command = %q, want %q", got.Command, e.Command)
|
|
}
|
|
if len(got.Args) != 1 || got.Args[0] != "busybox:latest" {
|
|
t.Errorf("Args = %v, want [busybox:latest]", got.Args)
|
|
}
|
|
if got.Timestamp == "" {
|
|
t.Error("Timestamp should be set")
|
|
}
|
|
}
|
|
|
|
// TestAppend_PortableReferenceOverridesStoreCopy verifies PortableReference only affects the store audit log
|
|
func TestAppend_PortableReferenceOverridesStoreCopy(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
absPath := filepath.Join(string(filepath.Separator), "home", "example", "scripts", "install.sh")
|
|
typedPath := filepath.Join(".", "scripts", "install.sh")
|
|
e := Entry{
|
|
Command: "store add file",
|
|
Type: "file",
|
|
Args: []string{typedPath},
|
|
Reference: absPath,
|
|
PortableReference: typedPath,
|
|
Store: filepath.Join(dir, "store"),
|
|
}
|
|
|
|
if err := Append(dir, e); err != nil {
|
|
t.Fatalf("Append: %v", err)
|
|
}
|
|
|
|
// global log keeps the full path and the as-typed args
|
|
globalData, err := os.ReadFile(filepath.Join(dir, "audit.log"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile global audit.log: %v", err)
|
|
}
|
|
var globalGot Entry
|
|
if err := json.Unmarshal(globalData, &globalGot); err != nil {
|
|
t.Fatalf("unmarshal global audit line: %v\nraw: %s", err, globalData)
|
|
}
|
|
if globalGot.Reference != absPath {
|
|
t.Errorf("global Reference = %q, want %q", globalGot.Reference, absPath)
|
|
}
|
|
if len(globalGot.Args) != 1 || globalGot.Args[0] != typedPath {
|
|
t.Errorf("global Args = %v, want [%s]", globalGot.Args, typedPath)
|
|
}
|
|
|
|
// store log uses PortableReference instead, and carries no args at all
|
|
storeData, err := os.ReadFile(filepath.Join(dir, "store", "audit.log"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile store audit log: %v", err)
|
|
}
|
|
var got portableEntry
|
|
if err := json.Unmarshal(storeData, &got); err != nil {
|
|
t.Fatalf("unmarshal store audit line: %v\nraw: %s", err, storeData)
|
|
}
|
|
if got.Reference != typedPath {
|
|
t.Errorf("store Reference = %q, want %q", got.Reference, typedPath)
|
|
}
|
|
if strings.Contains(string(storeData), `"args"`) {
|
|
t.Errorf("store entry should not carry args: %s", storeData)
|
|
}
|
|
}
|
|
|
|
// TestAppend_StoreDefaultsToReference verifies the fallback when PortableReference is unset
|
|
func TestAppend_StoreDefaultsToReference(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
e := Entry{
|
|
Command: "store remove",
|
|
Type: "file",
|
|
Args: []string{"install.sh"},
|
|
Reference: "hauler/install.sh:latest",
|
|
Store: filepath.Join(dir, "store"),
|
|
}
|
|
|
|
if err := Append(dir, e); err != nil {
|
|
t.Fatalf("Append: %v", err)
|
|
}
|
|
|
|
storeData, err := os.ReadFile(filepath.Join(dir, "store", "audit.log"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile store audit log: %v", err)
|
|
}
|
|
var got portableEntry
|
|
if err := json.Unmarshal(storeData, &got); err != nil {
|
|
t.Fatalf("unmarshal store audit line: %v\nraw: %s", err, storeData)
|
|
}
|
|
if got.Reference != e.Reference {
|
|
t.Errorf("store Reference = %q, want %q (unmodified)", got.Reference, e.Reference)
|
|
}
|
|
}
|
|
|
|
func TestAppend_StoreWriteSucceedsWhenGlobalWriteFails(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// occupy the path so appendLine's MkdirAll fails for the global write only
|
|
blockedHaulerDir := filepath.Join(dir, "blocked")
|
|
if err := os.WriteFile(blockedHaulerDir, []byte("not a dir"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
storeDir := filepath.Join(dir, "store")
|
|
e := Entry{
|
|
Command: "store add image",
|
|
Args: []string{"busybox:latest"},
|
|
Store: storeDir,
|
|
}
|
|
|
|
if err := Append(blockedHaulerDir, e); err == nil {
|
|
t.Fatal("Append: expected error from unwritable global haulerDir, got nil")
|
|
}
|
|
|
|
storeData, err := os.ReadFile(filepath.Join(storeDir, "audit.log"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile store audit log: %v", err)
|
|
}
|
|
var got portableEntry
|
|
if err := json.Unmarshal(storeData, &got); err != nil {
|
|
t.Fatalf("unmarshal store audit line: %v\nraw: %s", err, storeData)
|
|
}
|
|
if got.Command != e.Command {
|
|
t.Errorf("store command = %q, want = %q", got.Command, e.Command)
|
|
}
|
|
}
|
|
|
|
func TestAppend_MultipleEntries(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
for i := 0; i < 3; i++ {
|
|
if err := Append(dir, Entry{Command: "store add image", Args: []string{"img"}}); err != nil {
|
|
t.Fatalf("Append[%d]: %v", i, err)
|
|
}
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath.Join(dir, "audit.log"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
|
|
lines := 0
|
|
for _, b := range data {
|
|
if b == '\n' {
|
|
lines++
|
|
}
|
|
}
|
|
if lines != 3 {
|
|
t.Errorf("expected 3 lines, got %d\nlog:\n%s", lines, data)
|
|
}
|
|
}
|
|
|
|
func TestMergeStoreLog_AppendsOntoExisting(t *testing.T) {
|
|
temp, dest := t.TempDir(), t.TempDir()
|
|
|
|
if err := os.WriteFile(filepath.Join(dest, LogFileName), []byte(`{"command":"original"}`+"\n"), 0o644); err != nil {
|
|
t.Fatalf("seed dest log: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(temp, LogFileName), []byte(`{"command":"from-haul"}`+"\n"), 0o644); err != nil {
|
|
t.Fatalf("seed temp log: %v", err)
|
|
}
|
|
|
|
if err := MergeStoreLog(temp, dest); err != nil {
|
|
t.Fatalf("MergeStoreLog: %v", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath.Join(dest, LogFileName))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
if !strings.Contains(string(data), "original") || !strings.Contains(string(data), "from-haul") {
|
|
t.Fatalf("expected both entries preserved, got: %s", data)
|
|
}
|
|
}
|
|
|
|
func TestMergeStoreLog_CreatesDestWhenMissing(t *testing.T) {
|
|
temp := t.TempDir()
|
|
dest := filepath.Join(t.TempDir(), "not-yet-created")
|
|
|
|
if err := os.WriteFile(filepath.Join(temp, LogFileName), []byte(`{"command":"from-haul"}`+"\n"), 0o644); err != nil {
|
|
t.Fatalf("seed temp log: %v", err)
|
|
}
|
|
|
|
if err := MergeStoreLog(temp, dest); err != nil {
|
|
t.Fatalf("MergeStoreLog: %v", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath.Join(dest, LogFileName))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
if !strings.Contains(string(data), "from-haul") {
|
|
t.Fatalf("expected dest log created with haul's entry, got: %s", data)
|
|
}
|
|
}
|
|
|
|
func TestMergeStoreLog_NoopWhenTempHasNoLog(t *testing.T) {
|
|
temp, dest := t.TempDir(), t.TempDir()
|
|
|
|
if err := MergeStoreLog(temp, dest); err != nil {
|
|
t.Fatalf("MergeStoreLog: %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(dest, LogFileName)); !os.IsNotExist(err) {
|
|
t.Fatalf("expected no dest log to be created, stat err: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestShortFileRef(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"local relative path", "./scripts/install.sh", "install.sh"},
|
|
{"local absolute path", "/home/example/scripts/install.sh", "install.sh"},
|
|
{"bare filename", "install.sh", "install.sh"},
|
|
{"plain URL", "https://get.rke2.io/install.sh", "install.sh"},
|
|
{"URL with credentials", "https://user:pass@get.rke2.io/install.sh", "install.sh"},
|
|
{"URL with query token", "https://get.rke2.io/install.sh?token=abc123", "install.sh"},
|
|
{"URL with credentials and query", "https://user:pass@get.rke2.io/install.sh?sig=abc123", "install.sh"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := ShortFileRef(tc.in); got != tc.want {
|
|
t.Errorf("ShortFileRef(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveDir_Default(t *testing.T) {
|
|
got := resolveDir("")
|
|
if got == "" {
|
|
t.Error("resolveDir(\"\") returned empty string")
|
|
}
|
|
}
|
|
|
|
func TestBuildGlobal_IgnoreErrorsReflectsEnvVar(t *testing.T) {
|
|
// BuildGlobal must record the *effective* ignore-errors setting (flag OR
|
|
// env var) via flags.ShouldIgnoreErrors, not just the raw ro.IgnoreErrors
|
|
// field — callers no longer mutate ro.IgnoreErrors to reflect the env var.
|
|
ro := &flags.CliRootOpts{IgnoreErrors: false}
|
|
t.Setenv(consts.HaulerIgnoreErrors, "true")
|
|
|
|
g := BuildGlobal(ro, nil)
|
|
|
|
if !g.IgnoreErrors {
|
|
t.Fatal("expected BuildGlobal to report IgnoreErrors=true when HAULER_IGNORE_ERRORS is set, even though ro.IgnoreErrors is false")
|
|
}
|
|
if ro.IgnoreErrors {
|
|
t.Fatal("expected BuildGlobal to not mutate ro.IgnoreErrors")
|
|
}
|
|
}
|