328 lines
10 KiB
Go
328 lines
10 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"5803025020/w04/internal/middleware"
|
|
"5803025020/w04/internal/models"
|
|
)
|
|
|
|
func mustJSONBody(t *testing.T, v any) []byte {
|
|
t.Helper()
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal request body: %v", err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// authedRequest builds a request that already carries an authenticated user
|
|
// id in its context, simulating what RequireAuth does before calling the
|
|
// handler. This lets snippet handler tests run without any middleware.
|
|
func authedRequest(method, path string, body []byte, userID string) *http.Request {
|
|
var req *http.Request
|
|
if body != nil {
|
|
req = httptest.NewRequest(method, path, bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req = httptest.NewRequest(method, path, nil)
|
|
}
|
|
ctx := context.WithValue(req.Context(), middleware.UserIDKey, userID)
|
|
return req.WithContext(ctx)
|
|
}
|
|
|
|
func TestCreateSnippet(t *testing.T) {
|
|
t.Run("success returns 201 with exactly id, title, content", func(t *testing.T) {
|
|
ss := newFakeSnippetStore()
|
|
h := NewSnippetHandler(ss)
|
|
req := authedRequest(http.MethodPost, "/snippets", mustJSONBody(t, map[string]string{"title": "hello", "content": "world"}), "user-1")
|
|
rec := httptest.NewRecorder()
|
|
h.Create(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
var body map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("expected JSON body: %v", err)
|
|
}
|
|
wantKeys := []string{"id", "title", "content"}
|
|
if len(body) != len(wantKeys) {
|
|
t.Fatalf("expected exactly keys %v, got %v", wantKeys, body)
|
|
}
|
|
for _, k := range wantKeys {
|
|
if _, ok := body[k]; !ok {
|
|
t.Fatalf("missing key %q in response: %v", k, body)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("empty title returns 400", func(t *testing.T) {
|
|
ss := newFakeSnippetStore()
|
|
h := NewSnippetHandler(ss)
|
|
req := authedRequest(http.MethodPost, "/snippets", mustJSONBody(t, map[string]string{"title": "", "content": "world"}), "user-1")
|
|
rec := httptest.NewRecorder()
|
|
h.Create(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
})
|
|
|
|
t.Run("empty content returns 400", func(t *testing.T) {
|
|
ss := newFakeSnippetStore()
|
|
h := NewSnippetHandler(ss)
|
|
req := authedRequest(http.MethodPost, "/snippets", mustJSONBody(t, map[string]string{"title": "hello", "content": ""}), "user-1")
|
|
rec := httptest.NewRecorder()
|
|
h.Create(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
})
|
|
|
|
t.Run("malformed JSON returns 400", func(t *testing.T) {
|
|
ss := newFakeSnippetStore()
|
|
h := NewSnippetHandler(ss)
|
|
req := authedRequest(http.MethodPost, "/snippets", []byte("{bad"), "user-1")
|
|
rec := httptest.NewRecorder()
|
|
h.Create(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
})
|
|
|
|
t.Run("unauthenticated returns 401", func(t *testing.T) {
|
|
ss := newFakeSnippetStore()
|
|
h := NewSnippetHandler(ss)
|
|
req := httptest.NewRequest(http.MethodPost, "/snippets", bytes.NewReader(mustJSONBody(t, map[string]string{"title": "hello", "content": "world"})))
|
|
rec := httptest.NewRecorder()
|
|
h.Create(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
})
|
|
}
|
|
|
|
func TestGetSnippet(t *testing.T) {
|
|
newSeeded := func() (*SnippetHandler, *fakeSnippetStore) {
|
|
ss := newFakeSnippetStore()
|
|
_ = ss.Create(context.Background(), &models.Snippet{ID: "s1", Title: "hello", Content: "world", UserID: "owner-1"})
|
|
return NewSnippetHandler(ss), ss
|
|
}
|
|
|
|
t.Run("existing id returns 200 without any Authorization header", func(t *testing.T) {
|
|
h, _ := newSeeded()
|
|
req := httptest.NewRequest(http.MethodGet, "/snippets/s1", nil)
|
|
req.SetPathValue("id", "s1")
|
|
rec := httptest.NewRecorder()
|
|
h.Get(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
})
|
|
|
|
t.Run("missing id returns 404", func(t *testing.T) {
|
|
h, _ := newSeeded()
|
|
req := httptest.NewRequest(http.MethodGet, "/snippets/does-not-exist", nil)
|
|
req.SetPathValue("id", "does-not-exist")
|
|
rec := httptest.NewRecorder()
|
|
h.Get(rec, req)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
})
|
|
|
|
t.Run("response has exactly the keys id, title, content", func(t *testing.T) {
|
|
h, _ := newSeeded()
|
|
req := httptest.NewRequest(http.MethodGet, "/snippets/s1", nil)
|
|
req.SetPathValue("id", "s1")
|
|
rec := httptest.NewRecorder()
|
|
h.Get(rec, req)
|
|
assertJSONContentType(t, rec)
|
|
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
|
|
t.Fatalf("expected JSON body: %v", err)
|
|
}
|
|
wantKeys := []string{"id", "title", "content"}
|
|
if len(decoded) != len(wantKeys) {
|
|
t.Fatalf("expected exactly keys %v, got %v", wantKeys, decoded)
|
|
}
|
|
for _, k := range wantKeys {
|
|
if _, ok := decoded[k]; !ok {
|
|
t.Fatalf("missing key %q in response: %v", k, decoded)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestListSnippets(t *testing.T) {
|
|
t.Run("only the caller's snippets are returned", func(t *testing.T) {
|
|
ss := newFakeSnippetStore()
|
|
ctx := context.Background()
|
|
_ = ss.Create(ctx, &models.Snippet{ID: "s1", Title: "t1", Content: "c1", UserID: "user-1"})
|
|
_ = ss.Create(ctx, &models.Snippet{ID: "s2", Title: "t2", Content: "c2", UserID: "user-2"})
|
|
h := NewSnippetHandler(ss)
|
|
req := authedRequest(http.MethodGet, "/snippets", nil, "user-1")
|
|
rec := httptest.NewRecorder()
|
|
h.List(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
var got []map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("expected JSON array: %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected exactly 1 snippet, got %d: %v", len(got), got)
|
|
}
|
|
if got[0]["id"] != "s1" {
|
|
t.Fatalf("expected snippet s1, got %v", got[0])
|
|
}
|
|
})
|
|
|
|
t.Run("no snippets returns a literal empty array, not null", func(t *testing.T) {
|
|
ss := newFakeSnippetStore()
|
|
h := NewSnippetHandler(ss)
|
|
req := authedRequest(http.MethodGet, "/snippets", nil, "user-with-nothing")
|
|
rec := httptest.NewRecorder()
|
|
h.List(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
body := strings.TrimSpace(rec.Body.String())
|
|
if body != "[]" {
|
|
t.Fatalf("expected literal [], got %q", body)
|
|
}
|
|
})
|
|
|
|
t.Run("unauthenticated returns 401", func(t *testing.T) {
|
|
ss := newFakeSnippetStore()
|
|
h := NewSnippetHandler(ss)
|
|
req := httptest.NewRequest(http.MethodGet, "/snippets", nil)
|
|
rec := httptest.NewRecorder()
|
|
h.List(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
})
|
|
|
|
t.Run("each element has exactly the three keys", func(t *testing.T) {
|
|
ss := newFakeSnippetStore()
|
|
_ = ss.Create(context.Background(), &models.Snippet{ID: "s1", Title: "t1", Content: "c1", UserID: "user-1"})
|
|
h := NewSnippetHandler(ss)
|
|
req := authedRequest(http.MethodGet, "/snippets", nil, "user-1")
|
|
rec := httptest.NewRecorder()
|
|
h.List(rec, req)
|
|
assertJSONContentType(t, rec)
|
|
|
|
var got []map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("expected JSON array: %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 element, got %d", len(got))
|
|
}
|
|
wantKeys := []string{"id", "title", "content"}
|
|
if len(got[0]) != len(wantKeys) {
|
|
t.Fatalf("expected exactly keys %v, got %v", wantKeys, got[0])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDeleteSnippet(t *testing.T) {
|
|
newSeeded := func() (*SnippetHandler, *fakeSnippetStore) {
|
|
ss := newFakeSnippetStore()
|
|
_ = ss.Create(context.Background(), &models.Snippet{ID: "s1", Title: "t1", Content: "c1", UserID: "owner-1"})
|
|
return NewSnippetHandler(ss), ss
|
|
}
|
|
|
|
t.Run("owner deleting returns 204 with empty body and snippet is gone", func(t *testing.T) {
|
|
h, ss := newSeeded()
|
|
req := authedRequest(http.MethodDelete, "/snippets/s1", nil, "owner-1")
|
|
req.SetPathValue("id", "s1")
|
|
rec := httptest.NewRecorder()
|
|
h.Delete(rec, req)
|
|
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("expected 204, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
// 204 is exempt from the Content-Type:application/json requirement.
|
|
if rec.Body.Len() != 0 {
|
|
t.Fatalf("expected empty body, got %q", rec.Body.String())
|
|
}
|
|
if ss.has("s1") {
|
|
t.Fatal("expected snippet to be removed from the fake store")
|
|
}
|
|
})
|
|
|
|
t.Run("non-owner returns 403 and snippet still present", func(t *testing.T) {
|
|
h, ss := newSeeded()
|
|
req := authedRequest(http.MethodDelete, "/snippets/s1", nil, "someone-else")
|
|
req.SetPathValue("id", "s1")
|
|
rec := httptest.NewRecorder()
|
|
h.Delete(rec, req)
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
if !ss.has("s1") {
|
|
t.Fatal("expected snippet to still be present")
|
|
}
|
|
})
|
|
|
|
t.Run("nonexistent id returns 404", func(t *testing.T) {
|
|
h, _ := newSeeded()
|
|
req := authedRequest(http.MethodDelete, "/snippets/does-not-exist", nil, "owner-1")
|
|
req.SetPathValue("id", "does-not-exist")
|
|
rec := httptest.NewRecorder()
|
|
h.Delete(rec, req)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
})
|
|
|
|
t.Run("unauthenticated returns 401", func(t *testing.T) {
|
|
h, _ := newSeeded()
|
|
req := httptest.NewRequest(http.MethodDelete, "/snippets/s1", nil)
|
|
req.SetPathValue("id", "s1")
|
|
rec := httptest.NewRecorder()
|
|
h.Delete(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
})
|
|
|
|
t.Run("non-owner on a nonexistent id returns 404, not 403", func(t *testing.T) {
|
|
h, _ := newSeeded()
|
|
req := authedRequest(http.MethodDelete, "/snippets/does-not-exist", nil, "someone-else")
|
|
req.SetPathValue("id", "does-not-exist")
|
|
rec := httptest.NewRecorder()
|
|
h.Delete(rec, req)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", rec.Code)
|
|
}
|
|
assertJSONContentType(t, rec)
|
|
})
|
|
}
|