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