package crowdsec import ( "context" "io/fs" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" ) func writeFakeCSCLI(t *testing.T, script string) (binDir string) { t.Helper() binDir = t.TempDir() path := filepath.Join(binDir, "cscli") requireMode := fs.FileMode(0o755) err := os.WriteFile(path, []byte(script), requireMode) if err != nil { t.Fatalf("failed to write fake cscli: %v", err) } return binDir } func withEnv(t *testing.T, key, value string, fn func()) { t.Helper() old, had := os.LookupEnv(key) if value == "" { _ = os.Unsetenv(key) } else { _ = os.Setenv(key, value) } t.Cleanup(func() { if had { _ = os.Setenv(key, old) } else { _ = os.Unsetenv(key) } }) fn() } func withPath(t *testing.T, newPath string, fn func()) { t.Helper() old, had := os.LookupEnv("PATH") _ = os.Setenv("PATH", newPath) t.Cleanup(func() { if had { _ = os.Setenv("PATH", old) } else { _ = os.Unsetenv("PATH") } }) fn() } func TestCheckLAPIHealth_Healthy(t *testing.T) { // Create a mock LAPI server that returns 200 OK with JSON content-type server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/health" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"status":"ok"}`)) return } w.WriteHeader(http.StatusNotFound) })) defer server.Close() healthy := CheckLAPIHealth(server.URL) assert.True(t, healthy, "LAPI should be healthy") } func TestCheckLAPIHealth_Unhealthy(t *testing.T) { // Create a mock LAPI server that returns 500 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() healthy := CheckLAPIHealth(server.URL) assert.False(t, healthy, "LAPI should be unhealthy") } func TestCheckLAPIHealth_Unreachable(t *testing.T) { // Use an invalid URL that won't connect healthy := CheckLAPIHealth("http://127.0.0.1:19999") assert.False(t, healthy, "LAPI should be unreachable") } func TestCheckLAPIHealth_FallbackToDecisions(t *testing.T) { // Create a mock LAPI server where /health fails but /v1/decisions returns 401 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/health" { w.WriteHeader(http.StatusNotFound) return } if r.URL.Path == "/v1/decisions" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) // Expected without auth return } w.WriteHeader(http.StatusNotFound) })) defer server.Close() healthy := CheckLAPIHealth(server.URL) // Should fallback to decisions endpoint check which returns 401 (indicates running) assert.True(t, healthy, "LAPI should be healthy via decisions fallback") } func TestCheckLAPIHealth_DefaultURL(t *testing.T) { // With empty URL, should use default (which won't be running in test) healthy := CheckLAPIHealth("") assert.False(t, healthy, "Default LAPI should not be running in test environment") } func TestGetBouncerAPIKey_FromEnv(t *testing.T) { // Save and restore original env original := os.Getenv("CROWDSEC_API_KEY") defer func() { if original != "" { _ = os.Setenv("CROWDSEC_API_KEY", original) } else { _ = os.Unsetenv("CROWDSEC_API_KEY") } }() // Set test value _ = os.Setenv("CROWDSEC_API_KEY", "test-api-key-123") key := getBouncerAPIKey() assert.Equal(t, "test-api-key-123", key) } func TestGetBouncerAPIKey_Empty(t *testing.T) { // Save and restore original env vars envVars := []string{ "CROWDSEC_API_KEY", "CROWDSEC_BOUNCER_API_KEY", "CERBERUS_SECURITY_CROWDSEC_API_KEY", "CHARON_SECURITY_CROWDSEC_API_KEY", "CPM_SECURITY_CROWDSEC_API_KEY", } originals := make(map[string]string) for _, key := range envVars { originals[key] = os.Getenv(key) _ = os.Unsetenv(key) } defer func() { for key, val := range originals { if val != "" { _ = os.Setenv(key, val) } } }() key := getBouncerAPIKey() assert.Empty(t, key) } func TestGetBouncerAPIKey_Fallback(t *testing.T) { // Test fallback to secondary env var envVars := []string{ "CROWDSEC_API_KEY", "CROWDSEC_BOUNCER_API_KEY", "CERBERUS_SECURITY_CROWDSEC_API_KEY", "CHARON_SECURITY_CROWDSEC_API_KEY", "CPM_SECURITY_CROWDSEC_API_KEY", } originals := make(map[string]string) for _, key := range envVars { originals[key] = os.Getenv(key) _ = os.Unsetenv(key) } defer func() { for key, val := range originals { if val != "" { _ = os.Setenv(key, val) } } }() // Set only the fallback env var _ = os.Setenv("CERBERUS_SECURITY_CROWDSEC_API_KEY", "fallback-key-456") key := getBouncerAPIKey() assert.Equal(t, "fallback-key-456", key) } func TestEnsureBouncerRegistered_UsesEnvKey(t *testing.T) { withEnv(t, "CROWDSEC_API_KEY", "env-key", func() { key, err := EnsureBouncerRegistered(context.Background(), "") assert.NoError(t, err) assert.Equal(t, "env-key", key) }) } func TestEnsureBouncerRegistered_NoEnvNoCSCLI(t *testing.T) { // Ensure all key env vars are empty for _, k := range []string{"CROWDSEC_API_KEY", "CROWDSEC_BOUNCER_API_KEY", "CERBERUS_SECURITY_CROWDSEC_API_KEY", "CHARON_SECURITY_CROWDSEC_API_KEY", "CPM_SECURITY_CROWDSEC_API_KEY"} { withEnv(t, k, "", func() {}) } withPath(t, "", func() { _, err := EnsureBouncerRegistered(context.Background(), "") assert.Error(t, err) }) } func TestEnsureBouncerRegistered_ReturnsExistingBouncerKey(t *testing.T) { for _, k := range []string{"CROWDSEC_API_KEY", "CROWDSEC_BOUNCER_API_KEY", "CERBERUS_SECURITY_CROWDSEC_API_KEY", "CHARON_SECURITY_CROWDSEC_API_KEY", "CPM_SECURITY_CROWDSEC_API_KEY"} { withEnv(t, k, "", func() {}) } origPath := os.Getenv("PATH") binDir := writeFakeCSCLI(t, `#!/usr/bin/env bash set -euo pipefail if [[ "$1" == "bouncers" && "$2" == "list" ]]; then echo '[{"name":"caddy-bouncer","api_key":"existing-key","ip_address":"","valid":true,"created_at":"2025-01-01T00:00:00Z"}]' exit 0 fi echo "unexpected args" >&2 exit 2 `) withPath(t, binDir+":"+origPath, func() { key, err := EnsureBouncerRegistered(context.Background(), "") assert.NoError(t, err) assert.Equal(t, "existing-key", key) }) } func TestEnsureBouncerRegistered_RegistersNewWhenNoneExists(t *testing.T) { for _, k := range []string{"CROWDSEC_API_KEY", "CROWDSEC_BOUNCER_API_KEY", "CERBERUS_SECURITY_CROWDSEC_API_KEY", "CHARON_SECURITY_CROWDSEC_API_KEY", "CPM_SECURITY_CROWDSEC_API_KEY"} { withEnv(t, k, "", func() {}) } origPath := os.Getenv("PATH") binDir := writeFakeCSCLI(t, `#!/usr/bin/env bash set -euo pipefail if [[ "$1" == "bouncers" && "$2" == "list" ]]; then echo '[]' exit 0 fi if [[ "$1" == "bouncers" && "$2" == "add" ]]; then echo 'new-key' exit 0 fi echo "unexpected args" >&2 exit 2 `) withPath(t, binDir+":"+origPath, func() { key, err := EnsureBouncerRegistered(context.Background(), "") assert.NoError(t, err) assert.Equal(t, "new-key", key) }) } func TestGetLAPIVersion_JSON(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/version" { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"version":"1.2.3"}`)) })) defer server.Close() ver, err := GetLAPIVersion(context.Background(), server.URL) assert.NoError(t, err) assert.Equal(t, "1.2.3", ver) } func TestGetLAPIVersion_PlainText(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/version" { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("vX.Y.Z\n")) })) defer server.Close() ver, err := GetLAPIVersion(context.Background(), server.URL) assert.NoError(t, err) assert.Equal(t, "vX.Y.Z", ver) }