package policy import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "time" "github.com/adrg/xdg" ) // setupHotreloadTestPolicy creates a policy manager with a temporary config file for hotreload tests. func setupHotreloadTestPolicy(t *testing.T, appName string) (*P, func()) { t.Helper() configDir := filepath.Join(xdg.ConfigHome, appName) if err := os.MkdirAll(configDir, 0755); err != nil { t.Fatalf("Failed to create config dir: %v", err) } configPath := filepath.Join(configDir, "policy.json") defaultPolicy := []byte(`{"default_policy": "allow"}`) if err := os.WriteFile(configPath, defaultPolicy, 0644); err != nil { t.Fatalf("Failed to write policy file: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) policy := NewWithManager(ctx, appName, true) if policy == nil { cancel() os.RemoveAll(configDir) t.Fatal("Failed to create policy manager") } cleanup := func() { cancel() os.RemoveAll(configDir) } return policy, cleanup } // TestValidateJSON tests the ValidateJSON method with various inputs func TestValidateJSON(t *testing.T) { policy, cleanup := setupHotreloadTestPolicy(t, "test-validate-json") defer cleanup() tests := []struct { name string json []byte expectError bool errorSubstr string }{ { name: "valid empty policy", json: []byte(`{}`), expectError: false, }, { name: "valid complete policy", json: []byte(`{ "kind": {"whitelist": [1, 3, 7]}, "global": {"size_limit": 65536}, "rules": { "1": {"description": "Short text notes", "content_limit": 8192} }, "default_policy": "allow", "policy_admins": ["0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"], "policy_follow_whitelist_enabled": true }`), expectError: false, }, { name: "invalid JSON syntax", json: []byte(`{"invalid": json}`), expectError: true, errorSubstr: "invalid character", }, { name: "invalid JSON - missing closing brace", json: []byte(`{"kind": {"whitelist": [1]}`), expectError: true, }, { name: "invalid policy_admins - wrong length", json: []byte(`{ "policy_admins": ["not-64-chars"] }`), expectError: true, errorSubstr: "invalid policy_admin pubkey", }, { name: "invalid policy_admins - non-hex characters", json: []byte(`{ "policy_admins": ["zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"] }`), expectError: true, errorSubstr: "invalid policy_admin pubkey", }, { name: "valid policy_admins - multiple admins", json: []byte(`{ "policy_admins": [ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" ] }`), expectError: false, }, { name: "invalid tag_validation regex", json: []byte(`{ "rules": { "30023": { "tag_validation": { "d": "[invalid(regex" } } } }`), expectError: true, errorSubstr: "invalid regex", }, { name: "valid tag_validation regex", json: []byte(`{ "rules": { "30023": { "tag_validation": { "d": "^[a-z0-9-]{1,64}$", "t": "^[a-z0-9-]{1,32}$" } } } }`), expectError: false, }, { name: "invalid default_policy", json: []byte(`{ "default_policy": "invalid" }`), expectError: true, errorSubstr: "default_policy", }, { name: "valid default_policy allow", json: []byte(`{ "default_policy": "allow" }`), expectError: false, }, { name: "valid default_policy deny", json: []byte(`{ "default_policy": "deny" }`), expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := policy.ValidateJSON(tt.json) if tt.expectError { if err == nil { t.Errorf("Expected error but got none") return } if tt.errorSubstr != "" && !containsSubstring(err.Error(), tt.errorSubstr) { t.Errorf("Expected error containing %q, got: %v", tt.errorSubstr, err) } return } if err != nil { t.Errorf("Unexpected error: %v", err) } }) } } // TestReload tests the Reload method func TestReload(t *testing.T) { policy, cleanup := setupHotreloadTestPolicy(t, "test-reload") defer cleanup() // Create temp directory for policy files tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "policy.json") tests := []struct { name string initialJSON []byte reloadJSON []byte expectError bool checkAfter func(t *testing.T, p *P) }{ { name: "reload with valid policy", initialJSON: []byte(`{"default_policy": "allow"}`), reloadJSON: []byte(`{ "default_policy": "deny", "kind": {"whitelist": [1, 3]}, "policy_admins": ["0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"] }`), expectError: false, checkAfter: func(t *testing.T, p *P) { if p.DefaultPolicy != "deny" { t.Errorf("Expected default_policy to be 'deny', got %q", p.DefaultPolicy) } if len(p.Kind.Whitelist) != 2 { t.Errorf("Expected 2 whitelisted kinds, got %d", len(p.Kind.Whitelist)) } if len(p.PolicyAdmins) != 1 { t.Errorf("Expected 1 policy admin, got %d", len(p.PolicyAdmins)) } }, }, { name: "reload with invalid JSON fails without changes", initialJSON: []byte(`{"default_policy": "allow"}`), reloadJSON: []byte(`{"invalid json`), expectError: true, checkAfter: func(t *testing.T, p *P) { // Policy should remain unchanged if p.DefaultPolicy != "allow" { t.Errorf("Expected default_policy to remain 'allow', got %q", p.DefaultPolicy) } }, }, { name: "reload with invalid admin pubkey fails without changes", initialJSON: []byte(`{"default_policy": "allow"}`), reloadJSON: []byte(`{ "default_policy": "deny", "policy_admins": ["invalid-pubkey"] }`), expectError: true, checkAfter: func(t *testing.T, p *P) { // Policy should remain unchanged if p.DefaultPolicy != "allow" { t.Errorf("Expected default_policy to remain 'allow', got %q", p.DefaultPolicy) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Initialize policy with initial JSON if tt.initialJSON != nil { if err := policy.Reload(tt.initialJSON, configPath); err != nil { t.Fatalf("Failed to set initial policy: %v", err) } } // Attempt reload err := policy.Reload(tt.reloadJSON, configPath) if tt.expectError { if err == nil { t.Errorf("Expected error but got none") } } else { if err != nil { t.Errorf("Unexpected error: %v", err) } } // Run post-reload checks if tt.checkAfter != nil { tt.checkAfter(t, policy) } }) } } // TestSaveToFile tests atomic file writing func TestSaveToFile(t *testing.T) { policy, cleanup := setupHotreloadTestPolicy(t, "test-save-file") defer cleanup() tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "policy.json") // Load a policy policyJSON := []byte(`{ "default_policy": "allow", "kind": {"whitelist": [1, 3, 7]}, "policy_admins": ["0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"] }`) if err := policy.Reload(policyJSON, configPath); err != nil { t.Fatalf("Failed to reload policy: %v", err) } // Verify file was saved if _, err := os.Stat(configPath); os.IsNotExist(err) { t.Errorf("Policy file was not created at %s", configPath) } // Read and verify contents data, err := os.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read policy file: %v", err) } if len(data) == 0 { t.Error("Policy file is empty") } // Verify it's valid JSON var parsed map[string]interface{} if err := json.Unmarshal(data, &parsed); err != nil { t.Errorf("Policy file contains invalid JSON: %v", err) } } // TestPauseResume tests the Pause and Resume methods func TestPauseResume(t *testing.T) { policy, cleanup := setupHotreloadTestPolicy(t, "test-pause-resume") defer cleanup() // Test Pause if err := policy.Pause(); err != nil { t.Errorf("Pause failed: %v", err) } // Test Resume if err := policy.Resume(); err != nil { t.Errorf("Resume failed: %v", err) } // Test multiple pause/resume cycles for i := 0; i < 3; i++ { if err := policy.Pause(); err != nil { t.Errorf("Pause %d failed: %v", i, err) } if err := policy.Resume(); err != nil { t.Errorf("Resume %d failed: %v", i, err) } } } // TestReloadPreservesExistingOnFailure verifies that failed reloads don't corrupt state func TestReloadPreservesExistingOnFailure(t *testing.T) { policy, cleanup := setupHotreloadTestPolicy(t, "test-reload-preserve") defer cleanup() tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "policy.json") // Set up initial valid policy initialJSON := []byte(`{ "default_policy": "allow", "kind": {"whitelist": [1, 3, 7]}, "policy_admins": ["0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"], "policy_follow_whitelist_enabled": true }`) if err := policy.Reload(initialJSON, configPath); err != nil { t.Fatalf("Failed to set initial policy: %v", err) } // Store initial state initialDefaultPolicy := policy.DefaultPolicy initialKindWhitelist := len(policy.Kind.Whitelist) initialAdminCount := len(policy.PolicyAdmins) initialFollowEnabled := policy.PolicyFollowWhitelistEnabled // Attempt to reload with invalid JSON invalidJSON := []byte(`{"policy_admins": ["invalid"]}`) err := policy.Reload(invalidJSON, configPath) if err == nil { t.Fatal("Expected error for invalid policy_admins but got none") } // Verify state is preserved if policy.DefaultPolicy != initialDefaultPolicy { t.Errorf("DefaultPolicy changed from %q to %q after failed reload", initialDefaultPolicy, policy.DefaultPolicy) } if len(policy.Kind.Whitelist) != initialKindWhitelist { t.Errorf("Kind.Whitelist length changed from %d to %d after failed reload", initialKindWhitelist, len(policy.Kind.Whitelist)) } if len(policy.PolicyAdmins) != initialAdminCount { t.Errorf("PolicyAdmins length changed from %d to %d after failed reload", initialAdminCount, len(policy.PolicyAdmins)) } if policy.PolicyFollowWhitelistEnabled != initialFollowEnabled { t.Errorf("PolicyFollowWhitelistEnabled changed from %v to %v after failed reload", initialFollowEnabled, policy.PolicyFollowWhitelistEnabled) } } // containsSubstring checks if a string contains a substring (case-insensitive) func containsSubstring(s, substr string) bool { return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) }