follows_test.go raw

   1  package policy
   2  
   3  import (
   4  	"context"
   5  	"os"
   6  	"path/filepath"
   7  	"testing"
   8  	"time"
   9  
  10  	"github.com/adrg/xdg"
  11  	"next.orly.dev/pkg/nostr/encoders/hex"
  12  )
  13  
  14  // setupTestPolicy creates a policy manager with a temporary config file.
  15  // Returns the policy and a cleanup function.
  16  func setupTestPolicy(t *testing.T, appName string) (*P, func()) {
  17  	t.Helper()
  18  
  19  	// Create config directory at XDG path
  20  	configDir := filepath.Join(xdg.ConfigHome, appName)
  21  	if err := os.MkdirAll(configDir, 0755); err != nil {
  22  		t.Fatalf("Failed to create config dir: %v", err)
  23  	}
  24  
  25  	// Create default policy.json
  26  	configPath := filepath.Join(configDir, "policy.json")
  27  	defaultPolicy := []byte(`{"default_policy": "allow"}`)
  28  	if err := os.WriteFile(configPath, defaultPolicy, 0644); err != nil {
  29  		t.Fatalf("Failed to write policy file: %v", err)
  30  	}
  31  
  32  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  33  
  34  	policy := NewWithManager(ctx, appName, true, "")
  35  	if policy == nil {
  36  		cancel()
  37  		os.RemoveAll(configDir)
  38  		t.Fatal("Failed to create policy manager")
  39  	}
  40  
  41  	cleanup := func() {
  42  		cancel()
  43  		os.RemoveAll(configDir)
  44  	}
  45  
  46  	return policy, cleanup
  47  }
  48  
  49  // TestIsPolicyAdmin tests the IsPolicyAdmin method
  50  func TestIsPolicyAdmin(t *testing.T) {
  51  	policy, cleanup := setupTestPolicy(t, "test-policy-admin")
  52  	defer cleanup()
  53  
  54  	// Set up policy with admins
  55  	admin1Hex := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  56  	admin2Hex := "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
  57  	nonAdminHex := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
  58  
  59  	policyJSON := []byte(`{
  60  		"policy_admins": [
  61  			"` + admin1Hex + `",
  62  			"` + admin2Hex + `"
  63  		]
  64  	}`)
  65  
  66  	tmpDir := t.TempDir()
  67  	if err := policy.Reload(policyJSON, tmpDir+"/policy.json"); err != nil {
  68  		t.Fatalf("Failed to reload policy: %v", err)
  69  	}
  70  
  71  	// Convert hex to bytes for testing
  72  	admin1Bin, _ := hex.Dec(admin1Hex)
  73  	admin2Bin, _ := hex.Dec(admin2Hex)
  74  	nonAdminBin, _ := hex.Dec(nonAdminHex)
  75  
  76  	tests := []struct {
  77  		name     string
  78  		pubkey   []byte
  79  		expected bool
  80  	}{
  81  		{
  82  			name:     "first admin is recognized",
  83  			pubkey:   admin1Bin,
  84  			expected: true,
  85  		},
  86  		{
  87  			name:     "second admin is recognized",
  88  			pubkey:   admin2Bin,
  89  			expected: true,
  90  		},
  91  		{
  92  			name:     "non-admin is not recognized",
  93  			pubkey:   nonAdminBin,
  94  			expected: false,
  95  		},
  96  		{
  97  			name:     "nil pubkey returns false",
  98  			pubkey:   nil,
  99  			expected: false,
 100  		},
 101  		{
 102  			name:     "empty pubkey returns false",
 103  			pubkey:   []byte{},
 104  			expected: false,
 105  		},
 106  		{
 107  			name:     "wrong length pubkey returns false",
 108  			pubkey:   []byte{0x01, 0x02, 0x03},
 109  			expected: false,
 110  		},
 111  	}
 112  
 113  	for _, tt := range tests {
 114  		t.Run(tt.name, func(t *testing.T) {
 115  			result := policy.IsPolicyAdmin(tt.pubkey)
 116  			if result != tt.expected {
 117  				t.Errorf("IsPolicyAdmin() = %v, expected %v", result, tt.expected)
 118  			}
 119  		})
 120  	}
 121  }
 122  
 123  // TestIsPolicyFollow tests the IsPolicyFollow method
 124  func TestIsPolicyFollow(t *testing.T) {
 125  	policy, cleanup := setupTestPolicy(t, "test-policy-follow")
 126  	defer cleanup()
 127  
 128  	// Set up some follows
 129  	follow1Hex := "1111111111111111111111111111111111111111111111111111111111111111"
 130  	follow2Hex := "2222222222222222222222222222222222222222222222222222222222222222"
 131  	nonFollowHex := "3333333333333333333333333333333333333333333333333333333333333333"
 132  
 133  	follow1Bin, _ := hex.Dec(follow1Hex)
 134  	follow2Bin, _ := hex.Dec(follow2Hex)
 135  	nonFollowBin, _ := hex.Dec(nonFollowHex)
 136  
 137  	// Update policy follows directly
 138  	policy.UpdatePolicyFollows([][]byte{follow1Bin, follow2Bin})
 139  
 140  	tests := []struct {
 141  		name     string
 142  		pubkey   []byte
 143  		expected bool
 144  	}{
 145  		{
 146  			name:     "first follow is recognized",
 147  			pubkey:   follow1Bin,
 148  			expected: true,
 149  		},
 150  		{
 151  			name:     "second follow is recognized",
 152  			pubkey:   follow2Bin,
 153  			expected: true,
 154  		},
 155  		{
 156  			name:     "non-follow is not recognized",
 157  			pubkey:   nonFollowBin,
 158  			expected: false,
 159  		},
 160  		{
 161  			name:     "nil pubkey returns false",
 162  			pubkey:   nil,
 163  			expected: false,
 164  		},
 165  		{
 166  			name:     "empty pubkey returns false",
 167  			pubkey:   []byte{},
 168  			expected: false,
 169  		},
 170  	}
 171  
 172  	for _, tt := range tests {
 173  		t.Run(tt.name, func(t *testing.T) {
 174  			result := policy.IsPolicyFollow(tt.pubkey)
 175  			if result != tt.expected {
 176  				t.Errorf("IsPolicyFollow() = %v, expected %v", result, tt.expected)
 177  			}
 178  		})
 179  	}
 180  }
 181  
 182  // TestUpdatePolicyFollows tests the UpdatePolicyFollows method
 183  func TestUpdatePolicyFollows(t *testing.T) {
 184  	policy, cleanup := setupTestPolicy(t, "test-update-follows")
 185  	defer cleanup()
 186  
 187  	// Initially no follows
 188  	testPubkey, _ := hex.Dec("1111111111111111111111111111111111111111111111111111111111111111")
 189  	if policy.IsPolicyFollow(testPubkey) {
 190  		t.Error("Expected no follows initially")
 191  	}
 192  
 193  	// Add follows
 194  	follows := [][]byte{testPubkey}
 195  	policy.UpdatePolicyFollows(follows)
 196  
 197  	if !policy.IsPolicyFollow(testPubkey) {
 198  		t.Error("Expected pubkey to be a follow after update")
 199  	}
 200  
 201  	// Update with empty list
 202  	policy.UpdatePolicyFollows([][]byte{})
 203  	if policy.IsPolicyFollow(testPubkey) {
 204  		t.Error("Expected pubkey to not be a follow after clearing")
 205  	}
 206  
 207  	// Update with nil
 208  	policy.UpdatePolicyFollows(nil)
 209  	if policy.IsPolicyFollow(testPubkey) {
 210  		t.Error("Expected pubkey to not be a follow after nil update")
 211  	}
 212  }
 213  
 214  // TestIsPolicyFollowWhitelistEnabled tests the IsPolicyFollowWhitelistEnabled method
 215  func TestIsPolicyFollowWhitelistEnabled(t *testing.T) {
 216  	policy, cleanup := setupTestPolicy(t, "test-whitelist-enabled")
 217  	defer cleanup()
 218  
 219  	tmpDir := t.TempDir()
 220  
 221  	// Test with disabled
 222  	policyJSON := []byte(`{"policy_follow_whitelist_enabled": false}`)
 223  	if err := policy.Reload(policyJSON, tmpDir+"/policy.json"); err != nil {
 224  		t.Fatalf("Failed to reload policy: %v", err)
 225  	}
 226  
 227  	if policy.IsPolicyFollowWhitelistEnabled() {
 228  		t.Error("Expected follow whitelist to be disabled")
 229  	}
 230  
 231  	// Test with enabled
 232  	policyJSON = []byte(`{"policy_follow_whitelist_enabled": true}`)
 233  	if err := policy.Reload(policyJSON, tmpDir+"/policy.json"); err != nil {
 234  		t.Fatalf("Failed to reload policy: %v", err)
 235  	}
 236  
 237  	if !policy.IsPolicyFollowWhitelistEnabled() {
 238  		t.Error("Expected follow whitelist to be enabled")
 239  	}
 240  }
 241  
 242  // TestGetPolicyAdminsBin tests the GetPolicyAdminsBin method
 243  func TestGetPolicyAdminsBin(t *testing.T) {
 244  	policy, cleanup := setupTestPolicy(t, "test-get-admins-bin")
 245  	defer cleanup()
 246  
 247  	admin1Hex := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
 248  	admin2Hex := "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
 249  
 250  	policyJSON := []byte(`{
 251  		"policy_admins": ["` + admin1Hex + `", "` + admin2Hex + `"]
 252  	}`)
 253  
 254  	tmpDir := t.TempDir()
 255  	if err := policy.Reload(policyJSON, tmpDir+"/policy.json"); err != nil {
 256  		t.Fatalf("Failed to reload policy: %v", err)
 257  	}
 258  
 259  	admins := policy.GetPolicyAdminsBin()
 260  	if len(admins) != 2 {
 261  		t.Errorf("Expected 2 admins, got %d", len(admins))
 262  	}
 263  
 264  	// Verify it's a copy (modification shouldn't affect original)
 265  	if len(admins) > 0 {
 266  		admins[0][0] = 0xFF
 267  		originalAdmins := policy.GetPolicyAdminsBin()
 268  		if originalAdmins[0][0] == 0xFF {
 269  			t.Error("GetPolicyAdminsBin should return a copy, not the original slice")
 270  		}
 271  	}
 272  }
 273  
 274  // TestFollowListConcurrency tests concurrent access to follow list
 275  func TestFollowListConcurrency(t *testing.T) {
 276  	policy, cleanup := setupTestPolicy(t, "test-concurrency")
 277  	defer cleanup()
 278  
 279  	testPubkey, _ := hex.Dec("1111111111111111111111111111111111111111111111111111111111111111")
 280  
 281  	// Run concurrent reads and writes
 282  	done := make(chan bool)
 283  	for i := 0; i < 10; i++ {
 284  		go func() {
 285  			for j := 0; j < 100; j++ {
 286  				policy.UpdatePolicyFollows([][]byte{testPubkey})
 287  				_ = policy.IsPolicyFollow(testPubkey)
 288  				_ = policy.IsPolicyAdmin(testPubkey)
 289  			}
 290  			done <- true
 291  		}()
 292  	}
 293  
 294  	// Wait for all goroutines
 295  	for i := 0; i < 10; i++ {
 296  		<-done
 297  	}
 298  }
 299  
 300  // TestPolicyAdminAndFollowInteraction tests the interaction between admin and follow checks
 301  func TestPolicyAdminAndFollowInteraction(t *testing.T) {
 302  	policy, cleanup := setupTestPolicy(t, "test-admin-follow-interaction")
 303  	defer cleanup()
 304  
 305  	// An admin who is also followed
 306  	adminHex := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
 307  	adminBin, _ := hex.Dec(adminHex)
 308  
 309  	policyJSON := []byte(`{
 310  		"policy_admins": ["` + adminHex + `"],
 311  		"policy_follow_whitelist_enabled": true
 312  	}`)
 313  
 314  	tmpDir := t.TempDir()
 315  	if err := policy.Reload(policyJSON, tmpDir+"/policy.json"); err != nil {
 316  		t.Fatalf("Failed to reload policy: %v", err)
 317  	}
 318  
 319  	// Admin should be recognized as admin
 320  	if !policy.IsPolicyAdmin(adminBin) {
 321  		t.Error("Expected admin to be recognized as admin")
 322  	}
 323  
 324  	// Admin is not automatically a follow
 325  	if policy.IsPolicyFollow(adminBin) {
 326  		t.Error("Admin should not automatically be a follow")
 327  	}
 328  
 329  	// Now add admin as a follow
 330  	policy.UpdatePolicyFollows([][]byte{adminBin})
 331  
 332  	// Should be both admin and follow
 333  	if !policy.IsPolicyAdmin(adminBin) {
 334  		t.Error("Expected admin to still be recognized as admin")
 335  	}
 336  	if !policy.IsPolicyFollow(adminBin) {
 337  		t.Error("Expected admin to now be recognized as follow")
 338  	}
 339  }
 340