Ctrl K
ComunidadGuías…

server/internal/handlers/notes_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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(), &note))
	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)
}