package main import ( "bytes" "crypto/sha256" "encoding/hex" "io" "net/http" "os" "path/filepath" "strings" "testing" ) func TestUsageWritesSupportedCommands(t *testing.T) { stdout, stderr := captureStreams(t, func() { usage() }) if stdout != "" { t.Fatalf("usage wrote stdout: %q", stdout) } if !strings.Contains(stderr, "remote-flash") || !strings.Contains(stderr, "plan") { t.Fatalf("usage output missing commands: %q", stderr) } } func TestConfigFactsPlanBurnImageInjectAndServeCommands(t *testing.T) { root := t.TempDir() invPath, baseImage := writeTestInventory(t, root) t.Setenv("METIS_INVENTORY_PATH", invPath) t.Setenv("METIS_DATA_DIR", filepath.Join(root, "data")) t.Setenv("METIS_BOOT_PATH", "") t.Setenv("METIS_ROOT_PATH", "") stdout, _ := captureStreams(t, func() { configCmd([]string{"--inventory", invPath, "--node", "titan-15"}) }) if !strings.Contains(stdout, `"hostname": "titan-15"`) { t.Fatalf("config output missing hostname: %s", stdout) } snapDir := filepath.Join(root, "snapshots") if err := os.MkdirAll(snapDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(snapDir, "snap.json"), []byte(`{"hostname":"titan-15","kernel":"6.6.63","package_sample":{"containerd":"1.7"}}`), 0o644); err != nil { t.Fatal(err) } stdout, _ = captureStreams(t, func() { factsCmd([]string{"--inventory", invPath, "--snapshots", snapDir}) }) if !strings.Contains(stdout, `"class": "rpi4"`) { t.Fatalf("facts output missing class summary: %s", stdout) } stdout, _ = captureStreams(t, func() { planCmd([]string{"--inventory", invPath, "--node", "titan-15", "--device", "/dev/sdz", "--cache", filepath.Join(root, "cache")}) }) if !strings.Contains(stdout, `"node": "titan-15"`) || !strings.Contains(stdout, `"actions"`) { t.Fatalf("plan output missing plan JSON: %s", stdout) } rootTools := fakeCommandDir(t, map[string]string{ "sfdisk": `cat <<'JSON' {"partitiontable":{"sectorsize":512,"partitions":[{"start":3,"size":1,"type":"ef"},{"start":1,"size":2,"type":"83"}]}} JSON`, "debugfs": `if [[ "${1:-}" == "-w" ]]; then cp "${3:-}" "${4:-}.commands" exit 0 fi if [[ "${1:-}" == "-R" ]]; then state="${3:-}.commands" set -- $2 case "${1:-}" in stat) mode="$(awk -v path="${2:-}" '$1=="sif" && $2==path {print $4}' "${state}" | tail -n1)" mode="${mode: -4}" printf 'Mode: %s\n' "${mode}" exit 0 ;; dump) local_path="$(awk -v path="${2:-}" '$1=="write" && $3==path {print $2}' "${state}" | tail -n1)" cat "${local_path}" > "${3:-}" exit 0 ;; esac fi exit 0`, }) t.Setenv("PATH", rootTools+string(os.PathListSeparator)+os.Getenv("PATH")) stdout, _ = captureStreams(t, func() { imageCmd([]string{"--inventory", invPath, "--node", "titan-15", "--output", filepath.Join(root, "out.img"), "--cache", filepath.Join(root, "cache")}) }) if !strings.Contains(stdout, "Wrote ") { t.Fatalf("image output missing write confirmation: %s", stdout) } stdout, _ = captureStreams(t, func() { burnCmd([]string{"--inventory", invPath, "--node", "titan-15", "--device", "/dev/sdz", "--cache", filepath.Join(root, "cache")}) }) if !strings.Contains(stdout, "Plan for titan-15 to /dev/sdz") { t.Fatalf("burn output missing plan header: %s", stdout) } bootDir := filepath.Join(root, "boot") rootDir := filepath.Join(root, "root") if err := os.MkdirAll(bootDir, 0o755); err != nil { t.Fatal(err) } if err := os.MkdirAll(rootDir, 0o755); err != nil { t.Fatal(err) } captureStreams(t, func() { injectCmd([]string{"--inventory", invPath, "--node", "titan-15", "--boot", bootDir, "--root", rootDir}) }) if _, err := os.Stat(filepath.Join(rootDir, "etc/metis/node.json")); err != nil { t.Fatalf("injectCmd did not write root file: %v", err) } listenAndServe = func(addr string, handler http.Handler) error { if addr != ":0" { t.Fatalf("unexpected bind addr: %s", addr) } return nil } t.Cleanup(func() { listenAndServe = http.ListenAndServe }) t.Setenv("METIS_BIND_ADDR", ":0") t.Setenv("METIS_INVENTORY_PATH", invPath) serveCmd([]string{"--bind", ":0"}) _ = baseImage } func TestMainDispatchesConfig(t *testing.T) { root := t.TempDir() invPath, _ := writeTestInventory(t, root) oldArgs := os.Args os.Args = []string{"metis", "config", "--inventory", invPath, "--node", "titan-15"} t.Cleanup(func() { os.Args = oldArgs }) main() } func TestRemoteCommandsAndHelpers(t *testing.T) { root := t.TempDir() invPath, baseImage := writeTestInventory(t, root) fakeTools := fakeCommandDir(t, map[string]string{ "lsblk": `cat <<'JSON' {"blockdevices":[{"name":"sdz","path":"/dev/sdz","rm":true,"hotplug":true,"size":"32000000000","model":"Micro SD","tran":"usb","type":"disk"}]} JSON`, "sfdisk": `cat <<'JSON' {"partitiontable":{"sectorsize":512,"partitions":[{"start":3,"size":1,"type":"ef"},{"start":1,"size":2,"type":"83"}]}} JSON`, "debugfs": `if [[ "${1:-}" == "-w" ]]; then cp "${3:-}" "${4:-}.commands" exit 0 fi if [[ "${1:-}" == "-R" ]]; then state="${3:-}.commands" set -- $2 case "${1:-}" in stat) mode="$(awk -v path="${2:-}" '$1=="sif" && $2==path {print $4}' "${state}" | tail -n1)" mode="${mode: -4}" printf 'Mode: %s\n' "${mode}" exit 0 ;; dump) local_path="$(awk -v path="${2:-}" '$1=="write" && $3==path {print $2}' "${state}" | tail -n1)" cat "${local_path}" > "${3:-}" exit 0 ;; esac fi exit 0`, "xz": `dest="${@: -1}"; cp "$dest" "$dest.xz"`, "oras": `case "${1:-}" in login|tag) exit 0 ;; push) exit 0 ;; pull) outdir="${@: -1}" cp "` + baseImage + `" "${outdir}/titan-15.img" exit 0 ;; esac exit 0`, }) t.Setenv("PATH", fakeTools+string(os.PathListSeparator)+os.Getenv("PATH")) stdout, _ := captureStreams(t, func() { remoteDevicesCmd([]string{"--max-device-bytes", "40000000000", "--host-tmp-dir", filepath.Join(root, "host-tmp")}) }) if !strings.Contains(stdout, `"path":"/dev/sdz"`) { t.Fatalf("remoteDevicesCmd output missing device: %s", stdout) } stdout, _ = captureStreams(t, func() { remoteBuildCmd([]string{"--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "build"), "--cache", filepath.Join(root, "cache"), "--harbor-registry", "registry.example", "--harbor-username", "admin", "--harbor-password", "pw"}) }) if !strings.Contains(stdout, `"build_tag":"build-1"`) { t.Fatalf("remoteBuildCmd output missing build tag: %s", stdout) } stdout, _ = captureStreams(t, func() { remoteFlashCmd([]string{"--node", "titan-15", "--device", "hosttmp:///tmp", "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash"), "--host-tmp-dir", filepath.Join(root, "host-tmp"), "--harbor-registry", "registry.example", "--harbor-username", "admin", "--harbor-password", "pw"}) }) if !strings.Contains(stdout, `"dest_path"`) { t.Fatalf("remoteFlashCmd output missing dest_path: %s", stdout) } } func captureStreams(t *testing.T, fn func()) (string, string) { t.Helper() oldStdout := os.Stdout oldStderr := os.Stderr stdoutR, stdoutW, _ := os.Pipe() stderrR, stderrW, _ := os.Pipe() os.Stdout = stdoutW os.Stderr = stderrW defer func() { os.Stdout = oldStdout os.Stderr = oldStderr }() done := make(chan struct { out string err string }, 1) go func() { var outBuf bytes.Buffer var errBuf bytes.Buffer _, _ = io.Copy(&outBuf, stdoutR) _, _ = io.Copy(&errBuf, stderrR) done <- struct { out string err string }{out: outBuf.String(), err: errBuf.String()} }() fn() _ = stdoutW.Close() _ = stderrW.Close() captured := <-done return captured.out, captured.err } func writeTestInventory(t *testing.T, root string) (string, string) { t.Helper() baseImage := filepath.Join(root, "base.img") if err := os.WriteFile(baseImage, make([]byte, 4096), 0o644); err != nil { t.Fatal(err) } invPath := filepath.Join(root, "inventory.yaml") inv := `classes: - name: rpi4 arch: arm64 os: armbian image: file://` + baseImage + ` checksum: sha256:` + sha256SumHex(t, make([]byte, 4096)) + ` k3s_version: v1.31.5+k3s1 nodes: - name: titan-15 class: rpi4 hostname: titan-15 ip: 192.168.22.43 k3s_role: agent k3s_url: https://192.168.22.7:6443 k3s_token: token ssh_user: atlas ssh_authorized_keys: - ssh-ed25519 AAA ` if err := os.WriteFile(invPath, []byte(inv), 0o644); err != nil { t.Fatal(err) } return invPath, baseImage } func sha256SumHex(t *testing.T, data []byte) string { t.Helper() sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]) } func fakeCommandDir(t *testing.T, scripts map[string]string) string { t.Helper() dir := t.TempDir() for name, body := range scripts { path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte("#!/usr/bin/env bash\nset -eu\n"+body+"\n"), 0o755); err != nil { t.Fatalf("write %s: %v", name, err) } } return dir }