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:
@@ -0,0 +1,140 @@
|
||||
"""GC-4/GC-5: WOO_BASE_URL contract tests.
|
||||
|
||||
GC-4: WOO_BASE_URL is compose-only HTTPS; Settings GET returns it read-only;
|
||||
PUT rejects write attempts with 422.
|
||||
GC-5: FastAPI refuses to start when WOO_BASE_URL is missing or http://.
|
||||
|
||||
FUND-8: App factory creates fresh instance per test.
|
||||
FUND-9: TestClient context manager manages lifespan.
|
||||
FUND-10: dependency_overrides cleared after every test.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.config import Settings
|
||||
from app.main import create_app
|
||||
|
||||
|
||||
class TestWooBaseUrlValidation:
|
||||
"""GC-5: FastAPI refuses invalid WOO_BASE_URL at startup."""
|
||||
|
||||
def test_refuses_http_url(self) -> None:
|
||||
"""GC-5: Settings.validate() raises ValueError for http:// URL."""
|
||||
settings = Settings(
|
||||
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
|
||||
woo_base_url="http://insecure.example.com",
|
||||
)
|
||||
with pytest.raises(ValueError, match="(?i)https"):
|
||||
settings.validate()
|
||||
|
||||
def test_refuses_missing_url(self) -> None:
|
||||
"""GC-5: Settings.validate() raises ValueError when woo_base_url is None."""
|
||||
settings = Settings(
|
||||
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
settings.validate()
|
||||
|
||||
def test_accepts_https_url(self) -> None:
|
||||
"""GC-5: Settings.validate() accepts HTTPS URL."""
|
||||
settings = Settings(
|
||||
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
|
||||
woo_base_url="https://secure.example.com",
|
||||
)
|
||||
settings.validate() # Should not raise
|
||||
|
||||
|
||||
class TestWooBaseUrlReadOnly:
|
||||
"""GC-4: Settings endpoints handle woo_base_url correctly."""
|
||||
|
||||
def test_get_settings_returns_woo_base_url(self) -> None:
|
||||
"""GC-4: GET /api/v1/settings returns woo_base_url from compose env."""
|
||||
settings = Settings(
|
||||
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
|
||||
woo_base_url="https://store.example.com",
|
||||
)
|
||||
app = create_app(settings)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/settings")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["woo_base_url"] == "https://store.example.com"
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
def test_put_settings_rejects_woo_base_url(self) -> None:
|
||||
"""GC-4: PUT /api/v1/settings rejects woo_base_url field with 422."""
|
||||
settings = Settings(
|
||||
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
|
||||
woo_base_url="https://store.example.com",
|
||||
)
|
||||
app = create_app(settings)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.put(
|
||||
"/api/v1/settings",
|
||||
json={"woo_base_url": "https://hacked.example.com"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
data = response.json()
|
||||
assert "detail" in data
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
def test_put_settings_allows_other_fields(self) -> None:
|
||||
"""GC-4: PUT /api/v1/settings still accepts other settings fields."""
|
||||
settings = Settings(
|
||||
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
|
||||
woo_base_url="https://store.example.com",
|
||||
)
|
||||
app = create_app(settings)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.put(
|
||||
"/api/v1/settings",
|
||||
json={"woo_create_status": "publish"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["woo_create_status"] == "publish"
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
def test_get_settings_never_returns_app_password(self) -> None:
|
||||
"""GC-4: GET /api/v1/settings never returns woo_app_password."""
|
||||
settings = Settings(
|
||||
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
|
||||
woo_base_url="https://store.example.com",
|
||||
)
|
||||
app = create_app(settings)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/settings")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "woo_app_password" not in data
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
def test_put_settings_rejects_consumer_keys(self) -> None:
|
||||
"""GC-4: PUT /api/v1/settings rejects consumer key fields with 422."""
|
||||
settings = Settings(
|
||||
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
|
||||
woo_base_url="https://store.example.com",
|
||||
)
|
||||
app = create_app(settings)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.put(
|
||||
"/api/v1/settings",
|
||||
json={
|
||||
"woo_consumer_key": "ck_test",
|
||||
"woo_consumer_secret": "cs_test",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
Reference in New Issue
Block a user