package handler import ( "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" ) func snippetBody(title, content string) string { b, _ := json.Marshal(map[string]string{"title": title, "content": content}) return string(b) } func TestCreateSnippet(t *testing.T) { create := func(h *SnippetHandler, userID, body string) *httptest.ResponseRecorder { req := newJSONRequest(http.MethodPost, "/snippets", body) if userID != "" { req = withUser(req, userID) } rec := httptest.NewRecorder() h.Create(rec, req) return rec } t.Run("success returns 201 with exactly id, title, content", func(t *testing.T) { fs := newFakeSnippetStore() h := NewSnippetHandler(fs) rec := create(h, "u1", snippetBody("My title", "console.log('hi')")) if rec.Code != http.StatusCreated { t.Fatalf("status = %d, want 201", rec.Code) } assertJSONContentType(t, rec) m := decodeMap(t, rec.Body.Bytes()) assertKeys(t, m, "id", "title", "content") id, _ := m["id"].(string) if id == "" { t.Fatal("id must be a non-empty string") } if m["title"] != "My title" || m["content"] != "console.log('hi')" { t.Fatalf("title/content not echoed correctly: %v", m) } if !fs.has(id) { t.Fatal("snippet must be stored") } if fs.snippets[id].UserID != "u1" { t.Fatalf("stored owner = %q, want u1", fs.snippets[id].UserID) } }) t.Run("empty title returns 400", func(t *testing.T) { h := NewSnippetHandler(newFakeSnippetStore()) rec := create(h, "u1", snippetBody("", "content")) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } assertJSONContentType(t, rec) }) t.Run("empty content returns 400", func(t *testing.T) { h := NewSnippetHandler(newFakeSnippetStore()) rec := create(h, "u1", snippetBody("title", "")) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } assertJSONContentType(t, rec) }) t.Run("malformed JSON returns 400", func(t *testing.T) { h := NewSnippetHandler(newFakeSnippetStore()) rec := create(h, "u1", `{"title": "x", "content":`) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } assertJSONContentType(t, rec) }) t.Run("empty body and JSON array return 400", func(t *testing.T) { h := NewSnippetHandler(newFakeSnippetStore()) for _, body := range []string{``, `["a","b"]`} { if rec := create(h, "u1", body); rec.Code != http.StatusBadRequest { t.Fatalf("body %q: status = %d, want 400", body, rec.Code) } } }) t.Run("unauthenticated returns 401", func(t *testing.T) { fs := newFakeSnippetStore() h := NewSnippetHandler(fs) rec := create(h, "", snippetBody("title", "content")) if rec.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want 401", rec.Code) } assertJSONContentType(t, rec) if len(fs.snippets) != 0 { t.Fatal("nothing must be stored for an unauthenticated request") } }) t.Run("store failure returns 500", func(t *testing.T) { fs := newFakeSnippetStore() fs.createErr = errors.New("db down") h := NewSnippetHandler(fs) rec := create(h, "u1", snippetBody("title", "content")) if rec.Code != http.StatusInternalServerError { t.Fatalf("status = %d, want 500", rec.Code) } }) } func TestGetSnippet(t *testing.T) { setup := func() (*SnippetHandler, *fakeSnippetStore) { fs := newFakeSnippetStore() fs.seed("s1", "Title One", "Content One", "u1") return NewSnippetHandler(fs), fs } get := func(h *SnippetHandler, target string) *httptest.ResponseRecorder { // Sengaja tanpa header Authorization sama sekali. rec := httptest.NewRecorder() h.Get(rec, httptest.NewRequest(http.MethodGet, target, nil)) return rec } t.Run("existing id returns 200 without Authorization header", func(t *testing.T) { h, _ := setup() rec := get(h, "/snippets/s1") if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } assertJSONContentType(t, rec) m := decodeMap(t, rec.Body.Bytes()) if m["id"] != "s1" || m["title"] != "Title One" || m["content"] != "Content One" { t.Fatalf("unexpected body: %v", m) } }) t.Run("response has exactly the keys id, title, content", func(t *testing.T) { h, _ := setup() rec := get(h, "/snippets/s1") assertKeys(t, decodeMap(t, rec.Body.Bytes()), "id", "title", "content") }) t.Run("missing id returns 404", func(t *testing.T) { h, _ := setup() rec := get(h, "/snippets/does-not-exist") if rec.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", rec.Code) } assertJSONContentType(t, rec) }) t.Run("empty id returns 400", func(t *testing.T) { h, _ := setup() rec := get(h, "/snippets/") if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } }) t.Run("store failure returns 500", func(t *testing.T) { h, fs := setup() fs.getErr = errors.New("db down") rec := get(h, "/snippets/s1") if rec.Code != http.StatusInternalServerError { t.Fatalf("status = %d, want 500", rec.Code) } }) } func TestListSnippets(t *testing.T) { setup := func() (*SnippetHandler, *fakeSnippetStore) { fs := newFakeSnippetStore() fs.seed("a1", "A one", "content a1", "alice") fs.seed("a2", "A two", "content a2", "alice") fs.seed("b1", "B one", "content b1", "bob") return NewSnippetHandler(fs), fs } list := func(h *SnippetHandler, userID string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, "/snippets", nil) if userID != "" { req = withUser(req, userID) } rec := httptest.NewRecorder() h.List(rec, req) return rec } t.Run("only the caller's snippets are returned", func(t *testing.T) { h, _ := setup() rec := list(h, "alice") if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } assertJSONContentType(t, rec) var items []map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil { t.Fatalf("body is not a JSON array: %v", err) } if len(items) != 2 { t.Fatalf("got %d snippets, want 2", len(items)) } for _, it := range items { if id := it["id"]; id != "a1" && id != "a2" { t.Fatalf("snippet %v does not belong to the caller", id) } } }) t.Run("no snippets returns literally []", func(t *testing.T) { h, _ := setup() rec := list(h, "carol") if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } assertJSONContentType(t, rec) if got := strings.TrimSpace(rec.Body.String()); got != "[]" { t.Fatalf("body = %q, want []", got) } }) t.Run("unauthenticated returns 401", func(t *testing.T) { h, _ := setup() rec := list(h, "") if rec.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want 401", rec.Code) } assertJSONContentType(t, rec) }) t.Run("each element has exactly the three keys", func(t *testing.T) { h, _ := setup() rec := list(h, "alice") var items []map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil { t.Fatalf("body is not a JSON array: %v", err) } if len(items) == 0 { t.Fatal("expected at least one element") } for _, it := range items { assertKeys(t, it, "id", "title", "content") } }) t.Run("store failure returns 500", func(t *testing.T) { h, fs := setup() fs.listErr = errors.New("db down") rec := list(h, "alice") if rec.Code != http.StatusInternalServerError { t.Fatalf("status = %d, want 500", rec.Code) } }) } func TestDeleteSnippet(t *testing.T) { setup := func() (*SnippetHandler, *fakeSnippetStore) { fs := newFakeSnippetStore() fs.seed("s1", "Title", "Content", "owner") return NewSnippetHandler(fs), fs } del := func(h *SnippetHandler, userID, id string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodDelete, "/snippets/"+id, nil) if userID != "" { req = withUser(req, userID) } rec := httptest.NewRecorder() h.Delete(rec, req) return rec } t.Run("owner gets 204 with empty body and snippet is gone", func(t *testing.T) { h, fs := setup() rec := del(h, "owner", "s1") if rec.Code != http.StatusNoContent { t.Fatalf("status = %d, want 204", rec.Code) } if rec.Body.Len() != 0 { t.Fatalf("body must be empty, got %q", rec.Body.String()) } if fs.has("s1") { t.Fatal("snippet must be removed from the store") } }) t.Run("non-owner gets 403 and snippet remains", func(t *testing.T) { h, fs := setup() rec := del(h, "intruder", "s1") if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want 403", rec.Code) } assertJSONContentType(t, rec) if !fs.has("s1") { t.Fatal("snippet must still exist after a forbidden delete") } }) t.Run("nonexistent id returns 404", func(t *testing.T) { h, _ := setup() rec := del(h, "owner", "nope") if rec.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", rec.Code) } assertJSONContentType(t, rec) }) t.Run("unauthenticated returns 401", func(t *testing.T) { h, fs := setup() rec := del(h, "", "s1") if rec.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want 401", rec.Code) } assertJSONContentType(t, rec) if !fs.has("s1") { t.Fatal("snippet must still exist") } }) t.Run("non-owner on a nonexistent id returns 404 not 403", func(t *testing.T) { h, _ := setup() rec := del(h, "intruder", "nope") if rec.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", rec.Code) } }) t.Run("empty id returns 400", func(t *testing.T) { h, _ := setup() req := withUser(httptest.NewRequest(http.MethodDelete, "/snippets/", nil), "owner") rec := httptest.NewRecorder() h.Delete(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } }) t.Run("lookup failure returns 500", func(t *testing.T) { h, fs := setup() fs.getErr = errors.New("db down") rec := del(h, "owner", "s1") if rec.Code != http.StatusInternalServerError { t.Fatalf("status = %d, want 500", rec.Code) } }) t.Run("delete failure returns 500", func(t *testing.T) { h, fs := setup() fs.deleteErr = errors.New("db down") rec := del(h, "owner", "s1") if rec.Code != http.StatusInternalServerError { t.Fatalf("status = %d, want 500", rec.Code) } }) t.Run("snippet removed between lookup and delete returns 404", func(t *testing.T) { h, fs := setup() fs.deleteErr = errNotFoundForTest() rec := del(h, "owner", "s1") if rec.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", rec.Code) } }) }