| 1 | package tinyclient |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "net/http" |
| 7 | "net/http/httptest" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/FelineStateMachine/tinyrelay/internal/policy" |
| 13 | ) |
| 14 | |
| 15 | // peopleBackend answers listjoinrequests with the rows it holds, pending |
| 16 | // first as the relay lists them, and refuses callers other than the owner. |
| 17 | type peopleBackend struct { |
| 18 | fakeBackend |
| 19 | requests []any |
| 20 | calls []string |
| 21 | } |
| 22 | |
| 23 | var ( |
| 24 | askerPending = strings.Repeat("1" , 64) |
| 25 | askerQuiet = strings.Repeat("2" , 64) |
| 26 | askerDenied = strings.Repeat("3" , 64) |
| 27 | ) |
| 28 | |
| 29 | func (b *peopleBackend) Query(_ context.Context, method string, params []json.RawMessage, actor string) (any, error) { |
| 30 | b.calls = append(b.calls, method+":" +actor) |
| 31 | if method == "listjoinrequests" { |
| 32 | if actor != b.policy.Owner { |
| 33 | return nil, context.Canceled |
| 34 | } |
| 35 | return b.requests, nil |
| 36 | } |
| 37 | return map[string]any{"method" : method}, nil |
| 38 | } |
| 39 | |
| 40 | func TestPeoplePageListsAccessRequestsWithDecisions(t *testing.T) { |
| 41 | now := time.Now().Unix() |
| 42 | owner := strings.Repeat("a" , 64) |
| 43 | backend := &peopleBackend{fakeBackend: fakeBackend{policy: policy.Defaults(owner)}, requests: []any{ |
| 44 | map[string]any{"pubkey" : askerPending, "reason" : "Met you at the meetup, would like to follow the build." , "requested_at" : now - 600, "status" : "pending" , "decided_by" : "" , "decided_at" : 0}, |
| 45 | map[string]any{"pubkey" : askerQuiet, "reason" : "" , "requested_at" : now - 60, "status" : "pending" , "decided_by" : "" , "decided_at" : 0}, |
| 46 | map[string]any{"pubkey" : askerDenied, "reason" : "spam" , "requested_at" : now - 7200, "status" : "denied" , "decided_by" : owner, "decided_at" : now - 3600}, |
| 47 | }} |
| 48 | app, err := New(backend, Options{Actor: func(*http.Request) (string, error) { return owner, nil }}) |
| 49 | if err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | recorder := httptest.NewRecorder() |
| 53 | app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/manage/people" , nil)) |
| 54 | body := recorder.Body.String() |
| 55 | if recorder.Code != http.StatusOK { |
| 56 | t.Fatalf("status=%d body=%s" , recorder.Code, body) |
| 57 | } |
| 58 | if strings.Contains(body, "class=" ) { |
| 59 | t.Fatal("people page carries a class attribute" ) |
| 60 | } |
| 61 | if strings.Contains(body, "\u00b7" ) { |
| 62 | t.Fatal("people page contains a middle dot" ) |
| 63 | } |
| 64 | if backend.calls[0] != "listjoinrequests:" +owner { |
| 65 | t.Fatalf("requests were not loaded server-side: %v" , backend.calls) |
| 66 | } |
| 67 | for _, want := range []string{ |
| 68 | `<h3>Access requests</h3>` , |
| 69 | `<table id="requests"><thead><tr><th>person</th><th>reason</th><th>asked</th><th></th></tr></thead>` , |
| 70 | `<tr><td><nostr-name pubkey="` + askerPending + `" title="` + askerPending + `">` + shortID(askerPending) + `</nostr-name></td><td>Met you at the meetup, would like to follow the build.</td><td><time datetime="` , |
| 71 | `<rpc-form method="approvejoin" refresh><input type="hidden" name="param" value=""` + askerPending + `""><button>Approve</button></rpc-form><rpc-form method="denyjoin" refresh><input type="hidden" name="param" value=""` + askerPending + `""><button>Deny</button></rpc-form></td></tr>` , |
| 72 | `<td><small>no reason given</small></td>` , |
| 73 | `<rpc-form method="approvejoin" refresh><input type="hidden" name="param" value=""` + askerQuiet + `""><button>Approve</button></rpc-form>` , |
| 74 | `<ul id="decided"><li data-state="denied"><nostr-name pubkey="` + askerDenied + `"` , |
| 75 | `<small>denied <time datetime="` , |
| 76 | ` by <nostr-name pubkey="` + owner + `"` , |
| 77 | `<h4>People</h4><table><tr><th>requests</th><td>2 pending</td></tr></table>` , |
| 78 | `<h3>Invites</h3>` , |
| 79 | } { |
| 80 | if !strings.Contains(body, want) { |
| 81 | t.Errorf("people page missing %q" , want) |
| 82 | } |
| 83 | } |
| 84 | if strings.Index(body, `<h3>Access requests</h3>` ) > strings.Index(body, `<h3>Invites</h3>` ) { |
| 85 | t.Fatal("access requests are not above invites" ) |
| 86 | } |
| 87 | if strings.Contains(body, `value=""` +askerDenied+`""` ) { |
| 88 | t.Fatal("a decided request offers buttons" ) |
| 89 | } |
| 90 | if strings.Contains(body, "No pending requests." ) { |
| 91 | t.Fatal("empty state shown with pending requests" ) |
| 92 | } |
| 93 | |
| 94 | // With nothing pending the section says so and the panel counts zero. |
| 95 | backend.requests = []any{backend.requests[2]} |
| 96 | recorder = httptest.NewRecorder() |
| 97 | app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/manage/people" , nil)) |
| 98 | body = recorder.Body.String() |
| 99 | for _, want := range []string{`<h3>Access requests</h3>` , `<p>No pending requests.</p>` , `<ul id="decided">` , `<td>0 pending</td>` } { |
| 100 | if !strings.Contains(body, want) { |
| 101 | t.Errorf("empty people page missing %q" , want) |
| 102 | } |
| 103 | } |
| 104 | if strings.Contains(body, `<table id="requests">` ) { |
| 105 | t.Fatal("empty people page renders the requests table" ) |
| 106 | } |
| 107 | |
| 108 | // A member who may not review sees neither the section nor the count. |
| 109 | memberApp, err := New(backend, Options{Actor: func(*http.Request) (string, error) { return strings.Repeat("c" , 64), nil }}) |
| 110 | if err != nil { |
| 111 | t.Fatal(err) |
| 112 | } |
| 113 | recorder = httptest.NewRecorder() |
| 114 | memberApp.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/manage/people" , nil)) |
| 115 | body = recorder.Body.String() |
| 116 | if strings.Contains(body, "Access requests" ) || strings.Contains(body, "pending</td>" ) { |
| 117 | t.Fatal("a member sees the access requests section" ) |
| 118 | } |
| 119 | if !strings.Contains(body, `<h3>Invites</h3>` ) { |
| 120 | t.Fatal("a member lost the rest of the page" ) |
| 121 | } |
| 122 | } |