211 lines
6.7 KiB
Go
211 lines
6.7 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|
|
}
|