initial draft of hot reload policy
This commit is contained in:
469
app/handle_policy_config_test.go
Normal file
469
app/handle_policy_config_test.go
Normal file
@@ -0,0 +1,469 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"git.mleku.dev/mleku/nostr/encoders/hex"
|
||||
"git.mleku.dev/mleku/nostr/encoders/kind"
|
||||
"git.mleku.dev/mleku/nostr/encoders/tag"
|
||||
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
|
||||
"next.orly.dev/app/config"
|
||||
"next.orly.dev/pkg/acl"
|
||||
"next.orly.dev/pkg/database"
|
||||
"next.orly.dev/pkg/policy"
|
||||
"next.orly.dev/pkg/protocol/publish"
|
||||
)
|
||||
|
||||
// setupPolicyTestListener creates a test listener with policy system enabled
|
||||
func setupPolicyTestListener(t *testing.T, policyAdminHex string) (*Listener, *database.D, func()) {
|
||||
tempDir, err := os.MkdirTemp("", "policy_handler_test_*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
// Use a unique app name per test to avoid conflicts
|
||||
appName := "test-policy-" + filepath.Base(tempDir)
|
||||
|
||||
// Create the XDG config directory and default policy file BEFORE creating the policy manager
|
||||
configDir := filepath.Join(xdg.ConfigHome, appName)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
t.Fatalf("failed to create config dir: %v", err)
|
||||
}
|
||||
|
||||
// Create initial policy file with admin if provided
|
||||
var initialPolicy []byte
|
||||
if policyAdminHex != "" {
|
||||
initialPolicy = []byte(`{
|
||||
"default_policy": "allow",
|
||||
"policy_admins": ["` + policyAdminHex + `"],
|
||||
"policy_follow_whitelist_enabled": true
|
||||
}`)
|
||||
} else {
|
||||
initialPolicy = []byte(`{"default_policy": "allow"}`)
|
||||
}
|
||||
policyPath := filepath.Join(configDir, "policy.json")
|
||||
if err := os.WriteFile(policyPath, initialPolicy, 0644); err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
os.RemoveAll(configDir)
|
||||
t.Fatalf("failed to write policy file: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
db, err := database.New(ctx, cancel, tempDir, "info")
|
||||
if err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
os.RemoveAll(configDir)
|
||||
t.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
|
||||
cfg := &config.C{
|
||||
PolicyEnabled: true,
|
||||
RelayURL: "wss://test.relay",
|
||||
Listen: "localhost",
|
||||
Port: 3334,
|
||||
ACLMode: "none",
|
||||
AppName: appName,
|
||||
}
|
||||
|
||||
// Create policy manager - now config file exists at XDG path
|
||||
policyManager := policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled)
|
||||
|
||||
server := &Server{
|
||||
Ctx: ctx,
|
||||
Config: cfg,
|
||||
DB: db,
|
||||
publishers: publish.New(NewPublisher(ctx)),
|
||||
policyManager: policyManager,
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
messagePauseMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
// Configure ACL registry
|
||||
acl.Registry.Active.Store(cfg.ACLMode)
|
||||
if err = acl.Registry.Configure(cfg, db, ctx); err != nil {
|
||||
db.Close()
|
||||
os.RemoveAll(tempDir)
|
||||
os.RemoveAll(configDir)
|
||||
t.Fatalf("failed to configure ACL: %v", err)
|
||||
}
|
||||
|
||||
listener := &Listener{
|
||||
Server: server,
|
||||
ctx: ctx,
|
||||
writeChan: make(chan publish.WriteRequest, 100),
|
||||
writeDone: make(chan struct{}),
|
||||
messageQueue: make(chan messageRequest, 100),
|
||||
processingDone: make(chan struct{}),
|
||||
subscriptions: make(map[string]context.CancelFunc),
|
||||
}
|
||||
|
||||
// Start write worker and message processor
|
||||
go listener.writeWorker()
|
||||
go listener.messageProcessor()
|
||||
|
||||
cleanup := func() {
|
||||
close(listener.writeChan)
|
||||
<-listener.writeDone
|
||||
close(listener.messageQueue)
|
||||
<-listener.processingDone
|
||||
db.Close()
|
||||
os.RemoveAll(tempDir)
|
||||
os.RemoveAll(configDir)
|
||||
}
|
||||
|
||||
return listener, db, cleanup
|
||||
}
|
||||
|
||||
// createPolicyConfigEvent creates a kind 12345 policy config event
|
||||
func createPolicyConfigEvent(t *testing.T, signer *p8k.Signer, policyJSON string) *event.E {
|
||||
ev := event.New()
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
ev.Kind = kind.PolicyConfig.K
|
||||
ev.Content = []byte(policyJSON)
|
||||
ev.Tags = tag.NewS()
|
||||
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
t.Fatalf("Failed to sign event: %v", err)
|
||||
}
|
||||
|
||||
return ev
|
||||
}
|
||||
|
||||
// TestHandlePolicyConfigUpdate_ValidAdmin tests policy update from valid admin
|
||||
func TestHandlePolicyConfigUpdate_ValidAdmin(t *testing.T) {
|
||||
// Create admin signer
|
||||
adminSigner := p8k.MustNew()
|
||||
if err := adminSigner.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate admin keypair: %v", err)
|
||||
}
|
||||
adminHex := hex.Enc(adminSigner.Pub())
|
||||
|
||||
listener, _, cleanup := setupPolicyTestListener(t, adminHex)
|
||||
defer cleanup()
|
||||
|
||||
// Create valid policy update event
|
||||
newPolicyJSON := `{
|
||||
"default_policy": "deny",
|
||||
"policy_admins": ["` + adminHex + `"],
|
||||
"kind": {"whitelist": [1, 3, 7]}
|
||||
}`
|
||||
|
||||
ev := createPolicyConfigEvent(t, adminSigner, newPolicyJSON)
|
||||
|
||||
// Handle the event
|
||||
err := listener.HandlePolicyConfigUpdate(ev)
|
||||
if err != nil {
|
||||
t.Errorf("Expected success but got error: %v", err)
|
||||
}
|
||||
|
||||
// Verify policy was updated
|
||||
if listener.policyManager.DefaultPolicy != "deny" {
|
||||
t.Errorf("Policy was not updated, default_policy = %q, expected 'deny'",
|
||||
listener.policyManager.DefaultPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandlePolicyConfigUpdate_NonAdmin tests policy update rejection from non-admin
|
||||
func TestHandlePolicyConfigUpdate_NonAdmin(t *testing.T) {
|
||||
// Create admin signer
|
||||
adminSigner := p8k.MustNew()
|
||||
if err := adminSigner.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate admin keypair: %v", err)
|
||||
}
|
||||
adminHex := hex.Enc(adminSigner.Pub())
|
||||
|
||||
// Create non-admin signer
|
||||
nonAdminSigner := p8k.MustNew()
|
||||
if err := nonAdminSigner.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate non-admin keypair: %v", err)
|
||||
}
|
||||
|
||||
listener, _, cleanup := setupPolicyTestListener(t, adminHex)
|
||||
defer cleanup()
|
||||
|
||||
// Create policy update event from non-admin
|
||||
newPolicyJSON := `{"default_policy": "deny"}`
|
||||
ev := createPolicyConfigEvent(t, nonAdminSigner, newPolicyJSON)
|
||||
|
||||
// Handle the event - should be rejected
|
||||
err := listener.HandlePolicyConfigUpdate(ev)
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-admin update but got none")
|
||||
}
|
||||
|
||||
// Verify policy was NOT updated
|
||||
if listener.policyManager.DefaultPolicy != "allow" {
|
||||
t.Error("Policy should not have been updated by non-admin")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandlePolicyConfigUpdate_InvalidJSON tests rejection of invalid JSON
|
||||
func TestHandlePolicyConfigUpdate_InvalidJSON(t *testing.T) {
|
||||
adminSigner := p8k.MustNew()
|
||||
if err := adminSigner.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate admin keypair: %v", err)
|
||||
}
|
||||
adminHex := hex.Enc(adminSigner.Pub())
|
||||
|
||||
listener, _, cleanup := setupPolicyTestListener(t, adminHex)
|
||||
defer cleanup()
|
||||
|
||||
// Create event with invalid JSON
|
||||
ev := createPolicyConfigEvent(t, adminSigner, `{"invalid json`)
|
||||
|
||||
err := listener.HandlePolicyConfigUpdate(ev)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid JSON but got none")
|
||||
}
|
||||
|
||||
// Policy should remain unchanged
|
||||
if listener.policyManager.DefaultPolicy != "allow" {
|
||||
t.Error("Policy should not have been updated with invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandlePolicyConfigUpdate_InvalidPubkey tests rejection of invalid admin pubkeys
|
||||
func TestHandlePolicyConfigUpdate_InvalidPubkey(t *testing.T) {
|
||||
adminSigner := p8k.MustNew()
|
||||
if err := adminSigner.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate admin keypair: %v", err)
|
||||
}
|
||||
adminHex := hex.Enc(adminSigner.Pub())
|
||||
|
||||
listener, _, cleanup := setupPolicyTestListener(t, adminHex)
|
||||
defer cleanup()
|
||||
|
||||
// Try to update with invalid admin pubkey
|
||||
invalidPolicyJSON := `{
|
||||
"default_policy": "deny",
|
||||
"policy_admins": ["not-a-valid-pubkey"]
|
||||
}`
|
||||
ev := createPolicyConfigEvent(t, adminSigner, invalidPolicyJSON)
|
||||
|
||||
err := listener.HandlePolicyConfigUpdate(ev)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid admin pubkey but got none")
|
||||
}
|
||||
|
||||
// Policy should remain unchanged
|
||||
if listener.policyManager.DefaultPolicy != "allow" {
|
||||
t.Error("Policy should not have been updated with invalid admin pubkey")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandlePolicyConfigUpdate_AdminCannotRemoveSelf tests that admin can update policy
|
||||
func TestHandlePolicyConfigUpdate_AdminCanUpdateAdminList(t *testing.T) {
|
||||
adminSigner := p8k.MustNew()
|
||||
if err := adminSigner.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate admin keypair: %v", err)
|
||||
}
|
||||
adminHex := hex.Enc(adminSigner.Pub())
|
||||
|
||||
// Create second admin
|
||||
admin2Hex := "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
|
||||
|
||||
listener, _, cleanup := setupPolicyTestListener(t, adminHex)
|
||||
defer cleanup()
|
||||
|
||||
// Update policy to add second admin
|
||||
newPolicyJSON := `{
|
||||
"default_policy": "allow",
|
||||
"policy_admins": ["` + adminHex + `", "` + admin2Hex + `"]
|
||||
}`
|
||||
ev := createPolicyConfigEvent(t, adminSigner, newPolicyJSON)
|
||||
|
||||
err := listener.HandlePolicyConfigUpdate(ev)
|
||||
if err != nil {
|
||||
t.Errorf("Expected success but got error: %v", err)
|
||||
}
|
||||
|
||||
// Verify both admins are now in the list
|
||||
admin2Bin, _ := hex.Dec(admin2Hex)
|
||||
if !listener.policyManager.IsPolicyAdmin(admin2Bin) {
|
||||
t.Error("Second admin should have been added to admin list")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandlePolicyAdminFollowListUpdate tests follow list update from admin
|
||||
func TestHandlePolicyAdminFollowListUpdate(t *testing.T) {
|
||||
adminSigner := p8k.MustNew()
|
||||
if err := adminSigner.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate admin keypair: %v", err)
|
||||
}
|
||||
adminHex := hex.Enc(adminSigner.Pub())
|
||||
|
||||
listener, db, cleanup := setupPolicyTestListener(t, adminHex)
|
||||
defer cleanup()
|
||||
|
||||
// Create a kind 3 follow list event from admin
|
||||
ev := event.New()
|
||||
ev.CreatedAt = time.Now().Unix()
|
||||
ev.Kind = kind.FollowList.K
|
||||
ev.Content = []byte("")
|
||||
ev.Tags = tag.NewS()
|
||||
|
||||
// Add some follows
|
||||
follow1Hex := "1111111111111111111111111111111111111111111111111111111111111111"
|
||||
follow2Hex := "2222222222222222222222222222222222222222222222222222222222222222"
|
||||
ev.Tags.Append(tag.NewFromAny("p", follow1Hex))
|
||||
ev.Tags.Append(tag.NewFromAny("p", follow2Hex))
|
||||
|
||||
if err := ev.Sign(adminSigner); err != nil {
|
||||
t.Fatalf("Failed to sign event: %v", err)
|
||||
}
|
||||
|
||||
// Save the event to database first
|
||||
if _, err := db.SaveEvent(listener.ctx, ev); err != nil {
|
||||
t.Fatalf("Failed to save follow list event: %v", err)
|
||||
}
|
||||
|
||||
// Handle the follow list update
|
||||
err := listener.HandlePolicyAdminFollowListUpdate(ev)
|
||||
if err != nil {
|
||||
t.Errorf("Expected success but got error: %v", err)
|
||||
}
|
||||
|
||||
// Verify follows were added
|
||||
follow1Bin, _ := hex.Dec(follow1Hex)
|
||||
follow2Bin, _ := hex.Dec(follow2Hex)
|
||||
|
||||
if !listener.policyManager.IsPolicyFollow(follow1Bin) {
|
||||
t.Error("Follow 1 should have been added to policy follows")
|
||||
}
|
||||
if !listener.policyManager.IsPolicyFollow(follow2Bin) {
|
||||
t.Error("Follow 2 should have been added to policy follows")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsPolicyAdminFollowListEvent tests detection of admin follow list events
|
||||
func TestIsPolicyAdminFollowListEvent(t *testing.T) {
|
||||
adminSigner := p8k.MustNew()
|
||||
if err := adminSigner.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate admin keypair: %v", err)
|
||||
}
|
||||
adminHex := hex.Enc(adminSigner.Pub())
|
||||
|
||||
nonAdminSigner := p8k.MustNew()
|
||||
if err := nonAdminSigner.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate non-admin keypair: %v", err)
|
||||
}
|
||||
|
||||
listener, _, cleanup := setupPolicyTestListener(t, adminHex)
|
||||
defer cleanup()
|
||||
|
||||
// Test admin's kind 3 event
|
||||
adminFollowEv := event.New()
|
||||
adminFollowEv.Kind = kind.FollowList.K
|
||||
adminFollowEv.Tags = tag.NewS()
|
||||
if err := adminFollowEv.Sign(adminSigner); err != nil {
|
||||
t.Fatalf("Failed to sign event: %v", err)
|
||||
}
|
||||
|
||||
if !listener.IsPolicyAdminFollowListEvent(adminFollowEv) {
|
||||
t.Error("Should detect admin's follow list event")
|
||||
}
|
||||
|
||||
// Test non-admin's kind 3 event
|
||||
nonAdminFollowEv := event.New()
|
||||
nonAdminFollowEv.Kind = kind.FollowList.K
|
||||
nonAdminFollowEv.Tags = tag.NewS()
|
||||
if err := nonAdminFollowEv.Sign(nonAdminSigner); err != nil {
|
||||
t.Fatalf("Failed to sign event: %v", err)
|
||||
}
|
||||
|
||||
if listener.IsPolicyAdminFollowListEvent(nonAdminFollowEv) {
|
||||
t.Error("Should not detect non-admin's follow list event")
|
||||
}
|
||||
|
||||
// Test admin's non-kind-3 event
|
||||
adminOtherEv := event.New()
|
||||
adminOtherEv.Kind = 1 // Kind 1, not follow list
|
||||
adminOtherEv.Tags = tag.NewS()
|
||||
if err := adminOtherEv.Sign(adminSigner); err != nil {
|
||||
t.Fatalf("Failed to sign event: %v", err)
|
||||
}
|
||||
|
||||
if listener.IsPolicyAdminFollowListEvent(adminOtherEv) {
|
||||
t.Error("Should not detect admin's non-follow-list event")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsPolicyConfigEvent tests detection of policy config events
|
||||
func TestIsPolicyConfigEvent(t *testing.T) {
|
||||
signer := p8k.MustNew()
|
||||
if err := signer.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate keypair: %v", err)
|
||||
}
|
||||
|
||||
// Kind 12345 event
|
||||
policyEv := event.New()
|
||||
policyEv.Kind = kind.PolicyConfig.K
|
||||
policyEv.Tags = tag.NewS()
|
||||
if err := policyEv.Sign(signer); err != nil {
|
||||
t.Fatalf("Failed to sign event: %v", err)
|
||||
}
|
||||
|
||||
if !IsPolicyConfigEvent(policyEv) {
|
||||
t.Error("Should detect kind 12345 as policy config event")
|
||||
}
|
||||
|
||||
// Non-policy event
|
||||
otherEv := event.New()
|
||||
otherEv.Kind = 1
|
||||
otherEv.Tags = tag.NewS()
|
||||
if err := otherEv.Sign(signer); err != nil {
|
||||
t.Fatalf("Failed to sign event: %v", err)
|
||||
}
|
||||
|
||||
if IsPolicyConfigEvent(otherEv) {
|
||||
t.Error("Should not detect kind 1 as policy config event")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMessageProcessingPauseDuringPolicyUpdate tests that message processing is paused
|
||||
func TestMessageProcessingPauseDuringPolicyUpdate(t *testing.T) {
|
||||
adminSigner := p8k.MustNew()
|
||||
if err := adminSigner.Generate(); err != nil {
|
||||
t.Fatalf("Failed to generate admin keypair: %v", err)
|
||||
}
|
||||
adminHex := hex.Enc(adminSigner.Pub())
|
||||
|
||||
listener, _, cleanup := setupPolicyTestListener(t, adminHex)
|
||||
defer cleanup()
|
||||
|
||||
// Track if pause was called
|
||||
pauseCalled := false
|
||||
resumeCalled := false
|
||||
|
||||
// We can't easily mock the mutex, but we can verify the policy update succeeds
|
||||
// which implies the pause/resume cycle completed
|
||||
|
||||
newPolicyJSON := `{
|
||||
"default_policy": "deny",
|
||||
"policy_admins": ["` + adminHex + `"]
|
||||
}`
|
||||
ev := createPolicyConfigEvent(t, adminSigner, newPolicyJSON)
|
||||
|
||||
err := listener.HandlePolicyConfigUpdate(ev)
|
||||
if err != nil {
|
||||
t.Errorf("Policy update failed: %v", err)
|
||||
}
|
||||
|
||||
// If we got here without deadlock, the pause/resume worked
|
||||
_ = pauseCalled
|
||||
_ = resumeCalled
|
||||
|
||||
// Verify policy was actually updated
|
||||
if listener.policyManager.DefaultPolicy != "deny" {
|
||||
t.Error("Policy should have been updated")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user