- DELETE now demands the create-time deletion token (Authorization header: Bearer/Token/bare, or ?token= query param), compared with the constant-time store.DeletionTokenEqual. 403 otherwise. - Creator-browser deletes via the /mine button (matching vwr cookie, #37) remain allowed; other browsers and plain API clients get 403. - Regression tests: no token, wrong token (header+query), correct token (header+query), creator-cookie path, token extraction. - Adapted TestSoftDelete to pass the deletion token. - docs/API.md delete section updated. - Based on #58's SoftDelete (bool, error) signature.
173 lines
5.0 KiB
Go
173 lines
5.0 KiB
Go
package api
|
|
|
|
// Regression tests for #63: DELETE /api/pastes/{id} must require the
|
|
// deletion token (Authorization header or ?token= query param, constant-time
|
|
// compare). Without a token, or with a wrong token, the paste must survive
|
|
// and the response must be 403.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// createTestPaste creates a paste via the API and returns id + deletion token.
|
|
func createTestPaste(t *testing.T, h http.Handler) (string, string) {
|
|
t.Helper()
|
|
req := httptest.NewRequest("POST", "/api/pastes", strings.NewReader(`{"content":"delete me"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != 201 {
|
|
t.Fatalf("create: got %d want 201: %s", rec.Code, rec.Body.String())
|
|
}
|
|
var created struct {
|
|
ID string `json:"id"`
|
|
DeletionToken string `json:"deletion_token"`
|
|
}
|
|
json.Unmarshal(rec.Body.Bytes(), &created)
|
|
if created.ID == "" || created.DeletionToken == "" {
|
|
t.Fatalf("create response missing id/deletion_token: %s", rec.Body.String())
|
|
}
|
|
return created.ID, created.DeletionToken
|
|
}
|
|
|
|
func pasteExists(t *testing.T, h http.Handler, id string) bool {
|
|
t.Helper()
|
|
req := httptest.NewRequest("GET", "/api/pastes/"+id, nil)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == 200 {
|
|
return true
|
|
}
|
|
if rec.Code == 404 {
|
|
return false
|
|
}
|
|
t.Fatalf("get after delete: got %d", rec.Code)
|
|
return false
|
|
}
|
|
|
|
func TestDeleteWithoutTokenForbidden(t *testing.T) {
|
|
s := testServer(t)
|
|
h := s.routes()
|
|
id, _ := createTestPaste(t, h)
|
|
|
|
rec := doReq(t, h, "DELETE", "/api/pastes/"+id, "", "")
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("delete without token: got %d want 403", rec.Code)
|
|
}
|
|
if !pasteExists(t, h, id) {
|
|
t.Fatal("paste was deleted without a token")
|
|
}
|
|
}
|
|
|
|
func TestDeleteWithWrongTokenForbidden(t *testing.T) {
|
|
s := testServer(t)
|
|
h := s.routes()
|
|
id, _ := createTestPaste(t, h)
|
|
|
|
// query param
|
|
rec := doReq(t, h, "DELETE", "/api/pastes/"+id+"?token=wrong-token", "", "")
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("delete with wrong token (query): got %d want 403", rec.Code)
|
|
}
|
|
// header
|
|
req := httptest.NewRequest("DELETE", "/api/pastes/"+id, nil)
|
|
req.Header.Set("Authorization", "Bearer wrong-token")
|
|
rec = httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("delete with wrong token (header): got %d want 403", rec.Code)
|
|
}
|
|
if !pasteExists(t, h, id) {
|
|
t.Fatal("paste was deleted with a wrong token")
|
|
}
|
|
}
|
|
|
|
func TestDeleteWithCorrectToken(t *testing.T) {
|
|
s := testServer(t)
|
|
h := s.routes()
|
|
id, tok := createTestPaste(t, h)
|
|
|
|
// via Authorization header
|
|
req := httptest.NewRequest("DELETE", "/api/pastes/"+id, nil)
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != 200 {
|
|
t.Fatalf("delete with correct token (header): got %d want 200: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if pasteExists(t, h, id) {
|
|
t.Fatal("paste still exists after authorized delete")
|
|
}
|
|
|
|
// via query param
|
|
id, tok = createTestPaste(t, h)
|
|
rec = doReq(t, h, "DELETE", "/api/pastes/"+id+"?token="+tok, "", "")
|
|
if rec.Code != 200 {
|
|
t.Fatalf("delete with correct token (query): got %d want 200", rec.Code)
|
|
}
|
|
if pasteExists(t, h, id) {
|
|
t.Fatal("paste still exists after authorized delete (query)")
|
|
}
|
|
}
|
|
|
|
func TestDeleteByCreatorViewerCookieStillAllowed(t *testing.T) {
|
|
s := testServer(t)
|
|
h := s.routes()
|
|
// create from a specific browser
|
|
req := httptest.NewRequest("POST", "/api/pastes", strings.NewReader(`{"content":"mine"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.AddCookie(&http.Cookie{Name: "vwr", Value: "creator-abc"})
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
var created struct {
|
|
ID string `json:"id"`
|
|
}
|
|
json.Unmarshal(rec.Body.Bytes(), &created)
|
|
|
|
// creator browser deletes without a token: allowed (#37 /mine delete button)
|
|
rec = doReq(t, h, "DELETE", "/api/pastes/"+created.ID, "creator-abc", "")
|
|
if rec.Code != 200 {
|
|
t.Fatalf("creator delete: got %d want 200", rec.Code)
|
|
}
|
|
|
|
// a different browser is still forbidden
|
|
id, _ := createTestPaste(t, h)
|
|
rec = doReq(t, h, "DELETE", "/api/pastes/"+id, "someone-else", "")
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("other browser delete: got %d want 403", rec.Code)
|
|
}
|
|
if !pasteExists(t, h, id) {
|
|
t.Fatal("paste deleted by unrelated browser")
|
|
}
|
|
}
|
|
|
|
func TestDeletionAuthorizationExtract(t *testing.T) {
|
|
mk := func(hdr, q string) *http.Request {
|
|
req := httptest.NewRequest("DELETE", "/api/pastes/x"+q, nil)
|
|
if hdr != "" {
|
|
req.Header.Set("Authorization", hdr)
|
|
}
|
|
return req
|
|
}
|
|
cases := []struct {
|
|
hdr, q, want string
|
|
}{
|
|
{"", "", ""},
|
|
{"Bearer tok", "", "tok"},
|
|
{"bearer tok", "", "tok"},
|
|
{"Token tok", "", "tok"},
|
|
{"tok", "", "tok"},
|
|
{"", "?token=q", "q"},
|
|
{"Bearer hdr", "?token=q", "hdr"}, // header wins
|
|
}
|
|
for _, c := range cases {
|
|
if got := deletionAuthorization(mk(c.hdr, c.q)); got != c.want {
|
|
t.Errorf("deletionAuthorization(hdr=%q q=%q) = %q want %q", c.hdr, c.q, got, c.want)
|
|
}
|
|
}
|
|
}
|