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:
parent
c86f50fd76
commit
863bf1c33f
3 changed files with 148 additions and 83 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
97
charts/miroir/tests/run-tests.sh
Executable file
97
charts/miroir/tests/run-tests.sh
Executable 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
|
||||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue