614 lines
20 KiB
Go
614 lines
20 KiB
Go
package e2e
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var baseURL = func() string {
|
|
if v := os.Getenv("API_BASE_URL"); v != "" {
|
|
return strings.TrimRight(v, "/")
|
|
}
|
|
return "http://localhost:8080"
|
|
}()
|
|
|
|
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
|
|
|
// randomSuffix returns a short random hex string so every test run uses
|
|
// unique usernames — the grader truncates tables between runs, not between
|
|
// tests, and may run this suite against several servers back to back.
|
|
func randomSuffix() string {
|
|
b := make([]byte, 6)
|
|
if _, err := rand.Read(b); err != nil {
|
|
// crypto/rand failing is exceptional enough to just panic; every
|
|
// caller is inside a test anyway.
|
|
panic(err)
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func uniqueUsername() string {
|
|
return "e2e_" + randomSuffix()
|
|
}
|
|
|
|
type apiResponse struct {
|
|
status int
|
|
header http.Header
|
|
body []byte
|
|
}
|
|
|
|
// doRequest fails the test loudly (never skips) if the server cannot be
|
|
// reached at all, per the suite's "fail loudly if unreachable" requirement.
|
|
func doRequest(t *testing.T, method, path string, body any, headers map[string]string) apiResponse {
|
|
t.Helper()
|
|
|
|
var reader io.Reader
|
|
switch v := body.(type) {
|
|
case nil:
|
|
reader = nil
|
|
case []byte:
|
|
reader = bytes.NewReader(v)
|
|
default:
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal request body: %v", err)
|
|
}
|
|
reader = bytes.NewReader(b)
|
|
}
|
|
|
|
req, err := http.NewRequest(method, baseURL+path, reader)
|
|
if err != nil {
|
|
t.Fatalf("failed to build request: %v", err)
|
|
}
|
|
if reader != 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("request %s %s failed (is the server running at %s?): %v", method, path, baseURL, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read response body: %v", err)
|
|
}
|
|
return apiResponse{status: resp.StatusCode, header: resp.Header, body: b}
|
|
}
|
|
|
|
func register(t *testing.T, username, password string) apiResponse {
|
|
t.Helper()
|
|
return doRequest(t, http.MethodPost, "/register", map[string]string{"username": username, "password": password}, nil)
|
|
}
|
|
|
|
func login(t *testing.T, username, password string) apiResponse {
|
|
t.Helper()
|
|
return doRequest(t, http.MethodPost, "/login", map[string]string{"username": username, "password": password}, nil)
|
|
}
|
|
|
|
func sessionTokenFrom(t *testing.T, resp apiResponse) string {
|
|
t.Helper()
|
|
var body map[string]any
|
|
if err := json.Unmarshal(resp.body, &body); err != nil {
|
|
t.Fatalf("expected JSON login response, got %q: %v", resp.body, err)
|
|
}
|
|
tok, _ := body["session_token"].(string)
|
|
if tok == "" {
|
|
t.Fatalf("expected non-empty session_token in %q", resp.body)
|
|
}
|
|
return tok
|
|
}
|
|
|
|
func TestE2E_RegisterDuplicate(t *testing.T) {
|
|
username := uniqueUsername()
|
|
password := "password123"
|
|
|
|
t.Run("first registration succeeds with 201", func(t *testing.T) {
|
|
resp := register(t, username, password)
|
|
if resp.status != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", resp.status, resp.body)
|
|
}
|
|
})
|
|
|
|
t.Run("second registration with the same username returns 409", func(t *testing.T) {
|
|
resp := register(t, username, password)
|
|
if resp.status != http.StatusConflict {
|
|
t.Fatalf("expected 409, got %d: %s", resp.status, resp.body)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestE2E_PasswordNeverLeaked(t *testing.T) {
|
|
username := uniqueUsername()
|
|
password := "s3cr3t-" + randomSuffix()
|
|
|
|
t.Run("register response has no password key and no plaintext password", func(t *testing.T) {
|
|
resp := register(t, username, password)
|
|
if resp.status != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", resp.status, resp.body)
|
|
}
|
|
lower := strings.ToLower(string(resp.body))
|
|
if strings.Contains(lower, "password") {
|
|
t.Fatalf("register response must not contain any password-related key: %s", resp.body)
|
|
}
|
|
if strings.Contains(string(resp.body), password) {
|
|
t.Fatalf("register response must not leak the plaintext password: %s", resp.body)
|
|
}
|
|
})
|
|
|
|
t.Run("login response has no password key and no plaintext password", func(t *testing.T) {
|
|
resp := login(t, username, password)
|
|
if resp.status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", resp.status, resp.body)
|
|
}
|
|
lower := strings.ToLower(string(resp.body))
|
|
if strings.Contains(lower, "password") {
|
|
t.Fatalf("login response must not contain any password-related key: %s", resp.body)
|
|
}
|
|
if strings.Contains(string(resp.body), password) {
|
|
t.Fatalf("login response must not leak the plaintext password: %s", resp.body)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestE2E_LoginFailuresIndistinguishable(t *testing.T) {
|
|
username := uniqueUsername()
|
|
password := "password123"
|
|
if reg := register(t, username, password); reg.status != http.StatusCreated {
|
|
t.Fatalf("setup: expected 201 registering, got %d: %s", reg.status, reg.body)
|
|
}
|
|
|
|
var wrongPassBody, unknownUserBody []byte
|
|
|
|
t.Run("wrong password returns 401", func(t *testing.T) {
|
|
resp := login(t, username, "definitely-wrong-password")
|
|
if resp.status != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d: %s", resp.status, resp.body)
|
|
}
|
|
wrongPassBody = resp.body
|
|
})
|
|
|
|
t.Run("unknown username returns 401 with an identical body", func(t *testing.T) {
|
|
resp := login(t, "no-such-user-"+randomSuffix(), "whatever-password")
|
|
if resp.status != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d: %s", resp.status, resp.body)
|
|
}
|
|
unknownUserBody = resp.body
|
|
if string(wrongPassBody) != string(unknownUserBody) {
|
|
t.Fatalf("expected identical 401 bodies, got %q vs %q", wrongPassBody, unknownUserBody)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestE2E_ProtectedEndpointsRequireToken(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
body any
|
|
}{
|
|
{"POST /snippets", http.MethodPost, "/snippets", map[string]string{"title": "t", "content": "c"}},
|
|
{"GET /snippets", http.MethodGet, "/snippets", nil},
|
|
{"DELETE /snippets/{id}", http.MethodDelete, "/snippets/some-id-" + randomSuffix(), nil},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name+" with no header", func(t *testing.T) {
|
|
resp := doRequest(t, tc.method, tc.path, tc.body, nil)
|
|
if resp.status != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d: %s", resp.status, resp.body)
|
|
}
|
|
})
|
|
t.Run(tc.name+" with Bearer not-a-real-token", func(t *testing.T) {
|
|
resp := doRequest(t, tc.method, tc.path, tc.body, map[string]string{"Authorization": "Bearer not-a-real-token"})
|
|
if resp.status != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d: %s", resp.status, resp.body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestE2E_SnippetLifecycle(t *testing.T) {
|
|
username := uniqueUsername()
|
|
password := "password123"
|
|
if resp := register(t, username, password); resp.status != http.StatusCreated {
|
|
t.Fatalf("setup: register failed: %d %s", resp.status, resp.body)
|
|
}
|
|
loginResp := login(t, username, password)
|
|
if loginResp.status != http.StatusOK {
|
|
t.Fatalf("setup: login failed: %d %s", loginResp.status, loginResp.body)
|
|
}
|
|
token := sessionTokenFrom(t, loginResp)
|
|
authHeader := map[string]string{"Authorization": "Bearer " + token}
|
|
|
|
var snippetID string
|
|
|
|
t.Run("create snippet returns 201", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodPost, "/snippets", map[string]string{"title": "e2e title", "content": "e2e content"}, authHeader)
|
|
if resp.status != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", resp.status, resp.body)
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(resp.body, &body); err != nil {
|
|
t.Fatalf("expected JSON body: %v", err)
|
|
}
|
|
id, _ := body["id"].(string)
|
|
if id == "" {
|
|
t.Fatal("expected non-empty id in create response")
|
|
}
|
|
snippetID = id
|
|
})
|
|
|
|
t.Run("public GET matches what was created", func(t *testing.T) {
|
|
if snippetID == "" {
|
|
t.Fatal("previous subtest did not set snippetID")
|
|
}
|
|
resp := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
|
if resp.status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", resp.status, resp.body)
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(resp.body, &body); err != nil {
|
|
t.Fatalf("expected JSON body: %v", err)
|
|
}
|
|
if body["title"] != "e2e title" || body["content"] != "e2e content" {
|
|
t.Fatalf("expected matching title/content, got %v", body)
|
|
}
|
|
})
|
|
|
|
t.Run("owner DELETE returns 204 with an empty body", func(t *testing.T) {
|
|
if snippetID == "" {
|
|
t.Fatal("previous subtest did not set snippetID")
|
|
}
|
|
resp := doRequest(t, http.MethodDelete, "/snippets/"+snippetID, nil, authHeader)
|
|
if resp.status != http.StatusNoContent {
|
|
t.Fatalf("expected 204, got %d: %s", resp.status, resp.body)
|
|
}
|
|
if len(resp.body) != 0 {
|
|
t.Fatalf("expected empty body, got %q", resp.body)
|
|
}
|
|
})
|
|
|
|
t.Run("GET after delete returns 404", func(t *testing.T) {
|
|
if snippetID == "" {
|
|
t.Fatal("previous subtest did not set snippetID")
|
|
}
|
|
resp := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
|
if resp.status != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d: %s", resp.status, resp.body)
|
|
}
|
|
})
|
|
}
|
|
|
|
// registerAndLogin is a small setup helper shared by the tests below: it
|
|
// registers a fresh unique user, logs in, and returns an Authorization header
|
|
// map ready to use with doRequest.
|
|
func registerAndLogin(t *testing.T) (username string, authHeader map[string]string) {
|
|
t.Helper()
|
|
username = uniqueUsername()
|
|
password := "password123"
|
|
if resp := register(t, username, password); resp.status != http.StatusCreated {
|
|
t.Fatalf("setup: register failed: %d %s", resp.status, resp.body)
|
|
}
|
|
loginResp := login(t, username, password)
|
|
if loginResp.status != http.StatusOK {
|
|
t.Fatalf("setup: login failed: %d %s", loginResp.status, loginResp.body)
|
|
}
|
|
token := sessionTokenFrom(t, loginResp)
|
|
return username, map[string]string{"Authorization": "Bearer " + token}
|
|
}
|
|
|
|
func TestE2E_PublicRead(t *testing.T) {
|
|
_, authHeader := registerAndLogin(t)
|
|
|
|
var snippetID string
|
|
|
|
t.Run("user A creates a snippet", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodPost, "/snippets", map[string]string{"title": "public read title", "content": "public read content"}, authHeader)
|
|
if resp.status != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", resp.status, resp.body)
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(resp.body, &body); err != nil {
|
|
t.Fatalf("expected JSON body: %v", err)
|
|
}
|
|
id, _ := body["id"].(string)
|
|
if id == "" {
|
|
t.Fatal("expected non-empty id")
|
|
}
|
|
snippetID = id
|
|
})
|
|
|
|
t.Run("readable with no Authorization header at all", func(t *testing.T) {
|
|
if snippetID == "" {
|
|
t.Fatal("previous subtest did not set snippetID")
|
|
}
|
|
resp := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
|
if resp.status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", resp.status, resp.body)
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(resp.body, &body); err != nil {
|
|
t.Fatalf("expected JSON body: %v", err)
|
|
}
|
|
if body["title"] != "public read title" || body["content"] != "public read content" {
|
|
t.Fatalf("expected matching title/content, got %v", body)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestE2E_LogoutInvalidatesToken(t *testing.T) {
|
|
_, authHeader := registerAndLogin(t)
|
|
|
|
t.Run("logout returns 204", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodPost, "/logout", nil, authHeader)
|
|
if resp.status != http.StatusNoContent {
|
|
t.Fatalf("expected 204, got %d: %s", resp.status, resp.body)
|
|
}
|
|
})
|
|
|
|
t.Run("same token on GET /snippets returns 401", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodGet, "/snippets", nil, authHeader)
|
|
if resp.status != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d: %s", resp.status, resp.body)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestE2E_DataIsolation(t *testing.T) {
|
|
_, headerA := registerAndLogin(t)
|
|
_, headerB := registerAndLogin(t)
|
|
|
|
createA := doRequest(t, http.MethodPost, "/snippets", map[string]string{"title": "A's snippet", "content": "belongs to A"}, headerA)
|
|
if createA.status != http.StatusCreated {
|
|
t.Fatalf("setup: create for A failed: %d %s", createA.status, createA.body)
|
|
}
|
|
var bodyA map[string]any
|
|
if err := json.Unmarshal(createA.body, &bodyA); err != nil {
|
|
t.Fatalf("expected JSON body: %v", err)
|
|
}
|
|
idA, _ := bodyA["id"].(string)
|
|
if idA == "" {
|
|
t.Fatal("expected non-empty id for A's snippet")
|
|
}
|
|
|
|
createB := doRequest(t, http.MethodPost, "/snippets", map[string]string{"title": "B's snippet", "content": "belongs to B"}, headerB)
|
|
if createB.status != http.StatusCreated {
|
|
t.Fatalf("setup: create for B failed: %d %s", createB.status, createB.body)
|
|
}
|
|
var bodyB map[string]any
|
|
if err := json.Unmarshal(createB.body, &bodyB); err != nil {
|
|
t.Fatalf("expected JSON body: %v", err)
|
|
}
|
|
idB, _ := bodyB["id"].(string)
|
|
if idB == "" {
|
|
t.Fatal("expected non-empty id for B's snippet")
|
|
}
|
|
|
|
t.Run("user A's list contains exactly their own snippet", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodGet, "/snippets", nil, headerA)
|
|
if resp.status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", resp.status, resp.body)
|
|
}
|
|
var list []map[string]any
|
|
if err := json.Unmarshal(resp.body, &list); err != nil {
|
|
t.Fatalf("expected JSON array: %v", err)
|
|
}
|
|
if len(list) != 1 {
|
|
t.Fatalf("expected exactly 1 snippet for user A, got %d: %v", len(list), list)
|
|
}
|
|
if list[0]["id"] != idA {
|
|
t.Fatalf("expected A's own snippet id %q, got %v", idA, list[0])
|
|
}
|
|
})
|
|
|
|
t.Run("user B's list contains exactly their own snippet", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodGet, "/snippets", nil, headerB)
|
|
if resp.status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", resp.status, resp.body)
|
|
}
|
|
var list []map[string]any
|
|
if err := json.Unmarshal(resp.body, &list); err != nil {
|
|
t.Fatalf("expected JSON array: %v", err)
|
|
}
|
|
if len(list) != 1 {
|
|
t.Fatalf("expected exactly 1 snippet for user B, got %d: %v", len(list), list)
|
|
}
|
|
if list[0]["id"] != idB {
|
|
t.Fatalf("expected B's own snippet id %q, got %v", idB, list[0])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestE2E_DeleteOwnership(t *testing.T) {
|
|
_, ownerHeader := registerAndLogin(t)
|
|
_, otherHeader := registerAndLogin(t)
|
|
|
|
createResp := doRequest(t, http.MethodPost, "/snippets", map[string]string{"title": "owned by owner", "content": "do not delete me"}, ownerHeader)
|
|
if createResp.status != http.StatusCreated {
|
|
t.Fatalf("setup: create failed: %d %s", createResp.status, createResp.body)
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(createResp.body, &body); err != nil {
|
|
t.Fatalf("expected JSON body: %v", err)
|
|
}
|
|
snippetID, _ := body["id"].(string)
|
|
if snippetID == "" {
|
|
t.Fatal("expected non-empty id")
|
|
}
|
|
|
|
t.Run("non-owner delete returns 403 and the snippet is still readable afterward", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodDelete, "/snippets/"+snippetID, nil, otherHeader)
|
|
if resp.status != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d: %s", resp.status, resp.body)
|
|
}
|
|
getResp := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
|
if getResp.status != http.StatusOK {
|
|
t.Fatalf("expected snippet to still be readable (200) after a forbidden delete attempt, got %d: %s", getResp.status, getResp.body)
|
|
}
|
|
})
|
|
|
|
t.Run("non-owner delete of a nonexistent id returns 404", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodDelete, "/snippets/no-such-id-"+randomSuffix(), nil, otherHeader)
|
|
if resp.status != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d: %s", resp.status, resp.body)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestE2E_EmptyListIsArray(t *testing.T) {
|
|
_, authHeader := registerAndLogin(t)
|
|
|
|
resp := doRequest(t, http.MethodGet, "/snippets", nil, authHeader)
|
|
if resp.status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", resp.status, resp.body)
|
|
}
|
|
body := strings.TrimSpace(string(resp.body))
|
|
if body != "[]" {
|
|
t.Fatalf("expected a fresh user's empty snippet list to be the literal [], got %q", body)
|
|
}
|
|
}
|
|
|
|
func assertResponseJSONContentType(t *testing.T, resp apiResponse) {
|
|
t.Helper()
|
|
ct := resp.header.Get("Content-Type")
|
|
if !strings.HasPrefix(ct, "application/json") {
|
|
t.Fatalf("expected Content-Type application/json, got %q", ct)
|
|
}
|
|
}
|
|
|
|
func assertSnippetSchema(t *testing.T, resp apiResponse) {
|
|
t.Helper()
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(resp.body, &decoded); err != nil {
|
|
t.Fatalf("expected a JSON object body: %v (body=%s)", err, resp.body)
|
|
}
|
|
wantKeys := []string{"id", "title", "content"}
|
|
if len(decoded) != len(wantKeys) {
|
|
t.Fatalf("expected exactly the keys %v, got %v", wantKeys, decoded)
|
|
}
|
|
for _, k := range wantKeys {
|
|
if _, ok := decoded[k]; !ok {
|
|
t.Fatalf("missing key %q in %v", k, decoded)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestE2E_ContentTypeAndSchema(t *testing.T) {
|
|
_, authHeader := registerAndLogin(t)
|
|
|
|
var snippetID string
|
|
|
|
t.Run("201 on create has JSON content type and the exact snippet schema", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodPost, "/snippets", map[string]string{"title": "ct schema", "content": "ct schema content"}, authHeader)
|
|
if resp.status != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", resp.status, resp.body)
|
|
}
|
|
assertResponseJSONContentType(t, resp)
|
|
assertSnippetSchema(t, resp)
|
|
|
|
var body map[string]any
|
|
_ = json.Unmarshal(resp.body, &body)
|
|
snippetID, _ = body["id"].(string)
|
|
if snippetID == "" {
|
|
t.Fatal("expected non-empty id")
|
|
}
|
|
})
|
|
|
|
t.Run("200 on public get has JSON content type and the exact snippet schema", func(t *testing.T) {
|
|
if snippetID == "" {
|
|
t.Fatal("previous subtest did not set snippetID")
|
|
}
|
|
resp := doRequest(t, http.MethodGet, "/snippets/"+snippetID, nil, nil)
|
|
if resp.status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", resp.status, resp.body)
|
|
}
|
|
assertResponseJSONContentType(t, resp)
|
|
assertSnippetSchema(t, resp)
|
|
})
|
|
|
|
t.Run("400 on malformed body has JSON content type", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodPost, "/snippets", []byte("{not valid json"), authHeader)
|
|
if resp.status != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", resp.status, resp.body)
|
|
}
|
|
assertResponseJSONContentType(t, resp)
|
|
})
|
|
|
|
t.Run("401 on missing token has JSON content type", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodGet, "/snippets", nil, nil)
|
|
if resp.status != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d: %s", resp.status, resp.body)
|
|
}
|
|
assertResponseJSONContentType(t, resp)
|
|
})
|
|
|
|
t.Run("403 on non-owner delete has JSON content type", func(t *testing.T) {
|
|
if snippetID == "" {
|
|
t.Fatal("previous subtest did not set snippetID")
|
|
}
|
|
_, otherHeader := registerAndLogin(t)
|
|
resp := doRequest(t, http.MethodDelete, "/snippets/"+snippetID, nil, otherHeader)
|
|
if resp.status != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d: %s", resp.status, resp.body)
|
|
}
|
|
assertResponseJSONContentType(t, resp)
|
|
})
|
|
|
|
t.Run("404 on nonexistent id has JSON content type", func(t *testing.T) {
|
|
resp := doRequest(t, http.MethodGet, "/snippets/no-such-id-"+randomSuffix(), nil, nil)
|
|
if resp.status != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d: %s", resp.status, resp.body)
|
|
}
|
|
assertResponseJSONContentType(t, resp)
|
|
})
|
|
}
|
|
|
|
func TestE2E_BadInput(t *testing.T) {
|
|
_, authHeader := registerAndLogin(t)
|
|
|
|
badBodies := []struct {
|
|
name string
|
|
body []byte
|
|
}{
|
|
{"malformed JSON", []byte("{not valid json")},
|
|
{"empty body", []byte("")},
|
|
{"JSON array body", []byte(`["unexpected", "array"]`)},
|
|
}
|
|
|
|
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", authHeader},
|
|
}
|
|
|
|
for _, ep := range endpoints {
|
|
for _, bb := range badBodies {
|
|
ep, bb := ep, bb
|
|
t.Run(ep.name+" "+bb.name, func(t *testing.T) {
|
|
resp := doRequest(t, ep.method, ep.path, bb.body, ep.headers)
|
|
if resp.status != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 (never 5xx, never a dropped connection), got %d: %s", resp.status, resp.body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
} |