package api import ( "git.misaka.ren/m1saka/dsh-web/internal/store" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func TestWorkspaceInstanceScope(t *testing.T) { a, sid := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) { t.Error("unexpected Docker call") }) for _, x := range []struct{ id, uid string }{{"one", "owner"}, {"two", "owner"}, {"foreign", "someone"}} { if err := a.s.AddInstance(store.Instance{ID: x.id, UID: x.uid}); err != nil { t.Fatal(err) } dir := filepath.Join(a.root, "workspaces", x.uid, "instances", x.id, "home") if err := os.MkdirAll(dir, 0750); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, x.id+".txt"), []byte(x.id), 0600); err != nil { t.Fatal(err) } } for _, tc := range []struct { query string status int contains string }{ {"instance=one", 200, "one.txt"}, {"instance=two", 200, "two.txt"}, {"instance=foreign", 404, ""}, {"instance=missing", 404, ""}, {"instance=one&path=../two", 400, ""}, } { w := httptest.NewRecorder() a.workspace(w, runtimeRequest("GET", "/api/ws/tree?"+tc.query, "", sid)) if w.Code != tc.status || tc.contains != "" && !strings.Contains(w.Body.String(), tc.contains) { t.Fatalf("%s: %d %s", tc.query, w.Code, w.Body.String()) } } w := httptest.NewRecorder() a.workspace(w, runtimeRequest("POST", "/api/ws/mkdir?instance=two", `{"path":"created"}`, sid)) if w.Code != 200 { t.Fatal(w.Code, w.Body.String()) } if _, err := os.Stat(filepath.Join(a.root, "workspaces/owner/instances/two/home/created")); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(a.root, "workspaces/owner/instances/one/home/created")); !os.IsNotExist(err) { t.Fatal("cross instance write") } dir := filepath.Join(a.root, "workspaces/owner/instances/one/home") if err := os.Symlink(t.TempDir(), filepath.Join(dir, "escape")); err != nil { t.Fatal(err) } w = httptest.NewRecorder() a.workspace(w, runtimeRequest("GET", "/api/ws/tree?instance=one&path=escape", "", sid)) if w.Code != 404 { t.Fatal("symlink traversal allowed", w.Code) } if _, err := a.s.DB.Exec("UPDATE instances SET status='error' WHERE id='two'"); err != nil { t.Fatal(err) } w = httptest.NewRecorder() a.workspace(w, runtimeRequest("POST", "/api/ws/mkdir?instance=two", "{\"path\":\"blocked\"}", sid)) if w.Code != 409 { t.Fatal("recovery write permitted", w.Code) } }