330 lines
9.3 KiB
Go
330 lines
9.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/database"
|
|
)
|
|
|
|
// ── Notes: Validation (no DB needed) ────────
|
|
|
|
func TestCreateNoteMissingTitle(t *testing.T) {
|
|
h := NewNoteHandler()
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set("user_id", "test-user")
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/notes",
|
|
strings.NewReader(`{"content":"body only"}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.Create(c)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected 400 for missing title, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateNoteMissingContent(t *testing.T) {
|
|
h := NewNoteHandler()
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set("user_id", "test-user")
|
|
c.Request = httptest.NewRequest("POST", "/api/v1/notes",
|
|
strings.NewReader(`{"title":"title only"}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
h.Create(c)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected 400 for missing content, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// ── Notes: Full CRUD Integration ────────────
|
|
|
|
func TestNoteCRUDIntegration(t *testing.T) {
|
|
database.RequireTestDB(t)
|
|
database.TruncateAll(t)
|
|
|
|
userID := database.SeedTestUser(t, "noteuser", "note@test.com")
|
|
|
|
h := NewNoteHandler()
|
|
r := gin.New()
|
|
r.Use(func(c *gin.Context) { c.Set("user_id", userID); c.Next() })
|
|
r.POST("/notes", h.Create)
|
|
r.GET("/notes", h.List)
|
|
r.GET("/notes/search", h.Search)
|
|
r.GET("/notes/folders", h.ListFolders)
|
|
r.GET("/notes/:id", h.Get)
|
|
r.PUT("/notes/:id", h.Update)
|
|
r.DELETE("/notes/:id", h.Delete)
|
|
|
|
// ── Create ──
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/notes",
|
|
strings.NewReader(`{
|
|
"title": "Meeting Notes",
|
|
"content": "Discussed project timeline and deliverables",
|
|
"folder_path": "/work/meetings",
|
|
"tags": ["project", "planning"]
|
|
}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("Create: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var created map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &created)
|
|
noteID, ok := created["id"].(string)
|
|
if !ok || noteID == "" {
|
|
t.Fatal("Create: missing or empty id in response")
|
|
}
|
|
if created["title"] != "Meeting Notes" {
|
|
t.Errorf("Create: title mismatch: %v", created["title"])
|
|
}
|
|
if created["folder_path"] != "/work/meetings/" {
|
|
t.Errorf("Create: folder_path should be normalized, got %v", created["folder_path"])
|
|
}
|
|
|
|
// ── Create second note for search/list tests ──
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("POST", "/notes",
|
|
strings.NewReader(`{
|
|
"title": "Recipe Ideas",
|
|
"content": "Try making sourdough bread with rosemary",
|
|
"folder_path": "/personal",
|
|
"tags": ["food", "recipes"]
|
|
}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("Create 2nd note: expected 201, got %d", w.Code)
|
|
}
|
|
|
|
// ── Get ──
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes/"+noteID, nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Get: expected 200, got %d", w.Code)
|
|
}
|
|
var got map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &got)
|
|
if got["content"] != "Discussed project timeline and deliverables" {
|
|
t.Errorf("Get: wrong content: %v", got["content"])
|
|
}
|
|
|
|
// ── List (all) ──
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("List: expected 200, got %d", w.Code)
|
|
}
|
|
var listResp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &listResp)
|
|
total := listResp["total"].(float64)
|
|
if total != 2 {
|
|
t.Errorf("List: expected total=2, got %.0f", total)
|
|
}
|
|
|
|
// ── List (filtered by folder) ──
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes?folder=/work/meetings", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("List by folder: expected 200, got %d", w.Code)
|
|
}
|
|
json.Unmarshal(w.Body.Bytes(), &listResp)
|
|
total = listResp["total"].(float64)
|
|
if total != 1 {
|
|
t.Errorf("List by folder: expected total=1, got %.0f", total)
|
|
}
|
|
|
|
// ── List (filtered by tag) ──
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes?tag=food", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("List by tag: expected 200, got %d", w.Code)
|
|
}
|
|
json.Unmarshal(w.Body.Bytes(), &listResp)
|
|
total = listResp["total"].(float64)
|
|
if total != 1 {
|
|
t.Errorf("List by tag: expected total=1, got %.0f", total)
|
|
}
|
|
|
|
// ── Search ──
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes/search?q=sourdough", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Search: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var searchResp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &searchResp)
|
|
count := searchResp["count"].(float64)
|
|
if count != 1 {
|
|
t.Errorf("Search 'sourdough': expected count=1, got %.0f", count)
|
|
}
|
|
|
|
// ── Folders ──
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes/folders", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Folders: expected 200, got %d", w.Code)
|
|
}
|
|
var folders []interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &folders)
|
|
if len(folders) != 2 {
|
|
t.Errorf("Folders: expected 2 folders, got %d", len(folders))
|
|
}
|
|
|
|
// ── Update (replace) ──
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("PUT", "/notes/"+noteID,
|
|
strings.NewReader(`{"title":"Updated Meeting Notes","content":"New content","mode":"replace"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Update: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Verify update
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes/"+noteID, nil)
|
|
r.ServeHTTP(w, req)
|
|
json.Unmarshal(w.Body.Bytes(), &got)
|
|
if got["title"] != "Updated Meeting Notes" {
|
|
t.Errorf("Update title: got %v", got["title"])
|
|
}
|
|
if got["content"] != "New content" {
|
|
t.Errorf("Update content: got %v", got["content"])
|
|
}
|
|
|
|
// ── Update (append) ──
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("PUT", "/notes/"+noteID,
|
|
strings.NewReader(`{"content":"\nAppended line","mode":"append"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Append: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes/"+noteID, nil)
|
|
r.ServeHTTP(w, req)
|
|
json.Unmarshal(w.Body.Bytes(), &got)
|
|
content := got["content"].(string)
|
|
if !strings.Contains(content, "New content") || !strings.Contains(content, "Appended line") {
|
|
t.Errorf("Append: expected both parts in content, got %q", content)
|
|
}
|
|
|
|
// ── Delete ──
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("DELETE", "/notes/"+noteID, nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Delete: expected 200, got %d", w.Code)
|
|
}
|
|
|
|
// Verify gone
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes/"+noteID, nil)
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("Get after delete: expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// ── Notes: Search empty query ───────────────
|
|
|
|
func TestNoteSearchEmptyQuery(t *testing.T) {
|
|
database.RequireTestDB(t)
|
|
|
|
h := NewNoteHandler()
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Set("user_id", "test-user")
|
|
c.Request = httptest.NewRequest("GET", "/api/v1/notes/search", nil)
|
|
|
|
h.Search(c)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Search with no query: expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// ── Notes: Cross-user isolation ─────────────
|
|
|
|
func TestNoteIsolationBetweenUsers(t *testing.T) {
|
|
database.RequireTestDB(t)
|
|
database.TruncateAll(t)
|
|
|
|
userA := database.SeedTestUser(t, "alice", "alice@test.com")
|
|
userB := database.SeedTestUser(t, "bob", "bob@test.com")
|
|
|
|
h := NewNoteHandler()
|
|
|
|
// Alice creates a note
|
|
rA := gin.New()
|
|
rA.Use(func(c *gin.Context) { c.Set("user_id", userA); c.Next() })
|
|
rA.POST("/notes", h.Create)
|
|
rA.GET("/notes", h.List)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/notes",
|
|
strings.NewReader(`{"title":"Alice Secret","content":"private stuff"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rA.ServeHTTP(w, req)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("Alice create: expected 201, got %d", w.Code)
|
|
}
|
|
|
|
// Bob lists — should see zero
|
|
rB := gin.New()
|
|
rB.Use(func(c *gin.Context) { c.Set("user_id", userB); c.Next() })
|
|
rB.GET("/notes", h.List)
|
|
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes", nil)
|
|
rB.ServeHTTP(w, req)
|
|
|
|
var listResp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &listResp)
|
|
total := listResp["total"].(float64)
|
|
if total != 0 {
|
|
t.Errorf("Bob should see 0 notes, got %.0f", total)
|
|
}
|
|
|
|
// Alice lists — should see one
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/notes", nil)
|
|
rA.ServeHTTP(w, req)
|
|
|
|
json.Unmarshal(w.Body.Bytes(), &listResp)
|
|
total = listResp["total"].(float64)
|
|
if total != 1 {
|
|
t.Errorf("Alice should see 1 note, got %.0f", total)
|
|
}
|
|
}
|