dilutionrisk-mcp / tests /test_api_contract.py
mzx's picture
Exchange-aware API: accept NZX alongside ASX
c31f463 verified
Raw
History Blame Contribute Delete
5.35 kB
from __future__ import annotations
import os,unittest
from unittest.mock import patch
from fastapi.testclient import TestClient
import app
from api_models import CursorError,decode_cursor,encode_cursor
REVISION="d2b8896cabf2489d1b2f82694376f90a3b38049c"
READY={"status":"ready","ready":True,"dataset_revision":REVISION,"artifact_count":25,"cached_bytes":310651359,"cache_dir":"/not-used-by-contract-tests","error":None}
class ApiContractTests(unittest.TestCase):
def setUp(self):
self.stack=patch.multiple(app.CACHE,status=lambda:dict(READY));self.stack.start();self.env=patch.dict(os.environ,{"DILUTIONRISK_BUILD_SHA":"b"*40});self.env.start();self.client=TestClient(app.app)
def tearDown(self):self.env.stop();self.stack.stop()
def test_openapi_exposes_bounded_typed_contract(self):
schema=self.client.get("/openapi.json").json();self.assertIn("/api/v1/companies",schema["paths"]);self.assertIn("/api/v1/companies/{ticker}",schema["paths"]);self.assertIn("CompanyPage",schema["components"]["schemas"]);self.assertIn("CompanySnapshot",schema["components"]["schemas"]);operation=schema["paths"]["/api/v1/companies"]["get"];limit=next(x for x in operation["parameters"] if x["name"]=="limit");self.assertEqual(limit["schema"]["maximum"],100);self.assertEqual(operation["responses"]["422"]["content"]["application/json"]["schema"]["$ref"],"#/components/schemas/ErrorResponse")
def test_company_snapshot_combines_metrics_instruments_and_revision(self):
company={"issuer_id":"ASX:BHP","exchange":"ASX","ticker":"BHP","name":"BHP Group","sector":None,"industry":None,"as_of_date":"2026-07-17","needs_review":False,"basic_shares":5078913175,"fd_ratio":1.003,"worst_case_dilution_pct":0.3,"expiry_pct_lt_3m":0.0,"expiry_pct_lt_6m":0.0,"expiry_pct_lt_12m":0.0,"cash_runway_months":None};instrument={"instrument_id":"ASX:BHP:OPT","ticker":"BHP","instrument_type":"option","quantity":100,"series_name":"Options","asx_code":None,"exercise_price_aud":"1.0","expiry":"2027-01-01","holder_type":"broker","vesting_condition":"none","status":"active","as_of_date":"2026-07-17","document_ids":["ASX:BHP:1"]}
with patch("app.tool_context",return_value=(dict(READY),{"dataset_revision":REVISION,"published_at":"2026-07-17T12:49:16Z","source_watermark":{"provider":"ASX"},"dataset_citation":"https://example.test/dataset"})),patch("app.query_companies",return_value=([company],False,{})),patch("app.dilution_instruments",return_value=[instrument]):
response=self.client.get("/api/v1/companies/BHP");self.assertEqual(response.status_code,200);payload=response.json();self.assertEqual(payload["company"]["ticker"],"BHP");self.assertEqual(payload["instruments"][0]["document_ids"],["ASX:BHP:1"]);self.assertEqual(payload["dataset_revision"],REVISION)
with patch("app.tool_context",return_value=(dict(READY),{"dataset_revision":REVISION,"published_at":"2026-07-17T12:49:16Z","source_watermark":{"provider":"ASX"},"dataset_citation":"https://example.test/dataset"})),patch("app.company_at_date",return_value=dict(company)),patch("app.dilution_instruments",return_value=[instrument]) as instruments:
response=self.client.get("/api/v1/companies/BHP",params={"as_of_date":"2026-07-17"});self.assertEqual(response.status_code,200);instruments.assert_called_once_with(READY["cache_dir"],"BHP",100,"2026-07-17",positions_only=True,exchange="ASX")
def test_document_page_carries_batched_public_source_links(self):
document={"document_id":"ASX:CLA:03115626","exchange":"ASX","ticker":"CLA","source_document_id":"03115626","announcement_date":"2026-07-17","announced_at":"2026-07-17T01:00:00Z","title":"Capital raising","doc_type":"Announcement","doc_type_source":"rule","doc_type_confidence":1.0,"raw_artifact_key":None,"raw_size_bytes":None,"markdown_artifact_key":"artifacts/v1/asx/CLA/03115626.md","markdown_size_bytes":10,"publication_state":"public"}
with patch("app.query_documents",return_value=([document],None)),patch("app.document_citations",return_value={"ASX:CLA:03115626":"https://www.asx.com.au/asxpdf/example.pdf"}):
response=self.client.get("/api/v1/documents",params={"ticker":"CLA","limit":1});self.assertEqual(response.status_code,200);self.assertEqual(response.json()["items"][0]["source_link"],"https://www.asx.com.au/asxpdf/example.pdf")
def test_status_carries_exact_revision(self):
response=self.client.get("/api/v1/status");self.assertEqual(response.status_code,200);self.assertEqual(response.json()["dataset_revision"],REVISION)
def test_invalid_inputs_are_structured_4xx(self):
for url in ("/api/v1/companies?limit=0","/api/v1/companies?limit=101","/api/v1/companies?q="+"x"*121,"/api/v1/documents?ticker=../etc"):
response=self.client.get(url);self.assertEqual(response.status_code,422);self.assertEqual(response.json()["error"]["code"],"invalid_request")
def test_cursor_is_revision_and_endpoint_aware(self):
cursor=encode_cursor(REVISION,"companies",["ASX:BHP"]);self.assertEqual(decode_cursor(cursor,REVISION,"companies"),["ASX:BHP"])
self.assertEqual(self.client.get("/api/v1/documents",params={"cursor":cursor}).status_code,400)
stale=encode_cursor("a"*40,"companies",["ASX:BHP"]);response=self.client.get("/api/v1/companies",params={"cursor":stale});self.assertEqual(response.status_code,409);self.assertEqual(response.json()["error"]["code"],"stale_cursor")
if __name__=="__main__":unittest.main()