feat: implement WYSIWYG markdown editor with Go backend and React frontend
- 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
This commit is contained in:
237
internal/storage/storage_test.go
Normal file
237
internal/storage/storage_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStorage_Init(t *testing.T) {
|
||||
// Create temp directory for testing
|
||||
tempDir := t.TempDir()
|
||||
_ = NewStorage(tempDir)
|
||||
|
||||
// Verify directory exists
|
||||
if _, err := os.Stat(tempDir); os.IsNotExist(err) {
|
||||
t.Errorf("data directory should be created")
|
||||
}
|
||||
|
||||
// Verify it's empty
|
||||
files, err := os.ReadDir(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read directory: %v", err)
|
||||
}
|
||||
if len(files) != 0 {
|
||||
t.Errorf("directory should be empty initially")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_ListFiles_Empty(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
store := NewStorage(tempDir)
|
||||
|
||||
files, err := store.ListFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiles failed: %v", err)
|
||||
}
|
||||
if len(files) != 0 {
|
||||
t.Errorf("expected 0 files, got %d", len(files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_ListFiles_Multiple(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
store := NewStorage(tempDir)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
files, err := store.ListFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiles failed: %v", err)
|
||||
}
|
||||
if len(files) != 3 {
|
||||
t.Errorf("expected 3 files, got %d", len(files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_CRUD(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
store := NewStorage(tempDir)
|
||||
|
||||
// Test Create
|
||||
filename := "test.md"
|
||||
content := "# Hello World\n\nThis is a test file."
|
||||
created, err := store.CreateFile(filename, content)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateFile failed: %v", err)
|
||||
}
|
||||
if created.Filename != filename {
|
||||
t.Errorf("expected filename %s, got %s", filename, created.Filename)
|
||||
}
|
||||
if created.Content != content {
|
||||
t.Errorf("expected content %s, got %s", content, created.Content)
|
||||
}
|
||||
if created.Title != "test" {
|
||||
t.Errorf("expected title 'test', got '%s'", created.Title)
|
||||
}
|
||||
if created.Modified.IsZero() {
|
||||
t.Errorf("modified timestamp should not be zero")
|
||||
}
|
||||
if created.Size != int64(len(content)) {
|
||||
t.Errorf("expected size %d, got %d", len(content), created.Size)
|
||||
}
|
||||
|
||||
// Test Get
|
||||
retrieved, err := store.GetFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("GetFile failed: %v", err)
|
||||
}
|
||||
if retrieved.Content != content {
|
||||
t.Errorf("expected content %s, got %s", content, retrieved.Content)
|
||||
}
|
||||
|
||||
// Test Update
|
||||
newContent := "# Updated\n\nContent was updated."
|
||||
updated, err := store.UpdateFile(filename, newContent)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateFile failed: %v", err)
|
||||
}
|
||||
if updated.Content != newContent {
|
||||
t.Errorf("expected updated content %s, got %s", newContent, updated.Content)
|
||||
}
|
||||
if updated.Modified.Before(created.Modified) {
|
||||
t.Errorf("modified timestamp should be updated")
|
||||
}
|
||||
|
||||
// Test List includes updated file
|
||||
files, err := store.ListFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiles failed: %v", err)
|
||||
}
|
||||
var found bool
|
||||
for _, f := range files {
|
||||
if f.Filename == filename {
|
||||
found = true
|
||||
if f.Size != int64(len(newContent)) {
|
||||
t.Errorf("expected size %d for list, got %d", len(newContent), f.Size)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("updated file not found in list")
|
||||
}
|
||||
|
||||
// Test Delete
|
||||
if err := store.DeleteFile(filename); err != nil {
|
||||
t.Fatalf("DeleteFile failed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(tempDir, filename)); !os.IsNotExist(err) {
|
||||
t.Errorf("file should be deleted")
|
||||
}
|
||||
|
||||
// Verify file is gone
|
||||
if _, err := store.GetFile(filename); err == nil {
|
||||
t.Errorf("GetFile should fail after deletion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_ValidateFilename(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
store := NewStorage(tempDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
valid bool
|
||||
}{
|
||||
{"valid .md file", "test.md", true},
|
||||
{"valid with numbers", "file123.md", true},
|
||||
{"invalid without extension", "test", false},
|
||||
{"invalid wrong extension", "test.txt", false},
|
||||
{"invalid path traversal", "../etc/passwd", false},
|
||||
{"invalid with slash", "dir/file.md", false},
|
||||
{"empty filename", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := store.ValidateFilename(tt.filename)
|
||||
if tt.valid && err != nil {
|
||||
t.Errorf("expected valid, got error: %v", err)
|
||||
}
|
||||
if !tt.valid && err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_FileNotFound(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
store := NewStorage(tempDir)
|
||||
|
||||
_, err := store.GetFile("nonexistent.md")
|
||||
if err == nil {
|
||||
t.Errorf("GetFile should return error for non-existent file")
|
||||
}
|
||||
|
||||
_, err = store.UpdateFile("nonexistent.md", "content")
|
||||
if err == nil {
|
||||
t.Errorf("UpdateFile should return error for non-existent file")
|
||||
}
|
||||
|
||||
err = store.DeleteFile("nonexistent.md")
|
||||
if err == nil {
|
||||
t.Errorf("DeleteFile should return error for non-existent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_FileExists(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
store := NewStorage(tempDir)
|
||||
|
||||
// Create file
|
||||
_, err := store.CreateFile("exists.md", "content")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateFile failed: %v", err)
|
||||
}
|
||||
|
||||
// Try to create again
|
||||
_, err = store.CreateFile("exists.md", "content")
|
||||
if err == nil {
|
||||
t.Errorf("CreateFile should fail for existing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_ModificationTime(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
store := NewStorage(tempDir)
|
||||
|
||||
// Create file
|
||||
created, err := store.CreateFile("test.md", "content")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateFile failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait a moment
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Update file
|
||||
updated, err := store.UpdateFile("test.md", "new content")
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateFile failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify modification time was updated
|
||||
if !updated.Modified.After(created.Modified) {
|
||||
t.Errorf("modified time should be updated on update")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user