Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,10 @@ jobs:
--platform=managed \
--image="${image}" \
--service-account="${GCP_RUNTIME_SERVICE_ACCOUNT}" \
--no-allow-unauthenticated \
--ingress=internal \
--max-instances=1 \
--concurrency=80 \
--concurrency=1 \
--memory=512Mi \
--cpu=1 \
--timeout=300s \
Expand Down Expand Up @@ -756,6 +757,9 @@ jobs:
--project="${GCP_PROJECT_ID}" \
--location="${scheduler_location}" \
--uri="${scheduler_uri}" \
--http-method=POST \
--oidc-service-account-email="${GCP_SCHEDULER_SERVICE_ACCOUNT}" \
--oidc-token-audience="${service_url}" \
--schedule="${desired_schedule}" \
--time-zone="${market_timezone}" \
--max-retry-attempts=3 \
Expand Down
2 changes: 0 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,6 @@ def session_check():


@app.post("/run")
@app.get("/run")
def run_strategy():
if not _flag("FIRSTRADE_RUN_STRATEGY_ON_HTTP"):
return (
Expand Down Expand Up @@ -789,7 +788,6 @@ def paper_execution_command_consumer():


@app.post("/probe")
@app.get("/probe")
def probe():
return session_check()

Expand Down
63 changes: 48 additions & 15 deletions tests/test_request_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ def test_cloud_run_route_contracts_are_registered():
"/healthz": ["GET"],
"/profiles": ["GET"],
"/smoke": ["GET"],
"/run": ["GET", "POST"],
"/run": ["POST"],
"/dry-run": ["GET", "POST"],
"/paper-command-consumer": ["POST"],
"/monitor-dispatch": ["GET", "POST"],
"/probe": ["GET", "POST"],
"/probe": ["POST"],
"/static/<path:filename>": ["GET"],
}

Expand All @@ -56,16 +56,59 @@ def test_health_route_returns_service_contract(monkeypatch):
assert "as_of" in payload


def test_run_endpoint_is_disabled_without_explicit_http_gate(monkeypatch):
monkeypatch.delenv("FIRSTRADE_RUN_STRATEGY_ON_HTTP", raising=False)
@pytest.mark.parametrize("strategy_gate", [None, "false", "true"])
@pytest.mark.parametrize("path", ["/run", "/probe"])
def test_execution_routes_reject_get_without_runtime_calls(monkeypatch, strategy_gate, path):
if strategy_gate is None:
monkeypatch.delenv("FIRSTRADE_RUN_STRATEGY_ON_HTTP", raising=False)
else:
monkeypatch.setenv("FIRSTRADE_RUN_STRATEGY_ON_HTTP", strategy_gate)
monkeypatch.setenv("FIRSTRADE_RUN_SESSION_CHECK_ON_HTTP", "true")

def fail_runtime_call(*_args, **_kwargs):
pytest.fail("GET must not reach strategy, session, or broker runtime code")

monkeypatch.setattr(main, "_runtime_target_enabled_env", fail_runtime_call)
monkeypatch.setattr(main, "_run_strategy_cycle_with_report", fail_runtime_call)
monkeypatch.setattr(main, "run_session_check", fail_runtime_call)
monkeypatch.setattr(main, "FirstradeBrokerClient", fail_runtime_call)
client = main.app.test_client()

response = client.get("/run")
response = client.get(path)

assert response.status_code == 405
assert "POST" in response.headers["Allow"]


@pytest.mark.parametrize(
("path", "gate"),
[
("/run", "FIRSTRADE_RUN_STRATEGY_ON_HTTP"),
("/probe", "FIRSTRADE_RUN_SESSION_CHECK_ON_HTTP"),
],
)
def test_execution_posts_still_require_explicit_http_gate(monkeypatch, path, gate):
monkeypatch.delenv(gate, raising=False)

def fail_runtime_call(*_args, **_kwargs):
pytest.fail("disabled POST must not reach strategy, session, or broker runtime code")

monkeypatch.setattr(main, "_run_strategy_cycle_with_report", fail_runtime_call)
monkeypatch.setattr(main, "run_session_check", fail_runtime_call)
monkeypatch.setattr(main, "FirstradeBrokerClient", fail_runtime_call)

response = main.app.test_client().post(path)

assert response.status_code == 403
assert response.get_json()["ok"] is False


def test_health_endpoint_remains_available_via_get():
response = main.app.test_client().get("/health")

assert response.status_code == 200


def test_run_endpoint_calls_strategy_cycle_when_gate_enabled(monkeypatch):
monkeypatch.setenv("FIRSTRADE_RUN_STRATEGY_ON_HTTP", "true")
monkeypatch.setattr(main, "_should_skip_for_market_hours", lambda: (False, None))
Expand Down Expand Up @@ -185,16 +228,6 @@ def test_run_endpoint_returns_500_for_retryable_execution_block(monkeypatch):
assert payload["execution_block_retryable"] is True


def test_probe_endpoint_is_disabled_without_explicit_http_gate(monkeypatch):
monkeypatch.delenv("FIRSTRADE_RUN_SESSION_CHECK_ON_HTTP", raising=False)
client = main.app.test_client()

response = client.get("/probe")

assert response.status_code == 403
assert response.get_json()["ok"] is False


def test_probe_endpoint_calls_service_when_gate_enabled(monkeypatch):
monkeypatch.setenv("FIRSTRADE_RUN_SESSION_CHECK_ON_HTTP", "true")
sent_messages = []
Expand Down
35 changes: 35 additions & 0 deletions tests/test_sync_cloud_run_env_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,38 @@ def test_sync_cloud_run_env_workflow_syncs_scheduler_from_sync_plan():
assert direct_gate < workflow.index(
'desired_precheck_schedule="$(CURRENT_SCHEDULE="${desired_schedule}"'
)


def test_sync_cloud_run_env_workflow_hardens_deploy_runtime_boundary():
workflow_path = Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml"
workflow = workflow_path.read_text(encoding="utf-8")
deploy_block = workflow[
workflow.index('gcloud run deploy "${CLOUD_RUN_SERVICE}"') : workflow.index(
" - name: Check whether env sync is enabled"
)
]

assert "--no-allow-unauthenticated" in deploy_block
assert "--allow-unauthenticated" not in deploy_block
assert "--ingress=internal" in deploy_block
assert "--max-instances=1" in deploy_block
assert "--concurrency=1" in deploy_block
assert "--concurrency=80" not in deploy_block


def test_main_scheduler_update_and_create_are_authenticated_post_requests():
workflow_path = Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml"
workflow = workflow_path.read_text(encoding="utf-8")
scheduler_block = workflow[
workflow.index('scheduler_uri="${service_url}/run"') : workflow.index(
'managed_scheduler_jobs=("${job_name}")'
)
]

assert scheduler_block.count('gcloud scheduler jobs update http "${job_name}"') == 1
assert scheduler_block.count('gcloud scheduler jobs create http "${job_name}"') == 1
assert scheduler_block.count("--http-method=POST") == 2
assert scheduler_block.count(
'--oidc-service-account-email="${GCP_SCHEDULER_SERVICE_ACCOUNT}"'
) == 2
assert scheduler_block.count('--oidc-token-audience="${service_url}"') == 2