Tuas W06
This commit is contained in:
+614
@@ -0,0 +1,614 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user