new_fields_test.go raw
1 package policy
2
3 import (
4 "strconv"
5 "testing"
6 "time"
7
8 "next.orly.dev/pkg/nostr/encoders/event"
9 "next.orly.dev/pkg/nostr/encoders/hex"
10 "next.orly.dev/pkg/nostr/encoders/tag"
11 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
12 "next.orly.dev/pkg/lol/chk"
13 )
14
15 // =============================================================================
16 // parseDuration Tests (ISO-8601 format)
17 // =============================================================================
18
19 func TestParseDuration(t *testing.T) {
20 tests := []struct {
21 name string
22 input string
23 expected int64
24 expectError bool
25 }{
26 // Basic ISO-8601 time units (require T separator)
27 {name: "seconds only", input: "PT30S", expected: 30},
28 {name: "minutes only", input: "PT5M", expected: 300},
29 {name: "hours only", input: "PT2H", expected: 7200},
30
31 // Basic ISO-8601 date units
32 {name: "days only", input: "P1D", expected: 86400},
33 {name: "7 days", input: "P7D", expected: 604800},
34 {name: "30 days", input: "P30D", expected: 2592000},
35 {name: "weeks", input: "P1W", expected: 604800},
36 {name: "months", input: "P1M", expected: 2628000}, // ~30.44 days per library
37 {name: "years", input: "P1Y", expected: 31536000},
38
39 // Combinations
40 {name: "hours and minutes", input: "PT1H30M", expected: 5400},
41 {name: "days and hours", input: "P1DT12H", expected: 129600},
42 {name: "days hours minutes", input: "P1DT2H30M", expected: 95400},
43 {name: "full combo", input: "P1DT2H3M4S", expected: 93784},
44
45 // Edge cases
46 {name: "zero seconds", input: "PT0S", expected: 0},
47 {name: "large days", input: "P365D", expected: 31536000},
48 {name: "decimal values", input: "PT1.5H", expected: 5400},
49
50 // Whitespace handling
51 {name: "with leading space", input: " PT1H", expected: 3600},
52 {name: "with trailing space", input: "PT1H ", expected: 3600},
53
54 // Additional valid cases
55 {name: "leading zeros", input: "P007D", expected: 604800},
56 {name: "decimal days", input: "P0.5D", expected: 43200},
57 {name: "fractional minutes", input: "PT0.5M", expected: 30},
58 {name: "weeks with days", input: "P1W3D", expected: 864000},
59 {name: "zero everything", input: "P0DT0H0M0S", expected: 0},
60
61 // Errors (strict ISO-8601 via sosodev/duration library)
62 {name: "empty string", input: "", expectError: true},
63 {name: "whitespace only", input: " ", expectError: true},
64 {name: "missing P prefix", input: "1D", expectError: true},
65 {name: "invalid unit", input: "P5X", expectError: true},
66 {name: "H without T separator", input: "P1H", expectError: true},
67 {name: "S without T separator", input: "P30S", expectError: true},
68 {name: "D after T", input: "PT1D", expectError: true},
69 {name: "Y after T", input: "PT1Y", expectError: true},
70 {name: "W after T", input: "PT1W", expectError: true},
71 {name: "negative number", input: "P-5D", expectError: true},
72 {name: "unit without number", input: "PD", expectError: true},
73 {name: "unit without number time", input: "PTH", expectError: true},
74 }
75
76 for _, tt := range tests {
77 t.Run(tt.name, func(t *testing.T) {
78 result, err := parseDuration(tt.input)
79 if tt.expectError {
80 if err == nil {
81 t.Errorf("parseDuration(%q) expected error, got %d", tt.input, result)
82 }
83 return
84 }
85 if err != nil {
86 t.Errorf("parseDuration(%q) unexpected error: %v", tt.input, err)
87 return
88 }
89 if result != tt.expected {
90 t.Errorf("parseDuration(%q) = %d, expected %d", tt.input, result, tt.expected)
91 }
92 })
93 }
94 }
95
96 // =============================================================================
97 // MaxExpiryDuration Tests
98 // =============================================================================
99
100 func TestMaxExpiryDuration(t *testing.T) {
101 tests := []struct {
102 name string
103 maxExpiryDuration string
104 eventExpiry int64 // offset from created_at
105 hasExpiryTag bool
106 expectAllow bool
107 }{
108 {
109 name: "valid expiry within limit",
110 maxExpiryDuration: "PT1H",
111 eventExpiry: 1800, // 30 minutes
112 hasExpiryTag: true,
113 expectAllow: true,
114 },
115 {
116 name: "expiry at exact limit rejected",
117 maxExpiryDuration: "PT1H",
118 eventExpiry: 3600, // exactly 1 hour - >= means this is rejected
119 hasExpiryTag: true,
120 expectAllow: false,
121 },
122 {
123 name: "expiry exceeds limit",
124 maxExpiryDuration: "PT1H",
125 eventExpiry: 7200, // 2 hours
126 hasExpiryTag: true,
127 expectAllow: false,
128 },
129 {
130 name: "missing expiry tag when required",
131 maxExpiryDuration: "PT1H",
132 hasExpiryTag: false,
133 expectAllow: false,
134 },
135 {
136 name: "day-based duration",
137 maxExpiryDuration: "P7D",
138 eventExpiry: 86400, // 1 day
139 hasExpiryTag: true,
140 expectAllow: true,
141 },
142 {
143 name: "complex duration P1DT12H",
144 maxExpiryDuration: "P1DT12H",
145 eventExpiry: 86400, // 1 day (within 1.5 days)
146 hasExpiryTag: true,
147 expectAllow: true,
148 },
149 }
150
151 for _, tt := range tests {
152 t.Run(tt.name, func(t *testing.T) {
153 signer, pubkey := generateTestKeypair(t)
154
155 // Create policy with max_expiry_duration
156 policyJSON := []byte(`{
157 "default_policy": "allow",
158 "rules": {
159 "1": {
160 "description": "Test kind 1 with expiry",
161 "max_expiry_duration": "` + tt.maxExpiryDuration + `"
162 }
163 }
164 }`)
165
166 policy, err := New(policyJSON)
167 if err != nil {
168 t.Fatalf("Failed to create policy: %v", err)
169 }
170
171 // Create event
172 ev := createTestEventForNewFields(t, signer, "test content", 1)
173
174 // Add expiry tag if needed
175 if tt.hasExpiryTag {
176 expiryTs := ev.CreatedAt + tt.eventExpiry
177 addTag(ev, "expiration", string(rune(expiryTs)))
178 // Re-add as proper string
179 ev.Tags = tag.NewS()
180 addTagString(ev, "expiration", int64ToString(expiryTs))
181 if err := ev.Sign(signer); chk.E(err) {
182 t.Fatalf("Failed to re-sign event: %v", err)
183 }
184 }
185
186 allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
187 if err != nil {
188 t.Fatalf("CheckPolicy error: %v", err)
189 }
190
191 if allowed != tt.expectAllow {
192 t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
193 }
194 })
195 }
196 }
197
198 // Test MaxExpiryDuration takes precedence over MaxExpiry
199 func TestMaxExpiryDurationPrecedence(t *testing.T) {
200 signer, pubkey := generateTestKeypair(t)
201
202 // Policy where both max_expiry (seconds) and max_expiry_duration are set
203 // max_expiry_duration should take precedence
204 policyJSON := []byte(`{
205 "default_policy": "allow",
206 "rules": {
207 "1": {
208 "description": "Test precedence",
209 "max_expiry": 60,
210 "max_expiry_duration": "PT1H"
211 }
212 }
213 }`)
214
215 policy, err := New(policyJSON)
216 if err != nil {
217 t.Fatalf("Failed to create policy: %v", err)
218 }
219
220 // Create event with expiry at 30 minutes (would fail with max_expiry=60s, pass with PT1H)
221 ev := createTestEventForNewFields(t, signer, "test", 1)
222 expiryTs := ev.CreatedAt + 1800 // 30 minutes
223 addTagString(ev, "expiration", int64ToString(expiryTs))
224 if err := ev.Sign(signer); chk.E(err) {
225 t.Fatalf("Failed to sign: %v", err)
226 }
227
228 allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
229 if err != nil {
230 t.Fatalf("CheckPolicy error: %v", err)
231 }
232
233 if !allowed {
234 t.Error("MaxExpiryDuration should take precedence over MaxExpiry; expected allow")
235 }
236 }
237
238 // Test that max_expiry_duration only applies to writes, not reads
239 func TestMaxExpiryDurationWriteOnly(t *testing.T) {
240 signer, pubkey := generateTestKeypair(t)
241
242 // Policy with strict max_expiry_duration
243 policyJSON := []byte(`{
244 "default_policy": "allow",
245 "rules": {
246 "4": {
247 "description": "DM events with expiry",
248 "max_expiry_duration": "PT10M",
249 "privileged": true
250 }
251 }
252 }`)
253
254 policy, err := New(policyJSON)
255 if err != nil {
256 t.Fatalf("Failed to create policy: %v", err)
257 }
258
259 // Create event WITHOUT an expiry tag - this would fail write validation
260 // but should still be readable
261 ev := createTestEventForNewFields(t, signer, "test DM", 4)
262 if err := ev.Sign(signer); chk.E(err) {
263 t.Fatalf("Failed to sign: %v", err)
264 }
265
266 // Write should fail (no expiry tag when max_expiry_duration is set)
267 allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
268 if err != nil {
269 t.Fatalf("CheckPolicy write error: %v", err)
270 }
271 if allowed {
272 t.Error("Write should be denied for event without expiry tag when max_expiry_duration is set")
273 }
274
275 // Read should succeed (validation constraints don't apply to reads)
276 allowed, err = policy.CheckPolicy("read", ev, pubkey, "127.0.0.1")
277 if err != nil {
278 t.Fatalf("CheckPolicy read error: %v", err)
279 }
280 if !allowed {
281 t.Error("Read should be allowed - max_expiry_duration is write-only validation")
282 }
283
284 // Also test with an event that has expiry exceeding the limit
285 ev2 := createTestEventForNewFields(t, signer, "test DM 2", 4)
286 expiryTs := ev2.CreatedAt + 7200 // 2 hours - exceeds 10 minute limit
287 addTagString(ev2, "expiration", int64ToString(expiryTs))
288 if err := ev2.Sign(signer); chk.E(err) {
289 t.Fatalf("Failed to sign: %v", err)
290 }
291
292 // Write should fail (expiry exceeds limit)
293 allowed, err = policy.CheckPolicy("write", ev2, pubkey, "127.0.0.1")
294 if err != nil {
295 t.Fatalf("CheckPolicy write error: %v", err)
296 }
297 if allowed {
298 t.Error("Write should be denied for event with expiry exceeding max_expiry_duration")
299 }
300
301 // Read should still succeed
302 allowed, err = policy.CheckPolicy("read", ev2, pubkey, "127.0.0.1")
303 if err != nil {
304 t.Fatalf("CheckPolicy read error: %v", err)
305 }
306 if !allowed {
307 t.Error("Read should be allowed - max_expiry_duration is write-only validation")
308 }
309 }
310
311 // =============================================================================
312 // ProtectedRequired Tests
313 // =============================================================================
314
315 func TestProtectedRequired(t *testing.T) {
316 tests := []struct {
317 name string
318 hasProtectedTag bool
319 expectAllow bool
320 }{
321 {
322 name: "has protected tag",
323 hasProtectedTag: true,
324 expectAllow: true,
325 },
326 {
327 name: "missing protected tag",
328 hasProtectedTag: false,
329 expectAllow: false,
330 },
331 }
332
333 for _, tt := range tests {
334 t.Run(tt.name, func(t *testing.T) {
335 signer, pubkey := generateTestKeypair(t)
336
337 policyJSON := []byte(`{
338 "default_policy": "allow",
339 "rules": {
340 "1": {
341 "description": "Protected events only",
342 "protected_required": true
343 }
344 }
345 }`)
346
347 policy, err := New(policyJSON)
348 if err != nil {
349 t.Fatalf("Failed to create policy: %v", err)
350 }
351
352 ev := createTestEventForNewFields(t, signer, "test content", 1)
353
354 if tt.hasProtectedTag {
355 addTagString(ev, "-", "")
356 if err := ev.Sign(signer); chk.E(err) {
357 t.Fatalf("Failed to re-sign: %v", err)
358 }
359 }
360
361 allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
362 if err != nil {
363 t.Fatalf("CheckPolicy error: %v", err)
364 }
365
366 if allowed != tt.expectAllow {
367 t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
368 }
369 })
370 }
371 }
372
373 // =============================================================================
374 // IdentifierRegex Tests
375 // =============================================================================
376
377 func TestIdentifierRegex(t *testing.T) {
378 tests := []struct {
379 name string
380 regex string
381 dTagValue string
382 hasDTag bool
383 expectAllow bool
384 }{
385 {
386 name: "valid lowercase slug",
387 regex: "^[a-z0-9-]{1,64}$",
388 dTagValue: "my-article-slug",
389 hasDTag: true,
390 expectAllow: true,
391 },
392 {
393 name: "invalid - contains uppercase",
394 regex: "^[a-z0-9-]{1,64}$",
395 dTagValue: "My-Article-Slug",
396 hasDTag: true,
397 expectAllow: false,
398 },
399 {
400 name: "invalid - contains spaces",
401 regex: "^[a-z0-9-]{1,64}$",
402 dTagValue: "my article slug",
403 hasDTag: true,
404 expectAllow: false,
405 },
406 {
407 name: "invalid - too long",
408 regex: "^[a-z0-9-]{1,10}$",
409 dTagValue: "this-is-too-long",
410 hasDTag: true,
411 expectAllow: false,
412 },
413 {
414 name: "missing d tag when required",
415 regex: "^[a-z0-9-]{1,64}$",
416 hasDTag: false,
417 expectAllow: false,
418 },
419 {
420 name: "UUID pattern",
421 regex: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$",
422 dTagValue: "550e8400-e29b-41d4-a716-446655440000",
423 hasDTag: true,
424 expectAllow: true,
425 },
426 {
427 name: "alphanumeric only",
428 regex: "^[a-zA-Z0-9]+$",
429 dTagValue: "MyArticle123",
430 hasDTag: true,
431 expectAllow: true,
432 },
433 }
434
435 for _, tt := range tests {
436 t.Run(tt.name, func(t *testing.T) {
437 signer, pubkey := generateTestKeypair(t)
438
439 policyJSON := []byte(`{
440 "default_policy": "allow",
441 "rules": {
442 "30023": {
443 "description": "Long-form with identifier regex",
444 "identifier_regex": "` + tt.regex + `"
445 }
446 }
447 }`)
448
449 policy, err := New(policyJSON)
450 if err != nil {
451 t.Fatalf("Failed to create policy: %v", err)
452 }
453
454 ev := createTestEventForNewFields(t, signer, "test content", 30023)
455
456 if tt.hasDTag {
457 addTagString(ev, "d", tt.dTagValue)
458 if err := ev.Sign(signer); chk.E(err) {
459 t.Fatalf("Failed to re-sign: %v", err)
460 }
461 }
462
463 allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
464 if err != nil {
465 t.Fatalf("CheckPolicy error: %v", err)
466 }
467
468 if allowed != tt.expectAllow {
469 t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
470 }
471 })
472 }
473 }
474
475 // Test that IdentifierRegex validates multiple d tags
476 func TestIdentifierRegexMultipleDTags(t *testing.T) {
477 signer, pubkey := generateTestKeypair(t)
478
479 policyJSON := []byte(`{
480 "default_policy": "allow",
481 "rules": {
482 "30023": {
483 "description": "Test multiple d tags",
484 "identifier_regex": "^[a-z0-9-]+$"
485 }
486 }
487 }`)
488
489 policy, err := New(policyJSON)
490 if err != nil {
491 t.Fatalf("Failed to create policy: %v", err)
492 }
493
494 // Test with one valid and one invalid d tag
495 ev := createTestEventForNewFields(t, signer, "test", 30023)
496 addTagString(ev, "d", "valid-slug")
497 addTagString(ev, "d", "INVALID-SLUG") // uppercase should fail
498 if err := ev.Sign(signer); chk.E(err) {
499 t.Fatalf("Failed to sign: %v", err)
500 }
501
502 allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
503 if err != nil {
504 t.Fatalf("CheckPolicy error: %v", err)
505 }
506
507 if allowed {
508 t.Error("Should deny when any d tag fails regex validation")
509 }
510 }
511
512 // =============================================================================
513 // FollowsWhitelistAdmins Tests
514 // =============================================================================
515
516 func TestFollowsWhitelistAdmins(t *testing.T) {
517 // Generate admin and user keypairs
518 adminSigner, adminPubkey := generateTestKeypair(t)
519 userSigner, userPubkey := generateTestKeypair(t)
520 nonFollowSigner, nonFollowPubkey := generateTestKeypair(t)
521
522 adminHex := hex.Enc(adminPubkey)
523
524 policyJSON := []byte(`{
525 "default_policy": "deny",
526 "rules": {
527 "1": {
528 "description": "Only admin follows can write",
529 "follows_whitelist_admins": ["` + adminHex + `"]
530 }
531 }
532 }`)
533
534 policy, err := New(policyJSON)
535 if err != nil {
536 t.Fatalf("Failed to create policy: %v", err)
537 }
538
539 // Simulate loading admin's follows (user is followed by admin)
540 policy.UpdateRuleFollowsWhitelist(1, [][]byte{userPubkey})
541
542 tests := []struct {
543 name string
544 signer *p8k.Signer
545 pubkey []byte
546 expectAllow bool
547 }{
548 {
549 name: "followed user can write",
550 signer: userSigner,
551 pubkey: userPubkey,
552 expectAllow: true,
553 },
554 {
555 name: "non-followed user denied",
556 signer: nonFollowSigner,
557 pubkey: nonFollowPubkey,
558 expectAllow: false,
559 },
560 {
561 name: "admin can write (is in own follows conceptually)",
562 signer: adminSigner,
563 pubkey: adminPubkey,
564 expectAllow: false, // Admin not in follows list in this test
565 },
566 }
567
568 for _, tt := range tests {
569 t.Run(tt.name, func(t *testing.T) {
570 ev := createTestEventForNewFields(t, tt.signer, "test content", 1)
571
572 allowed, err := policy.CheckPolicy("write", ev, tt.pubkey, "127.0.0.1")
573 if err != nil {
574 t.Fatalf("CheckPolicy error: %v", err)
575 }
576
577 if allowed != tt.expectAllow {
578 t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
579 }
580 })
581 }
582 }
583
584 func TestGetAllFollowsWhitelistAdmins(t *testing.T) {
585 admin1 := "1111111111111111111111111111111111111111111111111111111111111111"
586 admin2 := "2222222222222222222222222222222222222222222222222222222222222222"
587 admin3 := "3333333333333333333333333333333333333333333333333333333333333333"
588
589 policyJSON := []byte(`{
590 "default_policy": "deny",
591 "global": {
592 "follows_whitelist_admins": ["` + admin1 + `"]
593 },
594 "rules": {
595 "1": {
596 "follows_whitelist_admins": ["` + admin2 + `"]
597 },
598 "30023": {
599 "follows_whitelist_admins": ["` + admin2 + `", "` + admin3 + `"]
600 }
601 }
602 }`)
603
604 policy, err := New(policyJSON)
605 if err != nil {
606 t.Fatalf("Failed to create policy: %v", err)
607 }
608
609 admins := policy.GetAllFollowsWhitelistAdmins()
610
611 // Should have 3 unique admins (admin2 is deduplicated)
612 if len(admins) != 3 {
613 t.Errorf("Expected 3 unique admins, got %d", len(admins))
614 }
615
616 // Check all admins are present
617 adminMap := make(map[string]bool)
618 for _, a := range admins {
619 adminMap[a] = true
620 }
621
622 for _, expected := range []string{admin1, admin2, admin3} {
623 if !adminMap[expected] {
624 t.Errorf("Missing admin %s", expected)
625 }
626 }
627 }
628
629 // =============================================================================
630 // Combinatorial Tests - New Fields with Existing Fields
631 // =============================================================================
632
633 // Test MaxExpiryDuration combined with SizeLimit
634 func TestMaxExpiryDurationWithSizeLimit(t *testing.T) {
635 signer, pubkey := generateTestKeypair(t)
636
637 policyJSON := []byte(`{
638 "default_policy": "allow",
639 "rules": {
640 "1": {
641 "max_expiry_duration": "PT1H",
642 "size_limit": 1000
643 }
644 }
645 }`)
646
647 policy, err := New(policyJSON)
648 if err != nil {
649 t.Fatalf("Failed to create policy: %v", err)
650 }
651
652 tests := []struct {
653 name string
654 contentSize int
655 hasExpiry bool
656 expiryOK bool
657 expectAllow bool
658 }{
659 {
660 name: "both constraints satisfied",
661 contentSize: 100,
662 hasExpiry: true,
663 expiryOK: true,
664 expectAllow: true,
665 },
666 {
667 name: "size exceeded",
668 contentSize: 2000,
669 hasExpiry: true,
670 expiryOK: true,
671 expectAllow: false,
672 },
673 {
674 name: "expiry exceeded",
675 contentSize: 100,
676 hasExpiry: true,
677 expiryOK: false,
678 expectAllow: false,
679 },
680 {
681 name: "missing expiry",
682 contentSize: 100,
683 hasExpiry: false,
684 expectAllow: false,
685 },
686 }
687
688 for _, tt := range tests {
689 t.Run(tt.name, func(t *testing.T) {
690 content := make([]byte, tt.contentSize)
691 for i := range content {
692 content[i] = 'a'
693 }
694
695 ev := event.New()
696 ev.CreatedAt = time.Now().Unix()
697 ev.Kind = 1
698 ev.Content = content
699 ev.Tags = tag.NewS()
700
701 if tt.hasExpiry {
702 var expiryOffset int64 = 1800 // 30 min (OK)
703 if !tt.expiryOK {
704 expiryOffset = 7200 // 2h (exceeds 1h limit)
705 }
706 addTagString(ev, "expiration", int64ToString(ev.CreatedAt+expiryOffset))
707 }
708
709 if err := ev.Sign(signer); chk.E(err) {
710 t.Fatalf("Failed to sign: %v", err)
711 }
712
713 allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
714 if err != nil {
715 t.Fatalf("CheckPolicy error: %v", err)
716 }
717
718 if allowed != tt.expectAllow {
719 t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
720 }
721 })
722 }
723 }
724
725 // Test ProtectedRequired combined with Privileged
726 func TestProtectedRequiredWithPrivileged(t *testing.T) {
727 authorSigner, authorPubkey := generateTestKeypair(t)
728 _, recipientPubkey := generateTestKeypair(t)
729 _, outsiderPubkey := generateTestKeypair(t)
730
731 policyJSON := []byte(`{
732 "default_policy": "deny",
733 "rules": {
734 "4": {
735 "description": "Encrypted DMs - protected and privileged",
736 "protected_required": true,
737 "privileged": true
738 }
739 }
740 }`)
741
742 policy, err := New(policyJSON)
743 if err != nil {
744 t.Fatalf("Failed to create policy: %v", err)
745 }
746
747 tests := []struct {
748 name string
749 hasProtected bool
750 readerPubkey []byte
751 isParty bool // is reader author or in p-tag
752 accessType string
753 expectAllow bool
754 }{
755 {
756 name: "author can read protected event",
757 hasProtected: true,
758 readerPubkey: authorPubkey,
759 isParty: true,
760 accessType: "read",
761 expectAllow: true,
762 },
763 {
764 name: "recipient in p-tag can read",
765 hasProtected: true,
766 readerPubkey: recipientPubkey,
767 isParty: true,
768 accessType: "read",
769 expectAllow: true,
770 },
771 {
772 name: "outsider cannot read privileged event",
773 hasProtected: true,
774 readerPubkey: outsiderPubkey,
775 isParty: false,
776 accessType: "read",
777 expectAllow: false,
778 },
779 {
780 name: "missing protected tag - write denied",
781 hasProtected: false,
782 readerPubkey: authorPubkey,
783 isParty: true,
784 accessType: "write",
785 expectAllow: false,
786 },
787 }
788
789 for _, tt := range tests {
790 t.Run(tt.name, func(t *testing.T) {
791 ev := createTestEventForNewFields(t, authorSigner, "encrypted content", 4)
792
793 // Add recipient to p-tag
794 addPTag(ev, recipientPubkey)
795
796 if tt.hasProtected {
797 addTagString(ev, "-", "")
798 }
799
800 if err := ev.Sign(authorSigner); chk.E(err) {
801 t.Fatalf("Failed to sign: %v", err)
802 }
803
804 allowed, err := policy.CheckPolicy(tt.accessType, ev, tt.readerPubkey, "127.0.0.1")
805 if err != nil {
806 t.Fatalf("CheckPolicy error: %v", err)
807 }
808
809 if allowed != tt.expectAllow {
810 t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
811 }
812 })
813 }
814 }
815
816 // Test IdentifierRegex combined with TagValidation
817 func TestIdentifierRegexWithTagValidation(t *testing.T) {
818 signer, pubkey := generateTestKeypair(t)
819
820 // Both identifier_regex (for d tag) and tag_validation (for t tag)
821 policyJSON := []byte(`{
822 "default_policy": "allow",
823 "rules": {
824 "30023": {
825 "identifier_regex": "^[a-z0-9-]+$",
826 "tag_validation": {
827 "t": "^[a-z]+$"
828 }
829 }
830 }
831 }`)
832
833 policy, err := New(policyJSON)
834 if err != nil {
835 t.Fatalf("Failed to create policy: %v", err)
836 }
837
838 tests := []struct {
839 name string
840 dTag string
841 tTag string
842 hasDTag bool
843 hasTTag bool
844 expectAllow bool
845 }{
846 {
847 name: "both tags valid",
848 dTag: "my-article",
849 tTag: "nostr",
850 hasDTag: true,
851 hasTTag: true,
852 expectAllow: true,
853 },
854 {
855 name: "d tag invalid",
856 dTag: "MY-ARTICLE",
857 tTag: "nostr",
858 hasDTag: true,
859 hasTTag: true,
860 expectAllow: false,
861 },
862 {
863 name: "t tag invalid",
864 dTag: "my-article",
865 tTag: "NOSTR123",
866 hasDTag: true,
867 hasTTag: true,
868 expectAllow: false,
869 },
870 {
871 name: "missing d tag",
872 tTag: "nostr",
873 hasDTag: false,
874 hasTTag: true,
875 expectAllow: false,
876 },
877 {
878 name: "missing t tag - allowed (tag_validation only validates present tags)",
879 dTag: "my-article",
880 hasDTag: true,
881 hasTTag: false,
882 expectAllow: true, // tag_validation doesn't require tags to exist, only validates if present
883 },
884 }
885
886 for _, tt := range tests {
887 t.Run(tt.name, func(t *testing.T) {
888 ev := createTestEventForNewFields(t, signer, "article content", 30023)
889
890 if tt.hasDTag {
891 addTagString(ev, "d", tt.dTag)
892 }
893 if tt.hasTTag {
894 addTagString(ev, "t", tt.tTag)
895 }
896
897 if err := ev.Sign(signer); chk.E(err) {
898 t.Fatalf("Failed to sign: %v", err)
899 }
900
901 allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
902 if err != nil {
903 t.Fatalf("CheckPolicy error: %v", err)
904 }
905
906 if allowed != tt.expectAllow {
907 t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
908 }
909 })
910 }
911 }
912
913 // Test FollowsWhitelistAdmins combined with WriteAllow
914 func TestFollowsWhitelistAdminsWithWriteAllow(t *testing.T) {
915 _, adminPubkey := generateTestKeypair(t)
916 followedSigner, followedPubkey := generateTestKeypair(t)
917 explicitSigner, explicitPubkey := generateTestKeypair(t)
918 _, deniedPubkey := generateTestKeypair(t)
919
920 adminHex := hex.Enc(adminPubkey)
921 explicitHex := hex.Enc(explicitPubkey)
922
923 // Both follows whitelist and explicit write_allow
924 policyJSON := []byte(`{
925 "default_policy": "deny",
926 "rules": {
927 "1": {
928 "follows_whitelist_admins": ["` + adminHex + `"],
929 "write_allow": ["` + explicitHex + `"]
930 }
931 }
932 }`)
933
934 policy, err := New(policyJSON)
935 if err != nil {
936 t.Fatalf("Failed to create policy: %v", err)
937 }
938
939 // Add followed user to whitelist
940 policy.UpdateRuleFollowsWhitelist(1, [][]byte{followedPubkey})
941
942 tests := []struct {
943 name string
944 signer *p8k.Signer
945 pubkey []byte
946 expectAllow bool
947 }{
948 {
949 name: "followed user allowed",
950 signer: followedSigner,
951 pubkey: followedPubkey,
952 expectAllow: true,
953 },
954 {
955 name: "explicit write_allow user allowed",
956 signer: explicitSigner,
957 pubkey: explicitPubkey,
958 expectAllow: true,
959 },
960 {
961 name: "user not in either list denied",
962 signer: p8k.MustNew(),
963 pubkey: deniedPubkey,
964 expectAllow: false,
965 },
966 }
967
968 for _, tt := range tests {
969 t.Run(tt.name, func(t *testing.T) {
970 // Generate if needed
971 if tt.signer.Pub() == nil {
972 _ = tt.signer.Generate()
973 }
974
975 ev := createTestEventForNewFields(t, tt.signer, "test", 1)
976
977 allowed, err := policy.CheckPolicy("write", ev, tt.pubkey, "127.0.0.1")
978 if err != nil {
979 t.Fatalf("CheckPolicy error: %v", err)
980 }
981
982 if allowed != tt.expectAllow {
983 t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
984 }
985 })
986 }
987 }
988
989 // Test all new fields combined
990 func TestAllNewFieldsCombined(t *testing.T) {
991 _, adminPubkey := generateTestKeypair(t)
992 userSigner, userPubkey := generateTestKeypair(t)
993
994 adminHex := hex.Enc(adminPubkey)
995
996 policyJSON := []byte(`{
997 "default_policy": "deny",
998 "rules": {
999 "30023": {
1000 "description": "All new constraints",
1001 "max_expiry_duration": "P7D",
1002 "protected_required": true,
1003 "identifier_regex": "^[a-z0-9-]{1,32}$",
1004 "follows_whitelist_admins": ["` + adminHex + `"]
1005 }
1006 }
1007 }`)
1008
1009 policy, err := New(policyJSON)
1010 if err != nil {
1011 t.Fatalf("Failed to create policy: %v", err)
1012 }
1013
1014 // Add user to follows whitelist
1015 policy.UpdateRuleFollowsWhitelist(30023, [][]byte{userPubkey})
1016
1017 tests := []struct {
1018 name string
1019 dTag string
1020 hasExpiry bool
1021 expiryOK bool
1022 hasProtect bool
1023 expectAllow bool
1024 }{
1025 {
1026 name: "all constraints satisfied",
1027 dTag: "my-article",
1028 hasExpiry: true,
1029 expiryOK: true,
1030 hasProtect: true,
1031 expectAllow: true,
1032 },
1033 {
1034 name: "missing protected tag",
1035 dTag: "my-article",
1036 hasExpiry: true,
1037 expiryOK: true,
1038 hasProtect: false,
1039 expectAllow: false,
1040 },
1041 {
1042 name: "invalid d tag",
1043 dTag: "INVALID",
1044 hasExpiry: true,
1045 expiryOK: true,
1046 hasProtect: true,
1047 expectAllow: false,
1048 },
1049 {
1050 name: "expiry too long",
1051 dTag: "my-article",
1052 hasExpiry: true,
1053 expiryOK: false,
1054 hasProtect: true,
1055 expectAllow: false,
1056 },
1057 }
1058
1059 for _, tt := range tests {
1060 t.Run(tt.name, func(t *testing.T) {
1061 ev := createTestEventForNewFields(t, userSigner, "article content", 30023)
1062
1063 addTagString(ev, "d", tt.dTag)
1064
1065 if tt.hasExpiry {
1066 var offset int64 = 86400 // 1 day (OK)
1067 if !tt.expiryOK {
1068 offset = 864000 // 10 days (exceeds 7d)
1069 }
1070 addTagString(ev, "expiration", int64ToString(ev.CreatedAt+offset))
1071 }
1072
1073 if tt.hasProtect {
1074 addTagString(ev, "-", "")
1075 }
1076
1077 if err := ev.Sign(userSigner); chk.E(err) {
1078 t.Fatalf("Failed to sign: %v", err)
1079 }
1080
1081 allowed, err := policy.CheckPolicy("write", ev, userPubkey, "127.0.0.1")
1082 if err != nil {
1083 t.Fatalf("CheckPolicy error: %v", err)
1084 }
1085
1086 if allowed != tt.expectAllow {
1087 t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
1088 }
1089 })
1090 }
1091 }
1092
1093 // Test new fields in global rule
1094 // Global rule is ONLY used as fallback when NO kind-specific rule exists.
1095 // If a kind-specific rule exists (even if empty), it takes precedence and global is ignored.
1096 func TestNewFieldsInGlobalRule(t *testing.T) {
1097 signer, pubkey := generateTestKeypair(t)
1098
1099 // Policy with global constraints and a kind-specific rule for kind 1
1100 policyJSON := []byte(`{
1101 "default_policy": "allow",
1102 "global": {
1103 "max_expiry_duration": "P1D",
1104 "protected_required": true
1105 },
1106 "rules": {
1107 "1": {
1108 "description": "Kind 1 events - has specific rule, so global is ignored"
1109 }
1110 }
1111 }`)
1112
1113 policy, err := New(policyJSON)
1114 if err != nil {
1115 t.Fatalf("Failed to create policy: %v", err)
1116 }
1117
1118 // Kind 1 has a specific rule, so global protected_required is IGNORED
1119 // Event should be ALLOWED even without protected tag
1120 ev := createTestEventForNewFields(t, signer, "test", 1)
1121 addTagString(ev, "expiration", int64ToString(ev.CreatedAt+3600))
1122 if err := ev.Sign(signer); chk.E(err) {
1123 t.Fatalf("Failed to sign: %v", err)
1124 }
1125
1126 allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
1127 if err != nil {
1128 t.Fatalf("CheckPolicy error: %v", err)
1129 }
1130
1131 if !allowed {
1132 t.Error("Kind 1 has specific rule - global protected_required should be ignored, event should be allowed")
1133 }
1134
1135 // Now test kind 999 which has NO specific rule - global should apply
1136 ev2 := createTestEventForNewFields(t, signer, "test", 999)
1137 addTagString(ev2, "expiration", int64ToString(ev2.CreatedAt+3600))
1138 if err := ev2.Sign(signer); chk.E(err) {
1139 t.Fatalf("Failed to sign: %v", err)
1140 }
1141
1142 allowed, err = policy.CheckPolicy("write", ev2, pubkey, "127.0.0.1")
1143 if err != nil {
1144 t.Fatalf("CheckPolicy error: %v", err)
1145 }
1146
1147 if allowed {
1148 t.Error("Kind 999 has NO specific rule - global protected_required should apply, event should be denied")
1149 }
1150
1151 // Add protected tag to kind 999 event - should now be allowed
1152 addTagString(ev2, "-", "")
1153 if err := ev2.Sign(signer); chk.E(err) {
1154 t.Fatalf("Failed to sign: %v", err)
1155 }
1156
1157 allowed, err = policy.CheckPolicy("write", ev2, pubkey, "127.0.0.1")
1158 if err != nil {
1159 t.Fatalf("CheckPolicy error: %v", err)
1160 }
1161
1162 if !allowed {
1163 t.Error("Kind 999 with protected tag and valid expiry should be allowed by global rule")
1164 }
1165 }
1166
1167 // =============================================================================
1168 // New() Validation Tests - Ensures invalid configs fail at load time
1169 // =============================================================================
1170
1171 // TestNewRejectsInvalidMaxExpiryDuration verifies that New() fails fast when
1172 // given an invalid max_expiry_duration format like "T10M" instead of "PT10M".
1173 // This prevents silent failures where constraints are ignored.
1174 func TestNewRejectsInvalidMaxExpiryDuration(t *testing.T) {
1175 tests := []struct {
1176 name string
1177 json string
1178 expectError bool
1179 errorMatch string
1180 }{
1181 {
1182 name: "valid PT10M format accepted",
1183 json: `{
1184 "rules": {
1185 "4": {"max_expiry_duration": "PT10M"}
1186 }
1187 }`,
1188 expectError: false,
1189 },
1190 {
1191 name: "invalid T10M format (missing P prefix) rejected",
1192 json: `{
1193 "rules": {
1194 "4": {"max_expiry_duration": "T10M"}
1195 }
1196 }`,
1197 expectError: true,
1198 errorMatch: "max_expiry_duration",
1199 },
1200 {
1201 name: "invalid 10M format (missing PT prefix) rejected",
1202 json: `{
1203 "rules": {
1204 "4": {"max_expiry_duration": "10M"}
1205 }
1206 }`,
1207 expectError: true,
1208 errorMatch: "max_expiry_duration",
1209 },
1210 {
1211 name: "valid P7D format accepted",
1212 json: `{
1213 "rules": {
1214 "1": {"max_expiry_duration": "P7D"}
1215 }
1216 }`,
1217 expectError: false,
1218 },
1219 {
1220 name: "invalid 7D format (missing P prefix) rejected",
1221 json: `{
1222 "rules": {
1223 "1": {"max_expiry_duration": "7D"}
1224 }
1225 }`,
1226 expectError: true,
1227 errorMatch: "max_expiry_duration",
1228 },
1229 }
1230
1231 for _, tt := range tests {
1232 t.Run(tt.name, func(t *testing.T) {
1233 policy, err := New([]byte(tt.json))
1234
1235 if tt.expectError {
1236 if err == nil {
1237 t.Errorf("New() should have rejected invalid config, but returned policy: %+v", policy)
1238 return
1239 }
1240 if tt.errorMatch != "" && !contains(err.Error(), tt.errorMatch) {
1241 t.Errorf("Error %q should contain %q", err.Error(), tt.errorMatch)
1242 }
1243 } else {
1244 if err != nil {
1245 t.Errorf("New() unexpected error for valid config: %v", err)
1246 }
1247 if policy == nil {
1248 t.Error("New() returned nil policy for valid config")
1249 }
1250 }
1251 })
1252 }
1253 }
1254
1255 // =============================================================================
1256 // ValidateJSON Tests for New Fields
1257 // =============================================================================
1258
1259 func TestValidateJSONNewFields(t *testing.T) {
1260 tests := []struct {
1261 name string
1262 json string
1263 expectError bool
1264 errorMatch string
1265 }{
1266 {
1267 name: "valid max_expiry_duration",
1268 json: `{
1269 "rules": {
1270 "1": {"max_expiry_duration": "P7DT12H30M"}
1271 }
1272 }`,
1273 expectError: false,
1274 },
1275 {
1276 name: "invalid max_expiry_duration - no P prefix",
1277 json: `{
1278 "rules": {
1279 "1": {"max_expiry_duration": "7D"}
1280 }
1281 }`,
1282 expectError: true,
1283 errorMatch: "max_expiry_duration",
1284 },
1285 {
1286 name: "invalid max_expiry_duration - invalid format",
1287 json: `{
1288 "rules": {
1289 "1": {"max_expiry_duration": "invalid"}
1290 }
1291 }`,
1292 expectError: true,
1293 errorMatch: "max_expiry_duration",
1294 },
1295 {
1296 name: "valid identifier_regex",
1297 json: `{
1298 "rules": {
1299 "30023": {"identifier_regex": "^[a-z0-9-]+$"}
1300 }
1301 }`,
1302 expectError: false,
1303 },
1304 {
1305 name: "invalid identifier_regex",
1306 json: `{
1307 "rules": {
1308 "30023": {"identifier_regex": "[invalid("}
1309 }
1310 }`,
1311 expectError: true,
1312 errorMatch: "identifier_regex",
1313 },
1314 {
1315 name: "valid follows_whitelist_admins",
1316 json: `{
1317 "rules": {
1318 "1": {"follows_whitelist_admins": ["1111111111111111111111111111111111111111111111111111111111111111"]}
1319 }
1320 }`,
1321 expectError: false,
1322 },
1323 {
1324 name: "invalid follows_whitelist_admins - wrong length",
1325 json: `{
1326 "rules": {
1327 "1": {"follows_whitelist_admins": ["tooshort"]}
1328 }
1329 }`,
1330 expectError: true,
1331 errorMatch: "follows_whitelist_admins",
1332 },
1333 {
1334 name: "invalid follows_whitelist_admins - not hex",
1335 json: `{
1336 "rules": {
1337 "1": {"follows_whitelist_admins": ["gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg"]}
1338 }
1339 }`,
1340 expectError: true,
1341 errorMatch: "follows_whitelist_admins",
1342 },
1343 {
1344 name: "valid global rule new fields",
1345 json: `{
1346 "global": {
1347 "max_expiry_duration": "P1D",
1348 "identifier_regex": "^[a-z]+$",
1349 "protected_required": true
1350 }
1351 }`,
1352 expectError: false,
1353 },
1354 // Tests for read_allow_permissive and write_allow_permissive
1355 {
1356 name: "valid read_allow_permissive alone with whitelist",
1357 json: `{
1358 "kind": {"whitelist": [1, 3, 5]},
1359 "global": {"read_allow_permissive": true}
1360 }`,
1361 expectError: false,
1362 },
1363 {
1364 name: "valid write_allow_permissive alone with whitelist",
1365 json: `{
1366 "kind": {"whitelist": [1, 3, 5]},
1367 "global": {"write_allow_permissive": true}
1368 }`,
1369 expectError: false,
1370 },
1371 {
1372 name: "invalid both permissive flags with whitelist",
1373 json: `{
1374 "kind": {"whitelist": [1, 3, 5]},
1375 "global": {
1376 "read_allow_permissive": true,
1377 "write_allow_permissive": true
1378 }
1379 }`,
1380 expectError: true,
1381 errorMatch: "read_allow_permissive and write_allow_permissive cannot be enabled together",
1382 },
1383 {
1384 name: "invalid both permissive flags with blacklist",
1385 json: `{
1386 "kind": {"blacklist": [2, 4, 6]},
1387 "global": {
1388 "read_allow_permissive": true,
1389 "write_allow_permissive": true
1390 }
1391 }`,
1392 expectError: true,
1393 errorMatch: "read_allow_permissive and write_allow_permissive cannot be enabled together",
1394 },
1395 {
1396 name: "valid both permissive flags without any kind restriction",
1397 json: `{
1398 "global": {
1399 "read_allow_permissive": true,
1400 "write_allow_permissive": true
1401 }
1402 }`,
1403 expectError: false,
1404 },
1405 }
1406
1407 for _, tt := range tests {
1408 t.Run(tt.name, func(t *testing.T) {
1409 policy := &P{}
1410 err := policy.ValidateJSON([]byte(tt.json))
1411
1412 if tt.expectError {
1413 if err == nil {
1414 t.Error("Expected validation error, got nil")
1415 return
1416 }
1417 if tt.errorMatch != "" && !contains(err.Error(), tt.errorMatch) {
1418 t.Errorf("Error %q should contain %q", err.Error(), tt.errorMatch)
1419 }
1420 } else {
1421 if err != nil {
1422 t.Errorf("Unexpected validation error: %v", err)
1423 }
1424 }
1425 })
1426 }
1427 }
1428
1429 // =============================================================================
1430 // Helper Functions
1431 // =============================================================================
1432
1433 func createTestEventForNewFields(t *testing.T, signer *p8k.Signer, content string, kind uint16) *event.E {
1434 ev := event.New()
1435 ev.CreatedAt = time.Now().Unix()
1436 ev.Kind = kind
1437 ev.Content = []byte(content)
1438 ev.Tags = tag.NewS()
1439
1440 if err := ev.Sign(signer); chk.E(err) {
1441 t.Fatalf("Failed to sign test event: %v", err)
1442 }
1443
1444 return ev
1445 }
1446
1447 func addTagString(ev *event.E, key, value string) {
1448 tagItem := tag.NewFromAny(key, value)
1449 ev.Tags.Append(tagItem)
1450 }
1451
1452 func int64ToString(i int64) string {
1453 return strconv.FormatInt(i, 10)
1454 }
1455
1456 func contains(s, substr string) bool {
1457 return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
1458 }
1459
1460 func containsHelper(s, substr string) bool {
1461 for i := 0; i <= len(s)-len(substr); i++ {
1462 if s[i:i+len(substr)] == substr {
1463 return true
1464 }
1465 }
1466 return false
1467 }
1468