package http import ( "context" "io" stdhttp "net/http" "net/http/httptest" "os" "path/filepath" "testing" "gitea.dcglab.co.uk/steve/restic-manager/internal/crypto" "gitea.dcglab.co.uk/steve/restic-manager/internal/server/config" "gitea.dcglab.co.uk/steve/restic-manager/internal/server/ws" "gitea.dcglab.co.uk/steve/restic-manager/internal/store" ) // newAssetsTestServer is a minimal scaffold for the /agent/binary and // /install/* handlers. Two roots: one acts as DataDir, the other as // the image-baked BundledAssetsDir. Either or both may be empty. func newAssetsTestServer(t *testing.T, populate func(dataDir, bundleDir string)) string { t.Helper() root := t.TempDir() dataDir := filepath.Join(root, "data") bundleDir := filepath.Join(root, "dist") for _, d := range []string{ filepath.Join(dataDir, "agent-binaries"), filepath.Join(dataDir, "install"), filepath.Join(bundleDir, "agent-binaries"), filepath.Join(bundleDir, "install"), } { if err := os.MkdirAll(d, 0o755); err != nil { t.Fatalf("mkdir: %v", err) } } if populate != nil { populate(dataDir, bundleDir) } st, err := store.Open(context.Background(), filepath.Join(root, "rm.db")) if err != nil { t.Fatalf("store: %v", err) } t.Cleanup(func() { _ = st.Close() }) keyPath := filepath.Join(root, "secret.key") _ = crypto.GenerateKeyFile(keyPath) key, _ := crypto.LoadKeyFromFile(keyPath) aead, _ := crypto.NewAEAD(key) deps := Deps{ Cfg: config.Config{ Listen: ":0", DataDir: dataDir, SecretKeyFile: keyPath, BundledAssetsDir: bundleDir, }, Store: st, AEAD: aead, Hub: ws.NewHub(), BootstrapToken: "test-token", } s := New(deps) ts := httptest.NewServer(s.srv.Handler) t.Cleanup(ts.Close) return ts.URL } func writeFile(t *testing.T, path string, body []byte) { t.Helper() if err := os.WriteFile(path, body, 0o644); err != nil { t.Fatalf("write %s: %v", path, err) } } func get(t *testing.T, url string) (int, []byte) { t.Helper() res, err := stdhttp.Get(url) if err != nil { t.Fatalf("GET %s: %v", url, err) } defer res.Body.Close() body, _ := io.ReadAll(res.Body) return res.StatusCode, body } func TestAgentBinary_DataDirHit(t *testing.T) { t.Parallel() url := newAssetsTestServer(t, func(dataDir, _ string) { writeFile(t, filepath.Join(dataDir, "agent-binaries", "restic-manager-agent-linux-amd64"), []byte("from-datadir")) }) code, body := get(t, url+"/agent/binary?os=linux&arch=amd64") if code != 200 || string(body) != "from-datadir" { t.Fatalf("got %d %q", code, string(body)) } } func TestAgentBinary_BundleFallback(t *testing.T) { t.Parallel() url := newAssetsTestServer(t, func(_, bundleDir string) { writeFile(t, filepath.Join(bundleDir, "agent-binaries", "restic-manager-agent-linux-amd64"), []byte("from-bundle")) }) code, body := get(t, url+"/agent/binary?os=linux&arch=amd64") if code != 200 || string(body) != "from-bundle" { t.Fatalf("got %d %q", code, string(body)) } } func TestAgentBinary_DataDirShadowsBundle(t *testing.T) { t.Parallel() url := newAssetsTestServer(t, func(dataDir, bundleDir string) { writeFile(t, filepath.Join(dataDir, "agent-binaries", "restic-manager-agent-linux-amd64"), []byte("from-datadir")) writeFile(t, filepath.Join(bundleDir, "agent-binaries", "restic-manager-agent-linux-amd64"), []byte("from-bundle")) }) code, body := get(t, url+"/agent/binary?os=linux&arch=amd64") if code != 200 || string(body) != "from-datadir" { t.Fatalf("operator override should win: got %d %q", code, string(body)) } } func TestAgentBinary_BothMiss(t *testing.T) { t.Parallel() url := newAssetsTestServer(t, nil) code, _ := get(t, url+"/agent/binary?os=linux&arch=amd64") if code != 404 { t.Fatalf("expected 404, got %d", code) } } func TestAgentBinary_WindowsNameHasExe(t *testing.T) { t.Parallel() url := newAssetsTestServer(t, func(_, bundleDir string) { writeFile(t, filepath.Join(bundleDir, "agent-binaries", "restic-manager-agent-windows-amd64.exe"), []byte("win")) }) code, body := get(t, url+"/agent/binary?os=windows&arch=amd64") if code != 200 || string(body) != "win" { t.Fatalf("got %d %q", code, string(body)) } } func TestInstallAsset_BundleFallback(t *testing.T) { t.Parallel() url := newAssetsTestServer(t, func(_, bundleDir string) { writeFile(t, filepath.Join(bundleDir, "install", "install.sh"), []byte("#!/bin/sh\n")) }) code, body := get(t, url+"/install/install.sh") if code != 200 || string(body) != "#!/bin/sh\n" { t.Fatalf("got %d %q", code, string(body)) } } func TestInstallAsset_PathTraversalRejected(t *testing.T) { t.Parallel() url := newAssetsTestServer(t, nil) // chi will normalise some traversal attempts, but the handler // also rejects any rel containing a slash or backslash. The // path component of the URL after /install/ is the rel. code, _ := get(t, url+"/install/..%2fpasswd") if code == 200 { t.Fatalf("traversal should not return 200") } }