mirror of
https://github.com/furyhawk/api_gateway.git
synced 2026-07-21 02:06:50 +00:00
Add unit tests for gateway configuration and proxy request handling
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from gateway_framework.app import create_app
|
||||
from gateway_framework.config import load_gateway_config
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_management_endpoints_and_external_openapi(tmp_path) -> None:
|
||||
external_openapi = tmp_path / "external-openapi.json"
|
||||
external_openapi.write_text(
|
||||
json.dumps({"openapi": "3.1.0", "info": {"title": "Upstream", "version": "1.0.0"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config_file = tmp_path / "gateway.yaml"
|
||||
config_file.write_text(
|
||||
f"""
|
||||
settings:
|
||||
title: Test Gateway
|
||||
version: 9.9.9
|
||||
description: test gateway
|
||||
external_openapi_file: {external_openapi}
|
||||
upstreams:
|
||||
demo:
|
||||
base_url: https://example.com/
|
||||
routes:
|
||||
- path: /api/v1/demo
|
||||
methods: [GET]
|
||||
upstream: demo
|
||||
upstream_path: /api/v1/demo
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
app = create_app(str(config_file))
|
||||
app.state.gateway_config = load_gateway_config(config_file)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
health = await client.get("/healthz")
|
||||
ready = await client.get("/readyz")
|
||||
routes = await client.get("/admin/routes")
|
||||
external = await client.get("/openapi/external.json")
|
||||
|
||||
assert health.status_code == 200
|
||||
assert health.json() == {"status": "ok"}
|
||||
assert ready.status_code == 200
|
||||
assert ready.json() == {"status": "ready"}
|
||||
assert routes.status_code == 200
|
||||
assert routes.json()[0]["path"] == "/api/v1/demo"
|
||||
assert external.status_code == 200
|
||||
assert external.json()["info"]["title"] == "Upstream"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_external_openapi_not_configured(tmp_path) -> None:
|
||||
config_file = tmp_path / "gateway.yaml"
|
||||
config_file.write_text(
|
||||
"""
|
||||
upstreams:
|
||||
demo:
|
||||
base_url: https://example.com/
|
||||
routes:
|
||||
- path: /api/v1/demo
|
||||
methods: [GET]
|
||||
upstream: demo
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
app = create_app(str(config_file))
|
||||
app.state.gateway_config = load_gateway_config(config_file)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
response = await client.get("/openapi/external.json")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json()["error"] == "external_openapi_not_configured"
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway_framework.config import load_gateway_config
|
||||
|
||||
|
||||
def test_load_gateway_config_success(tmp_path) -> None:
|
||||
config_file = tmp_path / "gateway.yaml"
|
||||
config_file.write_text(
|
||||
"""
|
||||
settings:
|
||||
title: Test Gateway
|
||||
upstreams:
|
||||
demo:
|
||||
base_url: https://example.com/
|
||||
routes:
|
||||
- path: /api/v1/demo
|
||||
methods: [GET]
|
||||
upstream: demo
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config = load_gateway_config(config_file)
|
||||
|
||||
assert config.settings.title == "Test Gateway"
|
||||
assert "demo" in config.upstreams
|
||||
assert config.routes[0].path == "/api/v1/demo"
|
||||
|
||||
|
||||
def test_load_gateway_config_unknown_upstream(tmp_path) -> None:
|
||||
config_file = tmp_path / "gateway.yaml"
|
||||
config_file.write_text(
|
||||
"""
|
||||
upstreams:
|
||||
known:
|
||||
base_url: https://example.com/
|
||||
routes:
|
||||
- path: /api/v1/demo
|
||||
methods: [GET]
|
||||
upstream: unknown
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="unknown upstreams"):
|
||||
load_gateway_config(config_file)
|
||||
|
||||
|
||||
def test_load_gateway_config_invalid_route_path(tmp_path) -> None:
|
||||
config_file = tmp_path / "gateway.yaml"
|
||||
config_file.write_text(
|
||||
"""
|
||||
upstreams:
|
||||
demo:
|
||||
base_url: https://example.com/
|
||||
routes:
|
||||
- path: api/v1/demo
|
||||
methods: [GET]
|
||||
upstream: demo
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="route path must start"):
|
||||
load_gateway_config(config_file)
|
||||
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from starlette.requests import Request
|
||||
|
||||
from gateway_framework.config import GatewayConfig, RouteConfig, UpstreamConfig
|
||||
from gateway_framework.proxy import build_target_path, proxy_request
|
||||
|
||||
|
||||
def _make_request(path: str, query: str = "", headers: list[tuple[str, str]] | None = None) -> Request:
|
||||
encoded_headers = [
|
||||
(key.lower().encode("utf-8"), value.encode("utf-8")) for key, value in (headers or [])
|
||||
]
|
||||
scope = {
|
||||
"type": "http",
|
||||
"http_version": "1.1",
|
||||
"method": "GET",
|
||||
"scheme": "http",
|
||||
"path": path,
|
||||
"query_string": query.encode("utf-8"),
|
||||
"headers": encoded_headers,
|
||||
"client": ("127.0.0.1", 51234),
|
||||
"server": ("gateway.local", 8000),
|
||||
}
|
||||
|
||||
async def receive() -> dict[str, object]:
|
||||
return {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
return Request(scope, receive)
|
||||
|
||||
|
||||
def test_build_target_path_uses_upstream_path_override() -> None:
|
||||
route = RouteConfig(path="/api/v1/bus", methods=["GET"], upstream="demo", upstream_path="/v2/bus")
|
||||
request = _make_request("/api/v1/bus")
|
||||
|
||||
assert build_target_path(request, route) == "/v2/bus"
|
||||
|
||||
|
||||
def test_build_target_path_applies_strip_prefix() -> None:
|
||||
route = RouteConfig(path="/api/v1/bus", methods=["GET"], upstream="demo", strip_prefix="/api")
|
||||
request = _make_request("/api/v1/bus")
|
||||
|
||||
assert build_target_path(request, route) == "/v1/bus"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_proxy_request_forwards_to_upstream() -> None:
|
||||
seen: dict[str, object] = {}
|
||||
|
||||
def handler(req: httpx.Request) -> httpx.Response:
|
||||
seen["url"] = str(req.url)
|
||||
seen["x_forwarded_proto"] = req.headers.get("x-forwarded-proto")
|
||||
seen["x_forwarded_host"] = req.headers.get("x-forwarded-host")
|
||||
seen["connection"] = req.headers.get("connection")
|
||||
return httpx.Response(
|
||||
status_code=200,
|
||||
content=b'{"ok":true}',
|
||||
headers={"content-type": "application/json", "connection": "close"},
|
||||
)
|
||||
|
||||
config = GatewayConfig(
|
||||
upstreams={"demo": UpstreamConfig(base_url="https://example.com/", timeout_seconds=5)},
|
||||
routes=[],
|
||||
)
|
||||
route = RouteConfig(path="/api/v1/bus", methods=["GET"], upstream="demo")
|
||||
request = _make_request("/api/v1/bus", query="a=1", headers=[("Connection", "keep-alive")])
|
||||
|
||||
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||
response = await proxy_request(request=request, client=client, config=config, route=route)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.body == b'{"ok":true}'
|
||||
assert response.headers.get("connection") is None
|
||||
assert seen["url"] == "https://example.com/api/v1/bus?a=1"
|
||||
assert seen["x_forwarded_proto"] == "http"
|
||||
assert seen["x_forwarded_host"] == "gateway.local"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_proxy_request_returns_502_when_upstream_unreachable() -> None:
|
||||
def handler(req: httpx.Request) -> httpx.Response:
|
||||
raise httpx.ConnectError("connection failed", request=req)
|
||||
|
||||
config = GatewayConfig(
|
||||
upstreams={"demo": UpstreamConfig(base_url="https://example.com/")},
|
||||
routes=[],
|
||||
)
|
||||
route = RouteConfig(path="/api/v1/bus", methods=["GET"], upstream="demo")
|
||||
request = _make_request("/api/v1/bus")
|
||||
|
||||
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||
response = await proxy_request(request=request, client=client, config=config, route=route)
|
||||
|
||||
assert response.status_code == 502
|
||||
assert b"upstream_unreachable" in response.body
|
||||
Reference in New Issue
Block a user