| 1 | package docs_test |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/carlosframework/docs/internal/docs" |
| 9 | ) |
| 10 | |
| 11 | // The difference between "sharing works" and "sharing is usable". Slice 2 |
| 12 | // left a grantee able to open a shared document at its direct URL and with |
| 13 | // no way to find it again: List was owner-only, so the document Alice |
| 14 | // shared was reachable only by somebody who had kept the link. |
| 15 | // |
| 16 | // A person who has to keep a URL in a chat scrollback to reach a document |
| 17 | // does not have the document. |
| 18 | func TestASharedDocumentAppearsInTheGranteesList(t *testing.T) { |
| 19 | app, _ := docs.NewForTest(t) |
| 20 | alice := signInCookies(t, app, "alice@example.com") |
| 21 | sqid := createDoc(t, app, alice, "Minutes") |
| 22 | |
| 23 | // Before the grant, Bob's list does not mention it. Without this the |
| 24 | // test would pass on a list that shows everybody everything. |
| 25 | bob := signInCookies(t, app, "bob@example.com") |
| 26 | if rec := get(t, app, bob, "/"); strings.Contains(rec.Body.String(), "Minutes") { |
| 27 | t.Fatal("Bob's list named a document nobody had shared with him") |
| 28 | } |
| 29 | |
| 30 | if rec := post(t, app, alice, "/d/"+sqid+"/share", "address=bob@example.com&role=reader"); rec.Code != http.StatusSeeOther { |
| 31 | t.Fatalf("POST share: status %d, want 303", rec.Code) |
| 32 | } |
| 33 | |
| 34 | rec := get(t, app, bob, "/") |
| 35 | if rec.Code != http.StatusOK { |
| 36 | t.Fatalf("GET / as Bob: status %d, want 200", rec.Code) |
| 37 | } |
| 38 | if !strings.Contains(rec.Body.String(), "Minutes") { |
| 39 | t.Error("Bob's list does not name the document shared with him") |
| 40 | } |
| 41 | if !strings.Contains(rec.Body.String(), "/d/"+sqid) { |
| 42 | t.Error("Bob's list names the document but does not link to it") |
| 43 | } |
| 44 | |
| 45 | // Carol was granted nothing and sees nothing: the list widened by |
| 46 | // exactly one grant, not into a list of everything. |
| 47 | carol := signInCookies(t, app, "carol@example.com") |
| 48 | if rec := get(t, app, carol, "/"); strings.Contains(rec.Body.String(), "Minutes") { |
| 49 | t.Error("Carol's list named a document she holds no grant on") |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | // A shared row says it is shared, and the reader's own rows do not. |
| 54 | // |
| 55 | // The list stopped being "your documents" the day sharing landed. A row |
| 56 | // that looks like yours and is not is how somebody opens a document |
| 57 | // expecting to edit it — and with a reader grant they now get a page with |
| 58 | // no editor, which is correct and needs explaining before the click, not |
| 59 | // after it. (Branch review, finding 9.) |
| 60 | func TestASharedRowSaysSo(t *testing.T) { |
| 61 | app, _ := docs.NewForTest(t) |
| 62 | alice := signInCookies(t, app, "alice@example.com") |
| 63 | sqid := createDoc(t, app, alice, "Alices") |
| 64 | bob := signInCookies(t, app, "bob@example.com") |
| 65 | createDoc(t, app, bob, "Bobs") |
| 66 | post(t, app, alice, "/d/"+sqid+"/share", "address=bob@example.com&role=reader") |
| 67 | |
| 68 | // The catalog KEY, not the English. NewForTest's app never wires |
| 69 | // rastrillo.Options.BaseCatalog (only cmd/docs/main.go does), so every |
| 70 | // catalog string renders as its own key here. Asserting the prose |
| 71 | // would be asserting a string that never appears — and, worse, a |
| 72 | // prose fragment can accidentally match a key: a nearby test checked |
| 73 | // for "published" and passed on the key "docs.share.public.published" |
| 74 | // rather than on anything being published. |
| 75 | const marker = "docs.list.shared" |
| 76 | |
| 77 | body := get(t, app, bob, "/").Body.String() |
| 78 | if !strings.Contains(body, marker) { |
| 79 | t.Error("the shared row carries no marker") |
| 80 | } |
| 81 | // Exactly one row is marked: Bob's own must not be. A list where |
| 82 | // every row is badged says nothing. |
| 83 | if n := strings.Count(body, marker); n != 1 { |
| 84 | t.Errorf("%d rows marked shared, want exactly 1", n) |
| 85 | } |
| 86 | |
| 87 | // And Alice, who owns hers, sees no marker at all. |
| 88 | if strings.Contains(get(t, app, alice, "/").Body.String(), marker) { |
| 89 | t.Error("the owner's own document is marked as shared with her") |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | // Revoking takes it back out of the list, not just out of the document. A |
| 94 | // revoke that left the row on the grantee's list would leave them clicking |
| 95 | // into a 404 and reading it as a bug. |
| 96 | func TestRevokingRemovesItFromTheGranteesList(t *testing.T) { |
| 97 | app, _ := docs.NewForTest(t) |
| 98 | alice := signInCookies(t, app, "alice@example.com") |
| 99 | sqid := createDoc(t, app, alice, "Minutes") |
| 100 | bob := signInCookies(t, app, "bob@example.com") |
| 101 | |
| 102 | post(t, app, alice, "/d/"+sqid+"/share", "address=bob@example.com&role=reader") |
| 103 | if rec := get(t, app, bob, "/"); !strings.Contains(rec.Body.String(), "Minutes") { |
| 104 | t.Fatal("precondition: the shared document is not on Bob's list") |
| 105 | } |
| 106 | |
| 107 | if rec := post(t, app, alice, "/d/"+sqid+"/share/revoke", "address=bob@example.com"); rec.Code != http.StatusSeeOther { |
| 108 | t.Fatalf("POST revoke: status %d, want 303", rec.Code) |
| 109 | } |
| 110 | if rec := get(t, app, bob, "/"); strings.Contains(rec.Body.String(), "Minutes") { |
| 111 | t.Error("a revoked document is still on Bob's list") |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | // Search is the list's other door. Slice 2's Search ran over List, so |
| 116 | // widening only the list handler would have produced an app where a shared |
| 117 | // document is on the list but cannot be searched for — the kind of split |
| 118 | // that reads as a broken search rather than as a scope decision. |
| 119 | func TestAGranteeCanSearchForASharedDocument(t *testing.T) { |
| 120 | app, _ := docs.NewForTest(t) |
| 121 | alice := signInCookies(t, app, "alice@example.com") |
| 122 | sqid := createDoc(t, app, alice, "Quarterly plan") |
| 123 | bob := signInCookies(t, app, "bob@example.com") |
| 124 | |
| 125 | post(t, app, alice, "/d/"+sqid+"/share", "address=bob@example.com&role=reader") |
| 126 | |
| 127 | rec := get(t, app, bob, "/?q=quarterly") |
| 128 | if rec.Code != http.StatusOK { |
| 129 | t.Fatalf("GET /?q=quarterly as Bob: status %d, want 200", rec.Code) |
| 130 | } |
| 131 | if !strings.Contains(rec.Body.String(), "Quarterly plan") { |
| 132 | t.Error("Bob cannot search for the document shared with him") |
| 133 | } |
| 134 | |
| 135 | carol := signInCookies(t, app, "carol@example.com") |
| 136 | if rec := get(t, app, carol, "/?q=quarterly"); strings.Contains(rec.Body.String(), "Quarterly plan") { |
| 137 | t.Error("Carol's search found a document she holds no grant on") |
| 138 | } |
| 139 | } |
| 140 | |