diff --git a/build.py b/build.py index 406ce41..94070f5 100644 --- a/build.py +++ b/build.py @@ -271,16 +271,17 @@ def copy_cfg(output_dir: Path, room: str): # Now in cfg//soleprint/ room_soleprint = room_cfg / "soleprint" if room_soleprint.exists(): + systems = {"artery", "atlas", "station"} for item in room_soleprint.iterdir(): - if item.is_file(): + if item.name in systems: + # Merge system dirs into already-copied framework code + log.info(f" Merging {room} {item.name}...") + merge_into(item, output_dir / item.name) + elif item.is_file(): + copy_path(item, output_dir / item.name) + elif item.is_dir(): + # Copy non-system dirs as-is (nginx/, etc.) copy_path(item, output_dir / item.name) - - # Merge room-specific system configs from soleprint subfolder - for system in ["artery", "atlas", "station"]: - room_system = room_soleprint / system - if room_system.exists(): - log.info(f" Merging {room} {system}...") - merge_into(room_system, output_dir / system) def build_soleprint(output_dir: Path, room: str): diff --git a/cfg/amar/soleprint/artery/shunts/amar/main.py b/cfg/amar/soleprint/artery/shunts/amar/main.py index ed85e50..1e9ae94 100644 --- a/cfg/amar/soleprint/artery/shunts/amar/main.py +++ b/cfg/amar/soleprint/artery/shunts/amar/main.py @@ -29,7 +29,7 @@ templates = Jinja2Templates(directory=Path(__file__).parent / "templates") @app.get("/", response_class=HTMLResponse) def index(request: Request): """Mock configuration UI.""" - return templates.TemplateResponse("index.html", {"request": request}) + return templates.TemplateResponse(request, "index.html") # Include router at root (matches real Amar API structure) app.include_router(router) diff --git a/cfg/amar/soleprint/station/tools/tester/tests/base.py b/cfg/amar/soleprint/station/tools/tester/tests/base.py index 3120c06..08d9570 100644 --- a/cfg/amar/soleprint/station/tools/tester/tests/base.py +++ b/cfg/amar/soleprint/station/tools/tester/tests/base.py @@ -1,164 +1,4 @@ -""" -Pure HTTP Contract Tests - Base Class +"""Re-export from parent — room tests import from here.""" +from ..base import ContractTestCase, get_base_url -Framework-agnostic: works against ANY backend implementation. -Does NOT manage database - expects a ready environment. - -Requirements: - - Server running at CONTRACT_TEST_URL - - Database migrated and seeded - - Test user exists OR CONTRACT_TEST_TOKEN provided - -Usage: - CONTRACT_TEST_URL=http://127.0.0.1:8000 pytest - CONTRACT_TEST_TOKEN=your_jwt_token pytest -""" - -import os -import unittest -import httpx - -from .endpoints import Endpoints - - -def get_base_url(): - """Get base URL from environment (required)""" - url = os.environ.get("CONTRACT_TEST_URL", "") - if not url: - raise ValueError("CONTRACT_TEST_URL environment variable required") - return url.rstrip("/") - - -class ContractTestCase(unittest.TestCase): - """ - Base class for pure HTTP contract tests. - - Features: - - Framework-agnostic (works with Django, FastAPI, Node, etc.) - - Pure HTTP via requests library - - No database access - all data through API - - JWT authentication - """ - - # Auth credentials - override via environment - TEST_USER_EMAIL = os.environ.get("CONTRACT_TEST_USER", "contract_test@example.com") - TEST_USER_PASSWORD = os.environ.get("CONTRACT_TEST_PASSWORD", "testpass123") - - # Class-level cache - _base_url = None - _token = None - - @classmethod - def setUpClass(cls): - """Set up once per test class""" - super().setUpClass() - cls._base_url = get_base_url() - - # Use provided token or fetch one - cls._token = os.environ.get("CONTRACT_TEST_TOKEN", "") - if not cls._token: - cls._token = cls._fetch_token() - - @classmethod - def _fetch_token(cls): - """Get JWT token for authentication""" - url = f"{cls._base_url}{Endpoints.TOKEN}" - try: - response = httpx.post(url, json={ - "username": cls.TEST_USER_EMAIL, - "password": cls.TEST_USER_PASSWORD, - }, timeout=10) - if response.status_code == 200: - return response.json().get("access", "") - else: - print(f"Warning: Token request failed with {response.status_code}") - except httpx.RequestError as e: - print(f"Warning: Token request failed: {e}") - return "" - - @property - def base_url(self): - return self._base_url - - @property - def token(self): - return self._token - - def _auth_headers(self): - """Get authorization headers""" - if self.token: - return {"Authorization": f"Bearer {self.token}"} - return {} - - # ========================================================================= - # HTTP helpers - # ========================================================================= - - def get(self, path: str, params: dict = None, **kwargs): - """GET request""" - url = f"{self.base_url}{path}" - headers = {**self._auth_headers(), **kwargs.pop("headers", {})} - response = httpx.get(url, params=params, headers=headers, timeout=30, **kwargs) - return self._wrap_response(response) - - def post(self, path: str, data: dict = None, **kwargs): - """POST request with JSON""" - url = f"{self.base_url}{path}" - headers = {**self._auth_headers(), **kwargs.pop("headers", {})} - response = httpx.post(url, json=data, headers=headers, timeout=30, **kwargs) - return self._wrap_response(response) - - def put(self, path: str, data: dict = None, **kwargs): - """PUT request with JSON""" - url = f"{self.base_url}{path}" - headers = {**self._auth_headers(), **kwargs.pop("headers", {})} - response = httpx.put(url, json=data, headers=headers, timeout=30, **kwargs) - return self._wrap_response(response) - - def patch(self, path: str, data: dict = None, **kwargs): - """PATCH request with JSON""" - url = f"{self.base_url}{path}" - headers = {**self._auth_headers(), **kwargs.pop("headers", {})} - response = httpx.patch(url, json=data, headers=headers, timeout=30, **kwargs) - return self._wrap_response(response) - - def delete(self, path: str, **kwargs): - """DELETE request""" - url = f"{self.base_url}{path}" - headers = {**self._auth_headers(), **kwargs.pop("headers", {})} - response = httpx.delete(url, headers=headers, timeout=30, **kwargs) - return self._wrap_response(response) - - def _wrap_response(self, response): - """Add .data attribute for consistency with DRF responses""" - try: - response.data = response.json() - except Exception: - response.data = None - return response - - # ========================================================================= - # Assertion helpers - # ========================================================================= - - def assert_status(self, response, expected_status: int): - """Assert response has expected status code""" - self.assertEqual( - response.status_code, - expected_status, - f"Expected {expected_status}, got {response.status_code}. " - f"Response: {response.data if hasattr(response, 'data') else response.content[:500]}" - ) - - def assert_has_fields(self, data: dict, *fields: str): - """Assert dictionary has all specified fields""" - missing = [f for f in fields if f not in data] - self.assertEqual(missing, [], f"Missing fields: {missing}. Got: {list(data.keys())}") - - def assert_is_list(self, data, min_length: int = 0): - """Assert data is a list with minimum length""" - self.assertIsInstance(data, list) - self.assertGreaterEqual(len(data), min_length) - - -__all__ = ["ContractTestCase"] +__all__ = ["ContractTestCase", "get_base_url"] diff --git a/soleprint/artery/shunts/mercadopago/main.py b/soleprint/artery/shunts/mercadopago/main.py index 0d7a39b..f9d9a0f 100755 --- a/soleprint/artery/shunts/mercadopago/main.py +++ b/soleprint/artery/shunts/mercadopago/main.py @@ -29,7 +29,7 @@ templates = Jinja2Templates(directory=Path(__file__).parent / "templates") @app.get("/", response_class=HTMLResponse) def index(request: Request): """Mock configuration UI.""" - return templates.TemplateResponse("index.html", {"request": request}) + return templates.TemplateResponse(request, "index.html") # Include router at root (matches real MercadoPago API structure) app.include_router(router) diff --git a/soleprint/run.py b/soleprint/run.py index 2a3540c..7c84d5d 100644 --- a/soleprint/run.py +++ b/soleprint/run.py @@ -512,9 +512,17 @@ def station_index(request: Request): ) +# Mount station tool routers +try: + from station.tools.tester.api import router as tester_router + app.include_router(tester_router, prefix="/station") +except ImportError as e: + print(f"Warning: Could not load tester router: {e}") + + @app.get("/station/{path:path}") def station_route(path: str): - """Station sub-routes.""" + """Station sub-routes (fallback).""" return {"system": "station", "path": path} diff --git a/soleprint/station/tools/tester/api.py b/soleprint/station/tools/tester/api.py index f457d47..200bb6a 100644 --- a/soleprint/station/tools/tester/api.py +++ b/soleprint/station/tools/tester/api.py @@ -2,6 +2,7 @@ FastAPI router for tester tool. """ +import os from pathlib import Path from typing import Optional from pydantic import BaseModel @@ -69,8 +70,7 @@ def index(request: Request): tests_tree = get_tests_tree() tests_list = discover_tests() - return templates.TemplateResponse("index.html", { - "request": request, + return templates.TemplateResponse(request, "index.html", context={ "config": config, "tests_tree": tests_tree, "total_tests": len(tests_list), @@ -86,8 +86,7 @@ def health(): @router.get("/filters", response_class=HTMLResponse) def test_filters(request: Request): """Show filterable test view with multiple filter options.""" - return templates.TemplateResponse("filters.html", { - "request": request, + return templates.TemplateResponse(request, "filters.html", context={ "config": config, }) @@ -95,8 +94,7 @@ def test_filters(request: Request): @router.get("/filters_v2", response_class=HTMLResponse) def test_filters_v2(request: Request): """Show Gherkin-driven filter view (v2 with pulse variables).""" - return templates.TemplateResponse("filters_v2.html", { - "request": request, + return templates.TemplateResponse(request, "filters_v2.html", context={ "config": config, }) @@ -140,10 +138,18 @@ def select_environment(env_id: str): if not env: raise HTTPException(status_code=404, detail=f"Environment {env_id} not found") - # Update config (in memory for this session) + # Update config and env vars (tests read from os.environ) config["CONTRACT_TEST_URL"] = env["url"] config["CONTRACT_TEST_API_KEY"] = env.get("api_key", "") + os.environ["CONTRACT_TEST_URL"] = env["url"] + if env.get("api_key"): + os.environ["CONTRACT_TEST_API_KEY"] = env["api_key"] + os.environ["CONTRACT_TEST_AUTH_TYPE"] = "api-key" + else: + os.environ.pop("CONTRACT_TEST_API_KEY", None) + os.environ["CONTRACT_TEST_AUTH_TYPE"] = "bearer" + return { "success": True, "environment": { diff --git a/soleprint/station/tools/tester/environments.json b/soleprint/station/tools/tester/environments.json index f63ebcc..52c58f3 100644 --- a/soleprint/station/tools/tester/environments.json +++ b/soleprint/station/tools/tester/environments.json @@ -4,7 +4,7 @@ "name": "Local", "url": "http://localhost:8000", "api_key": "", - "description": "Local development server", + "description": "Local development server (bare-metal)", "default": true } ] diff --git a/soleprint/station/tools/tester/templates/filters.html b/soleprint/station/tools/tester/templates/filters.html index 5cf7a53..b9032bc 100644 --- a/soleprint/station/tools/tester/templates/filters.html +++ b/soleprint/station/tools/tester/templates/filters.html @@ -340,8 +340,8 @@

Contract HTTP Tests - Filters

@@ -450,7 +450,7 @@ // Load tests on page load async function loadTests() { try { - const response = await fetch('/tools/tester/api/tests'); + const response = await fetch('/station/tools/tester/api/tests'); const data = await response.json(); allTests = data.tests; @@ -505,12 +505,12 @@ async function loadLastRunResults() { try { - const response = await fetch('/tools/tester/api/runs'); + const response = await fetch('/station/tools/tester/api/runs'); const data = await response.json(); if (data.runs && data.runs.length > 0) { const lastRunId = data.runs[0]; - const runResponse = await fetch(`/tools/tester/api/run/${lastRunId}`); + const runResponse = await fetch(`/station/tools/tester/api/run/${lastRunId}`); const runData = await runResponse.json(); runData.results.forEach(result => { @@ -774,7 +774,7 @@ const testIds = Array.from(selectedTests); try { - const response = await fetch('/tools/tester/api/run', { + const response = await fetch('/station/tools/tester/api/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test_ids: testIds }), @@ -798,7 +798,7 @@ } // Redirect to main runner with filters applied - window.location.href = `/tools/tester/?${params.toString()}`; + window.location.href = `/station/tools/tester/?${params.toString()}`; } catch (error) { console.error('Failed to start run:', error); alert('Failed to start test run'); @@ -814,7 +814,7 @@ // Load environments async function loadEnvironments() { try { - const response = await fetch('/tools/tester/api/environments'); + const response = await fetch('/station/tools/tester/api/environments'); const data = await response.json(); const selector = document.getElementById('environmentSelector'); const currentUrl = document.getElementById('currentUrl'); @@ -835,7 +835,7 @@ selector.addEventListener('change', async (e) => { const envId = e.target.value; try { - const response = await fetch(`/tools/tester/api/environment/select?env_id=${envId}`, { + const response = await fetch(`/station/tools/tester/api/environment/select?env_id=${envId}`, { method: 'POST' }); const data = await response.json(); diff --git a/soleprint/station/tools/tester/templates/filters_v2.html b/soleprint/station/tools/tester/templates/filters_v2.html index 99fcaad..b9b2c6d 100644 --- a/soleprint/station/tools/tester/templates/filters_v2.html +++ b/soleprint/station/tools/tester/templates/filters_v2.html @@ -464,9 +464,9 @@ Beta
@@ -704,7 +704,7 @@ // Load environments async function loadEnvironments() { try { - const response = await fetch('/tools/tester/api/environments'); + const response = await fetch('/station/tools/tester/api/environments'); const data = await response.json(); const selector = document.getElementById('environmentSelector'); const currentUrl = document.getElementById('currentUrl'); @@ -725,7 +725,7 @@ selector.addEventListener('change', async (e) => { const envId = e.target.value; try { - const response = await fetch(`/tools/tester/api/environment/select?env_id=${envId}`, { + const response = await fetch(`/station/tools/tester/api/environment/select?env_id=${envId}`, { method: 'POST' }); const data = await response.json(); @@ -747,10 +747,10 @@ async function loadFeatures() { try { // First sync features - await fetch('/tools/tester/api/features/sync', { method: 'POST' }); + await fetch('/station/tools/tester/api/features/sync', { method: 'POST' }); // Then load them - const response = await fetch('/tools/tester/api/features'); + const response = await fetch('/station/tools/tester/api/features'); const data = await response.json(); allFeatures = data.features; @@ -766,7 +766,7 @@ }); // Load tags - const tagsResponse = await fetch('/tools/tester/api/features/tags'); + const tagsResponse = await fetch('/station/tools/tester/api/features/tags'); const tagsData = await tagsResponse.json(); const tagFilters = document.getElementById('tagFilters'); @@ -787,7 +787,7 @@ // Load tests async function loadTests() { try { - const response = await fetch('/tools/tester/api/tests'); + const response = await fetch('/station/tools/tester/api/tests'); const data = await response.json(); // For now, all tests are backend (until we integrate Playwright discovery) @@ -1109,7 +1109,7 @@ const testIds = Array.from(selectedTests); try { - const response = await fetch('/tools/tester/api/run', { + const response = await fetch('/station/tools/tester/api/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test_ids: testIds }), @@ -1117,7 +1117,7 @@ const data = await response.json(); // Redirect to runner - window.location.href = `/tools/tester/?run=${data.run_id}`; + window.location.href = `/station/tools/tester/?run=${data.run_id}`; } catch (error) { console.error('Failed to start run:', error); alert('Failed to start test run'); diff --git a/soleprint/station/tools/tester/templates/index.html b/soleprint/station/tools/tester/templates/index.html index ea6c815..9b5b275 100644 --- a/soleprint/station/tools/tester/templates/index.html +++ b/soleprint/station/tools/tester/templates/index.html @@ -402,8 +402,8 @@

Contract HTTP Tests

- Runner - Filters + Runner + Filters
@@ -602,7 +602,7 @@ // Load environments async function loadEnvironments() { try { - const response = await fetch('/tools/tester/api/environments'); + const response = await fetch('/station/tools/tester/api/environments'); const data = await response.json(); const selector = document.getElementById('environmentSelector'); const currentUrl = document.getElementById('currentUrl'); @@ -627,7 +627,7 @@ selector.addEventListener('change', async (e) => { const envId = e.target.value; try { - const response = await fetch(`/tools/tester/api/environment/select?env_id=${envId}`, { + const response = await fetch(`/station/tools/tester/api/environment/select?env_id=${envId}`, { method: 'POST' }); const data = await response.json(); @@ -709,7 +709,7 @@ document.getElementById('resultsList').innerHTML = ''; try { - const response = await fetch('/tools/tester/api/run', { + const response = await fetch('/station/tools/tester/api/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test_ids: testIds }), @@ -731,7 +731,7 @@ if (!currentRunId) return; try { - const response = await fetch(`/tools/tester/api/run/${currentRunId}`); + const response = await fetch(`/station/tools/tester/api/run/${currentRunId}`); const data = await response.json(); updateUI(data);