//go:build unix package e2e import ( "encoding/json" "io" "net/http" "os" "os/exec" "path/filepath" "slices" "strconv" "strings" "testing" "albedo/cli/internal/webui" ) func loginManager(t *testing.T) { t.Helper() stub := filepath.Join(suite.root, "cli", "test", "e2e", "login_service_stub.py") original := suite.env suite.env = append(slices.Clone(suite.env), "ALBEDO_LAUNCHCTL="+stub, "ALBEDO_SYSTEMCTL="+stub) t.Cleanup(func() { suite.env = original }) } func TestWebLoginServiceStartsStopsAndForgets(t *testing.T) { loginManager(t) dist := t.TempDir() if err := os.WriteFile(filepath.Join(dist, "index.html"), []byte("web login service fixture"), 0o600); err != nil { t.Fatal(err) } suite.env = append(suite.env, "ALBEDO_WEB_DIST="+dist, "ALBEDO_DAEMON=/nix/store/stale-default/bin/albedo-daemon") t.Cleanup(func() { cli(t, "web", "--save") }) addresses, addressErr := webui.NetworkAddresses() if addressErr != nil { t.Fatal(addressErr) } if len(addresses) == 0 { t.Skip("no non-loopback IPv4 address") } network := addresses[0] + "/32" actualCLI := suite.cli wrapper := func(target string) string { t.Helper() root := t.TempDir() source := filepath.Join(root, "wrapper.go") code := "package main\nimport (\"os\"; \"syscall\")\nfunc main() { panic(syscall.Exec(" + strconv.Quote(target) + ", os.Args, os.Environ())) }\n" if err := os.WriteFile(source, []byte(code), 0o600); err != nil { t.Fatal(err) } binary := filepath.Join(root, "wrapped-albedo") build := exec.Command("go", "build", "-o", binary, source) if output, err := build.CombinedOutput(); err != nil { t.Fatalf("build argv-zero wrapper: %v: %s", err, output) } return binary } outer := wrapper(wrapper(actualCLI)) stable := filepath.Join(t.TempDir(), "albedo") if err := os.Symlink(outer, stable); err != nil { t.Fatal(err) } suite.cli = stable t.Cleanup(func() { suite.cli = actualCLI }) cli(t, "web", "--no-open", "--port", "0", "--allow", network, "--save") record := func() webui.Manifest { t.Helper() data, err := os.ReadFile(filepath.Join(suite.home, "web.json")) if err != nil { t.Fatal(err) } var manifest webui.Manifest if err := json.Unmarshal(data, &manifest); err != nil { t.Fatal(err) } return manifest } first := record() if !slices.Equal(first.Allow, []string{network}) { t.Fatalf("service ignored saved networks: %+v", first) } cli(t, "web", "--no-open") if record().PID != first.PID { t.Fatal("bare start duplicated the web server") } checkPage := func() { t.Helper() response, err := http.Get(record().URL) if err != nil { t.Fatal(err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil || !strings.Contains(string(body), "web login service fixture") { t.Fatalf("service did not serve its configured dist: %s %v", body, err) } } checkPage() cli(t, "web", "--stop") if webui.ManifestAlive(first) { t.Fatal("manager stop left the web server running") } cli(t, "web", "--no-open") if record().PID == first.PID { t.Fatal("start after stop did not launch a new managed process") } oldKey, err := os.ReadFile(filepath.Join(suite.home, "web-key")) if err != nil { t.Fatal(err) } cli(t, "web", "--new-link", "--no-open") newKey, err := os.ReadFile(filepath.Join(suite.home, "web-key")) if err != nil || string(newKey) == string(oldKey) { t.Fatalf("managed new-link did not rotate the key: %v", err) } checkPage() statePath := filepath.Join(suite.home, "web-service-stub.json") data, err := os.ReadFile(statePath) if err != nil { t.Fatal(err) } var state struct { Path string `json:"path"` } if decodeErr := json.Unmarshal(data, &state); decodeErr != nil { t.Fatal(decodeErr) } definition, err := os.ReadFile(state.Path) if err != nil { t.Fatal(err) } if strings.Contains(string(definition), "/nix/store/stale-default") { t.Fatal("service pinned a package-provided store default") } if !strings.Contains(string(definition), stable) || strings.Contains(string(definition), actualCLI) { t.Fatalf("service pinned the wrapper target instead of its stable link: %s", definition) } if !strings.Contains(filepath.Base(state.Path), ".web-") { t.Fatalf("web did not get a separate service: %s", state.Path) } cli(t, "web", "--save") for _, path := range []string{state.Path, statePath, filepath.Join(suite.home, "web.json")} { if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("forget left %s: %v", path, err) } } }