import pytest from pytest import raises from sanic.app import Sanic from sanic.blueprint_group import BlueprintGroup from sanic.blueprints import Blueprint from sanic.exceptions import BadRequest, Forbidden, SanicException, ServerError from sanic.request import Request from sanic.response import HTTPResponse, text MIDDLEWARE_INVOKE_COUNTER = {"request": 0, "response": 0} AUTH = "dGVzdDp0ZXN0Cg==" def test_bp_group_indexing(app: Sanic): blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") group = Blueprint.group(blueprint_1, blueprint_2) assert group[0] == blueprint_1 with raises(expected_exception=IndexError): _ = group[3] def test_bp_group_with_additional_route_params(app: Sanic): blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") @blueprint_1.route("/request_path", methods=frozenset({"PUT", "POST"}), version=2) def blueprint_1_v2_method_with_put_and_post(request: Request): if request.method == "PUT": return text("PUT_OK") elif request.method == "POST": return text("POST_OK") @blueprint_2.route( "/route/", methods=frozenset({"DELETE", "PATCH"}), name="test" ) def blueprint_2_named_method(request: Request, param): if request.method == "DELETE": return text(f"DELETE_{param}") elif request.method == "PATCH": return text(f"PATCH_{param}") blueprint_group = Blueprint.group(blueprint_1, blueprint_2, url_prefix="/api") @blueprint_group.middleware("request") def authenticate_request(request: Request): global AUTH auth = request.headers.get("authorization") if auth: # Dummy auth check. We can have anything here and it's fine. if AUTH not in auth: return text("Unauthorized", status=401) else: return text("Unauthorized", status=401) @blueprint_group.middleware("response") def enhance_response_middleware(request: Request, response: HTTPResponse): response.headers.add("x-test-middleware", "value") app.blueprint(blueprint_group) header = {"authorization": " ".join(["Basic", AUTH])} _, response = app.test_client.put("/v2/api/bp1/request_path", headers=header) assert response.text == "PUT_OK" assert response.headers.get("x-test-middleware") == "value" _, response = app.test_client.post("/v2/api/bp1/request_path", headers=header) assert response.text == "POST_OK" _, response = app.test_client.delete("/api/bp2/route/bp2", headers=header) assert response.text == "DELETE_bp2" _, response = app.test_client.patch("/api/bp2/route/bp2", headers=header) assert response.text == "PATCH_bp2" _, response = app.test_client.put("/v2/api/bp1/request_path") assert response.status == 401 def test_bp_group(app: Sanic): blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") @blueprint_1.route("/") def blueprint_1_default_route(request): return text("BP1_OK") @blueprint_1.route("/invalid") def blueprint_1_error(request: Request): raise BadRequest("Invalid") @blueprint_2.route("/") def blueprint_2_default_route(request): return text("BP2_OK") @blueprint_2.route("/error") def blueprint_2_error(request: Request): raise ServerError("Error") blueprint_group_1 = Blueprint.group(blueprint_1, blueprint_2, url_prefix="/bp") blueprint_3 = Blueprint("blueprint_3", url_prefix="/bp3") @blueprint_group_1.exception(BadRequest) def handle_group_exception(request, exception): return text("BP1_ERR_OK") @blueprint_group_1.middleware("request") def blueprint_group_1_middleware(request): global MIDDLEWARE_INVOKE_COUNTER MIDDLEWARE_INVOKE_COUNTER["request"] += 1 @blueprint_group_1.middleware def blueprint_group_1_middleware_not_called(request): global MIDDLEWARE_INVOKE_COUNTER MIDDLEWARE_INVOKE_COUNTER["request"] += 1 @blueprint_group_1.on_request def blueprint_group_1_convenience_1(request): global MIDDLEWARE_INVOKE_COUNTER MIDDLEWARE_INVOKE_COUNTER["request"] += 1 @blueprint_group_1.on_request() def blueprint_group_1_convenience_2(request): global MIDDLEWARE_INVOKE_COUNTER MIDDLEWARE_INVOKE_COUNTER["request"] += 1 @blueprint_3.route("/") def blueprint_3_default_route(request): return text("BP3_OK") @blueprint_3.route("/forbidden") def blueprint_3_forbidden(request: Request): raise Forbidden("Forbidden") blueprint_group_2 = Blueprint.group( blueprint_group_1, blueprint_3, url_prefix="/api" ) @blueprint_group_2.exception(SanicException) def handle_non_handled_exception(request, exception): return text("BP2_ERR_OK") @blueprint_group_2.middleware("response") def blueprint_group_2_middleware(request, response): global MIDDLEWARE_INVOKE_COUNTER MIDDLEWARE_INVOKE_COUNTER["response"] += 1 @blueprint_group_2.on_response def blueprint_group_2_middleware_convenience_1(request, response): global MIDDLEWARE_INVOKE_COUNTER MIDDLEWARE_INVOKE_COUNTER["response"] += 1 @blueprint_group_2.on_response() def blueprint_group_2_middleware_convenience_2(request, response): global MIDDLEWARE_INVOKE_COUNTER MIDDLEWARE_INVOKE_COUNTER["response"] += 1 app.blueprint(blueprint_group_2) @app.route("/") def app_default_route(request): return text("APP_OK") _, response = app.test_client.get("/") assert response.text == "APP_OK" _, response = app.test_client.get("/api/bp/bp1") assert response.text == "BP1_OK" _, response = app.test_client.get("/api/bp/bp1/invalid") assert response.text == "BP1_ERR_OK" _, response = app.test_client.get("/api/bp/bp2") assert response.text == "BP2_OK" _, response = app.test_client.get("/api/bp/bp2/error") assert response.text == "BP2_ERR_OK" _, response = app.test_client.get("/api/bp3") assert response.text == "BP3_OK" _, response = app.test_client.get("/api/bp3/forbidden") assert response.text == "BP2_ERR_OK" assert MIDDLEWARE_INVOKE_COUNTER["response"] == 18 assert MIDDLEWARE_INVOKE_COUNTER["request"] == 16 def test_bp_group_list_operations(app: Sanic): blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") @blueprint_1.route("/") def blueprint_1_default_route(request): return text("BP1_OK") @blueprint_2.route("/") def blueprint_2_default_route(request): return text("BP2_OK") blueprint_group_1 = Blueprint.group(blueprint_1, blueprint_2, url_prefix="/bp") blueprint_3 = Blueprint("blueprint_2", url_prefix="/bp3") @blueprint_3.route("/second") def blueprint_3_second_route(request): return text("BP3_OK") assert len(blueprint_group_1) == 2 blueprint_group_1.append(blueprint_3) assert len(blueprint_group_1) == 3 del blueprint_group_1[2] assert len(blueprint_group_1) == 2 blueprint_group_1[1] = blueprint_3 assert len(blueprint_group_1) == 2 assert blueprint_group_1.url_prefix == "/bp" def test_bp_group_as_list(): blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") blueprint_group_1 = Blueprint.group([blueprint_1, blueprint_2]) assert len(blueprint_group_1) == 2 def test_bp_group_as_nested_group(): blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") blueprint_group_1 = Blueprint.group(Blueprint.group(blueprint_1, blueprint_2)) assert len(blueprint_group_1) == 1 def test_blueprint_group_insert(): blueprint_1 = Blueprint( "blueprint_1", url_prefix="/bp1", strict_slashes=True, version=1 ) blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") blueprint_3 = Blueprint("blueprint_3", url_prefix=None) group = BlueprintGroup(url_prefix="/test", version=1.3, strict_slashes=False) group.insert(0, blueprint_1) group.insert(0, blueprint_2) group.insert(0, blueprint_3) @blueprint_1.route("/") def blueprint_1_default_route(request): return text("BP1_OK") @blueprint_2.route("/") def blueprint_2_default_route(request): return text("BP2_OK") @blueprint_3.route("/") def blueprint_3_default_route(request): return text("BP3_OK") app = Sanic("PropTest") app.blueprint(group) app.router.finalize() routes = [(route.path, route.strict) for route in app.router.routes] assert len(routes) == 3 assert ("v1/test/bp1/", True) in routes assert ("v1.3/test/bp2", False) in routes assert ("v1.3/test", False) in routes def test_bp_group_properties(): blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") group = Blueprint.group( blueprint_1, blueprint_2, version=1, version_prefix="/api/v", url_prefix="/grouped", strict_slashes=True, ) primary = Blueprint.group(group, url_prefix="/primary") @blueprint_1.route("/") def blueprint_1_default_route(request): return text("BP1_OK") @blueprint_2.route("/") def blueprint_2_default_route(request): return text("BP2_OK") app = Sanic("PropTest") app.blueprint(group) app.blueprint(primary) app.router.finalize() routes = [route.path for route in app.router.routes] assert len(routes) == 4 assert "api/v1/grouped/bp1/" in routes assert "api/v1/grouped/bp2/" in routes assert "api/v1/primary/grouped/bp1" in routes assert "api/v1/primary/grouped/bp2" in routes def test_nested_bp_group_properties(): one = Blueprint("one", url_prefix="/one") two = Blueprint.group(one) three = Blueprint.group(two, url_prefix="/three") @one.route("/four") def handler(request): return text("pi") app = Sanic("PropTest") app.blueprint(three) app.router.finalize() routes = [route.path for route in app.router.routes] assert routes == ["three/one/four"] @pytest.mark.asyncio async def test_multiple_nested_bp_group(): bp1 = Blueprint("bp1", url_prefix="/bp1") bp2 = Blueprint("bp2", url_prefix="/bp2") bp1.add_route(lambda _: ..., "/", name="route1") bp2.add_route(lambda _: ..., "/", name="route2") group_a = Blueprint.group(bp1, bp2, url_prefix="/group-a", name_prefix="group-a") group_b = Blueprint.group(bp1, bp2, url_prefix="/group-b", name_prefix="group-b") app = Sanic("PropTest") app.blueprint(group_a) app.blueprint(group_b) await app._startup() routes = [route.path for route in app.router.routes] assert routes == [ "group-a/bp1", "group-a/bp2", "group-b/bp1", "group-b/bp2", ] names = [route.name for route in app.router.routes] assert names == [ "PropTest.group-a_bp1.route1", "PropTest.group-a_bp2.route2", "PropTest.group-b_bp1.route1", "PropTest.group-b_bp2.route2", ]