68d236961a
- deploy/.env.example: placeholders only (no real URLs, proper comments) - frontend/Dockerfile: Node 20 stub server for compose build - deploy/tests/test-compose-health.sh: OPS-1 compose health smoke test - deploy/tests/test-woo-base-url.sh: GC-4/5/FUND-25 validation suite - backend/app/api/settings.py: PUT rejects woo_base_url (422), consumer keys (422), text() wrapping for SQLAlchemy 2.x, key primary key fix - backend/tests/contract/test_woo_base_url.py: 8 contract tests for GC-4/GC-5 Tests: 39/39 backend tests pass (8 new, 31 existing green) Deploy: 5/5 shell tests pass (Docker unavailable for compose-up path)
179 lines
5.9 KiB
Bash
179 lines
5.9 KiB
Bash
#!/usr/bin/env bash
|
|
# GC-4/5, FUND-25: WOO_BASE_URL validation tests.
|
|
# Tests WOO_BASE_URL is compose-only HTTPS, startup refusal of http://,
|
|
# and settings read-only behavior.
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
BACKEND_DIR="$REPO_ROOT/backend"
|
|
APP_HOSTNAME="${APP_HOSTNAME:-slab.local}"
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m'
|
|
|
|
pass() { echo -e "${GREEN}PASS${NC} $*"; }
|
|
fail() { echo -e "${RED}FAIL${NC} $*"; exit 1; }
|
|
warn() { echo -e "${YELLOW}WARN${NC} $*"; }
|
|
|
|
TOTAL=0
|
|
PASSED=0
|
|
FAILED=0
|
|
|
|
check() {
|
|
local label="$1"
|
|
shift
|
|
TOTAL=$((TOTAL + 1))
|
|
if "$@"; then
|
|
PASSED=$((PASSED + 1))
|
|
pass "$label"
|
|
else
|
|
FAILED=$((FAILED + 1))
|
|
fail "$label"
|
|
fi
|
|
}
|
|
|
|
# ── GC-5: FastAPI refuses http:// WOO_BASE_URL ────────────────────────────────
|
|
# Test via Python: import and validate Settings
|
|
test_gc5_http_rejected() {
|
|
cd "$BACKEND_DIR"
|
|
python3 -c "
|
|
from app.config import Settings
|
|
import sys
|
|
try:
|
|
s = Settings(aes_key='dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk', woo_base_url='http://test.example.com')
|
|
s.validate()
|
|
sys.exit(1) # Should have raised ValueError
|
|
except ValueError as e:
|
|
if 'https' in str(e).lower():
|
|
sys.exit(0)
|
|
sys.exit(1)
|
|
"
|
|
}
|
|
|
|
# ── GC-5: FastAPI refuses missing WOO_BASE_URL ────────────────────────────────
|
|
test_gc5_missing_rejected() {
|
|
cd "$BACKEND_DIR"
|
|
python3 -c "
|
|
from app.config import Settings
|
|
import sys
|
|
try:
|
|
s = Settings(aes_key='dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk')
|
|
s.validate()
|
|
sys.exit(1)
|
|
except ValueError as e:
|
|
if 'required' in str(e).lower() or 'missing' in str(e).lower():
|
|
sys.exit(0)
|
|
sys.exit(1)
|
|
"
|
|
}
|
|
|
|
# ── GC-5: HTTPS WOO_BASE_URL is accepted ──────────────────────────────────────
|
|
test_gc5_https_accepted() {
|
|
cd "$BACKEND_DIR"
|
|
python3 -c "
|
|
from app.config import Settings
|
|
s = Settings(aes_key='dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk', woo_base_url='https://test.example.com')
|
|
s.validate()
|
|
assert s.woo_base_url == 'https://test.example.com'
|
|
print('OK')
|
|
"
|
|
}
|
|
|
|
# ── GC-4: Settings GET returns woo_base_url ───────────────────────────────────
|
|
# Test via FastAPI TestClient (requires compose or local server).
|
|
# If no server is running, we test via Python directly.
|
|
test_gc4_settings_get_returns_url() {
|
|
cd "$BACKEND_DIR"
|
|
python3 -c "
|
|
from app.config import Settings
|
|
from app.main import create_app
|
|
from fastapi.testclient import TestClient
|
|
|
|
settings = Settings(
|
|
aes_key='dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk',
|
|
woo_base_url='https://test.example.com',
|
|
)
|
|
app = create_app(settings)
|
|
with TestClient(app) as client:
|
|
r = client.get('/api/v1/settings')
|
|
assert r.status_code == 200, f'Expected 200, got {r.status_code}'
|
|
data = r.json()
|
|
assert 'woo_base_url' in data, 'Missing woo_base_url in settings response'
|
|
assert data['woo_base_url'] == 'https://test.example.com', f'Unexpected woo_base_url: {data[\"woo_base_url\"]}'
|
|
app.dependency_overrides.clear()
|
|
print('OK')
|
|
"
|
|
}
|
|
|
|
# ── GC-4: Settings PUT rejects woo_base_url ───────────────────────────────────
|
|
test_gc4_put_rejects_url() {
|
|
cd "$BACKEND_DIR"
|
|
python3 -c "
|
|
from app.config import Settings
|
|
from app.main import create_app
|
|
from fastapi.testclient import TestClient
|
|
|
|
settings = Settings(
|
|
aes_key='dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk',
|
|
woo_base_url='https://test.example.com',
|
|
)
|
|
app = create_app(settings)
|
|
with TestClient(app) as client:
|
|
r = client.put('/api/v1/settings', json={'woo_base_url': 'https://hacked.example.com'})
|
|
assert r.status_code == 422, f'Expected 422, got {r.status_code}'
|
|
body = r.json()
|
|
assert 'detail' in body or 'errors' in body, 'Missing error detail'
|
|
app.dependency_overrides.clear()
|
|
print('OK')
|
|
"
|
|
}
|
|
|
|
# ── FUND-25: Port collision detection (compose-only check) ────────────────────
|
|
# Check that FRONTEND_PORT and FASTAPI_PORT defaults don't collide with known services.
|
|
test_fund25_port_free() {
|
|
FRONTEND_PORT="${FRONTEND_PORT:-18080}"
|
|
FASTAPI_PORT="${FASTAPI_PORT:-18081}"
|
|
if [ "$SKIP_COMPOSE" -eq 0 ]; then
|
|
# On a real host, check if ports are free
|
|
for PORT in "$FRONTEND_PORT" "$FASTAPI_PORT"; do
|
|
if ss -tlnp 2>/dev/null | grep -q ":$PORT "; then
|
|
echo "Port $PORT is in use"
|
|
return 1
|
|
fi
|
|
done
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# ── Run tests ──────────────────────────────────────────────────────────────────
|
|
SKIP_COMPOSE=0
|
|
if ! command -v docker &>/dev/null; then
|
|
SKIP_COMPOSE=1
|
|
warn "Docker not available — skipping compose-dependent tests"
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== WOO_BASE_URL validation tests ==="
|
|
echo ""
|
|
|
|
check "GC-5: HTTP WOO_BASE_URL rejected" test_gc5_http_rejected
|
|
check "GC-5: Missing WOO_BASE_URL rejected" test_gc5_missing_rejected
|
|
check "GC-5: HTTPS WOO_BASE_URL accepted" test_gc5_https_accepted
|
|
check "GC-4: Settings GET returns woo_base_url" test_gc4_settings_get_returns_url
|
|
check "GC-4: Settings PUT rejects woo_base_url" test_gc4_put_rejects_url
|
|
|
|
if [ "$SKIP_COMPOSE" -eq 0 ]; then
|
|
check "FUND-25: Port collision check" test_fund25_port_free
|
|
else
|
|
pass "FUND-25: Port collision check (skipped — no Docker)"
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Results: $PASSED passed, $FAILED failed, $TOTAL total ==="
|
|
if [ "$FAILED" -gt 0 ]; then
|
|
exit 1
|
|
fi
|
|
exit 0
|