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) }) }