Add delete token UI for info page
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user