handle_policy_config_test.go raw
1 package app
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "sync"
8 "testing"
9 "time"
10
11 "github.com/adrg/xdg"
12 "next.orly.dev/pkg/nostr/encoders/event"
13 "next.orly.dev/pkg/nostr/encoders/hex"
14 "next.orly.dev/pkg/nostr/encoders/kind"
15 "next.orly.dev/pkg/nostr/encoders/tag"
16 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
17 "next.orly.dev/app/config"
18 "next.orly.dev/pkg/acl"
19 "next.orly.dev/pkg/database"
20 "next.orly.dev/pkg/policy"
21 "next.orly.dev/pkg/protocol/publish"
22 )
23
24 // setupPolicyTestListener creates a test listener with policy system enabled
25 func setupPolicyTestListener(t *testing.T, policyAdminHex string) (*Listener, *database.D, func()) {
26 tempDir, err := os.MkdirTemp("", "policy_handler_test_*")
27 if err != nil {
28 t.Fatalf("failed to create temp dir: %v", err)
29 }
30
31 // Use a unique app name per test to avoid conflicts
32 appName := "test-policy-" + filepath.Base(tempDir)
33
34 // Create the XDG config directory and default policy file BEFORE creating the policy manager
35 configDir := filepath.Join(xdg.ConfigHome, appName)
36 if err := os.MkdirAll(configDir, 0755); err != nil {
37 os.RemoveAll(tempDir)
38 t.Fatalf("failed to create config dir: %v", err)
39 }
40
41 // Create initial policy file with admin if provided
42 var initialPolicy []byte
43 if policyAdminHex != "" {
44 initialPolicy = []byte(`{
45 "default_policy": "allow",
46 "policy_admins": ["` + policyAdminHex + `"],
47 "policy_follow_whitelist_enabled": true
48 }`)
49 } else {
50 initialPolicy = []byte(`{"default_policy": "allow"}`)
51 }
52 policyPath := filepath.Join(configDir, "policy.json")
53 if err := os.WriteFile(policyPath, initialPolicy, 0644); err != nil {
54 os.RemoveAll(tempDir)
55 os.RemoveAll(configDir)
56 t.Fatalf("failed to write policy file: %v", err)
57 }
58
59 ctx, cancel := context.WithCancel(context.Background())
60 db, err := database.New(ctx, cancel, tempDir, "info")
61 if err != nil {
62 os.RemoveAll(tempDir)
63 os.RemoveAll(configDir)
64 t.Fatalf("failed to open database: %v", err)
65 }
66
67 cfg := &config.C{
68 PolicyEnabled: true,
69 RelayURL: "wss://test.relay",
70 Listen: "localhost",
71 Port: 3334,
72 ACLMode: "none",
73 AppName: appName,
74 }
75
76 // Create policy manager - now config file exists at XDG path
77 policyManager := policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled, "")
78
79 server := &Server{
80 Ctx: ctx,
81 Config: cfg,
82 DB: db,
83 publishers: publish.New(NewPublisher(ctx)),
84 policyManager: policyManager,
85 cfg: cfg,
86 db: db,
87 messagePauseMutex: sync.RWMutex{},
88 }
89
90 // Configure ACL registry
91 acl.Registry.SetMode(cfg.ACLMode)
92 if err = acl.Registry.Configure(cfg, db, ctx); err != nil {
93 db.Close()
94 os.RemoveAll(tempDir)
95 os.RemoveAll(configDir)
96 t.Fatalf("failed to configure ACL: %v", err)
97 }
98
99 listener := &Listener{
100 Server: server,
101 ctx: ctx,
102 writeChan: make(chan publish.WriteRequest, 100),
103 writeDone: make(chan struct{}),
104 messageQueue: make(chan messageRequest, 100),
105 processingDone: make(chan struct{}),
106 subscriptions: make(map[string]context.CancelFunc),
107 }
108
109 // Start write worker and message processor
110 go listener.writeWorker()
111 go listener.messageProcessor()
112
113 cleanup := func() {
114 close(listener.writeChan)
115 <-listener.writeDone
116 close(listener.messageQueue)
117 <-listener.processingDone
118 db.Close()
119 os.RemoveAll(tempDir)
120 os.RemoveAll(configDir)
121 }
122
123 return listener, db, cleanup
124 }
125
126 // createPolicyConfigEvent creates a kind 12345 policy config event
127 func createPolicyConfigEvent(t *testing.T, signer *p8k.Signer, policyJSON string) *event.E {
128 ev := event.New()
129 ev.CreatedAt = time.Now().Unix()
130 ev.Kind = kind.PolicyConfig.K
131 ev.Content = []byte(policyJSON)
132 ev.Tags = tag.NewS()
133
134 if err := ev.Sign(signer); err != nil {
135 t.Fatalf("Failed to sign event: %v", err)
136 }
137
138 return ev
139 }
140
141 // TestHandlePolicyConfigUpdate_ValidAdmin tests policy update from valid admin
142 // Policy admins can extend rules but cannot modify protected fields (owners, policy_admins)
143 func TestHandlePolicyConfigUpdate_ValidAdmin(t *testing.T) {
144 // Create admin signer
145 adminSigner := p8k.MustNew()
146 if err := adminSigner.Generate(); err != nil {
147 t.Fatalf("Failed to generate admin keypair: %v", err)
148 }
149 adminHex := hex.Enc(adminSigner.Pub())
150
151 listener, _, cleanup := setupPolicyTestListener(t, adminHex)
152 defer cleanup()
153
154 // Create valid policy update event that ONLY extends, doesn't modify protected fields
155 // Note: policy_admins must stay the same (policy admins cannot change this field)
156 newPolicyJSON := `{
157 "default_policy": "allow",
158 "policy_admins": ["` + adminHex + `"],
159 "kind": {"whitelist": [1, 3, 7]}
160 }`
161
162 ev := createPolicyConfigEvent(t, adminSigner, newPolicyJSON)
163
164 // Handle the event
165 err := listener.HandlePolicyConfigUpdate(ev)
166 if err != nil {
167 t.Errorf("Expected success but got error: %v", err)
168 }
169
170 // Verify policy was updated (kind whitelist was extended)
171 // Note: default_policy should still be "allow" from original
172 if listener.policyManager.DefaultPolicy != "allow" {
173 t.Errorf("Policy was not updated correctly, default_policy = %q, expected 'allow'",
174 listener.policyManager.DefaultPolicy)
175 }
176 }
177
178 // TestHandlePolicyConfigUpdate_NonAdmin tests policy update rejection from non-admin
179 func TestHandlePolicyConfigUpdate_NonAdmin(t *testing.T) {
180 // Create admin signer
181 adminSigner := p8k.MustNew()
182 if err := adminSigner.Generate(); err != nil {
183 t.Fatalf("Failed to generate admin keypair: %v", err)
184 }
185 adminHex := hex.Enc(adminSigner.Pub())
186
187 // Create non-admin signer
188 nonAdminSigner := p8k.MustNew()
189 if err := nonAdminSigner.Generate(); err != nil {
190 t.Fatalf("Failed to generate non-admin keypair: %v", err)
191 }
192
193 listener, _, cleanup := setupPolicyTestListener(t, adminHex)
194 defer cleanup()
195
196 // Create policy update event from non-admin
197 newPolicyJSON := `{"default_policy": "deny"}`
198 ev := createPolicyConfigEvent(t, nonAdminSigner, newPolicyJSON)
199
200 // Handle the event - should be rejected
201 err := listener.HandlePolicyConfigUpdate(ev)
202 if err == nil {
203 t.Error("Expected error for non-admin update but got none")
204 }
205
206 // Verify policy was NOT updated
207 if listener.policyManager.DefaultPolicy != "allow" {
208 t.Error("Policy should not have been updated by non-admin")
209 }
210 }
211
212 // TestHandlePolicyConfigUpdate_InvalidJSON tests rejection of invalid JSON
213 func TestHandlePolicyConfigUpdate_InvalidJSON(t *testing.T) {
214 adminSigner := p8k.MustNew()
215 if err := adminSigner.Generate(); err != nil {
216 t.Fatalf("Failed to generate admin keypair: %v", err)
217 }
218 adminHex := hex.Enc(adminSigner.Pub())
219
220 listener, _, cleanup := setupPolicyTestListener(t, adminHex)
221 defer cleanup()
222
223 // Create event with invalid JSON
224 ev := createPolicyConfigEvent(t, adminSigner, `{"invalid json`)
225
226 err := listener.HandlePolicyConfigUpdate(ev)
227 if err == nil {
228 t.Error("Expected error for invalid JSON but got none")
229 }
230
231 // Policy should remain unchanged
232 if listener.policyManager.DefaultPolicy != "allow" {
233 t.Error("Policy should not have been updated with invalid JSON")
234 }
235 }
236
237 // TestHandlePolicyConfigUpdate_InvalidPubkey tests rejection of invalid admin pubkeys
238 func TestHandlePolicyConfigUpdate_InvalidPubkey(t *testing.T) {
239 adminSigner := p8k.MustNew()
240 if err := adminSigner.Generate(); err != nil {
241 t.Fatalf("Failed to generate admin keypair: %v", err)
242 }
243 adminHex := hex.Enc(adminSigner.Pub())
244
245 listener, _, cleanup := setupPolicyTestListener(t, adminHex)
246 defer cleanup()
247
248 // Try to update with invalid admin pubkey
249 invalidPolicyJSON := `{
250 "default_policy": "deny",
251 "policy_admins": ["not-a-valid-pubkey"]
252 }`
253 ev := createPolicyConfigEvent(t, adminSigner, invalidPolicyJSON)
254
255 err := listener.HandlePolicyConfigUpdate(ev)
256 if err == nil {
257 t.Error("Expected error for invalid admin pubkey but got none")
258 }
259
260 // Policy should remain unchanged
261 if listener.policyManager.DefaultPolicy != "allow" {
262 t.Error("Policy should not have been updated with invalid admin pubkey")
263 }
264 }
265
266 // TestHandlePolicyConfigUpdate_PolicyAdminCannotModifyProtectedFields tests that policy admins
267 // cannot modify the owners or policy_admins fields (these are protected, owner-only fields)
268 func TestHandlePolicyConfigUpdate_PolicyAdminCannotModifyProtectedFields(t *testing.T) {
269 adminSigner := p8k.MustNew()
270 if err := adminSigner.Generate(); err != nil {
271 t.Fatalf("Failed to generate admin keypair: %v", err)
272 }
273 adminHex := hex.Enc(adminSigner.Pub())
274
275 // Create second admin
276 admin2Hex := "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
277
278 listener, _, cleanup := setupPolicyTestListener(t, adminHex)
279 defer cleanup()
280
281 // Try to add second admin (policy_admins is a protected field)
282 newPolicyJSON := `{
283 "default_policy": "allow",
284 "policy_admins": ["` + adminHex + `", "` + admin2Hex + `"]
285 }`
286 ev := createPolicyConfigEvent(t, adminSigner, newPolicyJSON)
287
288 // This should FAIL because policy admins cannot modify the policy_admins field
289 err := listener.HandlePolicyConfigUpdate(ev)
290 if err == nil {
291 t.Error("Expected error when policy admin tries to modify policy_admins (protected field)")
292 }
293
294 // Second admin should NOT be in the list since update was rejected
295 admin2Bin, _ := hex.Dec(admin2Hex)
296 if listener.policyManager.IsPolicyAdmin(admin2Bin) {
297 t.Error("Second admin should NOT have been added - policy_admins is protected")
298 }
299 }
300
301 // TestHandlePolicyAdminFollowListUpdate tests follow list update from admin
302 func TestHandlePolicyAdminFollowListUpdate(t *testing.T) {
303 adminSigner := p8k.MustNew()
304 if err := adminSigner.Generate(); err != nil {
305 t.Fatalf("Failed to generate admin keypair: %v", err)
306 }
307 adminHex := hex.Enc(adminSigner.Pub())
308
309 listener, db, cleanup := setupPolicyTestListener(t, adminHex)
310 defer cleanup()
311
312 // Create a kind 3 follow list event from admin
313 ev := event.New()
314 ev.CreatedAt = time.Now().Unix()
315 ev.Kind = kind.FollowList.K
316 ev.Content = []byte("")
317 ev.Tags = tag.NewS()
318
319 // Add some follows
320 follow1Hex := "1111111111111111111111111111111111111111111111111111111111111111"
321 follow2Hex := "2222222222222222222222222222222222222222222222222222222222222222"
322 ev.Tags.Append(tag.NewFromAny("p", follow1Hex))
323 ev.Tags.Append(tag.NewFromAny("p", follow2Hex))
324
325 if err := ev.Sign(adminSigner); err != nil {
326 t.Fatalf("Failed to sign event: %v", err)
327 }
328
329 // Save the event to database first
330 if _, err := db.SaveEvent(listener.ctx, ev); err != nil {
331 t.Fatalf("Failed to save follow list event: %v", err)
332 }
333
334 // Handle the follow list update
335 err := listener.HandlePolicyAdminFollowListUpdate(ev)
336 if err != nil {
337 t.Errorf("Expected success but got error: %v", err)
338 }
339
340 // Verify follows were added
341 follow1Bin, _ := hex.Dec(follow1Hex)
342 follow2Bin, _ := hex.Dec(follow2Hex)
343
344 if !listener.policyManager.IsPolicyFollow(follow1Bin) {
345 t.Error("Follow 1 should have been added to policy follows")
346 }
347 if !listener.policyManager.IsPolicyFollow(follow2Bin) {
348 t.Error("Follow 2 should have been added to policy follows")
349 }
350 }
351
352 // TestIsPolicyAdminFollowListEvent tests detection of admin follow list events
353 func TestIsPolicyAdminFollowListEvent(t *testing.T) {
354 adminSigner := p8k.MustNew()
355 if err := adminSigner.Generate(); err != nil {
356 t.Fatalf("Failed to generate admin keypair: %v", err)
357 }
358 adminHex := hex.Enc(adminSigner.Pub())
359
360 nonAdminSigner := p8k.MustNew()
361 if err := nonAdminSigner.Generate(); err != nil {
362 t.Fatalf("Failed to generate non-admin keypair: %v", err)
363 }
364
365 listener, _, cleanup := setupPolicyTestListener(t, adminHex)
366 defer cleanup()
367
368 // Test admin's kind 3 event
369 adminFollowEv := event.New()
370 adminFollowEv.Kind = kind.FollowList.K
371 adminFollowEv.Tags = tag.NewS()
372 if err := adminFollowEv.Sign(adminSigner); err != nil {
373 t.Fatalf("Failed to sign event: %v", err)
374 }
375
376 if !listener.IsPolicyAdminFollowListEvent(adminFollowEv) {
377 t.Error("Should detect admin's follow list event")
378 }
379
380 // Test non-admin's kind 3 event
381 nonAdminFollowEv := event.New()
382 nonAdminFollowEv.Kind = kind.FollowList.K
383 nonAdminFollowEv.Tags = tag.NewS()
384 if err := nonAdminFollowEv.Sign(nonAdminSigner); err != nil {
385 t.Fatalf("Failed to sign event: %v", err)
386 }
387
388 if listener.IsPolicyAdminFollowListEvent(nonAdminFollowEv) {
389 t.Error("Should not detect non-admin's follow list event")
390 }
391
392 // Test admin's non-kind-3 event
393 adminOtherEv := event.New()
394 adminOtherEv.Kind = 1 // Kind 1, not follow list
395 adminOtherEv.Tags = tag.NewS()
396 if err := adminOtherEv.Sign(adminSigner); err != nil {
397 t.Fatalf("Failed to sign event: %v", err)
398 }
399
400 if listener.IsPolicyAdminFollowListEvent(adminOtherEv) {
401 t.Error("Should not detect admin's non-follow-list event")
402 }
403 }
404
405 // TestIsPolicyConfigEvent tests detection of policy config events
406 func TestIsPolicyConfigEvent(t *testing.T) {
407 signer := p8k.MustNew()
408 if err := signer.Generate(); err != nil {
409 t.Fatalf("Failed to generate keypair: %v", err)
410 }
411
412 // Kind 12345 event
413 policyEv := event.New()
414 policyEv.Kind = kind.PolicyConfig.K
415 policyEv.Tags = tag.NewS()
416 if err := policyEv.Sign(signer); err != nil {
417 t.Fatalf("Failed to sign event: %v", err)
418 }
419
420 if !IsPolicyConfigEvent(policyEv) {
421 t.Error("Should detect kind 12345 as policy config event")
422 }
423
424 // Non-policy event
425 otherEv := event.New()
426 otherEv.Kind = 1
427 otherEv.Tags = tag.NewS()
428 if err := otherEv.Sign(signer); err != nil {
429 t.Fatalf("Failed to sign event: %v", err)
430 }
431
432 if IsPolicyConfigEvent(otherEv) {
433 t.Error("Should not detect kind 1 as policy config event")
434 }
435 }
436
437 // TestMessageProcessingPauseDuringPolicyUpdate tests that message processing is paused
438 func TestMessageProcessingPauseDuringPolicyUpdate(t *testing.T) {
439 adminSigner := p8k.MustNew()
440 if err := adminSigner.Generate(); err != nil {
441 t.Fatalf("Failed to generate admin keypair: %v", err)
442 }
443 adminHex := hex.Enc(adminSigner.Pub())
444
445 listener, _, cleanup := setupPolicyTestListener(t, adminHex)
446 defer cleanup()
447
448 // Track if pause was called
449 pauseCalled := false
450 resumeCalled := false
451
452 // We can't easily mock the mutex, but we can verify the policy update succeeds
453 // which implies the pause/resume cycle completed
454 // Note: policy_admins must stay the same (protected field)
455 newPolicyJSON := `{
456 "default_policy": "allow",
457 "policy_admins": ["` + adminHex + `"],
458 "kind": {"whitelist": [1, 3, 5, 7]}
459 }`
460 ev := createPolicyConfigEvent(t, adminSigner, newPolicyJSON)
461
462 err := listener.HandlePolicyConfigUpdate(ev)
463 if err != nil {
464 t.Errorf("Policy update failed: %v", err)
465 }
466
467 // If we got here without deadlock, the pause/resume worked
468 _ = pauseCalled
469 _ = resumeCalled
470
471 // Verify policy was actually updated (kind whitelist was extended)
472 if listener.policyManager.DefaultPolicy != "allow" {
473 t.Error("Policy should have been updated")
474 }
475 }
476