package api import ( "bytes" "context" "encoding/binary" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "strings" "testing" "git.misaka.ren/m1saka/dsh-web/internal/docker" "git.misaka.ren/m1saka/dsh-web/internal/store" ) func previewFrame(stream byte, text string) []byte { header := make([]byte, 8) header[0] = stream binary.BigEndian.PutUint32(header[4:], uint32(len(text))) return append(header, []byte(text)...) } func TestProfileDumpFixedCommandAndRedaction(t *testing.T) { daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1.47/containers/container/exec": var request struct { Cmd []string AttachStdout, AttachStderr, Tty bool } if json.NewDecoder(r.Body).Decode(&request) != nil { t.Error("invalid exec request") } if r.Method != "POST" || !reflect.DeepEqual(request.Cmd, []string{"dsh", "--profile", "web", "--dump-config"}) || !request.AttachStdout || !request.AttachStderr || request.Tty { t.Errorf("unsafe command: %+v", request) } writeJSON(w, map[string]string{"Id": "exec"}) case "/v1.47/exec/exec/start": w.Write(previewFrame(2, "stderr-secret")) w.Write(previewFrame(1, "provider:\n apiKey: direct-secret\n API_KEY: underscore-secret\n password: numeric-secret\n nested:\n - token: nested-secret\n arbitrary: resolved-credential\n endpoint: https://name:password@example.test/?token=secret\n enabled: true\n")) case "/v1.47/exec/exec/json": writeJSON(w, map[string]any{"Running": false, "ExitCode": 0}) default: t.Errorf("unexpected Docker call %s", r.URL.Path) w.WriteHeader(404) } })) defer daemon.Close() a := &API{docker: docker.New(daemon.URL)} out, err := a.dumpProfile(context.Background(), "container") if err != nil { t.Fatal(err) } for _, secret := range []string{"secret", "resolved-credential", "https://"} { if strings.Contains(out, secret) { t.Fatalf("secret leaked: %s", out) } } if !strings.Contains(out, "[REDACTED]") || !strings.Contains(out, "enabled: true") { t.Fatalf("missing structure: %s", out) } } func TestProfileDumpPluginSequence(t *testing.T) { daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/exec"): writeJSON(w, map[string]string{"Id": "exec"}) case strings.HasSuffix(r.URL.Path, "/start"): w.Write(previewFrame(1, "# bundle\n- id: plugin\n disabled: !!js '!ctx.get(\"profileContext\")'\n config:\n apiKey: private-secret\n enabled: true\n")) default: writeJSON(w, map[string]any{"Running": false, "ExitCode": 0}) } })) defer daemon.Close() a := &API{docker: docker.New(daemon.URL)} out, err := a.dumpProfile(context.Background(), "container") if err != nil { t.Fatal(err) } if strings.Contains(out, "private-secret") || strings.Contains(out, "ctx.get") || !strings.Contains(out, "enabled: true") || !strings.Contains(out, "[REDACTED]") { t.Fatalf("invalid sequence redaction: %s", out) } } func TestProfileDumpRejectsUnsafeOutput(t *testing.T) { for _, tc := range []struct { name, output string exit int }{ {"invalid", "apiKey: [secret", 0}, {"plain", "secret", 0}, {"documents", "a: b\n---\napiKey: secret", 0}, {"failure", "a: b", 1}, {"empty", "", 0}, } { t.Run(tc.name, func(t *testing.T) { daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/exec"): writeJSON(w, map[string]string{"Id": "exec"}) case strings.HasSuffix(r.URL.Path, "/start"): w.Write(previewFrame(1, tc.output)) default: writeJSON(w, map[string]any{"Running": false, "ExitCode": tc.exit}) } })) defer daemon.Close() a := &API{docker: docker.New(daemon.URL)} if out, err := a.dumpProfile(context.Background(), "container"); err == nil || out != "" { t.Fatalf("unsafe output returned: %q, %v", out, err) } }) } } func TestPreviewStreamBounds(t *testing.T) { for _, data := range [][]byte{ []byte("raw secret"), {1, 0}, previewFrame(3, "secret"), previewFrame(1, strings.Repeat("x", previewLimit)), previewFrame(1, "secret")[:10], } { if _, err := previewStdout(bytes.NewReader(data)); err == nil { t.Fatal("accepted invalid stream") } } data := append(previewFrame(2, "secret"), previewFrame(1, "safe")...) got, err := previewStdout(bytes.NewReader(data)) if err != nil || string(got) != "safe" { t.Fatalf("stdout: %q %v", got, err) } } func previewFixture(t *testing.T) (*API, store.Instance, string) { t.Helper() root := t.TempDir() i := store.Instance{ID: "instance", UID: "owner", ContainerID: "container", Status: "stopped"} path := filepath.Join(root, "workspaces", i.UID, "instances", i.ID, "home", "profiles", "web") if err := os.MkdirAll(path, 0700); err != nil { t.Fatal(err) } return &API{root: root}, i, path } func TestProfilePreviewManifestAndErrors(t *testing.T) { a, i, path := previewFixture(t) if err := os.WriteFile(filepath.Join(path, "package.json"), []byte(`{"dsh":{"profile":{"bundles":["@deepseek/bundle"],"apiKey":"private-value"}},"scripts":{"secret":"ignored-value"}}`), 0600); err != nil { t.Fatal(err) } w := httptest.NewRecorder() a.profilePreview(w, httptest.NewRequest("GET", "/preview?cmd=cat&path=.credentials.yaml", nil), i) if w.Code != 200 || !strings.Contains(w.Body.String(), "@deepseek/bundle") || strings.Contains(w.Body.String(), "private-value") || strings.Contains(w.Body.String(), "ignored-value") { t.Fatalf("manifest response: %d %s", w.Code, w.Body.String()) } if w.Header().Get("Cache-Control") != "no-store" { t.Fatal("preview must not be cached") } daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "daemon-private-value", 500) })) defer daemon.Close() a.docker = docker.New(daemon.URL) i.Status = "running" w = httptest.NewRecorder() a.profilePreview(w, httptest.NewRequest("GET", "/preview", nil), i) if strings.Contains(w.Body.String(), "daemon-private-value") || !strings.Contains(w.Body.String(), "preview_error") { t.Fatalf("unsafe error: %s", w.Body.String()) } w = httptest.NewRecorder() a.profilePreview(w, httptest.NewRequest("POST", "/preview", nil), i) if w.Code != 405 { t.Fatalf("method: %d", w.Code) } } func TestProfileBundlesRejectsCredentialSymlink(t *testing.T) { a, i, path := previewFixture(t) credentials := filepath.Join(path, "..", "..", ".credentials.yaml") if err := os.WriteFile(credentials, []byte("private-value"), 0600); err != nil { t.Fatal(err) } if err := os.Symlink(credentials, filepath.Join(path, "package.json")); err != nil { t.Fatal(err) } if _, err := a.profileBundles(i); err == nil { t.Fatal("accepted credential symlink") } }