| 1 | package tinyclient |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | |
| 12 | "github.com/FelineStateMachine/tinyrelay/internal/policy" |
| 13 | ) |
| 14 | |
| 15 | const ( |
| 16 | wikiOwner = "1111111111111111111111111111111111111111111111111111111111111111" |
| 17 | wikiOther = "2222222222222222222222222222222222222222222222222222222222222222" |
| 18 | wikiMerge = "3333333333333333333333333333333333333333333333333333333333333333" |
| 19 | wikiOwnerV = "4444444444444444444444444444444444444444444444444444444444444444" |
| 20 | wikiOtherV = "5555555555555555555555555555555555555555555555555555555555555555" |
| 21 | // wikiOwnerOld and wikiOtherOld are archived revisions that the |
| 22 | // current versions replaced. |
| 23 | wikiOwnerOld = "7777777777777777777777777777777777777777777777777777777777777777" |
| 24 | wikiOtherOld = "8888888888888888888888888888888888888888888888888888888888888888" |
| 25 | ) |
| 26 | |
| 27 | // wikiBackend answers the three wiki queries with one page that has two |
| 28 | // versions and an open merge request from the second author to the owner. |
| 29 | type wikiBackend struct { |
| 30 | fakeBackend |
| 31 | calls []string |
| 32 | params map[string]map[string]any |
| 33 | private bool |
| 34 | // proposal makes the second author's version a proposal in this state |
| 35 | // (pending, approved or rejected), decided by the owner when it is not |
| 36 | // pending, with an approved earlier revision behind it in the history. |
| 37 | // approver marks the caller as allowed to decide, and shown makes the |
| 38 | // proposal the page's shown version. |
| 39 | proposal string |
| 40 | approver bool |
| 41 | shown bool |
| 42 | // archived gives the owner's version an earlier revision, which a |
| 43 | // request naming it by version opens. |
| 44 | archived bool |
| 45 | } |
| 46 | |
| 47 | func (b *wikiBackend) Query(_ context.Context, method string, params []json.RawMessage, actor string) (any, error) { |
| 48 | b.calls = append(b.calls, method) |
| 49 | var object map[string]any |
| 50 | if len(params) == 1 { |
| 51 | _ = json.Unmarshal(params[0], &object) |
| 52 | } |
| 53 | if b.params == nil { |
| 54 | b.params = map[string]map[string]any{} |
| 55 | } |
| 56 | b.params[method] = object |
| 57 | if b.private && actor == "" { |
| 58 | return nil, errors.New("auth-required: sign in to read this relay" ) |
| 59 | } |
| 60 | owner := map[string]any{"id" : wikiOwnerV, "author" : wikiOwner, "created_at" : float64(1788876000), "d" : "release-notes-1-4" , "title" : "Release notes 1.4 <b>" , "summary" : "What changed" , "coordinate" : "30818:" + wikiOwner + ":release-notes-1-4" , "likes" : float64(0), "revision" : float64(1), "revisions" : float64(1), |
| 61 | "content" : "# Highlights\n\nPrivate tenants and _encrypted_ uploads. See [[Files and Private Repositories]].\n\n<script>alert(1)</script>" , "links" : []any{"files-and-private-repositories" }} |
| 62 | other := map[string]any{"id" : wikiOtherV, "author" : wikiOther, "created_at" : float64(1788880000), "d" : "release-notes-1-4" , "title" : "Release notes 1.4" , "coordinate" : "30818:" + wikiOther + ":release-notes-1-4" , "likes" : float64(0), "revision" : float64(1), "revisions" : float64(1), "fork" : map[string]any{"a" : owner["coordinate" ], "e" : wikiOwnerV}, "content" : "# Highlights\n\nA proposed rewrite." } |
| 63 | merge := map[string]any{"id" : wikiMerge, "author" : wikiOther, "created_at" : float64(1788881000), "content" : "Please take my rewrite" , "target" : owner["coordinate" ], "target_d" : "release-notes-1-4" , "destination" : wikiOwner, "source" : wikiOtherV, "status" : "open" } |
| 64 | shown, preferredBy := owner, "owner" |
| 65 | history := []any{other, owner} |
| 66 | if b.proposal != "" { |
| 67 | other["proposal" ], other["approval" ], other["revision" ], other["revisions" ] = true, b.proposal, float64(2), float64(2) |
| 68 | if b.proposal != "pending" { |
| 69 | other["approval_event" ], other["approval_at" ], other["approval_by" ] = wikiMerge, float64(1788882000), wikiOwner |
| 70 | } |
| 71 | old := map[string]any{"id" : wikiOtherOld, "author" : wikiOther, "created_at" : float64(1788878000), "d" : "release-notes-1-4" , "title" : "Release notes 1.4" , "coordinate" : other["coordinate" ], "likes" : float64(0), "revision" : float64(1), "revisions" : float64(2), "superseded_by" : wikiOtherV, "proposal" : true, "approval" : "approved" , "approval_event" : wikiMerge, "approval_at" : float64(1788879000), "approval_by" : wikiOwner} |
| 72 | history = []any{other, old, owner} |
| 73 | if b.shown { |
| 74 | shown = other |
| 75 | } |
| 76 | } |
| 77 | if b.archived { |
| 78 | owner["revision" ], owner["revisions" ] = float64(2), float64(2) |
| 79 | old := map[string]any{"id" : wikiOwnerOld, "author" : wikiOwner, "created_at" : float64(1788870000), "d" : "release-notes-1-4" , "title" : "Release notes 1.3" , "coordinate" : owner["coordinate" ], "likes" : float64(1), "revision" : float64(1), "revisions" : float64(2), "superseded_by" : wikiOwnerV, "content" : "# Highlights\n\nThe first draft." } |
| 80 | history = append(history, old) |
| 81 | if object["version" ] == wikiOwnerOld { |
| 82 | shown, preferredBy = old, "version" |
| 83 | } |
| 84 | } |
| 85 | switch method { |
| 86 | case "browsewiki" : |
| 87 | return map[string]any{"items" : []any{map[string]any{"d" : "release-notes-1-4" , "title" : "Release notes 1.4 <b>" , "version" : shown, "versions" : float64(2), "open_merges" : float64(1)}}, "next_cursor" : "release-notes-1-4" , "can_approve" : b.approver}, nil |
| 88 | case "browsewikipage" : |
| 89 | if object["d" ] != "release-notes-1-4" { |
| 90 | return nil, errors.New("not found: wiki page" ) |
| 91 | } |
| 92 | return map[string]any{"d" : "release-notes-1-4" , "title" : "Release notes 1.4 <b>" , "version" : shown, "preferred_by" : preferredBy, "versions" : []any{other, owner}, "history" : history, "merges" : []any{merge}, "redirects_to" : []any{map[string]any{"d" : "changelog-1-4" , "target_d" : "release-notes-1-4" }}, "redirects_from" : []any{}, "can_approve" : b.approver}, nil |
| 93 | case "browsewikimerge" : |
| 94 | return map[string]any{"merge" : merge, "proposed" : other, "target" : owner}, nil |
| 95 | } |
| 96 | return nil, errors.New("unsupported: " + method) |
| 97 | } |
| 98 | |
| 99 | func wikiApp(t *testing.T, b *wikiBackend, actor string) *App { |
| 100 | t.Helper() |
| 101 | app, err := New(b, Options{Actor: func(*http.Request) (string, error) { return actor, nil }}) |
| 102 | if err != nil { |
| 103 | t.Fatal(err) |
| 104 | } |
| 105 | return app |
| 106 | } |
| 107 | |
| 108 | func wikiGet(t *testing.T, app *App, path string) string { |
| 109 | t.Helper() |
| 110 | recorder := httptest.NewRecorder() |
| 111 | app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, path, nil)) |
| 112 | if recorder.Code != http.StatusOK { |
| 113 | t.Fatalf("%s: status=%d body=%s" , path, recorder.Code, recorder.Body.String()) |
| 114 | } |
| 115 | body := recorder.Body.String() |
| 116 | if strings.Contains(body, "class=" ) { |
| 117 | t.Fatalf("%s carries a class attribute" , path) |
| 118 | } |
| 119 | if strings.Contains(body, "\u00b7" ) { |
| 120 | t.Fatalf("%s carries a middle dot" , path) |
| 121 | } |
| 122 | return body |
| 123 | } |
| 124 | |
| 125 | func wantAll(t *testing.T, path, body string, wants ...string) { |
| 126 | t.Helper() |
| 127 | for _, want := range wants { |
| 128 | if !strings.Contains(body, want) { |
| 129 | t.Errorf("%s missing %q" , path, want) |
| 130 | } |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | func wantNone(t *testing.T, path, body string, wants ...string) { |
| 135 | t.Helper() |
| 136 | for _, want := range wants { |
| 137 | if strings.Contains(body, want) { |
| 138 | t.Errorf("%s unexpectedly contains %q" , path, want) |
| 139 | } |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | func TestWikiListRendersPagesAndSearch(t *testing.T) { |
| 144 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}} |
| 145 | app := wikiApp(t, b, wikiOwner) |
| 146 | body := wikiGet(t, app, "/wiki?q=release" ) |
| 147 | if b.calls[0] != "browsewiki" || b.params["browsewiki" ]["q" ] != "release" { |
| 148 | t.Fatalf("calls=%v params=%v" , b.calls, b.params) |
| 149 | } |
| 150 | wantAll(t, "/wiki" , body, |
| 151 | `href="/wiki" aria-current="page"` , `id="wiki-search"` , `value="release"` , |
| 152 | `<a href="/wiki/release-notes-1-4">Release notes 1.4 <b></a>` , `<nostr-name pubkey="` +wikiOwner+`"` , |
| 153 | `<td>2</td><td>1 open</td>` , `2026-09-08 14:00 UTC` , `href="/wiki?cursor=release-notes-1-4&q=release"` , |
| 154 | `<b>demo</b> » <page-link url="http://relay.example/wiki` , `<a href="/wiki?new=1">new page</a>` ) |
| 155 | wantNone(t, "/wiki" , body, "<wiki-compose" ) |
| 156 | body = wikiGet(t, app, "/wiki?new=1" ) |
| 157 | wantAll(t, "/wiki?new=1" , body, `<h2>New page</h2>` , `<wiki-compose name="" author="" coordinate="" event="">` , `name="title"` , `name="name"` , `name="summary"` , `name="content"` , `value="publish"` , `<noscript>` ) |
| 158 | wantNone(t, "/wiki?new=1" , body, "Propose to" ) |
| 159 | } |
| 160 | |
| 161 | func TestWikiPageRendersArticleMergeBarAndPanel(t *testing.T) { |
| 162 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}} |
| 163 | path := "/wiki/Release%20Notes%201-4" |
| 164 | body := wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 165 | if b.params["browsewikipage" ]["d" ] != "release-notes-1-4" { |
| 166 | t.Fatalf("page name was not normalized: %v" , b.params["browsewikipage" ]) |
| 167 | } |
| 168 | wantAll(t, path, body, |
| 169 | `<title>Release notes 1.4 <b> | wiki | demo</title>` , `<h1>Release notes 1.4 <b></h1>` , |
| 170 | `<merge-request><nostr-name pubkey="` +wikiOther+`"` , `proposes a new version of this page` , `href="/wiki/release-notes-1-4?merge=` +wikiMerge+`">compare</a>` , |
| 171 | `<nostr-react event="` +wikiMerge+`" pubkey="` +wikiOther+`" kind="818"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react>` , |
| 172 | `<a href="/e/` +wikiMerge+`">Reply</a>` , |
| 173 | `<wiki-article><header>wiki | release-notes-1-4 | revision 1 by <nostr-name pubkey="` +wikiOwner+`"` , `version 1 of 2 | 1 fork</header>` , |
| 174 | `<h1>Highlights</h1>` , `<em>encrypted</em>` , `<a href="/wiki/files-and-private-repositories">Files and Private Repositories</a>` , `<script>alert(1)</script>` , |
| 175 | `<h4>Page</h4>` , `<th>versions</th><td>2</td>` , `<th>merge requests</th><td>1 open</td>` , `<th>links</th><td>1</td>` , |
| 176 | `<h4>Other versions</h4>` , `href="/wiki/release-notes-1-4?version=` +wikiOtherV+`">222222222222</a> | fork` , |
| 177 | `<h4>Also known as</h4>` , `<code>changelog-1-4</code>` , |
| 178 | `href="/wiki/release-notes-1-4?edit=1">edit</a>` , `href="/wiki/release-notes-1-4?history=1">history</a>` , |
| 179 | `<b>demo</b> » <page-link url="http://relay.example/wiki/release-notes-1-4" title="Copy the page address">wiki/release-notes-1-4</page-link>` ) |
| 180 | wantNone(t, path, body, "<script>alert" , ">fork</a>" ) |
| 181 | // The other author sees the request but cannot answer it, and forks |
| 182 | // instead of editing. |
| 183 | body = wikiGet(t, wikiApp(t, b, wikiOther), path) |
| 184 | wantAll(t, path, body, `<merge-request>` , `href="/wiki/release-notes-1-4?edit=1">fork</a>` ) |
| 185 | wantNone(t, path, body, "<nostr-react" , ">edit</a>" ) |
| 186 | } |
| 187 | |
| 188 | func TestWikiPageCompareEditorAndHistory(t *testing.T) { |
| 189 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}} |
| 190 | path := "/wiki/release-notes-1-4?merge=" + wikiMerge |
| 191 | body := wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 192 | if b.params["browsewikimerge" ]["id" ] != wikiMerge { |
| 193 | t.Fatalf("merge lookup params=%v" , b.params["browsewikimerge" ]) |
| 194 | } |
| 195 | wantAll(t, path, body, `<div id="wiki-compare">` , `| open | Please take my rewrite</p>` , `<h2>Proposed version</h2>` , `A proposed rewrite.` , `<h2>Current version</h2>` , `<em>encrypted</em>` , `href="/wiki/release-notes-1-4">back to the page</a>` ) |
| 196 | |
| 197 | path = "/wiki/release-notes-1-4?edit=1" |
| 198 | body = wikiGet(t, wikiApp(t, b, wikiOther), path) |
| 199 | wantAll(t, path, body, `<wiki-compose name="release-notes-1-4" author="` +wikiOwner+`" coordinate="30818:` +wikiOwner+`:release-notes-1-4" event="` +wikiOwnerV+`">` , |
| 200 | `value="Release notes 1.4 <b>"` , `value="release-notes-1-4"` , `value="What changed"` , `<script>alert(1)</script></textarea>` , |
| 201 | `<button name="action" value="propose">Propose to 111111111111</button>` , `JavaScript and a connected signer are required to publish.` ) |
| 202 | wantNone(t, path, body, "<wiki-article>" ) |
| 203 | body = wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 204 | wantNone(t, path, body, "Propose to" ) |
| 205 | |
| 206 | path = "/wiki/release-notes-1-4?history=1" |
| 207 | body = wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 208 | wantAll(t, path, body, `<th>author</th><th>revision</th><th>version</th>` , `<td>1 <small>current</small></td>` , `fork of 444444444444` , `href="/wiki/release-notes-1-4?version=` +wikiOwnerV+`">444444444444</a>` ) |
| 209 | wantAll(t, path, body, `<th>revisions</th><td>2</td>` ) |
| 210 | wantNone(t, path, body, "<small>replaced</small>" ) |
| 211 | } |
| 212 | |
| 213 | func TestWikiHistoryListsRevisionsWithStates(t *testing.T) { |
| 214 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, proposal: "pending" , approver: true, shown: true, archived: true} |
| 215 | path := "/wiki/release-notes-1-4?history=1" |
| 216 | body := wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 217 | wantAll(t, path, body, |
| 218 | `<tr data-state="pending"><td><nostr-name pubkey="` +wikiOther+`"` , `<td>2 <small>current</small></td><td><a href="/wiki/release-notes-1-4?version=` +wikiOtherV+`">555555555555</a></td><td>fork of 444444444444 | proposed pending</td>` , |
| 219 | `<tr data-state="approved"><td><nostr-name pubkey="` +wikiOther+`"` , `<td>1 <small>replaced</small></td><td><a href="/wiki/release-notes-1-4?version=` +wikiOtherOld+`">888888888888</a></td><td> | proposed approved</td>` , |
| 220 | `<td>2 <small>current</small></td><td><a href="/wiki/release-notes-1-4?version=` +wikiOwnerV+`">444444444444</a></td>` , |
| 221 | `<td>1 <small>replaced</small></td><td><a href="/wiki/release-notes-1-4?version=` +wikiOwnerOld+`">777777777777</a></td><td> | 1 likes</td>` , |
| 222 | `<th>revisions</th><td>4</td>` ) |
| 223 | // A member sees the rows without their proposal states. |
| 224 | b.approver = false |
| 225 | body = wikiGet(t, wikiApp(t, b, wikiMember), path) |
| 226 | wantAll(t, path, body, `<td>1 <small>replaced</small></td><td><a href="/wiki/release-notes-1-4?version=` +wikiOtherOld+`">888888888888</a></td><td></td>` ) |
| 227 | wantNone(t, path, body, "<tr data-state=" , "proposed " ) |
| 228 | } |
| 229 | |
| 230 | func TestWikiArchivedRevisionShowsANote(t *testing.T) { |
| 231 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, archived: true} |
| 232 | path := "/wiki/release-notes-1-4?version=" + wikiOwnerOld |
| 233 | body := wikiGet(t, wikiApp(t, b, wikiOther), path) |
| 234 | if b.params["browsewikipage" ]["version" ] != wikiOwnerOld { |
| 235 | t.Fatalf("version lookup params=%v" , b.params["browsewikipage" ]) |
| 236 | } |
| 237 | wantAll(t, path, body, |
| 238 | `<wiki-article><header>wiki | release-notes-1-4 | revision 1 by <nostr-name pubkey="` +wikiOwner+`"` , |
| 239 | `<p id="wiki-revision-note">revision 1 of 2 by this author; a newer revision exists | <a href="/wiki/release-notes-1-4?author=` +wikiOwner+`">show the newest</a></p>` , |
| 240 | `The first draft.` ) |
| 241 | // The author's current revision carries no note. |
| 242 | path = "/wiki/release-notes-1-4" |
| 243 | body = wikiGet(t, wikiApp(t, b, wikiOther), path) |
| 244 | wantAll(t, path, body, `revision 2 by <nostr-name pubkey="` +wikiOwner+`"` , `<em>encrypted</em>` ) |
| 245 | wantNone(t, path, body, `id="wiki-revision-note"` , "The first draft." ) |
| 246 | } |
| 247 | |
| 248 | func TestWikiMissingPageOffersTheEditorToMembers(t *testing.T) { |
| 249 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}} |
| 250 | path := "/wiki/Whats%20Up%3F" |
| 251 | body := wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 252 | wantAll(t, path, body, `<h1>whats-up</h1>` , `There is no page named <code>whats-up</code> yet.` , `<wiki-compose name="whats-up"` , `<th>name</th><td><code>whats-up</code></td>` ) |
| 253 | wantNone(t, path, body, `role="alert"` ) |
| 254 | body = wikiGet(t, wikiApp(t, b, "" ), path) |
| 255 | if !strings.Contains(body, `<a href="/signin?next=` ) || !strings.Contains(body, `">Sign in</a> to write it.` ) { |
| 256 | t.Fatalf("%s missing return-preserving sign-in link" , path) |
| 257 | } |
| 258 | wantNone(t, path, body, "<wiki-compose" ) |
| 259 | } |
| 260 | |
| 261 | func TestWikiGuestAccessFollowsTheReadsPolicy(t *testing.T) { |
| 262 | members := policy.Defaults(wikiOwner) |
| 263 | members.Reads = "members" |
| 264 | b := &wikiBackend{fakeBackend: fakeBackend{policy: members}, private: true} |
| 265 | for _, path := range []string{"/wiki" , "/wiki/release-notes-1-4" } { |
| 266 | body := wikiGet(t, wikiApp(t, b, "" ), path) |
| 267 | wantAll(t, path, body, `<p role="alert">auth-required: sign in to read this relay</p>` , `browsing as guest` ) |
| 268 | wantNone(t, path, body, "<wiki-article>" , "<merge-request>" , "Release notes 1.4" , "<wiki-compose" ) |
| 269 | body = wikiGet(t, wikiApp(t, b, wikiOther), path) |
| 270 | wantAll(t, path, body, "Release notes 1.4" ) |
| 271 | wantNone(t, path, body, `role="alert"` ) |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | const wikiMember = "6666666666666666666666666666666666666666666666666666666666666666" |
| 276 | |
| 277 | func TestWikiProposalBarOffersTheDecisionToApprovers(t *testing.T) { |
| 278 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, proposal: "pending" , approver: true, shown: true} |
| 279 | path := "/wiki/release-notes-1-4" |
| 280 | body := wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 281 | wantAll(t, path, body, |
| 282 | `<wiki-proposal data-state="pending">Proposed by <nostr-name pubkey="` +wikiOther+`"` , `| pending<nostr-react event="` +wikiOtherV+`" pubkey="` +wikiOther+`" kind="30818"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react><small>showing pending revision 2; readers see approved revision 1</small></wiki-proposal>` , |
| 283 | `<wiki-article><header>wiki | release-notes-1-4 | revision 2 by <nostr-name pubkey="` +wikiOther+`"` , `A proposed rewrite.` ) |
| 284 | // A decided proposal names the decision, its time and the deciding key, |
| 285 | // and offers no buttons. Readers still see the earlier approved |
| 286 | // revision while the edit is rejected, and the edit itself once it is |
| 287 | // approved. |
| 288 | for _, state := range []string{"approved" , "rejected" } { |
| 289 | b.proposal = state |
| 290 | body = wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 291 | wantAll(t, path, body, `<wiki-proposal data-state="` +state+`">Proposed by <nostr-name pubkey="` +wikiOther+`"` , `| ` +state+` 2026-09-08 15:40 UTC by <nostr-name pubkey="` +wikiOwner+`"` ) |
| 292 | wantNone(t, path, body, "<nostr-react" , ">Accept<" ) |
| 293 | if state == "rejected" { |
| 294 | wantAll(t, path, body, `<small>showing rejected revision 2; readers see approved revision 1</small></wiki-proposal>` ) |
| 295 | } else { |
| 296 | wantNone(t, path, body, "readers see approved revision" ) |
| 297 | } |
| 298 | } |
| 299 | // A pending proposal that is not the shown version is marked in the |
| 300 | // panel and the history, with no bar above the article. |
| 301 | b.proposal, b.shown = "pending" , false |
| 302 | body = wikiGet(t, wikiApp(t, b, wikiOwner), path) |
| 303 | wantAll(t, path, body, `<h4>Other versions</h4><ul><li data-state="pending"><a href="/wiki/release-notes-1-4?version=` +wikiOtherV+`">222222222222</a> | fork | proposed pending <small>` ) |
| 304 | wantNone(t, path, body, "<wiki-proposal" ) |
| 305 | body = wikiGet(t, wikiApp(t, b, wikiOwner), path+"?history=1" ) |
| 306 | wantAll(t, path, body, `<tr data-state="pending"><td><nostr-name pubkey="` +wikiOther+`"` , `fork of 444444444444 | proposed pending</td>` ) |
| 307 | } |
| 308 | |
| 309 | func TestWikiProposalAuthorSeesTheStateWithoutButtons(t *testing.T) { |
| 310 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, proposal: "pending" , shown: true} |
| 311 | path := "/wiki/release-notes-1-4" |
| 312 | body := wikiGet(t, wikiApp(t, b, wikiOther), path) |
| 313 | wantAll(t, path, body, `<wiki-proposal data-state="pending">Proposed by <nostr-name pubkey="` +wikiOther+`"` , `| pending<small>showing pending revision 2; readers see approved revision 1</small></wiki-proposal>` ) |
| 314 | wantNone(t, path, body, "<nostr-react" , ">Accept<" , ">Reject<" ) |
| 315 | body = wikiGet(t, wikiApp(t, b, wikiOther), "/wiki" ) |
| 316 | wantAll(t, "/wiki" , body, `<tr data-state="pending"><td><a href="/wiki/release-notes-1-4">Release notes 1.4 <b></a> <small>proposed | pending</small></td>` ) |
| 317 | } |
| 318 | |
| 319 | func TestWikiProposalStateIsHiddenFromMembers(t *testing.T) { |
| 320 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, proposal: "approved" , shown: true} |
| 321 | for _, path := range []string{"/wiki/release-notes-1-4" , "/wiki/release-notes-1-4?history=1" , "/wiki" } { |
| 322 | body := wikiGet(t, wikiApp(t, b, wikiMember), path) |
| 323 | wantAll(t, path, body, "Release notes 1.4" ) |
| 324 | wantNone(t, path, body, "<wiki-proposal" , "<tr data-state=" , "<li data-state=" , "proposed | " , "| proposed " , "<nostr-react" ) |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | func TestWikiListMarksProposalRowsForApprovers(t *testing.T) { |
| 329 | b := &wikiBackend{fakeBackend: fakeBackend{policy: policy.Defaults(wikiOwner)}, proposal: "rejected" , approver: true, shown: true} |
| 330 | body := wikiGet(t, wikiApp(t, b, wikiOwner), "/wiki" ) |
| 331 | wantAll(t, "/wiki" , body, `<tr data-state="rejected"><td><a href="/wiki/release-notes-1-4">Release notes 1.4 <b></a> <small>proposed | rejected</small></td><td><nostr-name pubkey="` +wikiOther+`"` ) |
| 332 | b.proposal = "" |
| 333 | body = wikiGet(t, wikiApp(t, b, wikiOwner), "/wiki" ) |
| 334 | wantNone(t, "/wiki" , body, "<tr data-state=" , "proposed | " ) |
| 335 | } |