Add delete token UI for info page

This commit is contained in:
2026-09-13 00:32:51 +02:00
parent ca34f1506d
commit 74bfdbfd8a
9 changed files with 293 additions and 25 deletions
+150
View File
@@ -1273,3 +1273,153 @@ func TestFormFieldsOverrideTheHeaders(t *testing.T) {
t.Errorf("id = %q, want the form field to win", res.ID)
}
}
// --- deleting from the info page -----------------------------------------
func (h *harness) postForm(t *testing.T, path string, form url.Values, cookie *http.Cookie) *http.Response {
t.Helper()
req, _ := http.NewRequest("POST", h.ts.URL+path, strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "text/html")
if cookie != nil {
req.AddCookie(cookie)
}
resp, err := h.ts.Client().Do(req)
if err != nil {
t.Fatal(err)
}
return resp
}
// The delete token is shown once and then has to be usable somewhere. The info
// page is the link an uploader would have kept, so the form lives there.
func TestInfoPageAcceptsTheDeleteToken(t *testing.T) {
h := newHarness(t, nil)
res := decode[uploadResult](t, h.upload(t, []byte("x"), nil))
info := h.get(t, "/i/"+res.ID, "")
raw, _ := io.ReadAll(info.Body)
info.Body.Close()
page := string(raw)
if !strings.Contains(page, "Remove this file") {
t.Error("the info page offers no way to use a delete token")
}
if !strings.Contains(page, `name="token"`) {
t.Error("the info page has no field for the delete token")
}
if strings.Contains(page, res.DeleteToken) {
t.Fatal("the info page leaks the delete token to anyone holding the link")
}
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
url.Values{"from": {"info"}, "token": {res.DeleteToken}}, nil)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("deleting with the right token => %s", resp.Status)
}
if _, err := h.store.Get(res.ID, h.now); err == nil {
t.Error("the file was not deleted")
}
}
// A mistyped token must land back on the file's page with the reason, not on a
// generic error page that has thrown the form away.
func TestWrongDeleteTokenReturnsToTheInfoPage(t *testing.T) {
h := newHarness(t, nil)
res := decode[uploadResult](t, h.upload(t, []byte("x"), map[string]string{
"Content-Disposition": `attachment; filename="keepme.bin"`}))
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
url.Values{"from": {"info"}, "token": {"wrong"}}, nil)
raw, _ := io.ReadAll(resp.Body)
resp.Body.Close()
page := string(raw)
if resp.StatusCode != http.StatusForbidden {
t.Errorf("status = %s, want 403", resp.Status)
}
if !strings.Contains(page, "keepme.bin") {
t.Error("the response is not the file's own page")
}
if !strings.Contains(page, "not correct") {
t.Error("the page does not say what went wrong")
}
if !strings.Contains(page, "<details open>") {
t.Error("the delete section is collapsed, hiding the error")
}
if _, err := h.store.Get(res.ID, h.now); err != nil {
t.Error("the file was deleted despite a wrong token")
}
}
// Someone whose own token already authorises removal gets a button, not a
// field asking for a token they do not have.
func TestInfoPageOffersADirectButtonToAnOwner(t *testing.T) {
h := newHarness(t, nil)
res := decode[uploadResult](t, h.upload(t, []byte("x"), map[string]string{
"Authorization": "Bearer " + h.token}))
for _, c := range []struct {
who string
token string
expectBtn bool
}{
{"the owner", h.token, true},
{"an admin", h.admin, true},
{"a stranger", "", false},
} {
req, _ := http.NewRequest("GET", h.ts.URL+"/i/"+res.ID, nil)
if c.token != "" {
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: c.token})
}
resp, err := h.ts.Client().Do(req)
if err != nil {
t.Fatal(err)
}
raw, _ := io.ReadAll(resp.Body)
resp.Body.Close()
got := strings.Contains(string(raw), "Your token can remove this file")
if got != c.expectBtn {
t.Errorf("direct delete button shown to %s = %v, want %v", c.who, got, c.expectBtn)
}
}
// And that button actually works with no token field at all.
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
url.Values{"from": {"info"}}, &http.Cookie{Name: tokenCookie, Value: h.token})
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("owner delete => %s", resp.Status)
}
}
// Guessing is throttled, but only the guessing: a correct token is never
// delayed by someone else's failed attempts.
func TestFailedDeletesAreThrottledAndSuccessIsNot(t *testing.T) {
h := newHarness(t, nil)
h.deleteLimiter = newLimiter(1, 3)
res := decode[uploadResult](t, h.upload(t, []byte("x"), nil))
var last *http.Response
for range 5 {
if last != nil {
last.Body.Close()
}
last = h.postForm(t, "/api/d/"+res.ID+"/delete",
url.Values{"from": {"info"}, "token": {"guess"}}, nil)
}
if last.StatusCode != http.StatusTooManyRequests {
t.Fatalf("repeated guesses => %s, want 429", last.Status)
}
last.Body.Close()
// The real token still works, having consumed nothing from the bucket.
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
url.Values{"from": {"info"}, "token": {res.DeleteToken}}, nil)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("the correct token was throttled: %s", resp.Status)
}
}