Files
next.orly.dev/pkg/policy/new_fields_test.go
mleku 042b47a4d9
Some checks failed
Go / build-and-release (push) Has been cancelled
Make policy validation write-only and add corresponding tests
Updated policy validation logic to apply only to write operations, ensuring constraints like max_expiry_duration and required tags do not affect read operations. Added corresponding test cases to verify behavior for both valid and invalid inputs. This change improves clarity between write and read validation rules.

bump tag to update binary
2025-12-02 12:41:41 +00:00

1397 lines
34 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: "expiry at exact limit rejected",
maxExpiryDuration: "PT1H",
eventExpiry: 3600, // exactly 1 hour - >= means this is rejected
hasExpiryTag: true,
expectAllow: false,
},
{
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")
}
}
// Test that max_expiry_duration only applies to writes, not reads
func TestMaxExpiryDurationWriteOnly(t *testing.T) {
signer, pubkey := generateTestKeypair(t)
// Policy with strict max_expiry_duration
policyJSON := []byte(`{
"default_policy": "allow",
"rules": {
"4": {
"description": "DM events with expiry",
"max_expiry_duration": "PT10M",
"privileged": true
}
}
}`)
policy, err := New(policyJSON)
if err != nil {
t.Fatalf("Failed to create policy: %v", err)
}
// Create event WITHOUT an expiry tag - this would fail write validation
// but should still be readable
ev := createTestEventForNewFields(t, signer, "test DM", 4)
if err := ev.Sign(signer); chk.E(err) {
t.Fatalf("Failed to sign: %v", err)
}
// Write should fail (no expiry tag when max_expiry_duration is set)
allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy write error: %v", err)
}
if allowed {
t.Error("Write should be denied for event without expiry tag when max_expiry_duration is set")
}
// Read should succeed (validation constraints don't apply to reads)
allowed, err = policy.CheckPolicy("read", ev, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy read error: %v", err)
}
if !allowed {
t.Error("Read should be allowed - max_expiry_duration is write-only validation")
}
// Also test with an event that has expiry exceeding the limit
ev2 := createTestEventForNewFields(t, signer, "test DM 2", 4)
expiryTs := ev2.CreatedAt + 7200 // 2 hours - exceeds 10 minute limit
addTagString(ev2, "expiration", int64ToString(expiryTs))
if err := ev2.Sign(signer); chk.E(err) {
t.Fatalf("Failed to sign: %v", err)
}
// Write should fail (expiry exceeds limit)
allowed, err = policy.CheckPolicy("write", ev2, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy write error: %v", err)
}
if allowed {
t.Error("Write should be denied for event with expiry exceeding max_expiry_duration")
}
// Read should still succeed
allowed, err = policy.CheckPolicy("read", ev2, pubkey, "127.0.0.1")
if err != nil {
t.Fatalf("CheckPolicy read error: %v", err)
}
if !allowed {
t.Error("Read should be allowed - max_expiry_duration is write-only validation")
}
}
// =============================================================================
// 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")
}
}
// =============================================================================
// New() Validation Tests - Ensures invalid configs fail at load time
// =============================================================================
// TestNewRejectsInvalidMaxExpiryDuration verifies that New() fails fast when
// given an invalid max_expiry_duration format like "T10M" instead of "PT10M".
// This prevents silent failures where constraints are ignored.
func TestNewRejectsInvalidMaxExpiryDuration(t *testing.T) {
tests := []struct {
name string
json string
expectError bool
errorMatch string
}{
{
name: "valid PT10M format accepted",
json: `{
"rules": {
"4": {"max_expiry_duration": "PT10M"}
}
}`,
expectError: false,
},
{
name: "invalid T10M format (missing P prefix) rejected",
json: `{
"rules": {
"4": {"max_expiry_duration": "T10M"}
}
}`,
expectError: true,
errorMatch: "max_expiry_duration",
},
{
name: "invalid 10M format (missing PT prefix) rejected",
json: `{
"rules": {
"4": {"max_expiry_duration": "10M"}
}
}`,
expectError: true,
errorMatch: "max_expiry_duration",
},
{
name: "valid P7D format accepted",
json: `{
"rules": {
"1": {"max_expiry_duration": "P7D"}
}
}`,
expectError: false,
},
{
name: "invalid 7D format (missing P prefix) rejected",
json: `{
"rules": {
"1": {"max_expiry_duration": "7D"}
}
}`,
expectError: true,
errorMatch: "max_expiry_duration",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
policy, err := New([]byte(tt.json))
if tt.expectError {
if err == nil {
t.Errorf("New() should have rejected invalid config, but returned policy: %+v", policy)
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("New() unexpected error for valid config: %v", err)
}
if policy == nil {
t.Error("New() returned nil policy for valid config")
}
}
})
}
}
// =============================================================================
// 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
}