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) }) } } }