323 lines
10 KiB
Go
323 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"codespace/internal/process"
|
|
"codespace/internal/service"
|
|
"codespace/internal/workspace"
|
|
)
|
|
|
|
func setupTestRouter(t *testing.T) http.Handler {
|
|
t.Helper()
|
|
return setupTestRouterWithMaxWriteBytes(t, 1<<20)
|
|
}
|
|
|
|
func setupTestRouterWithMaxWriteBytes(t *testing.T, maxWriteBytes int64) http.Handler {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
wsMgr := workspace.NewLocalManager(root)
|
|
procMgr := process.NewManager("")
|
|
wsSvc := service.NewWorkspaceService(wsMgr, procMgr, nil)
|
|
fileSvc := service.NewFileService(wsMgr, maxWriteBytes)
|
|
procSvc := service.NewProcessService(wsMgr, procMgr, nil)
|
|
return NewRouter(wsSvc, fileSvc, procSvc, nil)
|
|
}
|
|
|
|
func createWorkspaceForTest(t *testing.T, router http.Handler, id string) {
|
|
t.Helper()
|
|
payload, _ := json.Marshal(map[string]string{"id": id})
|
|
req := httptest.NewRequest("POST", "/api/workspaces", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create workspace %q: status %d, body %s", id, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func writeFileForTest(t *testing.T, router http.Handler, id, path, content string) {
|
|
t.Helper()
|
|
payload, _ := json.Marshal(map[string]string{"path": path, "content": content})
|
|
req := httptest.NewRequest("PUT", "/api/workspaces/"+id+"/files/write", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("write %q: status %d, body %s", path, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func assertErrorResponse(t *testing.T, rec *httptest.ResponseRecorder, status int, code string) {
|
|
t.Helper()
|
|
if rec.Code != status {
|
|
t.Fatalf("status = %d, want %d; body %s", rec.Code, status, rec.Body.String())
|
|
}
|
|
var body struct {
|
|
Error struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body.Error.Code != code {
|
|
t.Fatalf("code = %q, want %q", body.Error.Code, code)
|
|
}
|
|
if body.Error.Message == "" {
|
|
t.Fatal("error message is empty")
|
|
}
|
|
}
|
|
|
|
func TestHealthz(t *testing.T) {
|
|
router := setupTestRouter(t)
|
|
req := httptest.NewRequest("GET", "/healthz", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
|
|
var body map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if body["status"] != "ok" {
|
|
t.Errorf("status = %q, want ok", body["status"])
|
|
}
|
|
}
|
|
|
|
func TestCreateWorkspace(t *testing.T) {
|
|
router := setupTestRouter(t)
|
|
|
|
payload, _ := json.Marshal(map[string]string{"id": "user1"})
|
|
req := httptest.NewRequest("POST", "/api/workspaces", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusCreated, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp["id"] != "user1" {
|
|
t.Errorf("id = %q, want user1", resp["id"])
|
|
}
|
|
}
|
|
|
|
func TestListWorkspaces(t *testing.T) {
|
|
router := setupTestRouter(t)
|
|
for _, id := range []string{"beta", "alpha"} {
|
|
payload, _ := json.Marshal(map[string]string{"id": id})
|
|
req := httptest.NewRequest("POST", "/api/workspaces", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create %s: %d %s", id, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/api/workspaces", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Workspaces []struct {
|
|
ID string `json:"id"`
|
|
} `json:"workspaces"`
|
|
}
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(resp.Workspaces) != 2 || resp.Workspaces[0].ID != "alpha" || resp.Workspaces[1].ID != "beta" {
|
|
t.Fatalf("response = %#v", resp)
|
|
}
|
|
}
|
|
|
|
func TestWriteReadFile(t *testing.T) {
|
|
router := setupTestRouter(t)
|
|
createWorkspaceForTest(t, router, "user1")
|
|
writeFileForTest(t, router, "user1", "main.go", "package main\n")
|
|
|
|
// Read file
|
|
req := httptest.NewRequest("GET", "/api/workspaces/user1/files/read?path=main.go", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("read file: status=%d want=%d body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp["content"] != "package main\n" {
|
|
t.Errorf("content = %q, want 'package main\\n'", resp["content"])
|
|
}
|
|
}
|
|
|
|
func TestFileStat(t *testing.T) {
|
|
router := setupTestRouter(t)
|
|
createWorkspaceForTest(t, router, "user1")
|
|
writeFileForTest(t, router, "user1", "main.go", "package main\n")
|
|
|
|
req := httptest.NewRequest("GET", "/api/workspaces/user1/files/stat?path=main.go", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var resp struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
IsDir bool `json:"isDir"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Name != "main.go" || resp.Path != "main.go" || resp.IsDir || resp.Size != int64(len("package main\n")) {
|
|
t.Fatalf("resp=%#v", resp)
|
|
}
|
|
}
|
|
|
|
func TestRemoveWorkspaceRootIsRejected(t *testing.T) {
|
|
router := setupTestRouter(t)
|
|
createWorkspaceForTest(t, router, "user1")
|
|
req := httptest.NewRequest("DELETE", "/api/workspaces/user1/files?path=.", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestWriteFileRejectsOversizedContent(t *testing.T) {
|
|
router := setupTestRouterWithMaxWriteBytes(t, 4)
|
|
createWorkspaceForTest(t, router, "user1")
|
|
// Content length 5 exceeds maxWriteBytes of 4
|
|
payload, _ := json.Marshal(map[string]string{"path": "big.txt", "content": "12345"})
|
|
req := httptest.NewRequest("PUT", "/api/workspaces/user1/files/write", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
assertErrorResponse(t, rec, http.StatusBadRequest, "bad_request")
|
|
}
|
|
|
|
func TestFileLifecycle(t *testing.T) {
|
|
router := setupTestRouter(t)
|
|
createWorkspaceForTest(t, router, "user1")
|
|
|
|
// Mkdir
|
|
mkdirPayload, _ := json.Marshal(map[string]string{"path": "dir"})
|
|
req := httptest.NewRequest("POST", "/api/workspaces/user1/files/mkdir", bytes.NewReader(mkdirPayload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("mkdir status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// Write
|
|
writeFileForTest(t, router, "user1", "dir/old.txt", "hello")
|
|
|
|
// List
|
|
req = httptest.NewRequest("GET", "/api/workspaces/user1/files?path=dir", nil)
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// Rename
|
|
renamePayload, _ := json.Marshal(map[string]string{"oldPath": "dir/old.txt", "newPath": "dir/new.txt"})
|
|
req = httptest.NewRequest("POST", "/api/workspaces/user1/files/rename", bytes.NewReader(renamePayload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("rename status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// Read new location
|
|
req = httptest.NewRequest("GET", "/api/workspaces/user1/files/read?path=dir/new.txt", nil)
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("read status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// Remove
|
|
req = httptest.NewRequest("DELETE", "/api/workspaces/user1/files?path=dir/new.txt", nil)
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("remove status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestErrorResponsesAreConsistent(t *testing.T) {
|
|
router := setupTestRouter(t)
|
|
|
|
// Not found workspace
|
|
req := httptest.NewRequest("GET", "/api/workspaces/missing", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
assertErrorResponse(t, rec, http.StatusNotFound, "not_found")
|
|
|
|
// Invalid JSON
|
|
req = httptest.NewRequest("POST", "/api/workspaces", bytes.NewBufferString("{"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
assertErrorResponse(t, rec, http.StatusBadRequest, "bad_request")
|
|
|
|
// Path escape attempt
|
|
createWorkspaceForTest(t, router, "user1")
|
|
req = httptest.NewRequest("GET", "/api/workspaces/user1/files/read?path=../secret", nil)
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
assertErrorResponse(t, rec, http.StatusBadRequest, "bad_request")
|
|
}
|
|
|
|
func TestProcessStatus(t *testing.T) {
|
|
router := setupTestRouter(t)
|
|
createWorkspaceForTest(t, router, "user1")
|
|
|
|
req := httptest.NewRequest("GET", "/api/workspaces/user1/process/status", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
WorkspaceID string `json:"workspaceId"`
|
|
Running bool `json:"running"`
|
|
}
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.WorkspaceID != "user1" {
|
|
t.Errorf("workspaceId = %q, want user1", resp.WorkspaceID)
|
|
}
|
|
if resp.Running {
|
|
t.Error("expected Running=false")
|
|
}
|
|
}
|