| 1 | package sheets |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "net/http" |
| 6 | "net/http/httptest" |
| 7 | "net/url" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | // A sheet can never be left without an owner — through ANY door. |
| 13 | // |
| 14 | // TestTheLastOwnerCannotBeRemoved already covers revoke. This covers the |
| 15 | // other one, which was open: `grant` upserts on (sheet_id, member_id), |
| 16 | // so re-sharing with somebody who already has access CHANGES their role. |
| 17 | // Nothing counted owners on that path. |
| 18 | // |
| 19 | // So the sole owner typing their own address into the share form and |
| 20 | // picking "viewer" demoted themselves. Zero owner grants remain, mayShare |
| 21 | // is false for everybody, and — as the revoke path's own comment says — |
| 22 | // there is no admin path to repair it. One request, no undo, sheet |
| 23 | // permanently unshareable. |
| 24 | // |
| 25 | // It is a one-line form submission, not a contrived sequence, which is |
| 26 | // what makes it worth a test rather than a note. |
| 27 | func TestAnOwnerCannotDemoteThemselvesToNobody(t *testing.T) { |
| 28 | app, cookies := signIn(t) |
| 29 | s, _ := newSheet(t, app, cookies, "Only owner") |
| 30 | |
| 31 | form := url.Values{} |
| 32 | form.Set("address", "paul@example.com") |
| 33 | form.Set("role", string(SheetViewer)) |
| 34 | req := httptest.NewRequest(http.MethodPost, |
| 35 | fmt.Sprintf("/sheets/%d/share", s.ID), strings.NewReader(form.Encode())) |
| 36 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
| 37 | rec := do(t, app, as(req, cookies)) |
| 38 | if rec.Code != http.StatusSeeOther { |
| 39 | t.Fatalf("share POST = %d, want 303", rec.Code) |
| 40 | } |
| 41 | |
| 42 | // Whatever the screen says, the sheet must still have an owner. |
| 43 | people, err := app.grantsOn(s.ID) |
| 44 | if err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | owners := 0 |
| 48 | for _, p := range people { |
| 49 | if p.Role == SheetOwner { |
| 50 | owners++ |
| 51 | } |
| 52 | } |
| 53 | if owners == 0 { |
| 54 | t.Fatal("the only owner demoted themselves: the sheet now has no " + |
| 55 | "owner, nobody can ever share it again, and there is no path " + |
| 56 | "to repair it") |
| 57 | } |
| 58 | |
| 59 | // And the screen must still be reachable by them. |
| 60 | rec = do(t, app, as(httptest.NewRequest(http.MethodGet, |
| 61 | fmt.Sprintf("/sheets/%d/share", s.ID), nil), cookies)) |
| 62 | if rec.Code != http.StatusOK { |
| 63 | t.Fatalf("the owner can no longer open the share screen: %d", rec.Code) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | // Demoting one of two owners is fine, and must stay fine — the guard |
| 68 | // above must refuse the LAST owner, not every demotion. |
| 69 | func TestOneOfTwoOwnersCanBeDemoted(t *testing.T) { |
| 70 | app, cookies := signIn(t) |
| 71 | s, _ := newSheet(t, app, cookies, "Two owners") |
| 72 | guestOn(t, app, s.ID, "ada@elsewhere.example", SheetOwner) |
| 73 | |
| 74 | form := url.Values{} |
| 75 | form.Set("address", "ada@elsewhere.example") |
| 76 | form.Set("role", string(SheetEditor)) |
| 77 | req := httptest.NewRequest(http.MethodPost, |
| 78 | fmt.Sprintf("/sheets/%d/share", s.ID), strings.NewReader(form.Encode())) |
| 79 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
| 80 | if rec := do(t, app, as(req, cookies)); rec.Code != http.StatusSeeOther { |
| 81 | t.Fatalf("share POST = %d, want 303", rec.Code) |
| 82 | } |
| 83 | |
| 84 | people, err := app.grantsOn(s.ID) |
| 85 | if err != nil { |
| 86 | t.Fatal(err) |
| 87 | } |
| 88 | for _, p := range people { |
| 89 | if p.Member.Address == "ada@elsewhere.example" && p.Role != SheetEditor { |
| 90 | t.Errorf("ada is %q, want editor: demoting a second owner is "+ |
| 91 | "allowed and the guard has been drawn too wide", p.Role) |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | |