mirror of
https://github.com/sentdm/sent-plugin.git
synced 2026-09-14 16:23:54 +08:00
141 lines
6.0 KiB
Python
141 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Offline simulations for Sent contract and documentation freshness monitoring."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import check_live_contract as CHECKER
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MANIFEST = json.loads((ROOT / "schemas" / "sent" / "v3-contract-manifest.json").read_text(encoding="utf-8"))
|
|
|
|
|
|
def minimal_openapi() -> dict:
|
|
schemas = {
|
|
"SentDmServicesEndpointsCustomerAPIv3RequestsCreateTemplateRequest": {
|
|
"type": "object",
|
|
"required": MANIFEST["template_create"]["required_fields"],
|
|
"properties": {name: {} for name in MANIFEST["template_create"]["allowed_fields"]},
|
|
},
|
|
"SentDmServicesCommonEntitiesTemplateBodyContent": {
|
|
"type": "object",
|
|
"properties": {"template": {"type": "string", "maxLength": MANIFEST["template_create"]["body_max_length"]}},
|
|
},
|
|
"SentDmServicesCommonEntitiesTemplateButton": {
|
|
"type": "object",
|
|
"properties": {"type": {"description": " ".join(MANIFEST["template_create"]["button_types"])}},
|
|
},
|
|
"SentDmServicesEndpointsCustomerAPIv3RequestsCreateProfileRequest": {
|
|
"type": "object",
|
|
"required": MANIFEST["profile"]["create_required_fields"],
|
|
},
|
|
"SentDmServicesEndpointsCustomerAPIv3RequestsWhatsappBusinessAccountCredentials": {
|
|
"type": "object",
|
|
"required": MANIFEST["profile"]["waba_required_fields"],
|
|
},
|
|
"SentDmServicesEndpointsCustomerAPIv3RequestsProfilesCompleteProfileRequest": {
|
|
"type": "object",
|
|
"required": MANIFEST["profile"]["complete_required_fields"],
|
|
},
|
|
"SentDmServicesEndpointsCustomerAPIv3RequestsCampaignsCampaignData": {
|
|
"type": "object",
|
|
"required": MANIFEST["campaign"]["required_fields"],
|
|
},
|
|
"SentDmServicesCommonEnumsMessagingUseCaseUS": {
|
|
"type": "string",
|
|
"enum": MANIFEST["campaign"]["use_case_values"],
|
|
},
|
|
"SentDmServicesEndpointsCustomerAPIv3RequestsCampaignsCampaignUseCaseData": {
|
|
"type": "object",
|
|
"properties": {
|
|
"sampleMessages": {
|
|
"type": "array",
|
|
"minItems": MANIFEST["campaign"]["sample_min"],
|
|
"maxItems": MANIFEST["campaign"]["sample_max"],
|
|
}
|
|
},
|
|
},
|
|
}
|
|
paths = {
|
|
path: {method.lower(): {"responses": {"200": {"description": "ok"}}} for method in methods}
|
|
for path, methods in MANIFEST["critical_paths"].items()
|
|
}
|
|
return copy.deepcopy({"openapi": "3.1.0", "paths": paths, "components": {"schemas": schemas}})
|
|
|
|
|
|
def source(identifier: str, url: str, kind: str, checks: list[dict] | None = None) -> dict:
|
|
value = {
|
|
"id": identifier,
|
|
"url": url,
|
|
"kind": kind,
|
|
"affected_skills": ["sent"],
|
|
"last_verified": "2026-08-09",
|
|
}
|
|
if checks is not None:
|
|
value["checks"] = checks
|
|
return value
|
|
|
|
|
|
class OpenApiDriftTests(unittest.TestCase):
|
|
def assert_drift(self, document: dict, fact_fragment: str) -> None:
|
|
result = CHECKER.evaluate_openapi(document, MANIFEST, source("openapi", "https://example.com/openapi.json", "openapi"))
|
|
self.assertEqual(result["status"], "drift")
|
|
self.assertTrue(any(fact_fragment in fact["fact"] for fact in result["facts"] if fact["status"] == "drift"))
|
|
|
|
def test_endpoint_removal(self) -> None:
|
|
document = minimal_openapi()
|
|
document["paths"].pop("/v3/messages")
|
|
self.assert_drift(document, "path./v3/messages.methods")
|
|
|
|
def test_enum_addition(self) -> None:
|
|
document = minimal_openapi()
|
|
document["components"]["schemas"]["SentDmServicesCommonEnumsMessagingUseCaseUS"]["enum"].append("NEW_CASE")
|
|
self.assert_drift(document, "campaign.use_case_values")
|
|
|
|
def test_property_change(self) -> None:
|
|
document = minimal_openapi()
|
|
document["components"]["schemas"]["SentDmServicesEndpointsCustomerAPIv3RequestsCreateTemplateRequest"]["properties"]["newField"] = {}
|
|
self.assert_drift(document, "template.allowed_fields")
|
|
|
|
|
|
class SourceFailureTests(unittest.TestCase):
|
|
def test_source_conflict_is_semantic_exit_one(self) -> None:
|
|
check = {"fact": "template.body_max_length", "extractor": "regex_int", "pattern": r"Limit: (\d+)", "expected": 1024}
|
|
catalog = {"sources": [
|
|
source("first", "https://example.com/first.txt", "documentation", [check]),
|
|
source("second", "https://example.com/second.txt", "documentation", [check]),
|
|
]}
|
|
bodies = {"https://example.com/first.txt": "Limit: 1024", "https://example.com/second.txt": "Limit: 2048"}
|
|
report = CHECKER.run_monitor(catalog, MANIFEST, lambda url, timeout: bodies[url], timeout=1, retries=0)
|
|
self.assertEqual(CHECKER.exit_code(report), 1)
|
|
self.assertEqual({item["status"] for item in report["sources"]}, {"conflict"})
|
|
|
|
def test_timeout_retries_are_bounded_and_exit_two(self) -> None:
|
|
calls = 0
|
|
|
|
def timeout_fetcher(url: str, timeout: float) -> str:
|
|
nonlocal calls
|
|
calls += 1
|
|
raise TimeoutError("synthetic timeout")
|
|
|
|
catalog = {"sources": [source("docs", "https://example.com/docs.txt", "documentation", [])]}
|
|
report = CHECKER.run_monitor(catalog, MANIFEST, timeout_fetcher, timeout=0.01, retries=2)
|
|
self.assertEqual(calls, 3)
|
|
self.assertEqual(report["sources"][0]["status"], "unavailable")
|
|
self.assertEqual(CHECKER.exit_code(report), 2)
|
|
|
|
def test_malformed_openapi_response_exits_two(self) -> None:
|
|
catalog = {"sources": [source("openapi", "https://example.com/openapi.json", "openapi")]}
|
|
report = CHECKER.run_monitor(catalog, MANIFEST, lambda url, timeout: "{", timeout=1, retries=0)
|
|
self.assertEqual(report["sources"][0]["status"], "malformed")
|
|
self.assertEqual(CHECKER.exit_code(report), 2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|