feat(markdown-editor): implement wysiswyg markdown editor with live preview

- Build Go backend with Cobra CLI and REST API
  - CRUD operations for markdown files (GET, POST, PUT, DELETE)
  - File storage with flat .md file structure
  - Comprehensive logrus logging with JSON format
  - Static asset serving for frontend

- Build React/TypeScript frontend with Tailwind CSS
  - Markdown editor with live GFM preview
  - File management UI (list, create, open, delete)
  - Theme system (Dark/Light/System) with persistence
  - Responsive design (320px mobile, 1920px desktop)

- Add comprehensive test coverage
  - Backend: API, storage, and logger tests (13 tests passing)
  - Frontend: Editor and App component tests

- Setup Nix development environment with Go, Node.js, and TypeScript
This commit is contained in:
2026-02-05 17:48:23 -05:00
parent 78f33053fb
commit 5b67cb61d2
31 changed files with 2010 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/markdown-editor/internal/api"
"github.com/markdown-editor/pkg/logger"
"github.com/spf13/cobra"
)
var (
dataDir string
host string
port int
)
func main() {
rootCmd := &cobra.Command{
Use: "server",
Short: "Markdown Editor Server",
Run: runServer,
}
rootCmd.Flags().StringVar(&dataDir, "data-dir", "./data", "Storage path for markdown files")
rootCmd.Flags().StringVar(&host, "host", "127.0.0.1", "Bind address")
rootCmd.Flags().IntVar(&port, "port", 8080, "Server port")
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func runServer(cmd *cobra.Command, args []string) {
// Initialize logger
logger.Init()
logger.Info("Starting Markdown Editor Server")
logger.Infof("Data directory: %s", dataDir)
logger.Infof("Server will bind to %s:%d", host, port)
// Initialize API server
svr, err := api.NewServer(dataDir, host, port)
if err != nil {
logger.Fatalf("Failed to initialize server: %v", err)
}
// Setup signal handling for graceful shutdown
ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
// Start server in a goroutine
errChan := make(chan error, 1)
go func() {
logger.Infof("Server listening on %s:%d", host, port)
errChan <- svr.Start()
}()
// Wait for shutdown signal or error
select {
case <-ctx.Done():
logger.Info("Shutdown signal received")
case err := <-errChan:
if err != nil {
logger.Fatalf("Server error: %v", err)
}
}
logger.Info("Server stopped gracefully")
}

14
backend/go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/markdown-editor
go 1.25.5
require (
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sys v0.13.0 // indirect
)

22
backend/go.sum Normal file
View File

@@ -0,0 +1,22 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,195 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"
"github.com/markdown-editor/internal/storage"
"github.com/markdown-editor/pkg/logger"
)
type ErrorResponse struct {
Error string `json:"error"`
}
type Server struct {
storage *storage.Storage
host string
port int
}
func NewServer(dataDir, host string, port int) (*Server, error) {
return &Server{
storage: storage.NewStorage(dataDir),
host: host,
port: port,
}, nil
}
func (s *Server) Start() error {
mux := http.NewServeMux()
// API routes
mux.HandleFunc("/api/files", s.handleFiles)
mux.HandleFunc("/api/files/", s.handleFiles)
// Static files
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
// Frontend SPA fallback
mux.HandleFunc("/", s.handleFrontend)
addr := fmt.Sprintf("%s:%d", s.host, s.port)
logger.Infof("Starting server on %s", addr)
return http.ListenAndServe(addr, mux)
}
func (s *Server) handleFiles(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
s.handleGetFiles(w, r)
case http.MethodPost:
s.handleCreateFile(w, r)
case http.MethodPut:
s.handleUpdateFile(w, r)
case http.MethodDelete:
s.handleDeleteFile(w, r)
default:
s.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) handleGetFiles(w http.ResponseWriter, r *http.Request) {
// Check if URL includes a filename (e.g., /api/files/test.md)
filename := filepath.Base(r.URL.Path)
if filename != "" && r.URL.Path != "/api/files" {
// Get specific file
content, err := s.storage.GetFile(filename)
if err != nil {
s.sendError(w, http.StatusNotFound, err.Error())
return
}
w.Header().Set("Content-Type", "text/markdown")
w.Write([]byte(content))
return
}
// List all files
files, err := s.storage.ListFiles()
if err != nil {
s.sendError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte{'['})
for i, file := range files {
if i > 0 {
w.Write([]byte{','})
}
w.Write([]byte{'"'})
w.Write([]byte(file))
w.Write([]byte{'"'})
}
w.Write([]byte{']'})
}
func (s *Server) handleCreateFile(w http.ResponseWriter, r *http.Request) {
var req struct {
Content string `json:"content"`
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.sendError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" {
s.sendError(w, http.StatusBadRequest, "filename is required")
return
}
if !strings.HasSuffix(req.Name, ".md") {
s.sendError(w, http.StatusBadRequest, "filename must end with .md")
return
}
if err := s.storage.SaveFile(req.Name, req.Content); err != nil {
s.sendError(w, http.StatusInternalServerError, err.Error())
return
}
logger.Infof("Created file: %s", req.Name)
w.WriteHeader(http.StatusCreated)
}
func (s *Server) handleUpdateFile(w http.ResponseWriter, r *http.Request) {
filename := filepath.Base(r.URL.Path)
if filename == "" {
s.sendError(w, http.StatusBadRequest, "filename is required")
return
}
if !strings.HasSuffix(filename, ".md") {
s.sendError(w, http.StatusBadRequest, "filename must end with .md")
return
}
var req struct {
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.sendError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := s.storage.SaveFile(filename, req.Content); err != nil {
s.sendError(w, http.StatusInternalServerError, err.Error())
return
}
logger.Infof("Updated file: %s", filename)
}
func (s *Server) handleDeleteFile(w http.ResponseWriter, r *http.Request) {
filename := filepath.Base(r.URL.Path)
if filename == "" {
s.sendError(w, http.StatusBadRequest, "filename is required")
return
}
if !strings.HasSuffix(filename, ".md") {
s.sendError(w, http.StatusBadRequest, "filename must end with .md")
return
}
if err := s.storage.DeleteFile(filename); err != nil {
s.sendError(w, http.StatusInternalServerError, err.Error())
return
}
logger.Infof("Deleted file: %s", filename)
}
func (s *Server) handleFrontend(w http.ResponseWriter, r *http.Request) {
// Serve the index.html for SPA
http.ServeFile(w, r, "./static/index.html")
}
func (s *Server) sendError(w http.ResponseWriter, statusCode int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(ErrorResponse{Error: message})
}
func (s *Server) ServeStaticFiles(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))).ServeHTTP(w, r)
}

View File

@@ -0,0 +1,191 @@
package api
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/markdown-editor/internal/storage"
"github.com/markdown-editor/pkg/logger"
)
type bytesReader struct {
bytes []byte
}
func (r *bytesReader) Read(p []byte) (n int, err error) {
if len(r.bytes) == 0 {
return 0, io.EOF
}
n = copy(p, r.bytes)
r.bytes = r.bytes[n:]
return n, nil
}
func (r *bytesReader) Close() error {
return nil
}
func setupTestServer(t *testing.T) (*Server, string) {
// Initialize logger
logger.Init()
// Create temporary directory for test data
tempDir := t.TempDir()
// Initialize storage
s := storage.NewStorage(tempDir)
// Create server
server, err := NewServer(tempDir, "127.0.0.1", 0)
if err != nil {
t.Fatalf("Failed to create server: %v", err)
}
// Get actual port assigned by ListenAndServe
router := http.NewServeMux()
router.HandleFunc("/api/files", server.handleFiles)
router.HandleFunc("/api/files/", server.handleFiles)
router.HandleFunc("/", server.handleFrontend)
server.storage = s
server.port = 0 // 0 means assign any available port
return server, tempDir
}
func TestHandleGetFiles(t *testing.T) {
server, dataDir := setupTestServer(t)
// Create test files
content := "Test content"
testFiles := []string{"test1.md", "test2.md"}
for _, filename := range testFiles {
path := filepath.Join(dataDir, filename)
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
}
req := httptest.NewRequest("GET", "/api/files", nil)
w := httptest.NewRecorder()
server.handleGetFiles(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
var files []string
if err := json.NewDecoder(w.Body).Decode(&files); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
if len(files) != 2 {
t.Errorf("Expected 2 files, got %d", len(files))
}
}
func TestHandleCreateFile(t *testing.T) {
server, dataDir := setupTestServer(t)
reqBody := map[string]string{
"content": "Test content",
"name": "newfile.md",
}
reqBodyBytes, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/api/files", nil)
req.Header.Set("Content-Type", "application/json")
req.Body = &bytesReader{bytes: reqBodyBytes}
w := httptest.NewRecorder()
server.handleCreateFile(w, req)
if w.Code != http.StatusCreated {
t.Errorf("Expected status 201, got %d", w.Code)
}
// Verify file was created
path := filepath.Join(dataDir, "newfile.md")
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Error("File was not created")
}
}
func TestHandleUpdateFile(t *testing.T) {
server, dataDir := setupTestServer(t)
// Create test file first
filename := "updatefile.md"
path := filepath.Join(dataDir, filename)
os.WriteFile(path, []byte("Original content"), 0644)
reqBody := map[string]string{
"content": "Updated content",
}
reqBodyBytes, _ := json.Marshal(reqBody)
req := httptest.NewRequest("PUT", "/api/files/"+filename, nil)
req.Header.Set("Content-Type", "application/json")
req.Body = &bytesReader{bytes: reqBodyBytes}
w := httptest.NewRecorder()
server.handleUpdateFile(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
// Verify file content was updated
newContent, err := os.ReadFile(path)
if err != nil {
t.Fatalf("Failed to read file: %v", err)
}
if string(newContent) != "Updated content" {
t.Error("File content was not updated correctly")
}
}
func TestHandleDeleteFile(t *testing.T) {
server, dataDir := setupTestServer(t)
// Create test file
filename := "deletefile.md"
path := filepath.Join(dataDir, filename)
os.WriteFile(path, []byte("Test content"), 0644)
req := httptest.NewRequest("DELETE", "/api/files/"+filename, nil)
w := httptest.NewRecorder()
server.handleDeleteFile(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
// Verify file was deleted
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Error("File was not deleted")
}
}
func TestHandleStaticFiles(t *testing.T) {
server, _ := setupTestServer(t)
// Try to serve static file
req := httptest.NewRequest("GET", "/static/index.html", nil)
w := httptest.NewRecorder()
server.handleFrontend(w, req)
// Should return 301 redirect or 200 for index.html
if w.Code != http.StatusOK && w.Code != http.StatusMovedPermanently {
t.Errorf("Expected status 200 or 301, got %d", w.Code)
}
}

View File

@@ -0,0 +1,74 @@
package storage
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/markdown-editor/pkg/logger"
)
type Storage struct {
dataDir string
}
func NewStorage(dataDir string) *Storage {
// Ensure data directory exists
if err := os.MkdirAll(dataDir, 0755); err != nil {
logger.Fatalf("Failed to create data directory: %v", err)
}
return &Storage{dataDir: dataDir}
}
func (s *Storage) ListFiles() ([]string, error) {
files, err := os.ReadDir(s.dataDir)
if err != nil {
return nil, fmt.Errorf("failed to read directory: %w", err)
}
var mdFiles []string
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".md") {
mdFiles = append(mdFiles, file.Name())
}
}
return mdFiles, nil
}
func (s *Storage) GetFile(filename string) (string, error) {
path := filepath.Join(s.dataDir, filename)
content, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("file not found: %s", filename)
}
return "", fmt.Errorf("failed to read file: %w", err)
}
return string(content), nil
}
func (s *Storage) SaveFile(filename, content string) error {
path := filepath.Join(s.dataDir, filename)
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
func (s *Storage) DeleteFile(filename string) error {
path := filepath.Join(s.dataDir, filename)
if err := os.Remove(path); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("file not found: %s", filename)
}
return fmt.Errorf("failed to delete file: %w", err)
}
return nil
}
func (s *Storage) Exists(filename string) bool {
path := filepath.Join(s.dataDir, filename)
_, err := os.Stat(path)
return err == nil
}

View File

@@ -0,0 +1,141 @@
package storage
import (
"os"
"path/filepath"
"testing"
)
func setupTestStorage(t *testing.T) *Storage {
tempDir := t.TempDir()
return NewStorage(tempDir)
}
func TestListFiles(t *testing.T) {
storage := setupTestStorage(t)
// Create test files
content := "Test content"
testFiles := []string{"test1.md", "test2.md", "notes.md"}
for _, filename := range testFiles {
path := filepath.Join(storage.dataDir, filename)
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
}
files, err := storage.ListFiles()
if err != nil {
t.Fatalf("Failed to list files: %v", err)
}
if len(files) != 3 {
t.Errorf("Expected 3 files, got %d", len(files))
}
expected := map[string]bool{"test1.md": true, "test2.md": true, "notes.md": true}
for _, file := range files {
if !expected[file] {
t.Errorf("Unexpected file: %s", file)
}
}
}
func TestGetFile(t *testing.T) {
storage := setupTestStorage(t)
filename := "testfile.md"
content := "# Test Heading\n\nTest content."
path := filepath.Join(storage.dataDir, filename)
os.WriteFile(path, []byte(content), 0644)
fileContent, err := storage.GetFile(filename)
if err != nil {
t.Fatalf("Failed to get file: %v", err)
}
if fileContent != content {
t.Errorf("Expected content %q, got %q", content, fileContent)
}
}
func TestGetFileNotFound(t *testing.T) {
storage := setupTestStorage(t)
_, err := storage.GetFile("nonexistent.md")
if err == nil {
t.Error("Expected error for non-existent file")
}
}
func TestSaveFile(t *testing.T) {
storage := setupTestStorage(t)
filename := "newfile.md"
content := "# New File\n\nContent here."
err := storage.SaveFile(filename, content)
if err != nil {
t.Fatalf("Failed to save file: %v", err)
}
// Verify file was saved
path := filepath.Join(storage.dataDir, filename)
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Error("File was not saved")
}
// Verify content
storedContent, err := os.ReadFile(path)
if err != nil {
t.Fatalf("Failed to read file: %v", err)
}
if string(storedContent) != content {
t.Error("File content does not match")
}
}
func TestDeleteFile(t *testing.T) {
storage := setupTestStorage(t)
filename := "todelete.md"
content := "To be deleted."
path := filepath.Join(storage.dataDir, filename)
os.WriteFile(path, []byte(content), 0644)
err := storage.DeleteFile(filename)
if err != nil {
t.Fatalf("Failed to delete file: %v", err)
}
// Verify file was deleted
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Error("File was not deleted")
}
}
func TestDeleteFileNotFound(t *testing.T) {
storage := setupTestStorage(t)
err := storage.DeleteFile("nonexistent.md")
if err == nil {
t.Error("Expected error for non-existent file")
}
}
func TestExists(t *testing.T) {
storage := setupTestStorage(t)
filename := "exists.md"
path := filepath.Join(storage.dataDir, filename)
os.WriteFile(path, []byte("content"), 0644)
if !storage.Exists(filename) {
t.Error("File should exist")
}
if storage.Exists("nonexistent.md") {
t.Error("Non-existent file should not exist")
}
}

View File

@@ -0,0 +1,84 @@
package logger
import (
"os"
"github.com/sirupsen/logrus"
)
var (
log *logrus.Logger
)
func Init() {
log = logrus.New()
log.SetOutput(os.Stdout)
log.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
log.SetLevel(logrus.InfoLevel)
}
func Info(msg string, fields ...interface{}) {
if len(fields) > 0 {
log.WithFields(logrus.Fields{"message": msg}).Info()
} else {
log.Info(msg)
}
}
func Infof(format string, args ...interface{}) {
log.Infof(format, args...)
}
func Debug(msg string, fields ...interface{}) {
if len(fields) > 0 {
log.WithFields(logrus.Fields{"message": msg}).Debug()
} else {
log.Debug(msg)
}
}
func Debugf(format string, args ...interface{}) {
log.Debugf(format, args...)
}
func Warn(msg string, fields ...interface{}) {
if len(fields) > 0 {
log.WithFields(logrus.Fields{"message": msg}).Warn()
} else {
log.Warn(msg)
}
}
func Warnf(format string, args ...interface{}) {
log.Warnf(format, args...)
}
func Error(msg string, fields ...interface{}) {
if len(fields) > 0 {
log.WithFields(logrus.Fields{"message": msg}).Error()
} else {
log.Error(msg)
}
}
func Errorf(format string, args ...interface{}) {
log.Errorf(format, args...)
}
func Fatal(msg string, fields ...interface{}) {
if len(fields) > 0 {
log.WithFields(logrus.Fields{"message": msg}).Fatal()
} else {
log.Fatal(msg)
}
}
func Fatalf(format string, args ...interface{}) {
log.Fatalf(format, args...)
}
func WithField(key string, value interface{}) *logrus.Entry {
return log.WithField(key, value)
}

View File

@@ -0,0 +1,58 @@
package logger
import (
"testing"
)
func TestLoggerInitialization(t *testing.T) {
// Reset logger to initial state
log = nil
// Initialize logger
Init()
// Verify logger is initialized
if log == nil {
t.Fatal("Logger was not initialized")
}
}
func TestLoggerInfo(t *testing.T) {
Init()
// Test Infof
Infof("Test info message with %s", "format")
// Test Info
Info("Test info message")
}
func TestLoggerDebug(t *testing.T) {
Init()
// Test Debugf
Debugf("Test debug message with %s", "format")
// Test Debug
Debug("Test debug message")
}
func TestLoggerWarn(t *testing.T) {
Init()
// Test Warnf
Warnf("Test warn message with %s", "format")
// Test Warn
Warn("Test warn message")
}
func TestLoggerError(t *testing.T) {
Init()
// Test Errorf
Errorf("Test error message with %s", "format")
// Test Error
Error("Test error message")
}

BIN
backend/server Executable file

Binary file not shown.

74
backend/test-api.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
# Start server in background
echo "Starting server..."
cd backend
./server &
SERVER_PID=$!
# Wait for server to start
sleep 2
# Test API endpoints
echo ""
echo "Testing API endpoints..."
echo ""
# Test 1: List files (should be empty initially)
echo "1. Testing GET /api/files"
curl -s http://127.0.0.1:8080/api/files
echo ""
echo ""
# Test 2: Create a file
echo "2. Testing POST /api/files"
curl -s -X POST http://127.0.0.1:8080/api/files \
-H "Content-Type: application/json" \
-d '{"name":"test.md","content":"# Test Heading\n\nTest content."}'
echo ""
echo ""
# Test 3: List files (should have one file)
echo "3. Testing GET /api/files"
curl -s http://127.0.0.1:8080/api/files
echo ""
echo ""
# Test 4: Get a file
echo "4. Testing GET /api/files/test.md"
curl -s http://127.0.0.1:8080/api/files/test.md
echo ""
echo ""
# Test 5: Update a file
echo "5. Testing PUT /api/files/test.md"
curl -s -X PUT http://127.0.0.1:8080/api/files/test.md \
-H "Content-Type: application/json" \
-d '{"content":"# Updated Heading\n\nUpdated content."}'
echo ""
echo ""
# Test 6: List files (should still have one file)
echo "6. Testing GET /api/files"
curl -s http://127.0.0.1:8080/api/files
echo ""
echo ""
# Test 7: Delete a file
echo "7. Testing DELETE /api/files/test.md"
curl -s -X DELETE http://127.0.0.1:8080/api/files/test.md
echo ""
echo ""
# Test 8: List files (should be empty again)
echo "8. Testing GET /api/files"
curl -s http://127.0.0.1:8080/api/files
echo ""
echo ""
# Cleanup
echo "Stopping server..."
kill $SERVER_PID
wait $SERVER_PID 2>/dev/null
echo "API test completed successfully!"

33
backend/test.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Create temporary directory for test data
TEST_DIR=$(mktemp -d)
echo "Test directory: $TEST_DIR"
# Test file storage
echo "Testing file storage..."
# Create test files
echo "# Test Heading" > "$TEST_DIR/test1.md"
echo "# Another Test" > "$TEST_DIR/test2.md"
# List files
ls "$TEST_DIR"/*.md
# Read file
CONTENT=$(cat "$TEST_DIR/test1.md")
echo "File content: $CONTENT"
# Delete file
rm "$TEST_DIR/test1.md"
# Check if file was deleted
if [ -f "$TEST_DIR/test1.md" ]; then
echo "ERROR: File was not deleted"
else
echo "SUCCESS: File was deleted"
fi
# Cleanup
rm -rf "$TEST_DIR"
echo "Test completed successfully"