clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / tinyclient / people_test.go
people_test.go 5.0K
1package tinyclient23import (4 "context"5 "encoding/json"6 "net/http"7 "net/http/httptest"8 "strings"9 "testing"10 "time"1112 "github.com/FelineStateMachine/tinyrelay/internal/policy"13)1415// peopleBackend answers listjoinrequests with the rows it holds, pending16// first as the relay lists them, and refuses callers other than the owner.17type peopleBackend struct {18 fakeBackend19 requests []any20 calls []string21}2223var (24 askerPending = strings.Repeat("1", 64)25 askerQuiet = strings.Repeat("2", 64)26 askerDenied = strings.Repeat("3", 64)27)2829func (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.Canceled34 }35 return b.requests, nil36 }37 return map[string]any{"method": method}, nil38}3940func 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="&quot;` + askerPending + `&quot;"><button>Approve</button></rpc-form><rpc-form method="denyjoin" refresh><input type="hidden" name="param" value="&quot;` + askerPending + `&quot;"><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="&quot;` + askerQuiet + `&quot;"><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="&quot;`+askerDenied+`&quot;"`) {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 }9394 // 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 }107108 // 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}