Assignment 4: add unit, integration, and e2e tests; refactor for testability
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestE2E_LogoutInvalidatesToken(t *testing.T) {
|
||||
username := uniqueUsername("logout")
|
||||
registerUser(t, username, "pass1234")
|
||||
token := loginUser(t, username, "pass1234")
|
||||
|
||||
t.Run("logout returns 204", func(t *testing.T) {
|
||||
auth := map[string]string{"Authorization": "Bearer " + token}
|
||||
resp, respBody := doRequest(t, http.MethodPost, "/logout", nil, auth)
|
||||
assertStatus(t, resp.StatusCode, http.StatusNoContent, respBody)
|
||||
if len(respBody) != 0 {
|
||||
t.Fatalf("logout body should be empty, got %q", string(respBody))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("same token on GET /snippets now returns 401", func(t *testing.T) {
|
||||
auth := map[string]string{"Authorization": "Bearer " + token}
|
||||
resp, respBody := doRequest(t, http.MethodGet, "/snippets", nil, auth)
|
||||
assertStatus(t, resp.StatusCode, http.StatusUnauthorized, respBody)
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_DataIsolation(t *testing.T) {
|
||||
_, tokenA := registerAndLogin(t, "isoa")
|
||||
_, tokenB := registerAndLogin(t, "isob")
|
||||
|
||||
titleA := "title-A-" + randSuffix()
|
||||
titleB := "title-B-" + randSuffix()
|
||||
_ = createSnippet(t, tokenA, titleA, "content-A")
|
||||
_ = createSnippet(t, tokenB, titleB, "content-B")
|
||||
|
||||
t.Run("user A sees only their own snippets", func(t *testing.T) {
|
||||
auth := map[string]string{"Authorization": "Bearer " + tokenA}
|
||||
resp, respBody := doRequest(t, http.MethodGet, "/snippets", nil, auth)
|
||||
assertStatus(t, resp.StatusCode, http.StatusOK, respBody)
|
||||
|
||||
var arr []map[string]any
|
||||
if err := json.Unmarshal(respBody, &arr); err != nil {
|
||||
t.Fatalf("not JSON array: %v", err)
|
||||
}
|
||||
for _, s := range arr {
|
||||
if s["title"] == titleB {
|
||||
t.Fatalf("user A sees user B's snippet: %v", s)
|
||||
}
|
||||
}
|
||||
found := false
|
||||
for _, s := range arr {
|
||||
if s["title"] == titleA {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("user A's own snippet not present in their list")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("user B sees only their own snippets", func(t *testing.T) {
|
||||
auth := map[string]string{"Authorization": "Bearer " + tokenB}
|
||||
resp, respBody := doRequest(t, http.MethodGet, "/snippets", nil, auth)
|
||||
assertStatus(t, resp.StatusCode, http.StatusOK, respBody)
|
||||
|
||||
var arr []map[string]any
|
||||
if err := json.Unmarshal(respBody, &arr); err != nil {
|
||||
t.Fatalf("not JSON array: %v", err)
|
||||
}
|
||||
for _, s := range arr {
|
||||
if s["title"] == titleA {
|
||||
t.Fatalf("user B sees user A's snippet: %v", s)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_DeleteOwnership(t *testing.T) {
|
||||
_, tokenA := registerAndLogin(t, "delowna")
|
||||
_, tokenB := registerAndLogin(t, "delownb")
|
||||
snippetID := createSnippet(t, tokenA, "owned", "by-A")
|
||||
|
||||
t.Run("non-owner delete returns 403", func(t *testing.T) {
|
||||
auth := map[string]string{"Authorization": "Bearer " + tokenB}
|
||||
resp, respBody := doRequest(t, http.MethodDelete, "/snippets/"+snippetID, nil, auth)
|
||||
assertStatus(t, resp.StatusCode, http.StatusForbidden, respBody)
|
||||
})
|
||||
|
||||
t.Run("non-owner delete of nonexistent id returns 404, and snippet is still readable", func(t *testing.T) {
|
||||
auth := map[string]string{"Authorization": "Bearer " + tokenB}
|
||||
|
||||
resp, respBody := doRequest(t, http.MethodDelete, "/snippets/nonexistent-"+randSuffix(), nil, auth)
|
||||
assertStatus(t, resp.StatusCode, http.StatusNotFound, respBody)
|
||||
|
||||
// original snippet must still be readable
|
||||
resp2, respBody2 := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
||||
assertStatus(t, resp2.StatusCode, http.StatusOK, respBody2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_EmptyListIsArray(t *testing.T) {
|
||||
_, token := registerAndLogin(t, "empty")
|
||||
|
||||
t.Run("fresh user GET /snippets returns literal []", func(t *testing.T) {
|
||||
auth := map[string]string{"Authorization": "Bearer " + token}
|
||||
resp, respBody := doRequest(t, http.MethodGet, "/snippets", nil, auth)
|
||||
assertStatus(t, resp.StatusCode, http.StatusOK, respBody)
|
||||
|
||||
trimmed := strings.TrimSpace(string(respBody))
|
||||
if trimmed != "[]" {
|
||||
t.Fatalf("want literal [], got %q", trimmed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_ContentTypeAndSchema(t *testing.T) {
|
||||
_, token := registerAndLogin(t, "schema")
|
||||
auth := map[string]string{"Authorization": "Bearer " + token}
|
||||
snippetID := createSnippet(t, token, "s", "c")
|
||||
|
||||
t.Run("200 responses have application/json", func(t *testing.T) {
|
||||
resp, _ := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
||||
assertJSONContentType(t, resp.Header)
|
||||
})
|
||||
|
||||
t.Run("201 responses have application/json", func(t *testing.T) {
|
||||
body := []byte(`{"title":"t","content":"c"}`)
|
||||
resp, _ := doRequest(t, http.MethodPost, "/snippets", body, auth)
|
||||
assertJSONContentType(t, resp.Header)
|
||||
})
|
||||
|
||||
t.Run("400 responses have application/json", func(t *testing.T) {
|
||||
body := []byte(`not-json`)
|
||||
resp, _ := doRequest(t, http.MethodPost, "/snippets", body, auth)
|
||||
assertJSONContentType(t, resp.Header)
|
||||
})
|
||||
|
||||
t.Run("401 responses have application/json", func(t *testing.T) {
|
||||
resp, _ := doRequest(t, http.MethodGet, "/snippets", nil, nil)
|
||||
assertJSONContentType(t, resp.Header)
|
||||
})
|
||||
|
||||
t.Run("403 responses have application/json", func(t *testing.T) {
|
||||
_, tokenOther := registerAndLogin(t, "schema2")
|
||||
authOther := map[string]string{"Authorization": "Bearer " + tokenOther}
|
||||
resp, _ := doRequest(t, http.MethodDelete, "/snippets/"+snippetID, nil, authOther)
|
||||
assertJSONContentType(t, resp.Header)
|
||||
})
|
||||
|
||||
t.Run("snippet objects have exactly id, title, content keys", func(t *testing.T) {
|
||||
resp, respBody := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
||||
assertStatus(t, resp.StatusCode, http.StatusOK, respBody)
|
||||
|
||||
m := decodeObject(t, respBody)
|
||||
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.Fatalf("snippet response should not have user_id key; got %v", m)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- Test 12: BadInput ----------
|
||||
|
||||
func TestE2E_BadInput(t *testing.T) {
|
||||
_, token := registerAndLogin(t, "badinput")
|
||||
auth := map[string]string{"Authorization": "Bearer " + token}
|
||||
|
||||
endpoints := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
headers map[string]string
|
||||
}{
|
||||
{"POST /register", http.MethodPost, "/register", nil},
|
||||
{"POST /login", http.MethodPost, "/login", nil},
|
||||
{"POST /snippets", http.MethodPost, "/snippets", auth},
|
||||
}
|
||||
|
||||
bodies := []struct {
|
||||
name string
|
||||
body []byte
|
||||
}{
|
||||
{"malformed JSON", []byte(`{"username":`)},
|
||||
{"empty body", []byte(``)},
|
||||
{"JSON array body", []byte(`["a","b"]`)},
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
for _, b := range bodies {
|
||||
t.Run(ep.name+" with "+b.name+" -> 400", func(t *testing.T) {
|
||||
resp, respBody := doRequest(t, ep.method, ep.path, b.body, ep.headers)
|
||||
if resp.StatusCode >= 500 {
|
||||
t.Fatalf("got 5xx (%d), want 400 (body=%s)", resp.StatusCode, string(respBody))
|
||||
}
|
||||
assertStatus(t, resp.StatusCode, http.StatusBadRequest, respBody)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
func baseURL() string {
|
||||
if v := os.Getenv("API_BASE_URL"); v != "" {
|
||||
return strings.TrimRight(v, "/")
|
||||
}
|
||||
return "http://localhost:8080"
|
||||
}
|
||||
|
||||
func randSuffix() string {
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func uniqueUsername(prefix string) string {
|
||||
return fmt.Sprintf("%s_%s", prefix, randSuffix())
|
||||
}
|
||||
|
||||
func doRequest(t *testing.T, method, path string, body []byte, headers map[string]string) (*http.Response, []byte) {
|
||||
t.Helper()
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
reader = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequest(method, baseURL()+path, reader)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequest: %v", err)
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("HTTP %s %s failed (server running at %s?): %v", method, path, baseURL(), err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
return resp, respBody
|
||||
}
|
||||
|
||||
func assertStatus(t *testing.T, got, want int, body []byte) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Fatalf("status: got %d, want %d (body=%s)", got, want, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func assertJSONContentType(t *testing.T, h http.Header) {
|
||||
t.Helper()
|
||||
ct := h.Get("Content-Type")
|
||||
if !strings.HasPrefix(ct, "application/json") {
|
||||
t.Fatalf("Content-Type: got %q, want prefix application/json", ct)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeObject(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 is not a JSON object: %v (body=%s)", err, string(body))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func registerUser(t *testing.T, username, password string) {
|
||||
t.Helper()
|
||||
body, _ := json.Marshal(map[string]string{"username": username, "password": password})
|
||||
resp, respBody := doRequest(t, http.MethodPost, "/register", body, nil)
|
||||
assertStatus(t, resp.StatusCode, http.StatusCreated, respBody)
|
||||
}
|
||||
|
||||
func loginUser(t *testing.T, username, password string) string {
|
||||
t.Helper()
|
||||
body, _ := json.Marshal(map[string]string{"username": username, "password": password})
|
||||
resp, respBody := doRequest(t, http.MethodPost, "/login", body, nil)
|
||||
assertStatus(t, resp.StatusCode, http.StatusOK, respBody)
|
||||
m := decodeObject(t, respBody)
|
||||
tok, _ := m["session_token"].(string)
|
||||
if tok == "" {
|
||||
t.Fatalf("login: missing session_token; body=%s", string(respBody))
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
func registerAndLogin(t *testing.T, prefix string) (string, string) {
|
||||
t.Helper()
|
||||
username := uniqueUsername(prefix)
|
||||
registerUser(t, username, "pass1234")
|
||||
token := loginUser(t, username, "pass1234")
|
||||
return username, token
|
||||
}
|
||||
|
||||
func createSnippet(t *testing.T, token, title, content string) string {
|
||||
t.Helper()
|
||||
body, _ := json.Marshal(map[string]string{"title": title, "content": content})
|
||||
headers := map[string]string{"Authorization": "Bearer " + token}
|
||||
resp, respBody := doRequest(t, http.MethodPost, "/snippets", body, headers)
|
||||
assertStatus(t, resp.StatusCode, http.StatusCreated, respBody)
|
||||
m := decodeObject(t, respBody)
|
||||
id, _ := m["id"].(string)
|
||||
if id == "" {
|
||||
t.Fatalf("createSnippet: missing id; body=%s", string(respBody))
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func TestE2E_RegisterDuplicate(t *testing.T) {
|
||||
t.Run("first register returns 201", func(t *testing.T) {
|
||||
username := uniqueUsername("dup1")
|
||||
body, _ := json.Marshal(map[string]string{"username": username, "password": "pass1234"})
|
||||
resp, respBody := doRequest(t, http.MethodPost, "/register", body, nil)
|
||||
assertStatus(t, resp.StatusCode, http.StatusCreated, respBody)
|
||||
})
|
||||
|
||||
t.Run("second register with same username returns 409", func(t *testing.T) {
|
||||
username := uniqueUsername("dup2")
|
||||
body, _ := json.Marshal(map[string]string{"username": username, "password": "pass1234"})
|
||||
resp, _ := doRequest(t, http.MethodPost, "/register", body, nil)
|
||||
assertStatus(t, resp.StatusCode, http.StatusCreated, nil)
|
||||
|
||||
resp2, respBody2 := doRequest(t, http.MethodPost, "/register", body, nil)
|
||||
assertStatus(t, resp2.StatusCode, http.StatusConflict, respBody2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_PasswordNeverLeaked(t *testing.T) {
|
||||
t.Run("register body has no password keys or plaintext", func(t *testing.T) {
|
||||
username := uniqueUsername("leak1")
|
||||
plain := "secret_pw_leak_1"
|
||||
body, _ := json.Marshal(map[string]string{"username": username, "password": plain})
|
||||
resp, respBody := doRequest(t, http.MethodPost, "/register", body, nil)
|
||||
assertStatus(t, resp.StatusCode, http.StatusCreated, respBody)
|
||||
|
||||
raw := string(respBody)
|
||||
if strings.Contains(raw, "password") {
|
||||
t.Fatalf("register body contains 'password': %s", raw)
|
||||
}
|
||||
if strings.Contains(raw, plain) {
|
||||
t.Fatalf("register body leaks plaintext password: %s", raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("login body has no password keys or plaintext", func(t *testing.T) {
|
||||
username := uniqueUsername("leak2")
|
||||
plain := "secret_pw_leak_2"
|
||||
body, _ := json.Marshal(map[string]string{"username": username, "password": plain})
|
||||
resp, _ := doRequest(t, http.MethodPost, "/register", body, nil)
|
||||
assertStatus(t, resp.StatusCode, http.StatusCreated, nil)
|
||||
|
||||
resp2, respBody2 := doRequest(t, http.MethodPost, "/login", body, nil)
|
||||
assertStatus(t, resp2.StatusCode, http.StatusOK, respBody2)
|
||||
|
||||
raw := string(respBody2)
|
||||
if strings.Contains(raw, "password") {
|
||||
t.Fatalf("login body contains 'password': %s", raw)
|
||||
}
|
||||
if strings.Contains(raw, plain) {
|
||||
t.Fatalf("login body leaks plaintext password: %s", raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_LoginFailuresIndistinguishable(t *testing.T) {
|
||||
username := uniqueUsername("loginfail")
|
||||
registerUser(t, username, "correct_pw")
|
||||
|
||||
t.Run("wrong password and unknown user both return 401", func(t *testing.T) {
|
||||
wrongBody, _ := json.Marshal(map[string]string{"username": username, "password": "wrong_pw"})
|
||||
respWrong, _ := doRequest(t, http.MethodPost, "/login", wrongBody, nil)
|
||||
|
||||
ghostBody, _ := json.Marshal(map[string]string{"username": uniqueUsername("ghost"), "password": "any"})
|
||||
respGhost, _ := doRequest(t, http.MethodPost, "/login", ghostBody, nil)
|
||||
|
||||
if respWrong.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("wrong password: want 401, got %d", respWrong.StatusCode)
|
||||
}
|
||||
if respGhost.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("unknown user: want 401, got %d", respGhost.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the two response bodies are byte-for-byte identical", func(t *testing.T) {
|
||||
wrongBody, _ := json.Marshal(map[string]string{"username": username, "password": "wrong_pw"})
|
||||
_, b1 := doRequest(t, http.MethodPost, "/login", wrongBody, nil)
|
||||
|
||||
ghostBody, _ := json.Marshal(map[string]string{"username": uniqueUsername("ghost"), "password": "any"})
|
||||
_, b2 := doRequest(t, http.MethodPost, "/login", ghostBody, nil)
|
||||
|
||||
if !bytes.Equal(b1, b2) {
|
||||
t.Fatalf("bodies differ:\nwrong=%q\nghost=%q", b1, b2)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_ProtectedEndpointsRequireToken(t *testing.T) {
|
||||
_, token := registerAndLogin(t, "prot")
|
||||
id := createSnippet(t, token, "t", "c")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
headers map[string]string
|
||||
}{
|
||||
{"POST /snippets without Authorization", http.MethodPost, "/snippets", nil},
|
||||
{"POST /snippets with bad token", http.MethodPost, "/snippets", map[string]string{"Authorization": "Bearer not-a-real-token"}},
|
||||
{"GET /snippets without Authorization", http.MethodGet, "/snippets", nil},
|
||||
{"GET /snippets with bad token", http.MethodGet, "/snippets", map[string]string{"Authorization": "Bearer not-a-real-token"}},
|
||||
{"DELETE /snippets/{id} without Authorization", http.MethodDelete, "/snippets/" + id, nil},
|
||||
{"DELETE /snippets/{id} with bad token", http.MethodDelete, "/snippets/" + id, map[string]string{"Authorization": "Bearer not-a-real-token"}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var reqBody []byte
|
||||
if tc.method == http.MethodPost {
|
||||
reqBody = []byte(`{"title":"x","content":"y"}`)
|
||||
}
|
||||
resp, respBody := doRequest(t, tc.method, tc.path, reqBody, tc.headers)
|
||||
assertStatus(t, resp.StatusCode, http.StatusUnauthorized, respBody)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestE2E_SnippetLifecycle(t *testing.T) {
|
||||
_, token := registerAndLogin(t, "life")
|
||||
auth := map[string]string{"Authorization": "Bearer " + token}
|
||||
|
||||
t.Run("create snippet returns 201", func(t *testing.T) {
|
||||
body, _ := json.Marshal(map[string]string{"title": "hello", "content": "world"})
|
||||
resp, respBody := doRequest(t, http.MethodPost, "/snippets", body, auth)
|
||||
assertStatus(t, resp.StatusCode, http.StatusCreated, respBody)
|
||||
})
|
||||
|
||||
snippetID := createSnippet(t, token, "hello", "world")
|
||||
|
||||
t.Run("public GET matches what was created", func(t *testing.T) {
|
||||
resp, respBody := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
||||
assertStatus(t, resp.StatusCode, http.StatusOK, respBody)
|
||||
m := decodeObject(t, respBody)
|
||||
if m["title"] != "hello" || m["content"] != "world" {
|
||||
t.Fatalf("snippet mismatch: %v", m)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("owner DELETE returns 204 empty body", func(t *testing.T) {
|
||||
resp, respBody := doRequest(t, http.MethodDelete, "/snippets/"+snippetID, nil, auth)
|
||||
assertStatus(t, resp.StatusCode, http.StatusNoContent, respBody)
|
||||
if len(respBody) != 0 {
|
||||
t.Fatalf("expected empty body, got %q", string(respBody))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET after DELETE returns 404", func(t *testing.T) {
|
||||
resp, respBody := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
||||
assertStatus(t, resp.StatusCode, http.StatusNotFound, respBody)
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_PublicRead(t *testing.T) {
|
||||
_, tokenA := registerAndLogin(t, "puba")
|
||||
snippetID := createSnippet(t, tokenA, "public-title", "public-content")
|
||||
|
||||
t.Run("public GET of user A's snippet succeeds without Authorization", func(t *testing.T) {
|
||||
resp, respBody := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
||||
assertStatus(t, resp.StatusCode, http.StatusOK, respBody)
|
||||
m := decodeObject(t, respBody)
|
||||
if m["title"] != "public-title" || m["content"] != "public-content" {
|
||||
t.Fatalf("content mismatch: %v", m)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("response Content-Type is application/json", func(t *testing.T) {
|
||||
resp, _ := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
||||
assertJSONContentType(t, resp.Header)
|
||||
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user