Caddy 2.x emits JSON access logs; the failregex in infra/fail2ban/filter.d/familienarchiv-auth.conf depends on the "remote_ip" → "uri" → "status" key order being stable. A future Caddy upgrade that reorders fields would break the jail silently (regex no longer matches → fail2ban returns 0 hits → host stops banning brute-force, discovered only at the next incident). This job pins the contract: a sample /api/auth/login 401 line must match (1 hit) and a /api/auth/login 200 line must not (0 hits). Catches a regression at PR time instead of in production. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
149 lines
5.5 KiB
YAML
149 lines
5.5 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
pull_request:
|
|
|
|
jobs:
|
|
# ─── Unit & Browser Component Tests ──────────────────────────────────────────
|
|
# Runs inside the official Playwright Docker image — Chromium and all system
|
|
# deps are pre-installed, so no install or cache step is needed for the browser.
|
|
unit-tests:
|
|
name: Unit & Component Tests
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: mcr.microsoft.com/playwright:v1.58.2-noble
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Cache node_modules
|
|
id: node-modules-cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: frontend/node_modules
|
|
key: node-modules-${{ hashFiles('frontend/package-lock.json') }}
|
|
|
|
- name: Install dependencies
|
|
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
|
run: npm ci
|
|
working-directory: frontend
|
|
|
|
- name: Compile Paraglide i18n
|
|
run: npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide
|
|
working-directory: frontend
|
|
|
|
- name: Lint
|
|
run: npm run lint
|
|
working-directory: frontend
|
|
|
|
- name: Run unit and component tests
|
|
run: npm test
|
|
working-directory: frontend
|
|
env:
|
|
TZ: Europe/Berlin
|
|
|
|
- name: Run coverage (server + client)
|
|
run: npm run test:coverage
|
|
working-directory: frontend
|
|
env:
|
|
TZ: Europe/Berlin
|
|
|
|
- name: Upload coverage reports
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: coverage-reports
|
|
path: frontend/coverage/
|
|
|
|
- name: Build frontend
|
|
run: npm run build
|
|
working-directory: frontend
|
|
|
|
- name: Upload screenshots
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: unit-test-screenshots
|
|
path: frontend/test-results/screenshots/
|
|
|
|
# ─── OCR Service Unit Tests ───────────────────────────────────────────────────
|
|
# Only spell_check.py, test_confidence.py, test_sender_registry.py — no ML stack required.
|
|
ocr-tests:
|
|
name: OCR Service Tests
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Install test dependencies
|
|
run: pip install "pyspellchecker==0.9.0" pytest pytest-asyncio
|
|
working-directory: ocr-service
|
|
|
|
- name: Run OCR unit tests (no ML stack required)
|
|
run: python -m pytest test_spell_check.py test_confidence.py test_sender_registry.py -v
|
|
working-directory: ocr-service
|
|
|
|
# ─── Backend Unit & Slice Tests ───────────────────────────────────────────────
|
|
# Pure Mockito + WebMvcTest — no DB or S3 needed.
|
|
backend-unit-tests:
|
|
name: Backend Unit Tests
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
DOCKER_API_VERSION: "1.43" # NAS runner runs Docker 24.x (max API 1.43); Testcontainers 2.x defaults to 1.44
|
|
DOCKER_HOST: unix:///var/run/docker.sock
|
|
TESTCONTAINERS_RYUK_DISABLED: "true"
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-java@v4
|
|
with:
|
|
java-version: '21'
|
|
distribution: temurin
|
|
|
|
- name: Cache Maven repository
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/.m2/repository
|
|
key: maven-${{ hashFiles('backend/pom.xml') }}
|
|
restore-keys: maven-
|
|
|
|
- name: Run backend tests
|
|
run: |
|
|
chmod +x mvnw
|
|
./mvnw clean test
|
|
working-directory: backend
|
|
|
|
# ─── fail2ban Regex Regression ────────────────────────────────────────────────
|
|
# The filter parses Caddy's JSON access log; a Caddy upgrade that reorders
|
|
# the JSON keys would silently break it (fail2ban-regex would return
|
|
# "0 matches", fail2ban would stop banning, no error surface). This job
|
|
# pins the contract against a deterministic sample line.
|
|
fail2ban-regex:
|
|
name: fail2ban Regex
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install fail2ban
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y fail2ban
|
|
|
|
- name: Matches /api/auth/login 401
|
|
run: |
|
|
echo '{"level":"info","ts":1700000000.12,"logger":"http.log.access","msg":"handled request","request":{"remote_ip":"203.0.113.42","method":"POST","host":"archiv.raddatz.cloud","uri":"/api/auth/login"},"status":401}' > /tmp/sample.log
|
|
out=$(fail2ban-regex /tmp/sample.log infra/fail2ban/filter.d/familienarchiv-auth.conf)
|
|
echo "$out"
|
|
echo "$out" | grep -qE '1 matched' \
|
|
|| { echo "expected 1 match for /api/auth/login 401"; exit 1; }
|
|
|
|
- name: Does not match /api/auth/login 200
|
|
run: |
|
|
echo '{"level":"info","ts":1700000000.12,"logger":"http.log.access","msg":"handled request","request":{"remote_ip":"203.0.113.42","method":"POST","host":"archiv.raddatz.cloud","uri":"/api/auth/login"},"status":200}' > /tmp/sample.log
|
|
out=$(fail2ban-regex /tmp/sample.log infra/fail2ban/filter.d/familienarchiv-auth.conf)
|
|
echo "$out"
|
|
echo "$out" | grep -qE '0 matched' \
|
|
|| { echo "expected 0 matches for /api/auth/login 200"; exit 1; } |