package imagepull import ( "context" "os" "os/exec" "path/filepath" "strings" "testing" "banger/internal/system" ) func TestInjectGuestAgentsWritesExpectedFiles(t *testing.T) { if _, err := exec.LookPath("mkfs.ext4"); err != nil { t.Skip("mkfs.ext4 not available; skipping") } if _, err := exec.LookPath("debugfs"); err != nil { t.Skip("debugfs not available; skipping") } // Build a bare ext4 from an empty (but non-empty-dir) source so // debugfs has a valid filesystem to inject into. mkfs.ext4 -d // wants the source dir itself to contain at least something. src := t.TempDir() if err := os.MkdirAll(filepath.Join(src, "usr"), 0o755); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(src, "etc"), 0o755); err != nil { t.Fatal(err) } ext4 := filepath.Join(t.TempDir(), "rootfs.ext4") if err := BuildExt4(context.Background(), system.NewRunner(), src, ext4, MinExt4Size); err != nil { t.Fatalf("BuildExt4: %v", err) } // Fake vsock-agent binary content — InjectGuestAgents copies bytes // verbatim so any file passes as a stand-in. fakeAgent := filepath.Join(t.TempDir(), "banger-vsock-agent") if err := os.WriteFile(fakeAgent, []byte("#!/bin/true\n"), 0o755); err != nil { t.Fatal(err) } if err := InjectGuestAgents(context.Background(), system.NewRunner(), ext4, GuestAgentAssets{ VsockAgentBin: fakeAgent, }); err != nil { t.Fatalf("InjectGuestAgents: %v", err) } // Verify each expected path is present via debugfs stat. expectPaths := []string{ "/usr/local/bin/banger-vsock-agent", "/usr/local/libexec/banger-network-bootstrap", "/etc/systemd/system/banger-network.service", "/etc/systemd/system/banger-vsock-agent.service", "/etc/modules-load.d/banger-vsock.conf", "/etc/systemd/system/multi-user.target.wants/banger-network.service", "/etc/systemd/system/multi-user.target.wants/banger-vsock-agent.service", // Phase B-3 first-boot bits: FirstBootScriptPath, "/etc/systemd/system/" + FirstBootUnitName, "/etc/systemd/system/multi-user.target.wants/" + FirstBootUnitName, FirstBootMarkerPath, } for _, p := range expectPaths { out, err := exec.Command("debugfs", "-R", "stat "+p, ext4).CombinedOutput() if err != nil { t.Errorf("debugfs stat %s: %v: %s", p, err, out) continue } if strings.Contains(string(out), "couldn't find file") || strings.Contains(string(out), "File not found") { t.Errorf("path missing: %s\noutput:\n%s", p, out) } } // Verify ownership on one file (uid=0). statOut, err := exec.Command("debugfs", "-R", "stat /usr/local/bin/banger-vsock-agent", ext4).CombinedOutput() if err != nil { t.Fatalf("debugfs stat agent: %v: %s", err, statOut) } s := string(statOut) if !strings.Contains(s, "User: 0") && !strings.Contains(s, "User: 0") { t.Errorf("vsock-agent binary not uid=0:\n%s", s) } if !strings.Contains(s, "Mode: 0755") && !strings.Contains(s, "Mode: 100755") { t.Errorf("vsock-agent binary mode not 0755:\n%s", s) } } func TestInjectGuestAgentsRequiresVsockAgentBinary(t *testing.T) { err := InjectGuestAgents(context.Background(), system.NewRunner(), "/tmp/nonexistent.ext4", GuestAgentAssets{ VsockAgentBin: "", }) if err == nil || !strings.Contains(err.Error(), "required") { t.Fatalf("expected missing-binary error, got %v", err) } } func TestCollectAncestorsIsShallowFirst(t *testing.T) { files := []injectFile{ {guestPath: "/a/b/c/file"}, } symlinks := []injectSymlink{ {link: "/x/y/z/link"}, } got := collectAncestors(files, symlinks) want := []string{"/a", "/x", "/a/b", "/x/y", "/a/b/c", "/x/y/z"} if len(got) != len(want) { t.Fatalf("len got=%d want=%d: %v", len(got), len(want), got) } for i, g := range got { if g != want[i] { t.Errorf("index %d: got %q want %q", i, g, want[i]) } } }