tinyrelayfile
tinyrelay/internal/records/records_test.go/

internal/records/records_test.go raw
1package records
2
3import (
4 "context"
5 "path/filepath"
6 "testing"
7
8 "github.com/FelineStateMachine/tinyrelay/internal/communityread"
9 "github.com/FelineStateMachine/tinyrelay/internal/event"
10 "github.com/FelineStateMachine/tinyrelay/internal/policy"
11 "github.com/FelineStateMachine/tinyrelay/internal/storage"
12)
13
14type emptyCommunityReader struct{}
15
16func (emptyCommunityReader) Members(context.Context) ([]communityread.Member, error) {
17 return nil, nil
18}
19
20func (emptyCommunityReader) ModerationCounts(context.Context, int64) (communityread.ModerationCounts, error) {
21 return communityread.ModerationCounts{}, nil
22}
23
24func (emptyCommunityReader) MemberStatus(context.Context, string) (bool, int, error) {
25 return false, 0, nil
26}
27
28func testStore(t *testing.T) (*storage.Store, context.Context) {
29 t.Helper()
30 ctx := context.Background()
31 s, err := storage.Open(ctx, filepath.Join(t.TempDir(), "tenant.db"))
32 if err != nil {
33 t.Fatal(err)
34 }
35 t.Cleanup(func() { _ = s.Close() })
36 return s, ctx
37}
38
39func TestIdentitySurvivesRestartAndClockIsMonotonic(t *testing.T) {
40 ctx := context.Background()
41 path := filepath.Join(t.TempDir(), "tenant.db")
42 s, err := storage.Open(ctx, path)
43 if err != nil {
44 t.Fatal(err)
45 }
46 p := policy.Defaults("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
47 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, RelayURL: "wss://relay.example"})
48 if err != nil {
49 t.Fatal(err)
50 }
51 first, err := r.Generate(ctx, event.KIND_APP_DATA, [][]string{{"d", "x"}}, "one", 100)
52 if err != nil {
53 t.Fatal(err)
54 }
55 if err := event.Validate(first); err != nil {
56 t.Fatal(err)
57 }
58 key := r.PublicKey()
59 if err := s.Close(); err != nil {
60 t.Fatal(err)
61 }
62 s2, err := storage.Open(ctx, path)
63 if err != nil {
64 t.Fatal(err)
65 }
66 r2, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s2, Policy: func() policy.Policy { return p }, RelayURL: "wss://relay.example"})
67 if err != nil {
68 t.Fatal(err)
69 }
70 if r2.PublicKey() != key {
71 t.Fatalf("identity changed across restart: %s != %s", r2.PublicKey(), key)
72 }
73 second, err := r2.Generate(ctx, event.KIND_APP_DATA, [][]string{{"d", "x"}}, "two", 1)
74 if err != nil {
75 t.Fatal(err)
76 }
77 if second.CreatedAt <= first.CreatedAt {
78 t.Fatalf("clock regressed: %d <= %d", second.CreatedAt, first.CreatedAt)
79 }
80 if err := event.Validate(second); err != nil {
81 t.Fatal(err)
82 }
83 _ = s2.Close()
84}
85
86func TestIdentityContinuityAndViewPrivacy(t *testing.T) {
87 s, ctx := testStore(t)
88 p := policy.Defaults("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
89 p.Views = map[string]string{"profiles": "daily"}
90 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
91 p.Owner = owner
92 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, RelayURL: "wss://relay.example"})
93 if err != nil {
94 t.Fatal(err)
95 }
96 key := r.PublicKey()
97 p.Reads = "members"
98 if _, err := r.Views(ctx, policy.Access{}, 100); err == nil {
99 t.Fatal("private view leaked to guest")
100 }
101 if _, err := r.Views(ctx, policy.Access{Owner: true}, 100); err != nil {
102 t.Fatal(err)
103 }
104 if r.PublicKey() != key {
105 t.Fatal("public identity changed")
106 }
107}
108
109func TestDirectoryPrivateViewsRequireMembersAndDoNotTick(t *testing.T) {
110 s, ctx := testStore(t)
111 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
112 p := policy.Defaults(owner)
113 p.DirectoryPublic = false
114 p.Views = map[string]string{"profiles": "daily"}
115 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, RelayURL: "wss://relay.example"})
116 if err != nil {
117 t.Fatal(err)
118 }
119 if _, err := r.Views(ctx, policy.Access{}, 100); err == nil {
120 t.Fatal("private directory view leaked to guest")
121 }
122 if err := r.Tick(ctx, 100); err != nil {
123 t.Fatal(err)
124 }
125 var count int
126 if err := s.DB().QueryRow(`SELECT count(*) FROM events WHERE kind=?`, event.KIND_VIEW).Scan(&count); err != nil {
127 t.Fatal(err)
128 }
129 if count != 0 {
130 t.Fatalf("private tick stored %d views", count)
131 }
132}
133
134func TestViewAudienceAndMemberFoldAreNotStored(t *testing.T) {
135 s, ctx := testStore(t)
136 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
137 p := policy.Defaults(owner)
138 p.DirectoryPublic = false
139 p.Reads = "open"
140 p.Views = map[string]string{"profiles": "daily", "relays": "off", "calendar": "daily", "moderation": "off", "articles": "off", "zaps": "off", "presence": "off"}
141 generated := []event.Event{}
142 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, RelayURL: "wss://relay.example", OnGenerated: func(context.Context, event.Event) error {
143 generated = append(generated, event.Event{})
144 return nil
145 }})
146 if err != nil {
147 t.Fatal(err)
148 }
149 // Calendar is read-public even when the directory is private, while the
150 // profile fold is member-only and must be signed without persistence.
151 views, err := r.Views(ctx, policy.Access{}, 100)
152 if err == nil {
153 t.Fatal("guest received a member-only profile view")
154 }
155 if err := r.Tick(ctx, 100); err != nil {
156 t.Fatal(err)
157 }
158 if len(generated) != 1 {
159 t.Fatalf("generated callbacks=%d, want public calendar only", len(generated))
160 }
161 _ = views
162}
163
164func TestPresenceIsSignedWithoutGeneratedCallback(t *testing.T) {
165 s, ctx := testStore(t)
166 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
167 p := policy.Defaults(owner)
168 called := 0
169 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, OnGenerated: func(context.Context, event.Event) error {
170 called++
171 return nil
172 }})
173 if err != nil {
174 t.Fatal(err)
175 }
176 if _, err := r.Presence(ctx, policy.Access{Owner: true}, 100); err != nil {
177 t.Fatal(err)
178 }
179 if called != 0 {
180 t.Fatalf("presence invoked generated callback %d times", called)
181 }
182}
183
184func TestProfileFoldUsesMembersAndNIP05(t *testing.T) {
185 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
186 member := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
187 e := event.Event{PubKey: owner, Kind: event.KIND_PROFILE, CreatedAt: 1, Content: `{"name":"Owner"}`}
188 tags, _ := profileFold([]event.Event{e}, "wss://relay.example", map[string]string{owner: "", member: "alice"}, []string{owner, member})
189 if len(tags) != 2 || tags[1][1] != member || tags[1][4] != "alice@relay.example" {
190 t.Fatalf("profile fold=%v", tags)
191 }
192}
193
194func TestHourlyViewFingerprintSkipsUnchangedRun(t *testing.T) {
195 s, ctx := testStore(t)
196 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
197 p := policy.Defaults(owner)
198 p.Views = map[string]string{"profiles": "off", "relays": "off", "calendar": "hourly", "moderation": "off", "articles": "off", "zaps": "off", "presence": "off"}
199 generated := 0
200 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, OnGenerated: func(context.Context, event.Event) error {
201 generated++
202 return nil
203 }})
204 if err != nil {
205 t.Fatal(err)
206 }
207 if err := r.Tick(ctx, 100); err != nil {
208 t.Fatal(err)
209 }
210 if err := r.Tick(ctx, 3700); err != nil {
211 t.Fatal(err)
212 }
213 if generated != 1 {
214 t.Fatalf("unchanged hourly view generated %d records", generated)
215 }
216 runs, err := r.ViewRuns(ctx, "calendar")
217 if err != nil {
218 t.Fatal(err)
219 }
220 if len(runs) != 1 {
221 t.Fatalf("runs=%d, want only published run", len(runs))
222 }
223}
224
225func TestDefaultArticleWriteTriggerCoalescesDurably(t *testing.T) {
226 s, ctx := testStore(t)
227 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
228 p := policy.Defaults(owner)
229 for _, name := range []string{"profiles", "relays", "calendar", "moderation", "zaps", "presence"} {
230 p.Views[name] = "off"
231 }
232 generated := 0
233 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, OnGenerated: func(context.Context, event.Event) error { generated++; return nil }})
234 if err != nil {
235 t.Fatal(err)
236 }
237 if err := r.MarkView(ctx, "articles", 100); err != nil {
238 t.Fatal(err)
239 }
240 if err := r.MarkView(ctx, "articles", 101); err != nil {
241 t.Fatal(err)
242 }
243 if got, err := r.NextDue(ctx, 105); err != nil || got != 110 {
244 t.Fatalf("next due=%d err=%v", got, err)
245 }
246 if err := r.Tick(ctx, 105); err != nil {
247 t.Fatal(err)
248 }
249 if generated != 0 {
250 t.Fatal("published before coalescing window")
251 }
252 if err := r.Tick(ctx, 110); err != nil {
253 t.Fatal(err)
254 }
255 if generated != 1 {
256 t.Fatalf("generated=%d, want one coalesced article view", generated)
257 }
258 var dirty int64
259 if err := s.GetSetting(ctx, "records.view.articles.dirty", &dirty); err != nil || dirty != 0 {
260 t.Fatalf("dirty=%d err=%v", dirty, err)
261 }
262}
263
264func TestSuccessionWarningAndOwnerActivityAbort(t *testing.T) {
265 s, ctx := testStore(t)
266 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
267 heir := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
268 p := policy.Defaults(owner)
269 p.Notify.Succession = true
270 p.Succession = &policy.Succession{Heir: heir, AfterDays: 90}
271 transfers := 0
272 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, SetPolicy: func(next policy.Policy) error { p = next; return nil }, RelayURL: "wss://relay.example", OnTransfer: func(context.Context, string, string) error { transfers++; return nil }})
273 if err != nil {
274 t.Fatal(err)
275 }
276 base := int64(1000)
277 if err := r.Heartbeat(ctx, owner, base); err != nil {
278 t.Fatal(err)
279 }
280 if err := r.Tick(ctx, base+90*86400+29*86400); err != nil {
281 t.Fatal(err)
282 }
283 if transfers != 0 {
284 t.Fatal("transferred during warning period")
285 }
286 if err := r.Heartbeat(ctx, owner, base+90*86400+29*86400+1); err != nil {
287 t.Fatal(err)
288 }
289 if err := r.Tick(ctx, base+90*86400+31*86400); err != nil {
290 t.Fatal(err)
291 }
292 if transfers != 0 {
293 t.Fatal("owner heartbeat did not abort succession")
294 }
295 // Let the dead-man clock expire on a second relay instance: ownership must
296 // be changed through the policy callback, not merely reported as a hook.
297 p.Succession = &policy.Succession{Heir: heir, AfterDays: 90}
298 if err := r.Heartbeat(ctx, owner, base); err != nil {
299 t.Fatal(err)
300 }
301 if err := r.Tick(ctx, base+90*86400); err != nil {
302 t.Fatal(err)
303 }
304 if err := r.Tick(ctx, base+90*86400+30*86400+1); err != nil {
305 t.Fatal(err)
306 }
307 if p.Owner != heir || p.Succession != nil {
308 t.Fatalf("succession did not transfer ownership: %+v", p)
309 }
310}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs