diff --git a/charts/miroir/tests/README.md b/charts/miroir/tests/README.md index 2218da2..e44d1d0 100644 --- a/charts/miroir/tests/README.md +++ b/charts/miroir/tests/README.md @@ -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 | diff --git a/charts/miroir/tests/run-tests.sh b/charts/miroir/tests/run-tests.sh new file mode 100755 index 0000000..723933a --- /dev/null +++ b/charts/miroir/tests/run-tests.sh @@ -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 diff --git a/charts/miroir/values.schema.json b/charts/miroir/values.schema.json index 82831e9..2981825 100644 --- a/charts/miroir/values.schema.json +++ b/charts/miroir/values.schema.json @@ -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"] } } }