P8.3: Refine schema rejections and add test runner

Simplify values.schema.json if/then patterns for rules 3-4 (removed
verbose allOf in favor of direct enum constraint in then branch),
drop unsupported errorMessage fields, and add run-tests.sh for
automated CI validation of all 12 schema/template test cases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-19 13:10:58 -04:00
parent c86f50fd76
commit 863bf1c33f
3 changed files with 148 additions and 83 deletions

View file

@ -1,26 +1,39 @@
# Helm Chart Tests
# Helm Chart Validation Tests
This directory contains test cases for validating the `values.schema.json` constraints.
## Running Tests
Use `helm lint --strict` with the test values files:
## Running All Tests
```bash
# Valid: single replica with SQLite (should pass)
helm lint --strict miroir -f miroir/tests/valid-single-replica-sqlite.yaml
# Invalid: multiple replicas with SQLite (should fail with constraint error)
helm lint --strict miroir -f miroir/tests/invalid-multi-replica-sqlite.yaml
# Valid: multiple replicas with Redis (should pass)
helm lint --strict miroir -f miroir/tests/valid-multi-replica-redis.yaml
./charts/miroir/tests/run-tests.sh
```
This runs both `helm lint --strict` (schema rules) and `helm template` (render-time rules) for all test cases.
## Test Cases
| Test Case | Description | Expected Result |
|-----------|-------------|-----------------|
| `valid-single-replica-sqlite.yaml` | `replicas: 1, backend: sqlite` | ✅ Pass |
| `invalid-multi-replica-sqlite.yaml` | `replicas: 2, backend: sqlite` | ❌ Fail with constraint error |
| `valid-multi-replica-redis.yaml` | `replicas: 2, backend: redis` | ✅ Pass |
### Schema rejection tests (`helm lint --strict`)
| File | Rule | Description |
|------|------|-------------|
| `invalid-multi-replica-sqlite.yaml` | 1 | `replicas>1` with `taskStore.backend: sqlite` — SQLite cannot be shared across pods |
| `bad-hpa-no-redis.yaml` | 2a | `hpa.enabled: true` with `taskStore.backend: sqlite` — autoscaling requires Redis |
| `bad-hpa-single-replica.yaml` | 2b | `hpa.enabled: true` with `replicas: 1` — HPA requires `replicas >= 2` |
| `bad-search-ui-rate-limit-local-multi.yaml` | 3 | `search_ui.rate_limit.backend: local` with `replicas>1` — per-pod limits don't share state |
| `bad-admin-login-rate-limit-local-multi.yaml` | 4 | `admin_ui.login_rate_limit.backend: local` with `replicas>1` — per-pod limits don't share state |
### Template rejection tests (`helm template`)
| File | Rule | Description |
|------|------|-------------|
| `bad-scoped-key-rotate-gte-max.yaml` | 5a | `rotate_before_expiry >= max_age` — rotation fires at/before issuance |
| `bad-scoped-key-rotate-gt-max.yaml` | 5b | `rotate_before_expiry > max_age` — negative rotation window |
Rule 5 uses template-level `fail()` because JSON Schema draft-7 cannot compare sibling property values.
### Positive tests
| File | Description |
|------|-------------|
| `valid-single-replica-sqlite.yaml` | Single replica with SQLite (dev default) |
| `valid-multi-replica-redis.yaml` | Multi-replica with Redis |
| `good-production.yaml` | Full production config with HPA, Redis rate limiting, and scoped keys |
| `good-dev-no-ui.yaml` | Minimal dev defaults |

View file

@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Run all values.schema.json and template validation tests for the miroir Helm chart.
# Exit non-zero if any test fails.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CHART_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
PASS=0 FAIL=0
red() { printf '\033[31m%s\033[0m\n' "$*"; }
green() { printf '\033[32m%s\033[0m\n' "$*"; }
# expect_fail_lint VALUES_FILE DESCRIPTION
expect_fail_lint() {
local vals="$1" desc="$2"
if helm lint --strict "$CHART_DIR" -f "$vals" >/dev/null 2>&1; then
red "FAIL: $desc (lint should have rejected $vals)"
FAIL=$((FAIL+1))
else
green "PASS: $desc"
PASS=$((PASS+1))
fi
}
# expect_fail_template VALUES_FILE DESCRIPTION
expect_fail_template() {
local vals="$1" desc="$2"
if helm template test-release "$CHART_DIR" -f "$vals" >/dev/null 2>&1; then
red "FAIL: $desc (template should have rejected $vals)"
FAIL=$((FAIL+1))
else
green "PASS: $desc (template)"
PASS=$((PASS+1))
fi
}
# expect_pass_lint VALUES_FILE DESCRIPTION
expect_pass_lint() {
local vals="$1" desc="$2"
if helm lint --strict "$CHART_DIR" -f "$vals" >/dev/null 2>&1; then
green "PASS: $desc"
PASS=$((PASS+1))
else
red "FAIL: $desc (lint should have accepted $vals)"
FAIL=$((FAIL+1))
fi
}
# expect_pass_template VALUES_FILE DESCRIPTION
expect_pass_template() {
local vals="$1" desc="$2"
if helm template test-release "$CHART_DIR" -f "$vals" >/dev/null 2>&1; then
green "PASS: $desc (template)"
PASS=$((PASS+1))
else
red "FAIL: $desc (template should have accepted $vals)"
FAIL=$((FAIL+1))
fi
}
echo "=== Schema rejection tests (helm lint --strict) ==="
expect_fail_lint "$SCRIPT_DIR/invalid-multi-replica-sqlite.yaml" \
"Rule 1: replicas>1 with sqlite backend"
expect_fail_lint "$SCRIPT_DIR/bad-hpa-no-redis.yaml" \
"Rule 2a: hpa enabled with sqlite backend"
expect_fail_lint "$SCRIPT_DIR/bad-hpa-single-replica.yaml" \
"Rule 2b: hpa enabled with replicas=1"
expect_fail_lint "$SCRIPT_DIR/bad-search-ui-rate-limit-local-multi.yaml" \
"Rule 3: search_ui local rate limit with multi-replica"
expect_fail_lint "$SCRIPT_DIR/bad-admin-login-rate-limit-local-multi.yaml" \
"Rule 4: admin_ui local rate limit with multi-replica"
echo ""
echo "=== Template rejection tests (helm template) ==="
expect_fail_template "$SCRIPT_DIR/bad-scoped-key-rotate-gte-max.yaml" \
"Rule 5a: scoped_key_rotate >= scoped_key_max_age"
expect_fail_template "$SCRIPT_DIR/bad-scoped-key-rotate-gt-max.yaml" \
"Rule 5b: scoped_key_rotate > scoped_key_max_age"
echo ""
echo "=== Positive tests (should all pass) ==="
expect_pass_lint "$SCRIPT_DIR/valid-single-replica-sqlite.yaml" \
"valid: single replica, sqlite"
expect_pass_lint "$SCRIPT_DIR/valid-multi-replica-redis.yaml" \
"valid: multi replica, redis"
expect_pass_lint "$SCRIPT_DIR/good-production.yaml" \
"valid: full production config"
expect_pass_lint "$SCRIPT_DIR/good-dev-no-ui.yaml" \
"valid: dev defaults"
expect_pass_template "$SCRIPT_DIR/good-production.yaml" \
"valid: full production config"
echo ""
echo "=== Results: $PASS passed, $FAIL failed ==="
if [ "$FAIL" -gt 0 ]; then
exit 1
fi

View file

@ -250,8 +250,7 @@
"taskStore": {
"properties": {
"backend": {
"const": "redis",
"errorMessage": "SQLite task store cannot run with multiple replicas; set taskStore.backend=redis"
"const": "redis"
}
},
"required": ["backend"]
@ -281,8 +280,7 @@
"properties": {
"replicas": {
"type": "integer",
"minimum": 2,
"errorMessage": "HPA requires miroir.replicas >= 2; increase replicas or disable hpa"
"minimum": 2
}
},
"required": ["replicas"]
@ -295,8 +293,7 @@
"taskStore": {
"properties": {
"backend": {
"const": "redis",
"errorMessage": "HPA requires taskStore.backend=redis; SQLite is single-writer and cannot be shared across autoscaled pods"
"const": "redis"
}
},
"required": ["backend"]
@ -308,37 +305,17 @@
}
},
{
"description": "Rule 3: search_ui.rate_limit.backend: local rejected when miroir.replicas > 1",
"description": "Rule 3: search_ui.rate_limit.backend must be redis when miroir.replicas > 1 (local is per-pod, not shared)",
"if": {
"allOf": [
{
"properties": {
"miroir": {
"properties": {
"miroir": {
"properties": {
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
},
"required": ["replicas"]
}
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
},
"required": ["miroir"]
},
{
"properties": {
"search_ui": {
"properties": {
"rate_limit": {
"properties": {
"backend": { "const": "local" }
},
"required": ["backend"]
}
},
"required": ["rate_limit"]
}
},
"required": ["search_ui"]
"required": ["replicas"]
}
]
},
"required": ["miroir"]
},
"then": {
"properties": {
@ -347,8 +324,7 @@
"rate_limit": {
"properties": {
"backend": {
"not": { "const": "local" },
"description": "Local rate limiting is incompatible with multiple replicas — effective per_ip limit is multiplied by pod count. Use backend: redis instead."
"enum": ["redis"]
}
}
}
@ -358,37 +334,17 @@
}
},
{
"description": "Rule 4: admin_ui.login_rate_limit.backend: local rejected when miroir.replicas > 1",
"description": "Rule 4: admin_ui.login_rate_limit.backend must be redis when miroir.replicas > 1 (local is per-pod, not shared)",
"if": {
"allOf": [
{
"properties": {
"miroir": {
"properties": {
"miroir": {
"properties": {
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
},
"required": ["replicas"]
}
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
},
"required": ["miroir"]
},
{
"properties": {
"admin_ui": {
"properties": {
"login_rate_limit": {
"properties": {
"backend": { "const": "local" }
},
"required": ["backend"]
}
},
"required": ["login_rate_limit"]
}
},
"required": ["admin_ui"]
"required": ["replicas"]
}
]
},
"required": ["miroir"]
},
"then": {
"properties": {
@ -397,8 +353,7 @@
"login_rate_limit": {
"properties": {
"backend": {
"not": { "const": "local" },
"description": "Local login rate limiting is incompatible with multiple replicas — limits are not shared across pods. Use backend: redis instead."
"enum": ["redis"]
}
}
}