package files_test import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "rickub.com/bots-garden/ori/internal/files" ) // workspace builds a small tree and returns a mux serving the file API on it. func workspace(t *testing.T) (string, http.Handler) { t.Helper() root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "src"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("# hello"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "src", "main.go"), []byte("package main"), 0o644); err != nil { t.Fatal(err) } mux := http.NewServeMux() for pattern, handler := range files.New(root).Routes() { mux.Handle(pattern, handler) } return root, mux } func do(t *testing.T, h http.Handler, method, target, body string) *httptest.ResponseRecorder { t.Helper() var req *http.Request if body == "" { req = httptest.NewRequest(method, target, nil) } else { req = httptest.NewRequest(method, target, strings.NewReader(body)) } rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } func decodeBody(t *testing.T, rec *httptest.ResponseRecorder) map[string]any { t.Helper() var payload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("response is not JSON: %v (%q)", err, rec.Body.String()) } return payload } func TestListRootSortsDirectoriesFirst(t *testing.T) { _, h := workspace(t) rec := do(t, h, http.MethodGet, "/api/files", "") if rec.Code != http.StatusOK { t.Fatalf("GET /api/files = %d, want 200 (%s)", rec.Code, rec.Body.String()) } payload := decodeBody(t, rec) entries := payload["entries"].([]any) if len(entries) != 2 { t.Fatalf("entries = %d, want 2", len(entries)) } first := entries[0].(map[string]any) if first["name"] != "src" || first["isDir"] != true { t.Errorf("first entry = %v, want the src directory first", first) } } func TestListSubdirectoryByRelativePath(t *testing.T) { _, h := workspace(t) rec := do(t, h, http.MethodGet, "/api/files?path=src", "") if rec.Code != http.StatusOK { t.Fatalf("GET /api/files?path=src = %d (%s)", rec.Code, rec.Body.String()) } entries := decodeBody(t, rec)["entries"].([]any) if len(entries) != 1 || entries[0].(map[string]any)["name"] != "main.go" { t.Errorf("entries = %v, want [main.go]", entries) } } func TestListMissingDirectoryIs404(t *testing.T) { _, h := workspace(t) rec := do(t, h, http.MethodGet, "/api/files?path=nope", "") if rec.Code != http.StatusNotFound { t.Errorf("GET missing dir = %d, want 404", rec.Code) } } func TestReadFileByAbsoluteAndRelativePath(t *testing.T) { root, h := workspace(t) rec := do(t, h, http.MethodGet, "/api/file?path=README.md", "") if rec.Code != http.StatusOK { t.Fatalf("GET relative = %d (%s)", rec.Code, rec.Body.String()) } if decodeBody(t, rec)["content"] != "# hello" { t.Errorf("relative read content = %v", decodeBody(t, rec)["content"]) } abs := filepath.Join(root, "src", "main.go") rec = do(t, h, http.MethodGet, "/api/file?path="+abs, "") if rec.Code != http.StatusOK || decodeBody(t, rec)["content"] != "package main" { t.Errorf("absolute read = %d %v", rec.Code, decodeBody(t, rec)) } } func TestReadDirectoryIsRejected(t *testing.T) { _, h := workspace(t) rec := do(t, h, http.MethodGet, "/api/file?path=src", "") if rec.Code != http.StatusBadRequest { t.Errorf("GET a directory = %d, want 400", rec.Code) } } func TestReadBinaryFileIs415(t *testing.T) { root, h := workspace(t) if err := os.WriteFile(filepath.Join(root, "blob.bin"), []byte{0xff, 0xfe, 0x00, 0x80}, 0o644); err != nil { t.Fatal(err) } rec := do(t, h, http.MethodGet, "/api/file?path=blob.bin", "") if rec.Code != http.StatusUnsupportedMediaType { t.Errorf("GET binary = %d, want 415", rec.Code) } } func TestReadMissingFileIs404(t *testing.T) { _, h := workspace(t) rec := do(t, h, http.MethodGet, "/api/file?path=ghost.txt", "") if rec.Code != http.StatusNotFound { t.Errorf("GET missing file = %d, want 404", rec.Code) } } func TestWriteRoundTripAndParentCreation(t *testing.T) { root, h := workspace(t) rec := do(t, h, http.MethodPut, "/api/file?path=deep/dir/new.txt", `{"content":"saved by test"}`) if rec.Code != http.StatusOK { t.Fatalf("PUT = %d (%s)", rec.Code, rec.Body.String()) } onDisk, err := os.ReadFile(filepath.Join(root, "deep", "dir", "new.txt")) if err != nil || string(onDisk) != "saved by test" { t.Fatalf("written file = %q, %v", onDisk, err) } rec = do(t, h, http.MethodGet, "/api/file?path=deep/dir/new.txt", "") if decodeBody(t, rec)["content"] != "saved by test" { t.Errorf("read-back content = %v", decodeBody(t, rec)["content"]) } } func TestWriteRequiresPathAndValidJSON(t *testing.T) { _, h := workspace(t) if rec := do(t, h, http.MethodPut, "/api/file", `{"content":"x"}`); rec.Code != http.StatusBadRequest { t.Errorf("PUT without path = %d, want 400", rec.Code) } if rec := do(t, h, http.MethodPut, "/api/file?path=x.txt", `{not json`); rec.Code != http.StatusBadRequest { t.Errorf("PUT invalid JSON = %d, want 400", rec.Code) } }