feat(#30): Gate B [10/10]: Deploy compose + WOO_BASE_URL (OPS-1, GC-5)
- 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)
This commit is contained in:
@@ -14,6 +14,7 @@ from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.config import Settings
|
||||
from app.db.engine import get_session
|
||||
@@ -26,11 +27,15 @@ router = APIRouter()
|
||||
class SettingsUpdate(BaseModel):
|
||||
"""Partial settings update payload.
|
||||
|
||||
Woo consumer keys are rejected.
|
||||
Woo consumer keys and woo_base_url are rejected (woo_base_url is compose-only).
|
||||
Consumer keys are included as fields so they can be explicitly rejected.
|
||||
"""
|
||||
|
||||
woo_base_url: str | None = None
|
||||
woo_wp_username: str | None = None
|
||||
woo_app_password: str | None = None
|
||||
woo_consumer_key: str | None = None
|
||||
woo_consumer_secret: str | None = None
|
||||
inference_base_url: str | None = None
|
||||
inference_api_key: str | None = None
|
||||
inference_model: str | None = None
|
||||
@@ -95,7 +100,7 @@ async def _build_settings_view(app_settings: Settings = Depends(_get_app_setting
|
||||
key_bytes = get_aes_key(app_settings.aes_key)
|
||||
|
||||
session = await get_session()
|
||||
rows = await session.execute("SELECT key, value_enc FROM settings")
|
||||
rows = await session.execute(text("SELECT key, value_enc FROM settings"))
|
||||
all_rows = rows.all()
|
||||
await session.close()
|
||||
|
||||
@@ -174,13 +179,18 @@ async def update_settings(
|
||||
|
||||
Rejects consumer key fields. Returns 422 for invalid woo_create_status.
|
||||
"""
|
||||
# Reject consumer keys
|
||||
# Reject consumer keys and woo_base_url (compose-only, read-only in UI)
|
||||
payload = update.model_dump(exclude_unset=True)
|
||||
if "woo_consumer_key" in payload or "woo_consumer_secret" in payload:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="WooCommerce consumer keys are not supported. Use WordPress application passwords.",
|
||||
)
|
||||
if "woo_base_url" in payload:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="woo_base_url is set via compose environment, not editable via settings.",
|
||||
)
|
||||
|
||||
# Validate woo_create_status
|
||||
if update.woo_create_status is not None and update.woo_create_status not in (
|
||||
@@ -203,17 +213,17 @@ async def update_settings(
|
||||
|
||||
ct = encrypt_setting(key_bytes, str(value))
|
||||
setting = await session.execute(
|
||||
"SELECT id FROM settings WHERE key = :key", {"key": field}
|
||||
text("SELECT key FROM settings WHERE key = :key"), {"key": field}
|
||||
)
|
||||
existing = setting.fetchone()
|
||||
if existing:
|
||||
await session.execute(
|
||||
"UPDATE settings SET value_enc = :val, updated_at = :now WHERE key = :key",
|
||||
text("UPDATE settings SET value_enc = :val, updated_at = :now WHERE key = :key"),
|
||||
{"val": ct, "now": now, "key": field},
|
||||
)
|
||||
else:
|
||||
await session.execute(
|
||||
"INSERT INTO settings (key, value_enc, updated_at) VALUES (:key, :val, :now)",
|
||||
text("INSERT INTO settings (key, value_enc, updated_at) VALUES (:key, :val, :now)"),
|
||||
{"key": field, "val": ct, "now": now},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user