Files
W04_048_Zagar_Database/internal/handler/snippet_handler_test.go
T

365 lines
10 KiB
Go

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