composition_test.go raw
1 package policy
2
3 import (
4 "encoding/json"
5 "testing"
6 )
7
8 // TestValidateOwnerPolicyUpdate tests owner-specific validation
9 func TestValidateOwnerPolicyUpdate(t *testing.T) {
10 // Create a base policy
11 basePolicy := &P{
12 DefaultPolicy: "allow",
13 Owners: []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
14 PolicyAdmins: []string{"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},
15 }
16
17 tests := []struct {
18 name string
19 newPolicy string
20 expectError bool
21 errorMsg string
22 }{
23 {
24 name: "valid owner update with non-empty owners",
25 newPolicy: `{
26 "default_policy": "deny",
27 "owners": ["cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"],
28 "policy_admins": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"]
29 }`,
30 expectError: false,
31 },
32 {
33 name: "invalid - empty owners list",
34 newPolicy: `{
35 "default_policy": "deny",
36 "owners": [],
37 "policy_admins": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"]
38 }`,
39 expectError: true,
40 errorMsg: "owners list cannot be empty",
41 },
42 {
43 name: "invalid - missing owners field",
44 newPolicy: `{
45 "default_policy": "deny",
46 "policy_admins": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"]
47 }`,
48 expectError: true,
49 errorMsg: "owners list cannot be empty",
50 },
51 {
52 name: "invalid - bad owner pubkey format",
53 newPolicy: `{
54 "default_policy": "deny",
55 "owners": ["not-a-valid-pubkey"]
56 }`,
57 expectError: true,
58 errorMsg: "invalid owner pubkey",
59 },
60 {
61 name: "valid - owner can add multiple owners",
62 newPolicy: `{
63 "default_policy": "deny",
64 "owners": [
65 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
66 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
67 ]
68 }`,
69 expectError: false,
70 },
71 }
72
73 for _, tt := range tests {
74 t.Run(tt.name, func(t *testing.T) {
75 err := basePolicy.ValidateOwnerPolicyUpdate([]byte(tt.newPolicy))
76 if tt.expectError {
77 if err == nil {
78 t.Errorf("expected error containing %q, got nil", tt.errorMsg)
79 } else if tt.errorMsg != "" && !containsSubstring(err.Error(), tt.errorMsg) {
80 t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
81 }
82 } else {
83 if err != nil {
84 t.Errorf("unexpected error: %v", err)
85 }
86 }
87 })
88 }
89 }
90
91 // TestValidatePolicyAdminUpdate tests policy admin validation
92 func TestValidatePolicyAdminUpdate(t *testing.T) {
93 // Create a base policy with known owners and admins
94 ownerPubkey := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
95 adminPubkey := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
96 allowedPubkey := "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
97
98 baseJSON := `{
99 "default_policy": "allow",
100 "owners": ["` + ownerPubkey + `"],
101 "policy_admins": ["` + adminPubkey + `"],
102 "kind": {
103 "whitelist": [1, 3, 7]
104 },
105 "rules": {
106 "1": {
107 "description": "Text notes",
108 "write_allow": ["` + allowedPubkey + `"],
109 "size_limit": 10000
110 }
111 }
112 }`
113
114 basePolicy := &P{}
115 if err := json.Unmarshal([]byte(baseJSON), basePolicy); err != nil {
116 t.Fatalf("failed to create base policy: %v", err)
117 }
118
119 adminPubkeyBin := make([]byte, 32)
120 for i := range adminPubkeyBin {
121 adminPubkeyBin[i] = 0xbb
122 }
123
124 tests := []struct {
125 name string
126 newPolicy string
127 expectError bool
128 errorMsg string
129 }{
130 {
131 name: "valid - policy admin can extend write_allow",
132 newPolicy: `{
133 "default_policy": "allow",
134 "owners": ["` + ownerPubkey + `"],
135 "policy_admins": ["` + adminPubkey + `"],
136 "kind": {
137 "whitelist": [1, 3, 7]
138 },
139 "rules": {
140 "1": {
141 "description": "Text notes",
142 "write_allow": ["` + allowedPubkey + `", "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"],
143 "size_limit": 10000
144 }
145 }
146 }`,
147 expectError: false,
148 },
149 {
150 name: "valid - policy admin can add to kind whitelist",
151 newPolicy: `{
152 "default_policy": "allow",
153 "owners": ["` + ownerPubkey + `"],
154 "policy_admins": ["` + adminPubkey + `"],
155 "kind": {
156 "whitelist": [1, 3, 7, 30023]
157 },
158 "rules": {
159 "1": {
160 "description": "Text notes",
161 "write_allow": ["` + allowedPubkey + `"],
162 "size_limit": 10000
163 }
164 }
165 }`,
166 expectError: false,
167 },
168 {
169 name: "valid - policy admin can increase size limit",
170 newPolicy: `{
171 "default_policy": "allow",
172 "owners": ["` + ownerPubkey + `"],
173 "policy_admins": ["` + adminPubkey + `"],
174 "kind": {
175 "whitelist": [1, 3, 7]
176 },
177 "rules": {
178 "1": {
179 "description": "Text notes",
180 "write_allow": ["` + allowedPubkey + `"],
181 "size_limit": 20000
182 }
183 }
184 }`,
185 expectError: false,
186 },
187 {
188 name: "invalid - policy admin cannot modify owners",
189 newPolicy: `{
190 "default_policy": "allow",
191 "owners": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"],
192 "policy_admins": ["` + adminPubkey + `"],
193 "kind": {
194 "whitelist": [1, 3, 7]
195 },
196 "rules": {
197 "1": {
198 "description": "Text notes",
199 "write_allow": ["` + allowedPubkey + `"],
200 "size_limit": 10000
201 }
202 }
203 }`,
204 expectError: true,
205 errorMsg: "cannot modify the 'owners' field",
206 },
207 {
208 name: "invalid - policy admin cannot modify policy_admins",
209 newPolicy: `{
210 "default_policy": "allow",
211 "owners": ["` + ownerPubkey + `"],
212 "policy_admins": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"],
213 "kind": {
214 "whitelist": [1, 3, 7]
215 },
216 "rules": {
217 "1": {
218 "description": "Text notes",
219 "write_allow": ["` + allowedPubkey + `"],
220 "size_limit": 10000
221 }
222 }
223 }`,
224 expectError: true,
225 errorMsg: "cannot modify the 'policy_admins' field",
226 },
227 {
228 name: "invalid - policy admin cannot remove from kind whitelist",
229 newPolicy: `{
230 "default_policy": "allow",
231 "owners": ["` + ownerPubkey + `"],
232 "policy_admins": ["` + adminPubkey + `"],
233 "kind": {
234 "whitelist": [1, 3]
235 },
236 "rules": {
237 "1": {
238 "description": "Text notes",
239 "write_allow": ["` + allowedPubkey + `"],
240 "size_limit": 10000
241 }
242 }
243 }`,
244 expectError: true,
245 errorMsg: "cannot remove kind 7 from whitelist",
246 },
247 {
248 name: "invalid - policy admin cannot remove from write_allow",
249 newPolicy: `{
250 "default_policy": "allow",
251 "owners": ["` + ownerPubkey + `"],
252 "policy_admins": ["` + adminPubkey + `"],
253 "kind": {
254 "whitelist": [1, 3, 7]
255 },
256 "rules": {
257 "1": {
258 "description": "Text notes",
259 "write_allow": [],
260 "size_limit": 10000
261 }
262 }
263 }`,
264 expectError: true,
265 errorMsg: "cannot remove pubkey",
266 },
267 {
268 name: "invalid - policy admin cannot reduce size limit",
269 newPolicy: `{
270 "default_policy": "allow",
271 "owners": ["` + ownerPubkey + `"],
272 "policy_admins": ["` + adminPubkey + `"],
273 "kind": {
274 "whitelist": [1, 3, 7]
275 },
276 "rules": {
277 "1": {
278 "description": "Text notes",
279 "write_allow": ["` + allowedPubkey + `"],
280 "size_limit": 5000
281 }
282 }
283 }`,
284 expectError: true,
285 errorMsg: "cannot reduce size_limit",
286 },
287 {
288 name: "invalid - policy admin cannot remove rule",
289 newPolicy: `{
290 "default_policy": "allow",
291 "owners": ["` + ownerPubkey + `"],
292 "policy_admins": ["` + adminPubkey + `"],
293 "kind": {
294 "whitelist": [1, 3, 7]
295 },
296 "rules": {}
297 }`,
298 expectError: true,
299 errorMsg: "cannot remove rule for kind 1",
300 },
301 {
302 name: "valid - policy admin can add blacklist entries for non-admin users",
303 newPolicy: `{
304 "default_policy": "allow",
305 "owners": ["` + ownerPubkey + `"],
306 "policy_admins": ["` + adminPubkey + `"],
307 "kind": {
308 "whitelist": [1, 3, 7],
309 "blacklist": [4]
310 },
311 "rules": {
312 "1": {
313 "description": "Text notes",
314 "write_allow": ["` + allowedPubkey + `"],
315 "write_deny": ["eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"],
316 "size_limit": 10000
317 }
318 }
319 }`,
320 expectError: false,
321 },
322 {
323 name: "invalid - policy admin cannot blacklist owner in write_deny",
324 newPolicy: `{
325 "default_policy": "allow",
326 "owners": ["` + ownerPubkey + `"],
327 "policy_admins": ["` + adminPubkey + `"],
328 "kind": {
329 "whitelist": [1, 3, 7]
330 },
331 "rules": {
332 "1": {
333 "description": "Text notes",
334 "write_allow": ["` + allowedPubkey + `"],
335 "write_deny": ["` + ownerPubkey + `"],
336 "size_limit": 10000
337 }
338 }
339 }`,
340 expectError: true,
341 errorMsg: "cannot blacklist owner",
342 },
343 {
344 name: "invalid - policy admin cannot blacklist other policy admin",
345 newPolicy: `{
346 "default_policy": "allow",
347 "owners": ["` + ownerPubkey + `"],
348 "policy_admins": ["` + adminPubkey + `"],
349 "kind": {
350 "whitelist": [1, 3, 7]
351 },
352 "rules": {
353 "1": {
354 "description": "Text notes",
355 "write_allow": ["` + allowedPubkey + `"],
356 "write_deny": ["` + adminPubkey + `"],
357 "size_limit": 10000
358 }
359 }
360 }`,
361 expectError: true,
362 errorMsg: "cannot blacklist policy admin",
363 },
364 {
365 name: "valid - policy admin can blacklist whitelisted non-admin user",
366 newPolicy: `{
367 "default_policy": "allow",
368 "owners": ["` + ownerPubkey + `"],
369 "policy_admins": ["` + adminPubkey + `"],
370 "kind": {
371 "whitelist": [1, 3, 7]
372 },
373 "rules": {
374 "1": {
375 "description": "Text notes",
376 "write_allow": ["` + allowedPubkey + `"],
377 "write_deny": ["` + allowedPubkey + `"],
378 "size_limit": 10000
379 }
380 }
381 }`,
382 expectError: false,
383 },
384 }
385
386 for _, tt := range tests {
387 t.Run(tt.name, func(t *testing.T) {
388 err := basePolicy.ValidatePolicyAdminUpdate([]byte(tt.newPolicy), adminPubkeyBin)
389 if tt.expectError {
390 if err == nil {
391 t.Errorf("expected error containing %q, got nil", tt.errorMsg)
392 } else if tt.errorMsg != "" && !containsSubstring(err.Error(), tt.errorMsg) {
393 t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
394 }
395 } else {
396 if err != nil {
397 t.Errorf("unexpected error: %v", err)
398 }
399 }
400 })
401 }
402 }
403
404 // TestIsOwner tests the IsOwner method
405 func TestIsOwner(t *testing.T) {
406 ownerPubkey := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
407 nonOwnerPubkey := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
408
409 _ = nonOwnerPubkey // Silence unused variable warning
410
411 policyJSON := `{
412 "default_policy": "allow",
413 "owners": ["` + ownerPubkey + `"]
414 }`
415
416 policy, err := New([]byte(policyJSON))
417 if err != nil {
418 t.Fatalf("failed to create policy: %v", err)
419 }
420
421 // Create binary pubkeys
422 ownerBin := make([]byte, 32)
423 for i := range ownerBin {
424 ownerBin[i] = 0xaa
425 }
426
427 nonOwnerBin := make([]byte, 32)
428 for i := range nonOwnerBin {
429 nonOwnerBin[i] = 0xbb
430 }
431
432 tests := []struct {
433 name string
434 pubkey []byte
435 expected bool
436 }{
437 {
438 name: "owner is recognized",
439 pubkey: ownerBin,
440 expected: true,
441 },
442 {
443 name: "non-owner is not recognized",
444 pubkey: nonOwnerBin,
445 expected: false,
446 },
447 {
448 name: "nil pubkey returns false",
449 pubkey: nil,
450 expected: false,
451 },
452 {
453 name: "empty pubkey returns false",
454 pubkey: []byte{},
455 expected: false,
456 },
457 }
458
459 for _, tt := range tests {
460 t.Run(tt.name, func(t *testing.T) {
461 result := policy.IsOwner(tt.pubkey)
462 if result != tt.expected {
463 t.Errorf("expected %v, got %v", tt.expected, result)
464 }
465 })
466 }
467 }
468
469 // TestStringSliceEqual tests the helper function
470 func TestStringSliceEqual(t *testing.T) {
471 tests := []struct {
472 name string
473 a []string
474 b []string
475 expected bool
476 }{
477 {
478 name: "equal slices same order",
479 a: []string{"a", "b", "c"},
480 b: []string{"a", "b", "c"},
481 expected: true,
482 },
483 {
484 name: "equal slices different order",
485 a: []string{"a", "b", "c"},
486 b: []string{"c", "a", "b"},
487 expected: true,
488 },
489 {
490 name: "different lengths",
491 a: []string{"a", "b"},
492 b: []string{"a", "b", "c"},
493 expected: false,
494 },
495 {
496 name: "different contents",
497 a: []string{"a", "b", "c"},
498 b: []string{"a", "b", "d"},
499 expected: false,
500 },
501 {
502 name: "empty slices",
503 a: []string{},
504 b: []string{},
505 expected: true,
506 },
507 {
508 name: "nil slices",
509 a: nil,
510 b: nil,
511 expected: true,
512 },
513 {
514 name: "nil vs empty",
515 a: nil,
516 b: []string{},
517 expected: true,
518 },
519 {
520 name: "duplicates in both",
521 a: []string{"a", "a", "b"},
522 b: []string{"a", "b", "a"},
523 expected: true,
524 },
525 }
526
527 for _, tt := range tests {
528 t.Run(tt.name, func(t *testing.T) {
529 result := stringSliceEqual(tt.a, tt.b)
530 if result != tt.expected {
531 t.Errorf("expected %v, got %v", tt.expected, result)
532 }
533 })
534 }
535 }
536
537 // TestPolicyAdminContributionValidation tests the contribution validation
538 func TestPolicyAdminContributionValidation(t *testing.T) {
539 ownerPubkey := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
540 adminPubkey := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
541
542 ownerPolicy := &P{
543 DefaultPolicy: "allow",
544 Owners: []string{ownerPubkey},
545 PolicyAdmins: []string{adminPubkey},
546 Kind: Kinds{
547 Whitelist: []int{1, 3, 7},
548 },
549 rules: map[int]Rule{
550 1: {
551 Description: "Text notes",
552 Constraints: Constraints{
553 SizeLimit: ptr(int64(10000)),
554 },
555 },
556 },
557 }
558
559 tests := []struct {
560 name string
561 contribution *PolicyAdminContribution
562 expectError bool
563 errorMsg string
564 }{
565 {
566 name: "valid - add kinds to whitelist",
567 contribution: &PolicyAdminContribution{
568 AdminPubkey: adminPubkey,
569 CreatedAt: 1234567890,
570 EventID: "event123",
571 KindWhitelistAdd: []int{30023},
572 },
573 expectError: false,
574 },
575 {
576 name: "valid - add to blacklist",
577 contribution: &PolicyAdminContribution{
578 AdminPubkey: adminPubkey,
579 CreatedAt: 1234567890,
580 EventID: "event123",
581 KindBlacklistAdd: []int{4},
582 },
583 expectError: false,
584 },
585 {
586 name: "valid - extend existing rule with larger limit",
587 contribution: &PolicyAdminContribution{
588 AdminPubkey: adminPubkey,
589 CreatedAt: 1234567890,
590 EventID: "event123",
591 RulesExtend: map[int]RuleExtension{
592 1: {
593 SizeLimitOverride: ptr(int64(20000)),
594 },
595 },
596 },
597 expectError: false,
598 },
599 {
600 name: "invalid - extend non-existent rule",
601 contribution: &PolicyAdminContribution{
602 AdminPubkey: adminPubkey,
603 CreatedAt: 1234567890,
604 EventID: "event123",
605 RulesExtend: map[int]RuleExtension{
606 999: {
607 SizeLimitOverride: ptr(int64(20000)),
608 },
609 },
610 },
611 expectError: true,
612 errorMsg: "cannot extend rule for kind 999",
613 },
614 {
615 name: "invalid - size limit override smaller than owner's",
616 contribution: &PolicyAdminContribution{
617 AdminPubkey: adminPubkey,
618 CreatedAt: 1234567890,
619 EventID: "event123",
620 RulesExtend: map[int]RuleExtension{
621 1: {
622 SizeLimitOverride: ptr(int64(5000)),
623 },
624 },
625 },
626 expectError: true,
627 errorMsg: "size_limit_override for kind 1 must be >=",
628 },
629 {
630 name: "valid - add new rule for undefined kind",
631 contribution: &PolicyAdminContribution{
632 AdminPubkey: adminPubkey,
633 CreatedAt: 1234567890,
634 EventID: "event123",
635 RulesAdd: map[int]Rule{
636 30023: {
637 Description: "Long-form content",
638 Constraints: Constraints{
639 SizeLimit: ptr(int64(100000)),
640 },
641 },
642 },
643 },
644 expectError: false,
645 },
646 {
647 name: "invalid - add rule for already-defined kind",
648 contribution: &PolicyAdminContribution{
649 AdminPubkey: adminPubkey,
650 CreatedAt: 1234567890,
651 EventID: "event123",
652 RulesAdd: map[int]Rule{
653 1: {
654 Description: "Trying to override",
655 },
656 },
657 },
658 expectError: true,
659 errorMsg: "cannot add rule for kind 1: already defined",
660 },
661 {
662 name: "invalid - bad pubkey length in extension",
663 contribution: &PolicyAdminContribution{
664 AdminPubkey: "short",
665 CreatedAt: 1234567890,
666 EventID: "event123",
667 },
668 expectError: true,
669 errorMsg: "invalid admin pubkey length",
670 },
671 }
672
673 for _, tt := range tests {
674 t.Run(tt.name, func(t *testing.T) {
675 err := ValidatePolicyAdminContribution(ownerPolicy, tt.contribution, nil)
676 if tt.expectError {
677 if err == nil {
678 t.Errorf("expected error containing %q, got nil", tt.errorMsg)
679 } else if tt.errorMsg != "" && !containsSubstring(err.Error(), tt.errorMsg) {
680 t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
681 }
682 } else {
683 if err != nil {
684 t.Errorf("unexpected error: %v", err)
685 }
686 }
687 })
688 }
689 }
690
691 // Helper function for generic pointer
692 func ptr[T any](v T) *T {
693 return &v
694 }
695