Files
next.orly.dev/pkg/policy/new_fields_test.go
mleku 2166ff7013
Some checks failed
Go / build-and-release (push) Has been cancelled
Remove subscription_stability_test.go and improve test variable naming
Deleted `subscription_stability_test.go` to clean up unused or redundant code. Updated naming in test files for improved readability, replacing `tag` with `tg` for consistency. Also updated the `github.com/klauspost/compress` dependency to v1.18.2.
2025-12-01 18:47:15 +00:00

1236 lines
30 KiB
Go

package policy
import (
"strconv"
"testing"
"time"
"git.mleku.dev/mleku/nostr/encoders/event"
"git.mleku.dev/mleku/nostr/encoders/hex"
"git.mleku.dev/mleku/nostr/encoders/tag"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
"lol.mleku.dev/chk"
)
// =============================================================================
// parseDuration Tests (ISO-8601 format)
// =============================================================================
func TestParseDuration(t *testing.T) {
tests := []struct {
name string
input string
expected int64
expectError bool
}{
// Basic ISO-8601 time units (require T separator)
{name: "seconds only", input: "PT30S", expected: 30},
{name: "minutes only", input: "PT5M", expected: 300},
{name: "hours only", input: "PT2H", expected: 7200},
// Basic ISO-8601 date units
{name: "days only", input: "P1D", expected: 86400},
{name: "7 days", input: "P7D", expected: 604800},
{name: "30 days", input: "P30D", expected: 2592000},
{name: "weeks", input: "P1W", expected: 604800},
{name: "months", input: "P1M", expected: 2628000}, // ~30.44 days per library
{name: "years", input: "P1Y", expected: 31536000},
// Combinations
{name: "hours and minutes", input: "PT1H30M", expected: 5400},
{name: "days and hours", input: "P1DT12H", expected: 129600},
{name: "days hours minutes", input: "P1DT2H30M", expected: 95400},
{name: "full combo", input: "P1DT2H3M4S", expected: 93784},
// Edge cases
{name: "zero seconds", input: "PT0S", expected: 0},
{name: "large days", input: "P365D", expected: 31536000},
{name: "decimal values", input: "PT1.5H", expected: 5400},
// Whitespace handling
{name: "with leading space", input: " PT1H", expected: 3600},
{name: "with trailing space", input: "PT1H ", expected: 3600},
// Additional valid cases
{name: "leading zeros", input: "P007D", expected: 604800},
{name: "decimal days", input: "P0.5D", expected: 43200},
{name: "fractional minutes", input: "PT0.5M", expected: 30},
{name: "weeks with days", input: "P1W3D", expected: 864000},
{name: "zero everything", input: "P0DT0H0M0S", expected: 0},
// Errors (strict ISO-8601 via sosodev/duration library)
{name: "empty string", input: "", expectError: true},
{name: "whitespace only", input: " ", expectError: true},
{name: "missing P prefix", input: "1D", expectError: true},
{name: "invalid unit", input: "P5X", expectError: true},
{name: "H without T separator", input: "P1H", expectError: true},
{name: "S without T separator", input: "P30S", expectError: true},
{name: "D after T", input: "PT1D", expectError: true},
{name: "Y after T", input: "PT1Y", expectError: true},
{name: "W after T", input: "PT1W", expectError: true},
{name: "negative number", input: "P-5D", expectError: true},
{name: "unit without number", input: "PD", expectError: true},
{name: "unit without number time", input: "PTH", expectError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseDuration(tt.input)
if tt.expectError {
if err == nil {
t.Errorf("parseDuration(%q) expected error, got %d", tt.input, result)
}
return
}
if err != nil {
t.Errorf("parseDuration(%q) unexpected error: %v", tt.input, err)
return
}
if result != tt.expected {
t.Errorf("parseDuration(%q) = %d, expected %d", tt.input, result, tt.expected)
}
})
}
}
// =============================================================================
// MaxExpiryDuration Tests
// =============================================================================
func TestMaxExpiryDuration(t *testing.T) {
tests := []struct {
name string
maxExpiryDuration string
eventExpiry int64 // offset from created_at
hasExpiryTag bool
expectAllow bool
}{
{
name: "valid expiry within limit",
maxExpiryDuration: "PT1H",
eventExpiry: 1800, // 30 minutes
hasExpiryTag: true,
expectAllow: true,
},
{
name: "valid expiry at exact limit",
maxExpiryDuration: "PT1H",
eventExpiry: 3600, // exactly 1 hour
hasExpiryTag: true,
expectAllow: true,
},
{
name: "expiry exceeds limit",
maxExpiryDuration: "PT1H",
eventExpiry: 7200, // 2 hours
hasExpiryTag: true,
expectAllow: false,
},
{
name: "missing expiry tag when required",
maxExpiryDuration: "PT1H",
hasExpiryTag: false,
expectAllow: false,
},
{
name: "day-based duration",
maxExpiryDuration: "P7D",
eventExpiry: 86400, // 1 day
hasExpiryTag: true,
expectAllow: true,
},
{
name: "complex duration P1DT12H",
maxExpiryDuration: "P1DT12H",
eventExpiry: 86400, // 1 day (within 1.5 days)
hasExpiryTag: true,
expectAllow: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
signer, pubkey := generateTestKeypair(t)
// Create policy with max_expiry_duration
policyJSON := []byte(`{
"default_policy": "allow",
"rules": {
"1": {
"description": "Test kind 1 with expiry",
"max_expiry_duration": "` + tt.maxExpiryDuration + `"
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
// Create event
ev := createTestEventForNewFields(t, signer, "test content", 1)
// Add expiry tag if needed
if tt.hasExpiryTag {
expiryTs := ev.CreatedAt + tt.eventExpiry
addTag(ev, "expiration", string(rune(expiryTs)))
// Re-add as proper string
ev.Tags = tag.NewS()
addTagString(ev, "expiration", int64ToString(expiryTs))
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to re-sign event: %v", err)
}
}
allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed != tt.expectAllow {
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
}
})
}
}
// Test MaxExpiryDuration takes precedence over MaxExpiry
func TestMaxExpiryDurationPrecedence(t *testing.T) {
signer, pubkey := generateTestKeypair(t)
// Policy where both max_expiry (seconds) and max_expiry_duration are set
// max_expiry_duration should take precedence
policyJSON := []byte(`{
"default_policy": "allow",
"rules": {
"1": {
"description": "Test precedence",
"max_expiry": 60,
"max_expiry_duration": "PT1H"
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
// Create event with expiry at 30 minutes (would fail with max_expiry=60s, pass with PT1H)
ev := createTestEventForNewFields(t, signer, "test", 1)
expiryTs := ev.CreatedAt + 1800 // 30 minutes
addTagString(ev, "expiration", int64ToString(expiryTs))
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to sign: %v", err)
}
allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if !allowed {
t.Error("MaxExpiryDuration should take precedence over MaxExpiry; expected allow")
}
}
// =============================================================================
// ProtectedRequired Tests
// =============================================================================
func TestProtectedRequired(t *testing.T) {
tests := []struct {
name string
hasProtectedTag bool
expectAllow bool
}{
{
name: "has protected tag",
hasProtectedTag: true,
expectAllow: true,
},
{
name: "missing protected tag",
hasProtectedTag: false,
expectAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
signer, pubkey := generateTestKeypair(t)
policyJSON := []byte(`{
"default_policy": "allow",
"rules": {
"1": {
"description": "Protected events only",
"protected_required": true
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
ev := createTestEventForNewFields(t, signer, "test content", 1)
if tt.hasProtectedTag {
addTagString(ev, "-", "")
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to re-sign: %v", err)
}
}
allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed != tt.expectAllow {
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
}
})
}
}
// =============================================================================
// IdentifierRegex Tests
// =============================================================================
func TestIdentifierRegex(t *testing.T) {
tests := []struct {
name string
regex string
dTagValue string
hasDTag bool
expectAllow bool
}{
{
name: "valid lowercase slug",
regex: "^[a-z0-9-]{1,64}$",
dTagValue: "my-article-slug",
hasDTag: true,
expectAllow: true,
},
{
name: "invalid - contains uppercase",
regex: "^[a-z0-9-]{1,64}$",
dTagValue: "My-Article-Slug",
hasDTag: true,
expectAllow: false,
},
{
name: "invalid - contains spaces",
regex: "^[a-z0-9-]{1,64}$",
dTagValue: "my article slug",
hasDTag: true,
expectAllow: false,
},
{
name: "invalid - too long",
regex: "^[a-z0-9-]{1,10}$",
dTagValue: "this-is-too-long",
hasDTag: true,
expectAllow: false,
},
{
name: "missing d tag when required",
regex: "^[a-z0-9-]{1,64}$",
hasDTag: false,
expectAllow: false,
},
{
name: "UUID pattern",
regex: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$",
dTagValue: "550e8400-e29b-41d4-a716-446655440000",
hasDTag: true,
expectAllow: true,
},
{
name: "alphanumeric only",
regex: "^[a-zA-Z0-9]+$",
dTagValue: "MyArticle123",
hasDTag: true,
expectAllow: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
signer, pubkey := generateTestKeypair(t)
policyJSON := []byte(`{
"default_policy": "allow",
"rules": {
"30023": {
"description": "Long-form with identifier regex",
"identifier_regex": "` + tt.regex + `"
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
ev := createTestEventForNewFields(t, signer, "test content", 30023)
if tt.hasDTag {
addTagString(ev, "d", tt.dTagValue)
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to re-sign: %v", err)
}
}
allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed != tt.expectAllow {
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
}
})
}
}
// Test that IdentifierRegex validates multiple d tags
func TestIdentifierRegexMultipleDTags(t *testing.T) {
signer, pubkey := generateTestKeypair(t)
policyJSON := []byte(`{
"default_policy": "allow",
"rules": {
"30023": {
"description": "Test multiple d tags",
"identifier_regex": "^[a-z0-9-]+$"
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
// Test with one valid and one invalid d tag
ev := createTestEventForNewFields(t, signer, "test", 30023)
addTagString(ev, "d", "valid-slug")
addTagString(ev, "d", "INVALID-SLUG") // uppercase should fail
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to sign: %v", err)
}
allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed {
t.Error("Should deny when any d tag fails regex validation")
}
}
// =============================================================================
// FollowsWhitelistAdmins Tests
// =============================================================================
func TestFollowsWhitelistAdmins(t *testing.T) {
// Generate admin and user keypairs
adminSigner, adminPubkey := generateTestKeypair(t)
userSigner, userPubkey := generateTestKeypair(t)
nonFollowSigner, nonFollowPubkey := generateTestKeypair(t)
adminHex := hex.Enc(adminPubkey)
policyJSON := []byte(`{
"default_policy": "deny",
"rules": {
"1": {
"description": "Only admin follows can write",
"follows_whitelist_admins": ["` + adminHex + `"]
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
// Simulate loading admin's follows (user is followed by admin)
policy.UpdateRuleFollowsWhitelist(1, [][]byte{userPubkey})
tests := []struct {
name string
signer *p8k.Signer
pubkey []byte
expectAllow bool
}{
{
name: "followed user can write",
signer: userSigner,
pubkey: userPubkey,
expectAllow: true,
},
{
name: "non-followed user denied",
signer: nonFollowSigner,
pubkey: nonFollowPubkey,
expectAllow: false,
},
{
name: "admin can write (is in own follows conceptually)",
signer: adminSigner,
pubkey: adminPubkey,
expectAllow: false, // Admin not in follows list in this test
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ev := createTestEventForNewFields(t, tt.signer, "test content", 1)
allowed, err := policy.CheckPolicy("write", ev, tt.pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed != tt.expectAllow {
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
}
})
}
}
func TestGetAllFollowsWhitelistAdmins(t *testing.T) {
admin1 := "1111111111111111111111111111111111111111111111111111111111111111"
admin2 := "2222222222222222222222222222222222222222222222222222222222222222"
admin3 := "3333333333333333333333333333333333333333333333333333333333333333"
policyJSON := []byte(`{
"default_policy": "deny",
"global": {
"follows_whitelist_admins": ["` + admin1 + `"]
},
"rules": {
"1": {
"follows_whitelist_admins": ["` + admin2 + `"]
},
"30023": {
"follows_whitelist_admins": ["` + admin2 + `", "` + admin3 + `"]
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
admins := policy.GetAllFollowsWhitelistAdmins()
// Should have 3 unique admins (admin2 is deduplicated)
if len(admins) != 3 {
t.Errorf("Expected 3 unique admins, got %d", len(admins))
}
// Check all admins are present
adminMap := make(map[string]bool)
for _, a := range admins {
adminMap[a] = true
}
for _, expected := range []string{admin1, admin2, admin3} {
if !adminMap[expected] {
t.Errorf("Missing admin %s", expected)
}
}
}
// =============================================================================
// Combinatorial Tests - New Fields with Existing Fields
// =============================================================================
// Test MaxExpiryDuration combined with SizeLimit
func TestMaxExpiryDurationWithSizeLimit(t *testing.T) {
signer, pubkey := generateTestKeypair(t)
policyJSON := []byte(`{
"default_policy": "allow",
"rules": {
"1": {
"max_expiry_duration": "PT1H",
"size_limit": 1000
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
tests := []struct {
name string
contentSize int
hasExpiry bool
expiryOK bool
expectAllow bool
}{
{
name: "both constraints satisfied",
contentSize: 100,
hasExpiry: true,
expiryOK: true,
expectAllow: true,
},
{
name: "size exceeded",
contentSize: 2000,
hasExpiry: true,
expiryOK: true,
expectAllow: false,
},
{
name: "expiry exceeded",
contentSize: 100,
hasExpiry: true,
expiryOK: false,
expectAllow: false,
},
{
name: "missing expiry",
contentSize: 100,
hasExpiry: false,
expectAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
content := make([]byte, tt.contentSize)
for i := range content {
content[i] = 'a'
}
ev := event.New()
ev.CreatedAt = time.Now().Unix()
ev.Kind = 1
ev.Content = content
ev.Tags = tag.NewS()
if tt.hasExpiry {
var expiryOffset int64 = 1800 // 30 min (OK)
if !tt.expiryOK {
expiryOffset = 7200 // 2h (exceeds 1h limit)
}
addTagString(ev, "expiration", int64ToString(ev.CreatedAt+expiryOffset))
}
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to sign: %v", err)
}
allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed != tt.expectAllow {
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
}
})
}
}
// Test ProtectedRequired combined with Privileged
func TestProtectedRequiredWithPrivileged(t *testing.T) {
authorSigner, authorPubkey := generateTestKeypair(t)
_, recipientPubkey := generateTestKeypair(t)
_, outsiderPubkey := generateTestKeypair(t)
policyJSON := []byte(`{
"default_policy": "deny",
"rules": {
"4": {
"description": "Encrypted DMs - protected and privileged",
"protected_required": true,
"privileged": true
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
tests := []struct {
name string
hasProtected bool
readerPubkey []byte
isParty bool // is reader author or in p-tag
accessType string
expectAllow bool
}{
{
name: "author can read protected event",
hasProtected: true,
readerPubkey: authorPubkey,
isParty: true,
accessType: "read",
expectAllow: true,
},
{
name: "recipient in p-tag can read",
hasProtected: true,
readerPubkey: recipientPubkey,
isParty: true,
accessType: "read",
expectAllow: true,
},
{
name: "outsider cannot read privileged event",
hasProtected: true,
readerPubkey: outsiderPubkey,
isParty: false,
accessType: "read",
expectAllow: false,
},
{
name: "missing protected tag - write denied",
hasProtected: false,
readerPubkey: authorPubkey,
isParty: true,
accessType: "write",
expectAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ev := createTestEventForNewFields(t, authorSigner, "encrypted content", 4)
// Add recipient to p-tag
addPTag(ev, recipientPubkey)
if tt.hasProtected {
addTagString(ev, "-", "")
}
if err := ev.Sign(authorSigner); chk.E(err) {
t.Fatalf("Failed to sign: %v", err)
}
allowed, err := policy.CheckPolicy(tt.accessType, ev, tt.readerPubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed != tt.expectAllow {
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
}
})
}
}
// Test IdentifierRegex combined with TagValidation
func TestIdentifierRegexWithTagValidation(t *testing.T) {
signer, pubkey := generateTestKeypair(t)
// Both identifier_regex (for d tag) and tag_validation (for t tag)
policyJSON := []byte(`{
"default_policy": "allow",
"rules": {
"30023": {
"identifier_regex": "^[a-z0-9-]+$",
"tag_validation": {
"t": "^[a-z]+$"
}
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
tests := []struct {
name string
dTag string
tTag string
hasDTag bool
hasTTag bool
expectAllow bool
}{
{
name: "both tags valid",
dTag: "my-article",
tTag: "nostr",
hasDTag: true,
hasTTag: true,
expectAllow: true,
},
{
name: "d tag invalid",
dTag: "MY-ARTICLE",
tTag: "nostr",
hasDTag: true,
hasTTag: true,
expectAllow: false,
},
{
name: "t tag invalid",
dTag: "my-article",
tTag: "NOSTR123",
hasDTag: true,
hasTTag: true,
expectAllow: false,
},
{
name: "missing d tag",
tTag: "nostr",
hasDTag: false,
hasTTag: true,
expectAllow: false,
},
{
name: "missing t tag - allowed (tag_validation only validates present tags)",
dTag: "my-article",
hasDTag: true,
hasTTag: false,
expectAllow: true, // tag_validation doesn't require tags to exist, only validates if present
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ev := createTestEventForNewFields(t, signer, "article content", 30023)
if tt.hasDTag {
addTagString(ev, "d", tt.dTag)
}
if tt.hasTTag {
addTagString(ev, "t", tt.tTag)
}
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to sign: %v", err)
}
allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed != tt.expectAllow {
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
}
})
}
}
// Test FollowsWhitelistAdmins combined with WriteAllow
func TestFollowsWhitelistAdminsWithWriteAllow(t *testing.T) {
_, adminPubkey := generateTestKeypair(t)
followedSigner, followedPubkey := generateTestKeypair(t)
explicitSigner, explicitPubkey := generateTestKeypair(t)
_, deniedPubkey := generateTestKeypair(t)
adminHex := hex.Enc(adminPubkey)
explicitHex := hex.Enc(explicitPubkey)
// Both follows whitelist and explicit write_allow
policyJSON := []byte(`{
"default_policy": "deny",
"rules": {
"1": {
"follows_whitelist_admins": ["` + adminHex + `"],
"write_allow": ["` + explicitHex + `"]
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
// Add followed user to whitelist
policy.UpdateRuleFollowsWhitelist(1, [][]byte{followedPubkey})
tests := []struct {
name string
signer *p8k.Signer
pubkey []byte
expectAllow bool
}{
{
name: "followed user allowed",
signer: followedSigner,
pubkey: followedPubkey,
expectAllow: true,
},
{
name: "explicit write_allow user allowed",
signer: explicitSigner,
pubkey: explicitPubkey,
expectAllow: true,
},
{
name: "user not in either list denied",
signer: p8k.MustNew(),
pubkey: deniedPubkey,
expectAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Generate if needed
if tt.signer.Pub() == nil {
_ = tt.signer.Generate()
}
ev := createTestEventForNewFields(t, tt.signer, "test", 1)
allowed, err := policy.CheckPolicy("write", ev, tt.pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed != tt.expectAllow {
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
}
})
}
}
// Test all new fields combined
func TestAllNewFieldsCombined(t *testing.T) {
_, adminPubkey := generateTestKeypair(t)
userSigner, userPubkey := generateTestKeypair(t)
adminHex := hex.Enc(adminPubkey)
policyJSON := []byte(`{
"default_policy": "deny",
"rules": {
"30023": {
"description": "All new constraints",
"max_expiry_duration": "P7D",
"protected_required": true,
"identifier_regex": "^[a-z0-9-]{1,32}$",
"follows_whitelist_admins": ["` + adminHex + `"]
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
// Add user to follows whitelist
policy.UpdateRuleFollowsWhitelist(30023, [][]byte{userPubkey})
tests := []struct {
name string
dTag string
hasExpiry bool
expiryOK bool
hasProtect bool
expectAllow bool
}{
{
name: "all constraints satisfied",
dTag: "my-article",
hasExpiry: true,
expiryOK: true,
hasProtect: true,
expectAllow: true,
},
{
name: "missing protected tag",
dTag: "my-article",
hasExpiry: true,
expiryOK: true,
hasProtect: false,
expectAllow: false,
},
{
name: "invalid d tag",
dTag: "INVALID",
hasExpiry: true,
expiryOK: true,
hasProtect: true,
expectAllow: false,
},
{
name: "expiry too long",
dTag: "my-article",
hasExpiry: true,
expiryOK: false,
hasProtect: true,
expectAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ev := createTestEventForNewFields(t, userSigner, "article content", 30023)
addTagString(ev, "d", tt.dTag)
if tt.hasExpiry {
var offset int64 = 86400 // 1 day (OK)
if !tt.expiryOK {
offset = 864000 // 10 days (exceeds 7d)
}
addTagString(ev, "expiration", int64ToString(ev.CreatedAt+offset))
}
if tt.hasProtect {
addTagString(ev, "-", "")
}
if err := ev.Sign(userSigner); chk.E(err) {
t.Fatalf("Failed to sign: %v", err)
}
allowed, err := policy.CheckPolicy("write", ev, userPubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed != tt.expectAllow {
t.Errorf("CheckPolicy() = %v, expected %v", allowed, tt.expectAllow)
}
})
}
}
// Test new fields in global rule
func TestNewFieldsInGlobalRule(t *testing.T) {
signer, pubkey := generateTestKeypair(t)
policyJSON := []byte(`{
"default_policy": "allow",
"global": {
"max_expiry_duration": "P1D",
"protected_required": true
},
"rules": {
"1": {
"description": "Kind 1 events"
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
// Event without protected tag should fail global rule
ev := createTestEventForNewFields(t, signer, "test", 1)
addTagString(ev, "expiration", int64ToString(ev.CreatedAt+3600))
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to sign: %v", err)
}
allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if allowed {
t.Error("Global protected_required should deny event without - tag")
}
// Add protected tag
addTagString(ev, "-", "")
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to sign: %v", err)
}
allowed, err = policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy error: %v", err)
}
if !allowed {
t.Error("Should allow event with protected tag and valid expiry")
}
}
// =============================================================================
// ValidateJSON Tests for New Fields
// =============================================================================
func TestValidateJSONNewFields(t *testing.T) {
tests := []struct {
name string
json string
expectError bool
errorMatch string
}{
{
name: "valid max_expiry_duration",
json: `{
"rules": {
"1": {"max_expiry_duration": "P7DT12H30M"}
}
}`,
expectError: false,
},
{
name: "invalid max_expiry_duration - no P prefix",
json: `{
"rules": {
"1": {"max_expiry_duration": "7D"}
}
}`,
expectError: true,
errorMatch: "max_expiry_duration",
},
{
name: "invalid max_expiry_duration - invalid format",
json: `{
"rules": {
"1": {"max_expiry_duration": "invalid"}
}
}`,
expectError: true,
errorMatch: "max_expiry_duration",
},
{
name: "valid identifier_regex",
json: `{
"rules": {
"30023": {"identifier_regex": "^[a-z0-9-]+$"}
}
}`,
expectError: false,
},
{
name: "invalid identifier_regex",
json: `{
"rules": {
"30023": {"identifier_regex": "[invalid("}
}
}`,
expectError: true,
errorMatch: "identifier_regex",
},
{
name: "valid follows_whitelist_admins",
json: `{
"rules": {
"1": {"follows_whitelist_admins": ["1111111111111111111111111111111111111111111111111111111111111111"]}
}
}`,
expectError: false,
},
{
name: "invalid follows_whitelist_admins - wrong length",
json: `{
"rules": {
"1": {"follows_whitelist_admins": ["tooshort"]}
}
}`,
expectError: true,
errorMatch: "follows_whitelist_admins",
},
{
name: "invalid follows_whitelist_admins - not hex",
json: `{
"rules": {
"1": {"follows_whitelist_admins": ["gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg"]}
}
}`,
expectError: true,
errorMatch: "follows_whitelist_admins",
},
{
name: "valid global rule new fields",
json: `{
"global": {
"max_expiry_duration": "P1D",
"identifier_regex": "^[a-z]+$",
"protected_required": true
}
}`,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
policy := &P{}
err := policy.ValidateJSON([]byte(tt.json))
if tt.expectError {
if err == nil {
t.Error("Expected validation error, got nil")
return
}
if tt.errorMatch != "" && !contains(err.Error(), tt.errorMatch) {
t.Errorf("Error %q should contain %q", err.Error(), tt.errorMatch)
}
} else {
if err != nil {
t.Errorf("Unexpected validation error: %v", err)
}
}
})
}
}
// =============================================================================
// Helper Functions
// =============================================================================
func createTestEventForNewFields(t *testing.T, signer *p8k.Signer, content string, kind uint16) *event.E {
ev := event.New()
ev.CreatedAt = time.Now().Unix()
ev.Kind = kind
ev.Content = []byte(content)
ev.Tags = tag.NewS()
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to sign test event: %v", err)
}
return ev
}
func addTagString(ev *event.E, key, value string) {
tagItem := tag.NewFromAny(key, value)
ev.Tags.Append(tagItem)
}
func int64ToString(i int64) string {
return strconv.FormatInt(i, 10)
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
}
func containsHelper(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}