310 lines
11 KiB
Python
310 lines
11 KiB
Python
from fastapi.testclient import TestClient
|
|
|
|
import gateway.app as gateway_module
|
|
|
|
|
|
class DummyResponse:
|
|
def __init__(
|
|
self,
|
|
status_code: int,
|
|
headers: dict[str, str],
|
|
json_payload: dict | None = None,
|
|
text: str = '',
|
|
content: bytes | None = None,
|
|
):
|
|
self.status_code = status_code
|
|
self.headers = headers
|
|
self._json_payload = json_payload or {}
|
|
self.text = text
|
|
self.content = content if content is not None else text.encode("utf-8")
|
|
|
|
def json(self) -> dict:
|
|
return self._json_payload
|
|
|
|
|
|
class DummyAsyncClient:
|
|
last_request: dict = {}
|
|
|
|
def __init__(self, timeout: int):
|
|
self.timeout = timeout
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc, tb):
|
|
return None
|
|
|
|
async def request(self, method, url, params=None, content=None, headers=None):
|
|
DummyAsyncClient.last_request = {
|
|
'method': method,
|
|
'url': url,
|
|
'params': params,
|
|
'content': content,
|
|
'headers': headers or {},
|
|
}
|
|
return DummyResponse(200, {'content-type': 'application/json'}, {'ok': True})
|
|
|
|
|
|
def test_gateway_forwards_authorization_header(monkeypatch):
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.get(
|
|
'/proxy/auth/auth/me',
|
|
headers={'Authorization': 'Bearer test-token', 'X-User': 'admin', 'X-Role': 'admin'},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert DummyAsyncClient.last_request['headers']['Authorization'] == 'Bearer test-token'
|
|
assert DummyAsyncClient.last_request['headers']['X-User'] == 'admin'
|
|
|
|
|
|
def test_gateway_forwards_telegram_secret_header(monkeypatch):
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.post(
|
|
'/proxy/telegram/integrations/telegram/bot/webhook',
|
|
headers={'X-Telegram-Bot-Api-Secret-Token': 'secret-token'},
|
|
json={'update_id': 1},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert DummyAsyncClient.last_request['headers']['X-Telegram-Bot-Api-Secret-Token'] == 'secret-token'
|
|
|
|
|
|
def test_gateway_forwards_whatsapp_secret_header(monkeypatch):
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.post(
|
|
'/proxy/whatsapp/integrations/whatsapp/provider/webhook',
|
|
headers={'X-WhatsApp-Webhook-Secret': 'wa-secret'},
|
|
json={'entry': []},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert DummyAsyncClient.last_request['headers']['X-WhatsApp-Webhook-Secret'] == 'wa-secret'
|
|
assert DummyAsyncClient.last_request['content'] == b'{"entry":[]}'
|
|
|
|
|
|
def test_gateway_forwards_meta_signature_header_and_raw_body(monkeypatch):
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.post(
|
|
'/proxy/whatsapp/integrations/whatsapp/provider/webhook',
|
|
headers={
|
|
'X-Hub-Signature-256': 'sha256=test-signature',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
content=b'{"entry":[{"changes":[]}]}' ,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert DummyAsyncClient.last_request['headers']['X-Hub-Signature-256'] == 'sha256=test-signature'
|
|
assert DummyAsyncClient.last_request['headers']['Content-Type'] == 'application/json'
|
|
assert DummyAsyncClient.last_request['content'] == b'{"entry":[{"changes":[]}]}'
|
|
|
|
|
|
def test_gateway_preserves_html_response(monkeypatch):
|
|
async def request(self, method, url, params=None, content=None, headers=None): # noqa: ANN001, ANN201
|
|
return DummyResponse(200, {'content-type': 'text/html'}, text='<html>bridge</html>')
|
|
|
|
monkeypatch.setattr(DummyAsyncClient, 'request', request)
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.get('/proxy/auth/auth/oidc/callback')
|
|
|
|
assert response.status_code == 200
|
|
assert '<html>bridge</html>' in response.text
|
|
|
|
|
|
def test_gateway_preserves_plain_text_response(monkeypatch):
|
|
async def request(self, method, url, params=None, content=None, headers=None): # noqa: ANN001, ANN201
|
|
return DummyResponse(200, {'content-type': 'text/plain; charset=utf-8'}, text='123456')
|
|
|
|
monkeypatch.setattr(DummyAsyncClient, 'request', request)
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.get('/proxy/whatsapp/integrations/whatsapp/provider/webhook')
|
|
|
|
assert response.status_code == 200
|
|
assert response.text == '123456'
|
|
assert response.headers['content-type'].startswith('text/plain')
|
|
|
|
|
|
def test_gateway_forwards_delete_requests(monkeypatch):
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.delete(
|
|
'/proxy/routing/queues/que_delete_me',
|
|
headers={'X-User': 'admin', 'X-Role': 'admin'},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert DummyAsyncClient.last_request['method'] == 'DELETE'
|
|
assert DummyAsyncClient.last_request['url'].endswith('/queues/que_delete_me')
|
|
|
|
|
|
def test_gateway_prefixes_sales_public_api_paths(monkeypatch):
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.get('/proxy/sales/pipelines?limit=25')
|
|
|
|
assert response.status_code == 200
|
|
assert DummyAsyncClient.last_request['url'].endswith('/api/v1/pipelines')
|
|
assert DummyAsyncClient.last_request['params'] == {'limit': '25'}
|
|
|
|
|
|
def test_gateway_does_not_prefix_sales_internal_paths(monkeypatch):
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.post('/proxy/sales/internal/sales-sync/telegram', json={'chat_id': '123'})
|
|
|
|
assert response.status_code == 200
|
|
assert DummyAsyncClient.last_request['url'].endswith('/internal/sales-sync/telegram')
|
|
|
|
|
|
def test_gateway_preserves_binary_audio_response(monkeypatch):
|
|
async def request(self, method, url, params=None, content=None, headers=None): # noqa: ANN001, ANN201
|
|
return DummyResponse(
|
|
200,
|
|
{
|
|
'content-type': 'audio/wav',
|
|
'content-disposition': 'attachment; filename="demo-call.wav"',
|
|
},
|
|
content=b'RIFFdemo',
|
|
)
|
|
|
|
monkeypatch.setattr(DummyAsyncClient, 'request', request)
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.get('/proxy/recording/recordings/rec_demo/content')
|
|
|
|
assert response.status_code == 200
|
|
assert response.content == b'RIFFdemo'
|
|
assert response.headers['content-type'].startswith('audio/wav')
|
|
assert 'demo-call.wav' in response.headers['content-disposition']
|
|
|
|
|
|
def test_gateway_returns_bad_gateway_when_upstream_unavailable(monkeypatch):
|
|
async def request(self, method, url, params=None, content=None, headers=None): # noqa: ANN001, ANN201
|
|
raise gateway_module.httpx.ConnectError("All connection attempts failed")
|
|
|
|
monkeypatch.setattr(DummyAsyncClient, 'request', request)
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.get('/proxy/sales/pipelines')
|
|
|
|
assert response.status_code == 502
|
|
assert response.json() == {
|
|
'detail': 'Upstream service unavailable',
|
|
'service': 'sales',
|
|
}
|
|
|
|
|
|
def test_gateway_queues_error_alert_for_proxy_unreachable(monkeypatch):
|
|
async def request(self, method, url, params=None, content=None, headers=None): # noqa: ANN001, ANN201
|
|
raise gateway_module.httpx.ConnectError("All connection attempts failed")
|
|
|
|
alerts: list[dict] = []
|
|
|
|
def queue_alert(service: str, method: str, path: str, status: int, detail: str) -> None: # noqa: ARG001
|
|
alerts.append({
|
|
'service': service,
|
|
'method': method,
|
|
'path': path,
|
|
'status': status,
|
|
'detail': detail,
|
|
})
|
|
|
|
monkeypatch.setattr(DummyAsyncClient, 'request', request)
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
monkeypatch.setattr(gateway_module, '_queue_telegram_error_alert', queue_alert)
|
|
monkeypatch.setenv('ERROR_TELEGRAM_ALERTS', '1')
|
|
monkeypatch.setenv('ERROR_TELEGRAM_ALERT_BOT_TOKEN', 'token')
|
|
monkeypatch.setenv('ERROR_TELEGRAM_ALERT_CHAT_IDS', '999')
|
|
client = TestClient(gateway_module.app)
|
|
|
|
response = client.get('/proxy/sales/pipelines')
|
|
|
|
assert response.status_code == 502
|
|
assert alerts == [
|
|
{
|
|
'service': 'api-gateway',
|
|
'method': 'GET',
|
|
'path': '/proxy/sales/pipelines',
|
|
'status': 502,
|
|
'detail': 'Upstream service unavailable: service=sales base_url=http://localhost:8020',
|
|
},
|
|
]
|
|
|
|
|
|
def test_gateway_queues_error_alert_for_channel_alias(monkeypatch):
|
|
async def request(self, method, url, params=None, content=None, headers=None): # noqa: ANN001, ANN201
|
|
raise gateway_module.httpx.ConnectError("All connection attempts failed")
|
|
|
|
alerts: list[dict] = []
|
|
|
|
def queue_alert(service: str, method: str, path: str, status: int, detail: str) -> None: # noqa: ARG001
|
|
alerts.append({
|
|
'service': service,
|
|
'method': method,
|
|
'path': path,
|
|
'status': status,
|
|
'detail': detail,
|
|
})
|
|
|
|
monkeypatch.setattr(DummyAsyncClient, 'request', request)
|
|
monkeypatch.setattr(gateway_module.httpx, 'AsyncClient', DummyAsyncClient)
|
|
monkeypatch.setattr(gateway_module, '_queue_telegram_error_alert', queue_alert)
|
|
monkeypatch.setenv('ERROR_TELEGRAM_ALERTS', '1')
|
|
monkeypatch.setenv('ERROR_TELEGRAM_ALERT_BOT_TOKEN', 'token')
|
|
monkeypatch.setenv('ERROR_TELEGRAM_ALERT_CHANNEL_ID', '@my_alert_channel')
|
|
client = TestClient(gateway_module.app)
|
|
|
|
client.get('/proxy/sales/pipelines')
|
|
|
|
assert alerts
|
|
assert alerts[0]['status'] == 502
|
|
|
|
|
|
def test_gateway_queues_error_alert_for_internal_exception(monkeypatch):
|
|
async def broken_forward(method: str, service: str, path: str, request: object) -> None: # noqa: ANN001, ANN201
|
|
raise ValueError('proxy exploded')
|
|
|
|
alerts: list[dict] = []
|
|
|
|
def queue_alert(service: str, method: str, path: str, status: int, detail: str) -> None: # noqa: ARG001
|
|
alerts.append({
|
|
'service': service,
|
|
'method': method,
|
|
'path': path,
|
|
'status': status,
|
|
'detail': detail,
|
|
})
|
|
|
|
monkeypatch.setattr(gateway_module, '_forward', broken_forward)
|
|
monkeypatch.setattr(gateway_module, '_queue_telegram_error_alert', queue_alert)
|
|
monkeypatch.setenv('ERROR_TELEGRAM_ALERTS', '1')
|
|
monkeypatch.setenv('ERROR_TELEGRAM_ALERT_BOT_TOKEN', 'token')
|
|
monkeypatch.setenv('ERROR_TELEGRAM_ALERT_CHAT_IDS', '999')
|
|
|
|
client = TestClient(gateway_module.app, raise_server_exceptions=False)
|
|
|
|
response = client.get('/proxy/sales/pipelines')
|
|
|
|
assert response.status_code == 500
|
|
assert alerts
|
|
assert alerts[0]['service'] == 'api-gateway'
|
|
assert alerts[0]['status'] == 500
|
|
assert alerts[0]['path'] == '/proxy/sales/pipelines'
|