Files
bee/audit/internal/platform/export_test.go
Mikhail ChusavitinandClaude Sonnet 5 d108df7fe9 platform/webui: add scenario description, show it in the Scenario page's list
ScenarioSpec gains an optional "description" field. Listing (both
ListLocalScenarioFiles and ListScenarioFilesOnRemovableMedia, via the new
scenarioDescription helper) reads it out of each file without requiring
full ParseScenarioJSON validation to succeed, so a listing never hides a
scenario over an unrelated validation issue. The webui Scenario page now
renders Name/Description/Found-on/Run instead of just Name/Found-on — a
bare filename rarely tells anyone but the author what a scenario actually
does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 19:28:07 +03:00

279 lines
9.5 KiB
Go

package platform
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestExportFileToTargetUnmountsExistingMountpoint(t *testing.T) {
tmp := t.TempDir()
src := filepath.Join(tmp, "bundle.tar.gz")
mountpoint := filepath.Join(tmp, "mnt")
if err := os.MkdirAll(mountpoint, 0755); err != nil {
t.Fatalf("mkdir mountpoint: %v", err)
}
if err := os.WriteFile(src, []byte("bundle"), 0644); err != nil {
t.Fatalf("write src: %v", err)
}
var calls [][]string
oldExec := exportExecCommand
exportExecCommand = func(name string, args ...string) *exec.Cmd {
calls = append(calls, append([]string{name}, args...))
return exec.Command("sh", "-c", "exit 0")
}
t.Cleanup(func() { exportExecCommand = oldExec })
s := &System{}
dst, err := s.ExportFileToTarget(src, RemovableTarget{
Device: "/dev/sdb1",
Mountpoint: mountpoint,
})
if err != nil {
t.Fatalf("ExportFileToTarget error: %v", err)
}
if got, want := dst, filepath.Join(mountpoint, "bundle.tar.gz"); got != want {
t.Fatalf("dst=%q want %q", got, want)
}
if _, err := os.Stat(filepath.Join(mountpoint, "bundle.tar.gz")); err != nil {
t.Fatalf("exported file missing: %v", err)
}
foundUmount := false
for _, call := range calls {
if len(call) == 2 && call[0] == "umount" && call[1] == mountpoint {
foundUmount = true
break
}
}
if !foundUmount {
t.Fatalf("expected umount %q call, got %#v", mountpoint, calls)
}
}
func TestExportFileToTargetRejectsNonWritableMountpoint(t *testing.T) {
tmp := t.TempDir()
src := filepath.Join(tmp, "bundle.tar.gz")
mountpoint := filepath.Join(tmp, "mnt")
if err := os.MkdirAll(mountpoint, 0755); err != nil {
t.Fatalf("mkdir mountpoint: %v", err)
}
if err := os.WriteFile(src, []byte("bundle"), 0644); err != nil {
t.Fatalf("write src: %v", err)
}
if err := os.Chmod(mountpoint, 0555); err != nil {
t.Fatalf("chmod mountpoint: %v", err)
}
oldExec := exportExecCommand
exportExecCommand = func(name string, args ...string) *exec.Cmd {
return exec.Command("sh", "-c", "exit 0")
}
t.Cleanup(func() { exportExecCommand = oldExec })
s := &System{}
_, err := s.ExportFileToTarget(src, RemovableTarget{
Device: "/dev/sdb1",
Mountpoint: mountpoint,
})
if err == nil {
t.Fatal("expected error for non-writable mountpoint")
}
if !strings.Contains(err.Error(), "target filesystem is not writable") {
t.Fatalf("err=%q want writable message", err)
}
}
func TestListRemovableTargetsSkipsReadOnlyMedia(t *testing.T) {
oldExec := exportExecCommand
lsblkOut := `NAME="sda1" TYPE="part" PKNAME="sda" RM="1" RO="1" FSTYPE="iso9660" MOUNTPOINT="/run/live/medium" SIZE="3.7G" LABEL="BEE" MODEL=""
NAME="sdb1" TYPE="part" PKNAME="sdb" RM="1" RO="0" FSTYPE="vfat" MOUNTPOINT="/media/bee/USB" SIZE="29.8G" LABEL="USB" MODEL=""`
exportExecCommand = func(name string, args ...string) *exec.Cmd {
cmd := exec.Command("sh", "-c", "printf '%s\n' \"$LSBLK_OUT\"")
cmd.Env = append(os.Environ(), "LSBLK_OUT="+lsblkOut)
return cmd
}
t.Cleanup(func() { exportExecCommand = oldExec })
s := &System{}
targets, err := s.ListRemovableTargets()
if err != nil {
t.Fatalf("ListRemovableTargets error: %v", err)
}
if len(targets) != 1 {
t.Fatalf("len(targets)=%d want 1 (%+v)", len(targets), targets)
}
if got := targets[0].Device; got != "/dev/sdb1" {
t.Fatalf("device=%q want /dev/sdb1", got)
}
}
func TestReadScenarioFromRemovableMediaFindsFileOnAlreadyMountedTarget(t *testing.T) {
mountpoint := t.TempDir()
if err := os.MkdirAll(filepath.Join(mountpoint, "scenarios"), 0755); err != nil {
t.Fatalf("mkdir scenarios: %v", err)
}
want := []byte(`{"name":"power-watch","jobs":[{"name":"a","type":"command","cmd":["true"]}]}`)
if err := os.WriteFile(filepath.Join(mountpoint, "scenarios", "power-watch.json"), want, 0644); err != nil {
t.Fatalf("write scenario file: %v", err)
}
oldExec := exportExecCommand
lsblkOut := `NAME="sdb1" TYPE="part" PKNAME="sdb" RM="1" RO="0" FSTYPE="vfat" MOUNTPOINT="` + mountpoint + `" SIZE="29.8G" LABEL="USB" MODEL=""`
exportExecCommand = func(name string, args ...string) *exec.Cmd {
cmd := exec.Command("sh", "-c", "printf '%s\n' \"$LSBLK_OUT\"")
cmd.Env = append(os.Environ(), "LSBLK_OUT="+lsblkOut)
return cmd
}
t.Cleanup(func() { exportExecCommand = oldExec })
s := &System{}
got, err := s.ReadScenarioFromRemovableMedia("power-watch")
if err != nil {
t.Fatalf("ReadScenarioFromRemovableMedia error: %v", err)
}
if string(got) != string(want) {
t.Fatalf("got=%q want=%q", got, want)
}
}
func TestReadScenarioFromRemovableMediaNotFound(t *testing.T) {
mountpoint := t.TempDir() // no scenarios/ dir at all
oldExec := exportExecCommand
lsblkOut := `NAME="sdb1" TYPE="part" PKNAME="sdb" RM="1" RO="0" FSTYPE="vfat" MOUNTPOINT="` + mountpoint + `" SIZE="29.8G" LABEL="USB" MODEL=""`
exportExecCommand = func(name string, args ...string) *exec.Cmd {
cmd := exec.Command("sh", "-c", "printf '%s\n' \"$LSBLK_OUT\"")
cmd.Env = append(os.Environ(), "LSBLK_OUT="+lsblkOut)
return cmd
}
t.Cleanup(func() { exportExecCommand = oldExec })
s := &System{}
if _, err := s.ReadScenarioFromRemovableMedia("nope"); err == nil {
t.Fatal("expected error for missing scenario file")
}
}
func withLocalScenariosDir(t *testing.T) string {
t.Helper()
dir := t.TempDir()
old := LocalScenariosDir
LocalScenariosDir = dir
t.Cleanup(func() { LocalScenariosDir = old })
return dir
}
func TestReadScenarioPrefersLocalOverRemovableMedia(t *testing.T) {
localDir := withLocalScenariosDir(t)
localData := []byte(`{"name":"local-copy"}`)
if err := os.WriteFile(filepath.Join(localDir, "power-watch.json"), localData, 0644); err != nil {
t.Fatalf("write local scenario: %v", err)
}
// A removable target also has a same-named file with different content
// — ReadScenario must prefer the local (always-available) copy.
mountpoint := t.TempDir()
if err := os.MkdirAll(filepath.Join(mountpoint, "scenarios"), 0755); err != nil {
t.Fatalf("mkdir scenarios: %v", err)
}
if err := os.WriteFile(filepath.Join(mountpoint, "scenarios", "power-watch.json"), []byte(`{"name":"usb-copy"}`), 0644); err != nil {
t.Fatalf("write usb scenario: %v", err)
}
oldExec := exportExecCommand
lsblkOut := `NAME="sdb1" TYPE="part" PKNAME="sdb" RM="1" RO="0" FSTYPE="vfat" MOUNTPOINT="` + mountpoint + `" SIZE="29.8G" LABEL="USB" MODEL=""`
exportExecCommand = func(name string, args ...string) *exec.Cmd {
cmd := exec.Command("sh", "-c", "printf '%s\n' \"$LSBLK_OUT\"")
cmd.Env = append(os.Environ(), "LSBLK_OUT="+lsblkOut)
return cmd
}
t.Cleanup(func() { exportExecCommand = oldExec })
s := &System{}
got, err := s.ReadScenario("power-watch")
if err != nil {
t.Fatalf("ReadScenario error: %v", err)
}
if string(got) != string(localData) {
t.Fatalf("got=%q want local copy %q", got, localData)
}
}
func TestReadScenarioFallsBackToRemovableMediaWhenNotLocal(t *testing.T) {
withLocalScenariosDir(t) // empty — nothing local
mountpoint := t.TempDir()
if err := os.MkdirAll(filepath.Join(mountpoint, "scenarios"), 0755); err != nil {
t.Fatalf("mkdir scenarios: %v", err)
}
want := []byte(`{"name":"usb-only"}`)
if err := os.WriteFile(filepath.Join(mountpoint, "scenarios", "usb-only.json"), want, 0644); err != nil {
t.Fatalf("write usb scenario: %v", err)
}
oldExec := exportExecCommand
lsblkOut := `NAME="sdb1" TYPE="part" PKNAME="sdb" RM="1" RO="0" FSTYPE="vfat" MOUNTPOINT="` + mountpoint + `" SIZE="29.8G" LABEL="USB" MODEL=""`
exportExecCommand = func(name string, args ...string) *exec.Cmd {
cmd := exec.Command("sh", "-c", "printf '%s\n' \"$LSBLK_OUT\"")
cmd.Env = append(os.Environ(), "LSBLK_OUT="+lsblkOut)
return cmd
}
t.Cleanup(func() { exportExecCommand = oldExec })
s := &System{}
got, err := s.ReadScenario("usb-only")
if err != nil {
t.Fatalf("ReadScenario error: %v", err)
}
if string(got) != string(want) {
t.Fatalf("got=%q want=%q", got, want)
}
}
func TestListAvailableScenariosMergesLocalAndRemovable(t *testing.T) {
localDir := withLocalScenariosDir(t)
shipped := []byte(`{"name":"shipped","description":"reproduces the reboot"}`)
if err := os.WriteFile(filepath.Join(localDir, "shipped.json"), shipped, 0644); err != nil {
t.Fatalf("write local scenario: %v", err)
}
mountpoint := t.TempDir()
if err := os.MkdirAll(filepath.Join(mountpoint, "scenarios"), 0755); err != nil {
t.Fatalf("mkdir scenarios: %v", err)
}
if err := os.WriteFile(filepath.Join(mountpoint, "scenarios", "from-usb.json"), []byte(`{}`), 0644); err != nil {
t.Fatalf("write usb scenario: %v", err)
}
oldExec := exportExecCommand
lsblkOut := `NAME="sdb1" TYPE="part" PKNAME="sdb" RM="1" RO="0" FSTYPE="vfat" MOUNTPOINT="` + mountpoint + `" SIZE="29.8G" LABEL="USB" MODEL=""`
exportExecCommand = func(name string, args ...string) *exec.Cmd {
cmd := exec.Command("sh", "-c", "printf '%s\n' \"$LSBLK_OUT\"")
cmd.Env = append(os.Environ(), "LSBLK_OUT="+lsblkOut)
return cmd
}
t.Cleanup(func() { exportExecCommand = oldExec })
s := &System{}
got, err := s.ListAvailableScenarios()
if err != nil {
t.Fatalf("ListAvailableScenarios error: %v", err)
}
if len(got) != 2 {
t.Fatalf("got=%v want 2 entries", got)
}
if got[0].Name != "shipped" || got[0].Device != "local (shipped with image)" {
t.Fatalf("got[0]=%+v want local shipped entry first", got[0])
}
if got[0].Description != "reproduces the reboot" {
t.Fatalf("got[0].Description=%q want %q", got[0].Description, "reproduces the reboot")
}
if got[1].Name != "from-usb" {
t.Fatalf("got[1]=%+v want from-usb", got[1])
}
if got[1].Description != "" {
t.Fatalf("got[1].Description=%q want empty (file has no description field)", got[1].Description)
}
}