server/internal/handlers/notes_test.go
Rama main · réplica de lectura del repositorio original.
package handlers_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"blackbox/server/internal/auth"
"blackbox/server/internal/handlers"
"blackbox/server/internal/models"
"blackbox/shared/types"
"github.com/go-chi/chi/v5"
"github.com/oklog/ulid/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func ctxWithClaims(userID, username string) context.Context {
claims := &auth.Claims{UserID: userID, Username: username}
return context.WithValue(context.Background(), auth.ClaimsKey, claims)
}
func TestCreateNote_Success(t *testing.T) {
database := newTestDB(t)
userID := ulid.Make().String()
entry := types.Entry{
ID: ulid.Make().String(),
Timestamp: time.Now().UTC(),
NodeName: "homelab-01",
Source: "docker",
Event: "die",
Content: "container nginx died",
}
require.NoError(t, database.Create(&entry).Error)
body, err := json.Marshal(map[string]string{"content": "OOM killer, memcache misconfigured."})
require.NoError(t, err)
router := chi.NewRouter()
router.Post("/api/entries/{id}/notes", handlers.CreateNote(database))
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/entries/%s/notes", entry.ID), bytes.NewReader(body))
req = req.WithContext(ctxWithClaims(userID, "alice"))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusCreated, rr.Code, rr.Body.String())
var note models.EntryNote
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), ¬e))
assert.Equal(t, "OOM killer, memcache misconfigured.", note.Content)
assert.Equal(t, "alice", note.Username)
assert.Equal(t, entry.ID, note.EntryID)
}
func TestListNotes_Success(t *testing.T) {
database := newTestDB(t)
entryID := ulid.Make().String()
baseTime := time.Now().UTC().Round(0)
require.NoError(t, database.Create(&types.Entry{
ID: entryID,
Timestamp: baseTime,
NodeName: "h",
Source: "docker",
Event: "die",
Content: "x",
}).Error)
require.NoError(t, database.Create(&models.EntryNote{
ID: ulid.Make().String(),
EntryID: entryID,
UserID: "u1",
Username: "alice",
Content: "note 1",
CreatedAt: baseTime,
}).Error)
require.NoError(t, database.Create(&models.EntryNote{
ID: ulid.Make().String(),
EntryID: entryID,
UserID: "u2",
Username: "bob",
Content: "note 2",
CreatedAt: baseTime.Add(time.Millisecond),
}).Error)
router := chi.NewRouter()
router.Get("/api/entries/{id}/notes", handlers.ListNotes(database))
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/entries/%s/notes", entryID), nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
var resp struct {
Notes []models.EntryNote `json:"notes"`
HasMore bool `json:"has_more"`
NextOffset int `json:"next_offset"`
}
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
notes := resp.Notes
require.Len(t, notes, 2)
assert.Equal(t, "note 1", notes[0].Content)
assert.Equal(t, "note 2", notes[1].Content)
assert.False(t, resp.HasMore)
}
func TestCreateNote_WhitespaceContentRejected(t *testing.T) {
database := newTestDB(t)
userID := ulid.Make().String()
entryID := ulid.Make().String()
require.NoError(t, database.Create(&types.Entry{
ID: entryID,
Timestamp: time.Now().UTC(),
NodeName: "homelab-01",
Source: "docker",
Event: "die",
Content: "container nginx died",
}).Error)
router := chi.NewRouter()
router.Post("/api/entries/{id}/notes", handlers.CreateNote(database))
req := httptest.NewRequest(
http.MethodPost,
fmt.Sprintf("/api/entries/%s/notes", entryID),
bytes.NewBufferString(`{"content":" "}`),
)
req = req.WithContext(ctxWithClaims(userID, "alice"))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestCreateNote_ContentTooLongRejected(t *testing.T) {
database := newTestDB(t)
userID := ulid.Make().String()
entryID := ulid.Make().String()
require.NoError(t, database.Create(&types.Entry{
ID: entryID,
Timestamp: time.Now().UTC(),
NodeName: "homelab-01",
Source: "docker",
Event: "die",
Content: "container nginx died",
}).Error)
router := chi.NewRouter()
router.Post("/api/entries/{id}/notes", handlers.CreateNote(database))
req := httptest.NewRequest(
http.MethodPost,
fmt.Sprintf("/api/entries/%s/notes", entryID),
bytes.NewBufferString(`{"content":"`+strings.Repeat("x", 2001)+`"}`),
)
req = req.WithContext(ctxWithClaims(userID, "alice"))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestListNotes_Paginates(t *testing.T) {
database := newTestDB(t)
entryID := ulid.Make().String()
baseTime := time.Now().UTC().Round(0)
require.NoError(t, database.Create(&types.Entry{
ID: entryID,
Timestamp: baseTime,
NodeName: "h",
Source: "docker",
Event: "die",
Content: "x",
}).Error)
for i := 0; i < 4; i++ {
require.NoError(t, database.Create(&models.EntryNote{
ID: ulid.Make().String(),
EntryID: entryID,
UserID: fmt.Sprintf("u%d", i),
Username: fmt.Sprintf("user-%d", i),
Content: fmt.Sprintf("note %d", i),
CreatedAt: baseTime.Add(time.Duration(i) * time.Millisecond),
}).Error)
}
router := chi.NewRouter()
router.Get("/api/entries/{id}/notes", handlers.ListNotes(database))
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/entries/%s/notes?limit=2&offset=1", entryID), nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
var resp struct {
Notes []models.EntryNote `json:"notes"`
HasMore bool `json:"has_more"`
NextOffset int `json:"next_offset"`
}
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
require.Len(t, resp.Notes, 2)
assert.Equal(t, "note 1", resp.Notes[0].Content)
assert.Equal(t, "note 2", resp.Notes[1].Content)
assert.True(t, resp.HasMore)
assert.Equal(t, 3, resp.NextOffset)
}
func TestCreateNote_EntryNotFound(t *testing.T) {
database := newTestDB(t)
userID := ulid.Make().String()
router := chi.NewRouter()
router.Post("/api/entries/{id}/notes", handlers.CreateNote(database))
req := httptest.NewRequest(
http.MethodPost,
fmt.Sprintf("/api/entries/%s/notes", ulid.Make().String()),
bytes.NewBufferString(`{"content":"hello"}`),
)
req = req.WithContext(ctxWithClaims(userID, "alice"))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusNotFound, rr.Code)
}
func TestDeleteNote_OwnNote(t *testing.T) {
database := newTestDB(t)
userID := ulid.Make().String()
noteID := ulid.Make().String()
require.NoError(t, database.Create(&types.Entry{
ID: "e1",
Timestamp: time.Now().UTC(),
NodeName: "homelab-01",
Source: "docker",
Event: "die",
Content: "container nginx died",
}).Error)
require.NoError(t, database.Create(&models.EntryNote{
ID: noteID,
EntryID: "e1",
UserID: userID,
Username: "alice",
Content: "x",
CreatedAt: time.Now().UTC(),
}).Error)
router := chi.NewRouter()
router.Delete("/api/notes/{id}", handlers.DeleteNote(database))
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/notes/%s", noteID), nil)
req = req.WithContext(ctxWithClaims(userID, ""))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusNoContent, rr.Code, rr.Body.String())
}
func TestDeleteNote_OtherUserForbidden(t *testing.T) {
database := newTestDB(t)
noteID := ulid.Make().String()
require.NoError(t, database.Create(&types.Entry{
ID: "e1",
Timestamp: time.Now().UTC(),
NodeName: "homelab-01",
Source: "docker",
Event: "die",
Content: "container nginx died",
}).Error)
require.NoError(t, database.Create(&models.EntryNote{
ID: noteID,
EntryID: "e1",
UserID: "other-user",
Username: "bob",
Content: "x",
CreatedAt: time.Now().UTC(),
}).Error)
router := chi.NewRouter()
router.Delete("/api/notes/{id}", handlers.DeleteNote(database))
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/notes/%s", noteID), nil)
req = req.WithContext(ctxWithClaims("different-user", ""))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code)
}