Files
dsh-web/internal/api/profile_preview_test.go
T

189 lines
6.8 KiB
Go

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:[email protected]/?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")
}
}