- Backend: Go HTTP server with Cobra CLI (--data-dir, --port, --host flags) - CRUD REST API for markdown files with JSON error responses - File storage in flat directory structure (flat structure, .md files only) - Comprehensive logrus logging for all operations - Static file serving for frontend build (./frontend/dist) - Frontend: React + TypeScript + Tailwind CSS - Markdown editor with live GFM preview - File management: list, create, open, save, delete - Theme system (Dark, Light, System) with persistence - Responsive design for desktop and mobile - Backend tests (storage, API handlers) and frontend tests
380 lines
9.5 KiB
Go
380 lines
9.5 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"eval/internal/config"
|
|
"eval/internal/storage"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
func setupTestEnvironment(t *testing.T) (*Handlers, string) {
|
|
t.Helper()
|
|
tempDir := t.TempDir()
|
|
cfg := config.NewConfig()
|
|
cfg.DataDir = tempDir
|
|
|
|
logger := logrus.New()
|
|
logger.SetLevel(logrus.FatalLevel)
|
|
|
|
store := storage.NewStorage(tempDir)
|
|
handlers := NewHandlers(store, cfg, logger)
|
|
return handlers, tempDir
|
|
}
|
|
|
|
func TestHandlers_ListFiles_Empty(t *testing.T) {
|
|
handlers, _ := setupTestEnvironment(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/files", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.listFilesHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", w.Code)
|
|
}
|
|
|
|
var files []storage.FileMetadata
|
|
if err := json.Unmarshal(w.Body.Bytes(), &files); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if len(files) != 0 {
|
|
t.Errorf("expected 0 files, got %d", len(files))
|
|
}
|
|
}
|
|
|
|
func TestHandlers_ListFiles_Multiple(t *testing.T) {
|
|
handlers, tempDir := setupTestEnvironment(t)
|
|
|
|
// Create test files
|
|
testFiles := []string{"file1.md", "file2.md", "file3.md"}
|
|
for _, name := range testFiles {
|
|
content := "# Test " + name
|
|
if err := os.WriteFile(filepath.Join(tempDir, name), []byte(content), 0644); err != nil {
|
|
t.Fatalf("failed to create test file: %v", err)
|
|
}
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/api/files", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.listFilesHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", w.Code)
|
|
}
|
|
|
|
var files []storage.FileMetadata
|
|
if err := json.Unmarshal(w.Body.Bytes(), &files); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if len(files) != 3 {
|
|
t.Errorf("expected 3 files, got %d", len(files))
|
|
}
|
|
}
|
|
|
|
func TestHandlers_GetFile(t *testing.T) {
|
|
handlers, tempDir := setupTestEnvironment(t)
|
|
|
|
// Create test file
|
|
filename := "test.md"
|
|
content := "# Hello World\n\nThis is a test file."
|
|
if err := os.WriteFile(filepath.Join(tempDir, filename), []byte(content), 0644); err != nil {
|
|
t.Fatalf("failed to create test file: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/api/files/"+filename, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.getFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", w.Code)
|
|
}
|
|
|
|
var file storage.FileContent
|
|
if err := json.Unmarshal(w.Body.Bytes(), &file); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if file.Content != content {
|
|
t.Errorf("expected content %s, got %s", content, file.Content)
|
|
}
|
|
if file.Filename != filename {
|
|
t.Errorf("expected filename %s, got %s", filename, file.Filename)
|
|
}
|
|
}
|
|
|
|
func TestHandlers_GetFile_NotFound(t *testing.T) {
|
|
handlers, _ := setupTestEnvironment(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/files/nonexistent.md", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.getFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d", w.Code)
|
|
}
|
|
|
|
var errResp ErrorResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &errResp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandlers_CreateFile(t *testing.T) {
|
|
handlers, _ := setupTestEnvironment(t)
|
|
|
|
filename := "newfile.md"
|
|
content := "# New File\n\nCreated via API."
|
|
body := &storage.FileContent{
|
|
Filename: filename,
|
|
Content: content,
|
|
Title: "New File",
|
|
}
|
|
jsonBody, _ := json.Marshal(body)
|
|
|
|
req := httptest.NewRequest("POST", "/api/files", bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.createFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("expected status 201, got %d", w.Code)
|
|
}
|
|
|
|
var file storage.FileContent
|
|
if err := json.Unmarshal(w.Body.Bytes(), &file); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if file.Content != content {
|
|
t.Errorf("expected content %s, got %s", content, file.Content)
|
|
}
|
|
}
|
|
|
|
func TestHandlers_CreateFile_Validation(t *testing.T) {
|
|
handlers, _ := setupTestEnvironment(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
filename string
|
|
expected int
|
|
}{
|
|
{"empty filename", "", http.StatusBadRequest},
|
|
{"missing extension", "noext", http.StatusInternalServerError},
|
|
{"invalid extension", "file.txt", http.StatusInternalServerError},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
body := &storage.FileContent{
|
|
Filename: tt.filename,
|
|
Content: "content",
|
|
}
|
|
jsonBody, _ := json.Marshal(body)
|
|
|
|
req := httptest.NewRequest("POST", "/api/files", bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.createFileHandler(w, req)
|
|
|
|
if w.Code != tt.expected {
|
|
t.Errorf("expected status %d, got %d", tt.expected, w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandlers_UpdateFile(t *testing.T) {
|
|
handlers, tempDir := setupTestEnvironment(t)
|
|
|
|
// Create initial file
|
|
filename := "update.md"
|
|
content := "# Original"
|
|
if err := os.WriteFile(filepath.Join(tempDir, filename), []byte(content), 0644); err != nil {
|
|
t.Fatalf("failed to create test file: %v", err)
|
|
}
|
|
|
|
// Update file
|
|
newContent := "# Updated\n\nContent changed."
|
|
body := &storage.FileContent{
|
|
Filename: filename,
|
|
Content: newContent,
|
|
Title: "Updated",
|
|
}
|
|
jsonBody, _ := json.Marshal(body)
|
|
|
|
req := httptest.NewRequest("PUT", "/api/files/"+filename, bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.updateFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", w.Code)
|
|
}
|
|
|
|
var file storage.FileContent
|
|
if err := json.Unmarshal(w.Body.Bytes(), &file); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if file.Content != newContent {
|
|
t.Errorf("expected content %s, got %s", newContent, file.Content)
|
|
}
|
|
}
|
|
|
|
func TestHandlers_UpdateFile_NotFound(t *testing.T) {
|
|
handlers, _ := setupTestEnvironment(t)
|
|
|
|
body := &storage.FileContent{
|
|
Filename: "nonexistent.md",
|
|
Content: "content",
|
|
}
|
|
jsonBody, _ := json.Marshal(body)
|
|
|
|
req := httptest.NewRequest("PUT", "/api/files/nonexistent.md", bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.updateFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlers_DeleteFile(t *testing.T) {
|
|
handlers, tempDir := setupTestEnvironment(t)
|
|
|
|
// Create file
|
|
filename := "delete.md"
|
|
content := "# To be deleted"
|
|
if err := os.WriteFile(filepath.Join(tempDir, filename), []byte(content), 0644); err != nil {
|
|
t.Fatalf("failed to create test file: %v", err)
|
|
}
|
|
|
|
// Delete file
|
|
req := httptest.NewRequest("DELETE", "/api/files/"+filename, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.deleteFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", w.Code)
|
|
}
|
|
|
|
// Verify file is deleted
|
|
if _, err := os.Stat(filepath.Join(tempDir, filename)); !os.IsNotExist(err) {
|
|
t.Errorf("file should be deleted")
|
|
}
|
|
}
|
|
|
|
func TestHandlers_DeleteFile_NotFound(t *testing.T) {
|
|
handlers, _ := setupTestEnvironment(t)
|
|
|
|
req := httptest.NewRequest("DELETE", "/api/files/nonexistent.md", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.deleteFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandlers_CRUDRoundTrip(t *testing.T) {
|
|
handlers, tempDir := setupTestEnvironment(t)
|
|
|
|
// Create
|
|
filename := "roundtrip.md"
|
|
content := "# Round Trip Test"
|
|
body := &storage.FileContent{
|
|
Filename: filename,
|
|
Content: content,
|
|
Title: "Round Trip Test",
|
|
}
|
|
jsonBody, _ := json.Marshal(body)
|
|
|
|
req := httptest.NewRequest("POST", "/api/files", bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.createFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("Create failed with status %d", w.Code)
|
|
}
|
|
|
|
var created storage.FileContent
|
|
json.Unmarshal(w.Body.Bytes(), &created)
|
|
|
|
// Read
|
|
req = httptest.NewRequest("GET", "/api/files/"+filename, nil)
|
|
w = httptest.NewRecorder()
|
|
handlers.getFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Read failed with status %d", w.Code)
|
|
}
|
|
|
|
var retrieved storage.FileContent
|
|
json.Unmarshal(w.Body.Bytes(), &retrieved)
|
|
|
|
if retrieved.Content != content {
|
|
t.Errorf("Content mismatch after read: expected %s, got %s", content, retrieved.Content)
|
|
}
|
|
|
|
// Update
|
|
newContent := "# Round Trip Updated"
|
|
body = &storage.FileContent{
|
|
Filename: filename,
|
|
Content: newContent,
|
|
Title: "Round Trip Updated",
|
|
}
|
|
jsonBody, _ = json.Marshal(body)
|
|
|
|
req = httptest.NewRequest("PUT", "/api/files/"+filename, bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w = httptest.NewRecorder()
|
|
handlers.updateFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Update failed with status %d", w.Code)
|
|
}
|
|
|
|
// Delete
|
|
req = httptest.NewRequest("DELETE", "/api/files/"+filename, nil)
|
|
w = httptest.NewRecorder()
|
|
handlers.deleteFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Delete failed with status %d", w.Code)
|
|
}
|
|
|
|
// Verify deletion
|
|
req = httptest.NewRequest("GET", "/api/files/"+filename, nil)
|
|
w = httptest.NewRecorder()
|
|
handlers.getFileHandler(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("Expected 404 after delete, got %d", w.Code)
|
|
}
|
|
|
|
// Verify file is deleted on disk
|
|
if _, err := os.Stat(filepath.Join(tempDir, filename)); !os.IsNotExist(err) {
|
|
t.Errorf("file should be deleted on disk")
|
|
}
|
|
} |