Files

294 lines
9.1 KiB
Go

package handler
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"5803025015/w04/internal/middleware"
"5803025015/w04/internal/models"
)
// ---------- helpers ----------
func setupSnippetHandler() (*SnippetHandler, *fakeSnippetStore) {
ss := newFakeSnippetStore()
return NewSnippetHandler(ss), ss
}
// withUserID injects user_id into request context (simulating middleware).
func withUserID(r *http.Request, userID string) *http.Request {
ctx := context.WithValue(r.Context(), middleware.UserIDKey, userID)
return r.WithContext(ctx)
}
// doSnippetRequest fires handler with optional user_id injected.
func doSnippetRequest(
h http.HandlerFunc,
method, path string,
body []byte,
userID string,
) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, path, bytes.NewReader(body))
if userID != "" {
req = withUserID(req, userID)
}
rec := httptest.NewRecorder()
h(rec, req)
return rec
}
// decodeSnippetMap decodes into map[string]any to check EXACT key set.
func decodeSnippetMap(t *testing.T, body []byte) map[string]any {
t.Helper()
var m map[string]any
if err := json.Unmarshal(body, &m); err != nil {
t.Fatalf("body not valid JSON: %v; body=%q", err, string(body))
}
return m
}
// ---------- TestCreateSnippet ----------
func TestCreateSnippet(t *testing.T) {
t.Run("success -> 201 with exactly id, title, content", func(t *testing.T) {
h, _ := setupSnippetHandler()
rec := doSnippetRequest(h.Create, http.MethodPost, "/snippets",
[]byte(`{"title":"hello","content":"world"}`), "u1")
if rec.Code != http.StatusCreated {
t.Fatalf("want 201, got %d (body=%s)", rec.Code, rec.Body.String())
}
assertContentTypeJSON(t, rec)
m := decodeSnippetMap(t, rec.Body.Bytes())
if len(m) != 3 {
t.Fatalf("want exactly 3 keys, got %d: %v", len(m), m)
}
for _, k := range []string{"id", "title", "content"} {
if _, ok := m[k]; !ok {
t.Fatalf("missing key %q in %v", k, m)
}
}
if _, extra := m["user_id"]; extra {
t.Fatal("response should not have user_id key")
}
})
t.Run("empty title -> 400", func(t *testing.T) {
h, _ := setupSnippetHandler()
rec := doSnippetRequest(h.Create, http.MethodPost, "/snippets",
[]byte(`{"title":"","content":"world"}`), "u1")
if rec.Code != http.StatusBadRequest {
t.Fatalf("want 400, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("empty content -> 400", func(t *testing.T) {
h, _ := setupSnippetHandler()
rec := doSnippetRequest(h.Create, http.MethodPost, "/snippets",
[]byte(`{"title":"hello","content":""}`), "u1")
if rec.Code != http.StatusBadRequest {
t.Fatalf("want 400, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("malformed JSON -> 400", func(t *testing.T) {
h, _ := setupSnippetHandler()
rec := doSnippetRequest(h.Create, http.MethodPost, "/snippets",
[]byte(`{"title":"hello"`), "u1")
if rec.Code != http.StatusBadRequest {
t.Fatalf("want 400, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("unauthenticated -> 401", func(t *testing.T) {
h, _ := setupSnippetHandler()
rec := doSnippetRequest(h.Create, http.MethodPost, "/snippets",
[]byte(`{"title":"hello","content":"world"}`), "") // no user_id
if rec.Code != http.StatusUnauthorized {
t.Fatalf("want 401, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
}
// ---------- TestGetSnippet ----------
func TestGetSnippet(t *testing.T) {
t.Run("existing id -> 200 without Authorization header", func(t *testing.T) {
h, store := setupSnippetHandler()
_ = store.Create(nil, &models.Snippet{ID: "s1", Title: "t", Content: "c", UserID: "owner"})
// PathValue needs Go 1.22; simulate via SetPathValue on the request
rec := doSnippetRequest(h.Get, http.MethodGet, "/snippets/s1", nil, "")
if rec.Code != http.StatusOK {
t.Fatalf("want 200, got %d (body=%s)", rec.Code, rec.Body.String())
}
assertContentTypeJSON(t, rec)
})
t.Run("missing id -> 404", func(t *testing.T) {
h, _ := setupSnippetHandler()
rec := doSnippetRequest(h.Get, http.MethodGet, "/snippets/ghost", nil, "")
if rec.Code != http.StatusNotFound {
t.Fatalf("want 404, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("response has exactly id, title, content keys", func(t *testing.T) {
h, store := setupSnippetHandler()
_ = store.Create(nil, &models.Snippet{ID: "s2", Title: "t", Content: "c", UserID: "owner"})
rec := doSnippetRequest(h.Get, http.MethodGet, "/snippets/s2", nil, "")
if rec.Code != http.StatusOK {
t.Fatalf("want 200, got %d", rec.Code)
}
m := decodeSnippetMap(t, rec.Body.Bytes())
if len(m) != 3 {
t.Fatalf("want exactly 3 keys, got %d: %v", len(m), m)
}
for _, k := range []string{"id", "title", "content"} {
if _, ok := m[k]; !ok {
t.Fatalf("missing key %q", k)
}
}
if _, extra := m["user_id"]; extra {
t.Fatal("response should not have user_id key")
}
})
}
// ---------- TestListSnippets ----------
func TestListSnippets(t *testing.T) {
t.Run("only caller's snippets returned (seed two users)", func(t *testing.T) {
h, store := setupSnippetHandler()
_ = store.Create(nil, &models.Snippet{ID: "s1", Title: "a", Content: "1", UserID: "u1"})
_ = store.Create(nil, &models.Snippet{ID: "s2", Title: "b", Content: "2", UserID: "u2"})
_ = store.Create(nil, &models.Snippet{ID: "s3", Title: "c", Content: "3", UserID: "u1"})
rec := doSnippetRequest(h.List, http.MethodGet, "/snippets", nil, "u1")
if rec.Code != http.StatusOK {
t.Fatalf("want 200, got %d", rec.Code)
}
var arr []map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &arr); err != nil {
t.Fatalf("body not valid JSON array: %v", err)
}
if len(arr) != 2 {
t.Fatalf("want 2 snippets for u1, got %d", len(arr))
}
for _, item := range arr {
if _, ok := item["user_id"]; ok {
t.Fatal("list response should not have user_id key")
}
}
})
t.Run("no snippets -> body is literally []", func(t *testing.T) {
h, _ := setupSnippetHandler()
rec := doSnippetRequest(h.List, http.MethodGet, "/snippets", nil, "empty-user")
if rec.Code != http.StatusOK {
t.Fatalf("want 200, got %d", rec.Code)
}
raw := bytes.TrimSpace(rec.Body.Bytes())
if string(raw) != "[]" {
t.Fatalf("want literal [], got %q", string(raw))
}
})
t.Run("unauthenticated -> 401", func(t *testing.T) {
h, _ := setupSnippetHandler()
rec := doSnippetRequest(h.List, http.MethodGet, "/snippets", nil, "")
if rec.Code != http.StatusUnauthorized {
t.Fatalf("want 401, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("each element has exactly 3 keys", func(t *testing.T) {
h, store := setupSnippetHandler()
_ = store.Create(nil, &models.Snippet{ID: "s1", Title: "a", Content: "1", UserID: "u1"})
rec := doSnippetRequest(h.List, http.MethodGet, "/snippets", nil, "u1")
var arr []map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &arr); err != nil {
t.Fatalf("body not JSON array: %v", err)
}
if len(arr) != 1 {
t.Fatalf("want 1, got %d", len(arr))
}
if len(arr[0]) != 3 {
t.Fatalf("want 3 keys per element, got %d: %v", len(arr[0]), arr[0])
}
})
}
// ---------- TestDeleteSnippet ----------
func TestDeleteSnippet(t *testing.T) {
t.Run("owner -> 204 empty body, snippet removed", func(t *testing.T) {
h, store := setupSnippetHandler()
_ = store.Create(nil, &models.Snippet{ID: "s1", Title: "t", Content: "c", UserID: "u1"})
rec := doSnippetRequest(h.Delete, http.MethodDelete, "/snippets/s1", nil, "u1")
if rec.Code != http.StatusNoContent {
t.Fatalf("want 204, got %d (body=%s)", rec.Code, rec.Body.String())
}
if rec.Body.Len() != 0 {
t.Fatalf("want empty body, got %q", rec.Body.String())
}
if store.HasSnippet("s1") {
t.Fatal("snippet should be gone")
}
})
t.Run("non-owner -> 403, snippet still present", func(t *testing.T) {
h, store := setupSnippetHandler()
_ = store.Create(nil, &models.Snippet{ID: "s1", Title: "t", Content: "c", UserID: "owner"})
rec := doSnippetRequest(h.Delete, http.MethodDelete, "/snippets/s1", nil, "other")
if rec.Code != http.StatusForbidden {
t.Fatalf("want 403, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
if !store.HasSnippet("s1") {
t.Fatal("snippet should still exist")
}
})
t.Run("nonexistent id -> 404", func(t *testing.T) {
h, _ := setupSnippetHandler()
rec := doSnippetRequest(h.Delete, http.MethodDelete, "/snippets/ghost", nil, "u1")
if rec.Code != http.StatusNotFound {
t.Fatalf("want 404, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("unauthenticated -> 401", func(t *testing.T) {
h, store := setupSnippetHandler()
_ = store.Create(nil, &models.Snippet{ID: "s1", Title: "t", Content: "c", UserID: "owner"})
rec := doSnippetRequest(h.Delete, http.MethodDelete, "/snippets/s1", nil, "")
if rec.Code != http.StatusUnauthorized {
t.Fatalf("want 401, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("non-owner on nonexistent id -> 404 (not 403)", func(t *testing.T) {
h, _ := setupSnippetHandler()
rec := doSnippetRequest(h.Delete, http.MethodDelete, "/snippets/ghost", nil, "someone")
if rec.Code != http.StatusNotFound {
t.Fatalf("want 404, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
}