add testing for redis with mock

This commit is contained in:
maxlerebourg
2026-07-25 16:30:36 +02:00
parent 6958432a7c
commit 1e0dadedb2
5 changed files with 109 additions and 4 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ export GO111MODULE=on
# Binary/mock suite (Traefik binary + mock LAPI). This is what CI runs. # Binary/mock suite (Traefik binary + mock LAPI). This is what CI runs.
# The local Docker suite (make e2e) lives in a separate PR/branch. # The local Docker suite (make e2e) lives in a separate PR/branch.
E2E_MOCK_SCENARIOS := stream-mode live-mode none-mode trusted-ips custom-ban-page captcha appsec tls-system-ca E2E_MOCK_SCENARIOS := $(notdir $(wildcard tests/e2e/mock/scenarios/*))
default: lint test default: lint test
+3
View File
@@ -20,6 +20,7 @@ WEB_PORT="${WEB_PORT:-8000}"
LAPI_PORT="${LAPI_PORT:-8090}" LAPI_PORT="${LAPI_PORT:-8090}"
BACKEND_PORT="${BACKEND_PORT:-8091}" BACKEND_PORT="${BACKEND_PORT:-8091}"
APPSEC_PORT="${APPSEC_PORT:-8092}" APPSEC_PORT="${APPSEC_PORT:-8092}"
REDIS_PORT="${REDIS_PORT:-8093}"
LAPI_KEY="${LAPI_KEY:-e2e-mock-key}" LAPI_KEY="${LAPI_KEY:-e2e-mock-key}"
MOCK_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MOCK_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -187,6 +188,7 @@ start_stack() {
-e "s|@@LAPI_HOST@@|127.0.0.1:${LAPI_PORT}|g" \ -e "s|@@LAPI_HOST@@|127.0.0.1:${LAPI_PORT}|g" \
-e "s|@@APPSEC_HOST@@|127.0.0.1:${APPSEC_PORT}|g" \ -e "s|@@APPSEC_HOST@@|127.0.0.1:${APPSEC_PORT}|g" \
-e "s|@@BACKEND_URL@@|http://127.0.0.1:${BACKEND_PORT}|g" \ -e "s|@@BACKEND_URL@@|http://127.0.0.1:${BACKEND_PORT}|g" \
-e "s|@@REDIS_HOST@@|127.0.0.1:${REDIS_PORT}|g" \
-e "s|@@SCENARIO_DIR@@|${scenario_dir}|g" \ -e "s|@@SCENARIO_DIR@@|${scenario_dir}|g" \
"$scenario_dir/dynamic.yml" > "$WORKDIR/dynamic.yml" "$scenario_dir/dynamic.yml" > "$WORKDIR/dynamic.yml"
@@ -203,6 +205,7 @@ start_stack() {
--lapi-addr "127.0.0.1:${LAPI_PORT}" \ --lapi-addr "127.0.0.1:${LAPI_PORT}" \
--backend-addr "127.0.0.1:${BACKEND_PORT}" \ --backend-addr "127.0.0.1:${BACKEND_PORT}" \
--appsec-addr "127.0.0.1:${APPSEC_PORT}" \ --appsec-addr "127.0.0.1:${APPSEC_PORT}" \
--redis-addr "127.0.0.1:${REDIS_PORT}" \
"${mock_tls_args[@]}" >"$WORKDIR/mock.log" 2>&1 & "${mock_tls_args[@]}" >"$WORKDIR/mock.log" 2>&1 &
MOCK_PID=$! MOCK_PID=$!
+53 -3
View File
@@ -2,7 +2,8 @@
// suite. It answers only the few LAPI routes the plugin calls — live/none // suite. It answers only the few LAPI routes the plugin calls — live/none
// decision lookups, the stream poll and the usage-metrics push — and lets the // decision lookups, the stream poll and the usage-metrics push — and lets the
// test drive decisions through /admin instead of `cscli`. It also serves the // test drive decisions through /admin instead of `cscli`. It also serves the
// stub upstream that Traefik proxies allowed requests to. // stub upstream that Traefik proxies allowed requests to, and a hardcoded Redis
// stand-in for exercising the redis cache path.
// //
// It is NOT a Crowdsec/AppSec conformance harness — the real WAF engine (OWASP // It is NOT a Crowdsec/AppSec conformance harness — the real WAF engine (OWASP
// CRS, virtual patching) is out of scope. The AppSec endpoint here emulates a // CRS, virtual patching) is out of scope. The AppSec endpoint here emulates a
@@ -11,10 +12,12 @@
package main package main
import ( import (
"bufio"
"encoding/json" "encoding/json"
"flag" "flag"
"io" "io"
"log" "log"
"net"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@@ -46,6 +49,48 @@ func list(m map[string]Decision) []Decision {
return out return out
} }
// --- Redis mock (inline-command wire format, as spoken by simpleredis) ---
// serveRedis is a hardcoded stand-in. Every line is scanned for known IPs:
// 1.2.3.4 → "t", 1.2.3.5 → "f", GET for anything else → miss ($-1).
// SET, DEL, AUTH, SELECT get +OK (they don't read the response anyway).
func serveRedis(addr string) {
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal(err)
}
defer ln.Close()
log.Printf("mocklapi: Redis mock listening on %s", addr)
for {
conn, err := ln.Accept()
if err != nil {
continue
}
go func(conn net.Conn) {
defer conn.Close()
rd := bufio.NewReader(conn)
for {
line, _, err := rd.ReadLine()
if err != nil {
return
}
s := string(line)
switch {
case strings.Contains(s, "1.2.3.4"):
conn.Write([]byte("$1\r\nf\r\n"))
case strings.Contains(s, "1.2.3.5"):
conn.Write([]byte("$1\r\nt\r\n"))
case strings.HasPrefix(strings.ToUpper(s), "GET "):
conn.Write([]byte("$-1\r\n"))
default:
conn.Write([]byte("+OK\r\n"))
}
}
}(conn)
}
}
func main() { func main() {
lapiAddr := flag.String("lapi-addr", "127.0.0.1:8090", "address for the LAPI mock") lapiAddr := flag.String("lapi-addr", "127.0.0.1:8090", "address for the LAPI mock")
// The stub upstream Traefik proxies allowed requests to — the binary-suite // The stub upstream Traefik proxies allowed requests to — the binary-suite
@@ -53,6 +98,9 @@ func main() {
backendAddr := flag.String("backend-addr", "127.0.0.1:8091", "address for the stub upstream service") backendAddr := flag.String("backend-addr", "127.0.0.1:8091", "address for the stub upstream service")
// AppSec WAF stand-in (the real engine listens on :7422). Not a CRS engine. // AppSec WAF stand-in (the real engine listens on :7422). Not a CRS engine.
appsecAddr := flag.String("appsec-addr", "127.0.0.1:8092", "address for the AppSec mock") appsecAddr := flag.String("appsec-addr", "127.0.0.1:8092", "address for the AppSec mock")
// Redis stand-in: the mock always serves a hardcoded GET on a plain TCP
// port, enough to exercise the plugin's redis cache path.
redisAddr := flag.String("redis-addr", "127.0.0.1:8093", "address for the Redis mock")
// Optional TLS for the LAPI: when both are set the LAPI is served over HTTPS // Optional TLS for the LAPI: when both are set the LAPI is served over HTTPS
// (cert signed by the scenario's throwaway CA) so the suite can exercise the // (cert signed by the scenario's throwaway CA) so the suite can exercise the
// bouncer's system-trust-store path. Backend and AppSec stay plaintext. // bouncer's system-trust-store path. Backend and AppSec stay plaintext.
@@ -96,6 +144,8 @@ func main() {
}))) })))
}() }()
go serveRedis(*redisAddr)
mux := http.NewServeMux() mux := http.NewServeMux()
// Readiness probe for the test harness (empty body, 200). // Readiness probe for the test harness (empty body, 200).
@@ -154,9 +204,9 @@ func main() {
}) })
if *lapiTLSCert != "" && *lapiTLSKey != "" { if *lapiTLSCert != "" && *lapiTLSKey != "" {
log.Printf("mocklapi: LAPI on %s (TLS), backend on %s, appsec on %s", *lapiAddr, *backendAddr, *appsecAddr) log.Printf("mocklapi: LAPI on %s (TLS), backend on %s, appsec on %s, redis on %s", *lapiAddr, *backendAddr, *appsecAddr, *redisAddr)
log.Fatal(http.ListenAndServeTLS(*lapiAddr, *lapiTLSCert, *lapiTLSKey, mux)) log.Fatal(http.ListenAndServeTLS(*lapiAddr, *lapiTLSCert, *lapiTLSKey, mux))
} }
log.Printf("mocklapi: LAPI on %s, backend on %s, appsec on %s", *lapiAddr, *backendAddr, *appsecAddr) log.Printf("mocklapi: LAPI on %s, backend on %s, appsec on %s, redis on %s", *lapiAddr, *backendAddr, *appsecAddr, *redisAddr)
log.Fatal(http.ListenAndServe(*lapiAddr, mux)) log.Fatal(http.ListenAndServe(*lapiAddr, mux))
} }
@@ -0,0 +1,27 @@
http:
routers:
r:
rule: "PathPrefix(`/foo`)"
entryPoints:
- web
service: backend
middlewares:
- bouncer
services:
backend:
loadBalancer:
servers:
- url: "@@BACKEND_URL@@"
middlewares:
bouncer:
plugin:
bouncer:
enabled: "true"
crowdsecMode: live
crowdsecLapiScheme: http
crowdsecLapiHost: "@@LAPI_HOST@@"
crowdsecLapiKey: "@@APIKEY@@"
redisCacheEnabled: "true"
redisCacheHost: "@@REDIS_HOST@@"
forwardedHeadersTrustedIps:
- "127.0.0.1/32"
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=../../lib/common.sh
source "$HERE/../../lib/common.sh"
SCENARIO=redis
# Redis cache check: the mock returns "t" (banned) for 1.2.3.4 and "f" (not
# banned) for 1.2.3.5. All other IPs return a miss, which falls through to the
# LAPI (no decision → allowed). This proves the plugin reads cached decisions
# from Redis correctly.
body() {
echo "[$SCENARIO] cached clean IP must pass"
assert_status "http://127.0.0.1:${WEB_PORT}/foo" 200 -H "X-Forwarded-For: 1.2.3.4"
echo "[$SCENARIO] cached banned IP must be blocked"
assert_status "http://127.0.0.1:${WEB_PORT}/foo" 403 -H "X-Forwarded-For: 1.2.3.5"
echo "[$SCENARIO] unknown IP (redis miss) must fall through to LAPI and pass"
assert_status "http://127.0.0.1:${WEB_PORT}/foo" 200 -H "X-Forwarded-For: 1.2.3.6"
}
run_scenario "$SCENARIO" "$HERE" body