tinyrelayfile
tinyrelay/internal/community/community_test.go/

internal/community/community_test.go raw
1package community
2
3import (
4 "context"
5 "crypto/sha256"
6 "database/sql"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "strings"
11 "testing"
12
13 "github.com/FelineStateMachine/tinyrelay/internal/event"
14 "github.com/FelineStateMachine/tinyrelay/internal/policy"
15 "github.com/FelineStateMachine/tinyrelay/internal/storage"
16)
17
18func TestMembershipAuthorizationAndInviteClaim(t *testing.T) {
19 ctx := context.Background()
20 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")
21 if err != nil {
22 t.Fatal(err)
23 }
24 defer st.Close()
25 owner, moderator, member, stranger := strings.Repeat("a", 64), strings.Repeat("b", 64), strings.Repeat("c", 64), strings.Repeat("d", 64)
26 svc, err := New(ctx, st, owner)
27 if err != nil {
28 t.Fatal(err)
29 }
30 if _, err := svc.Execute(ctx, stranger, "setmember", raws(member, "alice", "member")); err == nil {
31 t.Fatal("stranger changed membership")
32 }
33 if _, err := svc.Execute(ctx, owner, "setmember", raws(moderator, map[string]any{"name": "mod", "role": "moderator"})); err != nil {
34 t.Fatal(err)
35 }
36 if _, err := svc.Execute(ctx, moderator, "setmember", raws(member, map[string]any{"name": "alice", "role": "member"})); err != nil {
37 t.Fatal(err)
38 }
39 inv, err := svc.Execute(ctx, owner, "createinvite", raws(int64(3600), 1, "test"))
40 if err != nil {
41 t.Fatal(err)
42 }
43 code := inv.(Invite).Code
44 if _, err := svc.Claim(ctx, code, stranger); err != nil {
45 t.Fatal(err)
46 }
47 if _, err := svc.Claim(ctx, code, strings.Repeat("e", 64)); err == nil {
48 t.Fatal("single-use claim spent twice")
49 }
50 if got, err := svc.Role(ctx, stranger); err != nil || got != "member" {
51 t.Fatalf("role=%q err=%v", got, err)
52 }
53 if inv == nil {
54 t.Fatal("missing invite result")
55 }
56}
57
58func TestMemberInvitePolicyTermsAndRevokedParent(t *testing.T) {
59 ctx := context.Background()
60 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")
61 if err != nil {
62 t.Fatal(err)
63 }
64 defer st.Close()
65 owner, member, stranger := strings.Repeat("a", 64), strings.Repeat("b", 64), strings.Repeat("c", 64)
66 p := policy.Defaults(owner)
67 p.MemberInvites = policy.MemberInvites{Depth: 2, Quota: 1}
68 p.JoinTerms = "be kind"
69 svc, err := New(ctx, st, owner)
70 if err != nil {
71 t.Fatal(err)
72 }
73 svc.ConfigurePolicy(func() policy.Policy { return p })
74 if _, err := svc.Execute(ctx, owner, "setmember", raws(member, map[string]any{"name": "member", "role": "member"})); err != nil {
75 t.Fatal(err)
76 }
77 inv, err := svc.Execute(ctx, member, "createinvite", raws(int64(3600), 1, "child"))
78 if err != nil {
79 t.Fatal(err)
80 }
81 code := inv.(Invite).Code
82 if _, err := svc.Execute(ctx, member, "createinvite", raws(int64(3600), 1, "over-quota")); err == nil {
83 t.Fatal("member exceeded live invite quota")
84 }
85 terms := sha256.Sum256([]byte(p.JoinTerms))
86 if _, err := svc.ClaimWithTerms(ctx, code, stranger, "wrong"); err == nil {
87 t.Fatal("terms mismatch accepted")
88 }
89 if role, _ := svc.Role(ctx, stranger); role != "" {
90 t.Fatalf("terms mismatch wrote membership: %q", role)
91 }
92 if _, err := svc.Execute(ctx, owner, "removesubtree", raws(member)); err != nil {
93 t.Fatal(err)
94 }
95 if _, err := svc.ClaimWithTerms(ctx, code, stranger, hex.EncodeToString(terms[:])); err == nil {
96 t.Fatal("revoked parent invite accepted")
97 }
98}
99
100func TestIssueNIP43InviteRequiresMembership(t *testing.T) {
101 ctx := context.Background()
102 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")
103 if err != nil {
104 t.Fatal(err)
105 }
106 defer st.Close()
107 owner, member, stranger := strings.Repeat("a", 64), strings.Repeat("b", 64), strings.Repeat("c", 64)
108 svc, err := New(ctx, st, owner)
109 if err != nil {
110 t.Fatal(err)
111 }
112 p := policy.Defaults(owner)
113 p.MemberInvites = policy.MemberInvites{Depth: 1, Quota: 1}
114 svc.ConfigurePolicy(func() policy.Policy { return p })
115 if _, err := svc.IssueNIP43Invite(ctx, stranger); err == nil {
116 t.Fatal("stranger received invite")
117 }
118 if _, err := svc.Execute(ctx, owner, "setmember", raws(member, "member", "member")); err != nil {
119 t.Fatal(err)
120 }
121 invite, err := svc.IssueNIP43Invite(ctx, member)
122 if err != nil {
123 t.Fatal(err)
124 }
125 if invite.Code == "" || invite.ExpiresAt <= invite.CreatedAt {
126 t.Fatalf("invalid invite: %+v", invite)
127 }
128}
129
130func TestNIP56PubkeyReportDoesNotHideEventID(t *testing.T) {
131 ctx := context.Background()
132 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")
133 if err != nil {
134 t.Fatal(err)
135 }
136 defer st.Close()
137 owner, reporter, target := strings.Repeat("a", 64), strings.Repeat("b", 64), strings.Repeat("c", 64)
138 svc, err := New(ctx, st, owner)
139 if err != nil {
140 t.Fatal(err)
141 }
142 if err := svc.SubmitReportTarget(ctx, reporter, target, "pubkey", "spam", "bad actor", 1); err != nil {
143 t.Fatal(err)
144 }
145 var hidden int
146 if err := st.DB().QueryRowContext(ctx, `SELECT count(*) FROM hidden_events WHERE id=?`, target).Scan(&hidden); err != nil {
147 t.Fatal(err)
148 }
149 if hidden != 0 {
150 t.Fatal("pubkey report was treated as an event hide")
151 }
152}
153
154func TestMembershipProjectionIntentRollsBackWithTransaction(t *testing.T) {
155 ctx := context.Background()
156 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")
157 if err != nil {
158 t.Fatal(err)
159 }
160 defer st.Close()
161 owner := strings.Repeat("a", 64)
162 if _, err := New(ctx, st, owner); err != nil {
163 t.Fatal(err)
164 }
165 var before int
166 if err := st.DB().QueryRow(`SELECT count(*) FROM work_intents WHERE kind='records-projection'`).Scan(&before); err != nil {
167 t.Fatal(err)
168 }
169 member := strings.Repeat("b", 64)
170 if err := st.WithTx(ctx, func(tx *sql.Tx) error {
171 if _, err := tx.Exec(`INSERT INTO community_members(pubkey,role,created_at) VALUES(?,?,?)`, member, "member", 1); err != nil {
172 return err
173 }
174 return errors.New("force rollback")
175 }); err == nil {
176 t.Fatal("expected rollback")
177 }
178 var n int
179 if err := st.DB().QueryRow(`SELECT count(*) FROM community_members WHERE pubkey=?`, member).Scan(&n); err != nil {
180 t.Fatal(err)
181 }
182 if n != 0 {
183 t.Fatal("membership committed despite rollback")
184 }
185 if err := st.DB().QueryRow(`SELECT count(*) FROM work_intents WHERE kind='records-projection'`).Scan(&n); err != nil {
186 t.Fatal(err)
187 }
188 if n != before {
189 t.Fatal("projection intent committed despite rollback")
190 }
191}
192
193func TestTransactionalMembershipPersistenceRollback(t *testing.T) {
194 ctx := context.Background()
195 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")
196 if err != nil {
197 t.Fatal(err)
198 }
199 defer st.Close()
200 owner, stranger := strings.Repeat("a", 64), strings.Repeat("b", 64)
201 svc, err := New(ctx, st, owner)
202 if err != nil {
203 t.Fatal(err)
204 }
205 p := policy.Defaults(owner)
206 svc.ConfigurePolicy(func() policy.Policy { return p })
207 _, err = svc.HandleMembershipEventTx(ctx, event.Event{ID: strings.Repeat("c", 64), PubKey: stranger, Kind: event.KIND_JOIN}, func(*sql.Tx) error { return errors.New("event write failed") })
208 if err == nil {
209 t.Fatal("expected event persistence failure")
210 }
211 if role, _ := svc.Role(ctx, stranger); role != "" {
212 t.Fatal("membership committed after event failure")
213 }
214 var n int
215 if err := st.DB().QueryRow(`SELECT count(*) FROM work_intents WHERE event_id=?`, strings.Repeat("c", 64)).Scan(&n); err != nil {
216 t.Fatal(err)
217 }
218 if n != 0 {
219 t.Fatal("projection intent committed after event failure")
220 }
221}
222
223func TestTransferIsAtomicAndDemotesOldOwner(t *testing.T) {
224 ctx := context.Background()
225 st, err := storage.Open(ctx, t.TempDir()+"/relay.db")
226 if err != nil {
227 t.Fatal(err)
228 }
229 defer st.Close()
230 owner, member := strings.Repeat("a", 64), strings.Repeat("b", 64)
231 svc, err := New(ctx, st, owner)
232 if err != nil {
233 t.Fatal(err)
234 }
235 if _, err := svc.Execute(ctx, owner, "setmember", raws(member, "new", "member")); err != nil {
236 t.Fatal(err)
237 }
238 if _, err := svc.Execute(ctx, owner, "transferowner", raws(member)); err != nil {
239 t.Fatal(err)
240 }
241 if role, _ := svc.Role(ctx, owner); role != "moderator" {
242 t.Fatalf("old owner role=%q", role)
243 }
244 if role, _ := svc.Role(ctx, member); role != "owner" {
245 t.Fatalf("new owner role=%q", role)
246 }
247}
248
249func raws(values ...any) []json.RawMessage {
250 out := make([]json.RawMessage, len(values))
251 for i, value := range values {
252 out[i], _ = json.Marshal(value)
253 }
254 return out
255}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs