package engine import ( "fmt" "os" "path/filepath" "testing" "git.dern.ovh/infrastructure/caddy-opnsense-blocker/internal/config" "git.dern.ovh/infrastructure/caddy-opnsense-blocker/internal/model" ) func TestEvaluatorManualOverridesTakePriority(t *testing.T) { t.Parallel() profile := loadProfile(t, ` auto_block: true block_unexpected_posts: true block_php_paths: true suspicious_path_prefixes: - /wp-admin `) evaluator := NewEvaluator() record := model.AccessLogRecord{ClientIP: "203.0.113.10", Status: 404, Method: "GET", Path: "/wp-admin/install.php", UserAgent: "curl/8.0"} if decision := evaluator.Evaluate(record, profile, model.ManualOverrideForceAllow); decision.Action != model.DecisionActionAllow { t.Fatalf("expected manual allow to win, got %+v", decision) } if decision := evaluator.Evaluate(record, profile, model.ManualOverrideForceBlock); decision.Action != model.DecisionActionBlock { t.Fatalf("expected manual block to win, got %+v", decision) } } func TestEvaluatorBlocksSuspiciousRequests(t *testing.T) { t.Parallel() profile := loadProfile(t, ` auto_block: true block_unexpected_posts: true block_php_paths: true allowed_post_paths: - /search suspicious_path_prefixes: - /wp-admin `) evaluator := NewEvaluator() record := model.AccessLogRecord{ClientIP: "203.0.113.11", Status: 404, Method: "POST", Path: "/wp-admin/install.php", UserAgent: "curl/8.0"} decision := evaluator.Evaluate(record, profile, model.ManualOverrideNone) if decision.Action != model.DecisionActionBlock { t.Fatalf("expected block decision, got %+v", decision) } if len(decision.Reasons) < 2 { t.Fatalf("expected multiple blocking reasons, got %+v", decision) } } func TestEvaluatorAllowsExcludedCIDRAndKnownAgents(t *testing.T) { t.Parallel() profile := loadProfile(t, ` auto_block: true excluded_cidrs: - 10.0.0.0/8 known_agents: - name: friendly-bot decision: allow user_agent_prefixes: - FriendlyBot/ `) evaluator := NewEvaluator() excluded := model.AccessLogRecord{ClientIP: "10.0.0.5", Status: 404, Method: "GET", Path: "/wp-login.php", UserAgent: "curl/8.0"} if decision := evaluator.Evaluate(excluded, profile, model.ManualOverrideNone); decision.Action != model.DecisionActionAllow { t.Fatalf("expected excluded cidr to be allowed, got %+v", decision) } knownAgent := model.AccessLogRecord{ClientIP: "203.0.113.12", Status: 404, Method: "GET", Path: "/wp-login.php", UserAgent: "FriendlyBot/2.0"} if decision := evaluator.Evaluate(knownAgent, profile, model.ManualOverrideNone); decision.Action != model.DecisionActionAllow { t.Fatalf("expected known agent to be allowed, got %+v", decision) } } func TestEvaluatorReturnsReviewWhenAutoBlockIsDisabled(t *testing.T) { t.Parallel() profile := loadProfile(t, ` auto_block: false block_unexpected_posts: true suspicious_path_prefixes: - /admin `) evaluator := NewEvaluator() record := model.AccessLogRecord{ClientIP: "203.0.113.13", Status: 404, Method: "POST", Path: "/admin", UserAgent: "curl/8.0"} decision := evaluator.Evaluate(record, profile, model.ManualOverrideNone) if decision.Action != model.DecisionActionReview { t.Fatalf("expected review decision, got %+v", decision) } } func loadProfile(t *testing.T, profileSnippet string) config.ProfileConfig { t.Helper() tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.yaml") payload := fmt.Sprintf(`profiles: main:%s sources: - name: main path: %s/access.json profile: main `, indent(profileSnippet, 4), tempDir) if err := os.WriteFile(configPath, []byte(payload), 0o600); err != nil { t.Fatalf("write config file: %v", err) } cfg, err := config.Load(configPath) if err != nil { t.Fatalf("load config: %v", err) } return cfg.Profiles["main"] } func indent(value string, spaces int) string { padding := "" for range spaces { padding += " " } lines := []byte(value) _ = lines var output string for _, line := range splitLines(value) { trimmed := line if trimmed == "" { output += "\n" continue } output += "\n" + padding + trimmed } return output } func splitLines(value string) []string { var lines []string start := 0 for index, character := range value { if character == '\n' { lines = append(lines, value[start:index]) start = index + 1 } } lines = append(lines, value[start:]) return lines }