chore: rename project coven → hack-house ⛧
Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"), README, CLI strings, and branch (coven → hack-house). Gitea repo renamed cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest + golden-vector test still green, binary is now `hack-house`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import empty, text
|
||||
|
||||
from sanic_ext.bootstrap import Extend
|
||||
|
||||
|
||||
def test_trace_and_connect_available(app: Sanic):
|
||||
@app.route("/", methods=["trace", "connect"])
|
||||
async def handler(_):
|
||||
return empty()
|
||||
|
||||
_, response = app.test_client.request("", http_method="trace")
|
||||
assert response.status == 204
|
||||
_, response = app.test_client.request("", http_method="connect")
|
||||
assert response.status == 204
|
||||
_, response = app.test_client.request("", http_method="get")
|
||||
assert response.status == 405
|
||||
|
||||
|
||||
def test_auto_head(app: Sanic, get_docs):
|
||||
app.config.TOUCHUP = False
|
||||
|
||||
@app.get("/foo")
|
||||
async def foo_handler(_):
|
||||
return text("...")
|
||||
|
||||
assert app.config.HTTP_AUTO_HEAD
|
||||
_, response = app.test_client.head("/foo")
|
||||
assert response.status == 200
|
||||
assert len(response.body) == 0
|
||||
assert int(response.headers["content-length"]) == 3
|
||||
|
||||
schema = get_docs()
|
||||
assert "get" in schema["paths"]["/foo"]
|
||||
|
||||
|
||||
def test_auto_options(app: Sanic, get_docs):
|
||||
@app.post("/foo")
|
||||
async def foo_handler(_):
|
||||
return text("...")
|
||||
|
||||
_, response = app.test_client.options("/foo")
|
||||
assert response.status == 204
|
||||
assert len(response.body) == 0
|
||||
assert "POST" in response.headers["allow"]
|
||||
assert "OPTIONS" in response.headers["allow"]
|
||||
|
||||
schema = get_docs()
|
||||
assert "post" in schema["paths"]["/foo"]
|
||||
|
||||
|
||||
def test_auto_trace(bare_app: Sanic):
|
||||
Extend(bare_app, config={"http_auto_trace": True})
|
||||
|
||||
@bare_app.get("/foo")
|
||||
async def foo_handler(_):
|
||||
return text("...")
|
||||
|
||||
request, response = bare_app.test_client.request(
|
||||
"/foo", http_method="trace"
|
||||
)
|
||||
assert response.status == 200
|
||||
assert response.body.startswith(request.head)
|
||||
|
||||
|
||||
def test_auto_head_with_vhosts(app: Sanic, get_docs):
|
||||
@app.get("/foo", host="one.com", name="one")
|
||||
async def foo_handler_one(_):
|
||||
return text(".")
|
||||
|
||||
@app.get("/foo", host="two.com", name="two")
|
||||
async def foo_handler_two(_):
|
||||
return text("..")
|
||||
|
||||
assert app.config.HTTP_AUTO_HEAD
|
||||
_, response = app.test_client.head("/foo", headers={"host": "one.com"})
|
||||
assert response.status == 200
|
||||
assert len(response.body) == 0
|
||||
assert int(response.headers["content-length"]) == 1
|
||||
|
||||
_, response = app.test_client.head("/foo", headers={"host": "two.com"})
|
||||
assert response.status == 200
|
||||
assert len(response.body) == 0
|
||||
assert int(response.headers["content-length"]) == 2
|
||||
|
||||
schema = get_docs()
|
||||
assert "get" in schema["paths"]["/foo"]
|
||||
|
||||
|
||||
def test_auto_options_with_vhosts(app: Sanic, get_docs):
|
||||
@app.post("/foo", host="one.com", name="one")
|
||||
async def foo_handler_one(_):
|
||||
return text(".")
|
||||
|
||||
@app.post("/foo", host="two.com", name="two")
|
||||
async def foo_handler_two(_):
|
||||
return text("..")
|
||||
|
||||
assert app.config.HTTP_AUTO_OPTIONS
|
||||
_, response = app.test_client.options("/foo", headers={"host": "one.com"})
|
||||
assert response.status == 204
|
||||
assert len(response.body) == 0
|
||||
assert "POST" in response.headers["allow"]
|
||||
assert "OPTIONS" in response.headers["allow"]
|
||||
|
||||
_, response = app.test_client.options("/foo", headers={"host": "two.com"})
|
||||
assert response.status == 204
|
||||
assert len(response.body) == 0
|
||||
assert "POST" in response.headers["allow"]
|
||||
assert "OPTIONS" in response.headers["allow"]
|
||||
|
||||
|
||||
# This test also appears in Core Sanic tests but is added here as well
|
||||
# because of: https://github.com/sanic-org/sanic-ext/issues/148
|
||||
@pytest.mark.parametrize("unquote", [True, False, None])
|
||||
def test_unquote_add_route(app, unquote):
|
||||
async def handler1(_, foo):
|
||||
return text(foo)
|
||||
|
||||
app.add_route(handler1, "/<foo>", unquote=unquote)
|
||||
value = "啊" if unquote else r"%E5%95%8A"
|
||||
|
||||
_, response = app.test_client.get("/啊")
|
||||
assert response.text == value
|
||||
|
||||
_, response = app.test_client.get(r"/%E5%95%8A")
|
||||
assert response.text == value
|
||||
@@ -0,0 +1,416 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from dataclasses import dataclass
|
||||
from itertools import count
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Request, json, text
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.views import HTTPMethodView
|
||||
|
||||
from sanic_ext import Extend
|
||||
|
||||
|
||||
@dataclass
|
||||
class Name:
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PersonID:
|
||||
person_id: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class NamedPerson:
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Person:
|
||||
person_id: PersonID
|
||||
name: str
|
||||
age: int
|
||||
|
||||
@classmethod
|
||||
async def create(cls, person_id: int) -> Person:
|
||||
return cls(person_id=PersonID(person_id), name="noname", age=111)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AsyncName:
|
||||
name: str
|
||||
|
||||
def __await__(self):
|
||||
return self
|
||||
|
||||
|
||||
counter = count()
|
||||
|
||||
|
||||
class A:
|
||||
@classmethod
|
||||
def create(cls, request: Request):
|
||||
next(counter)
|
||||
return cls()
|
||||
|
||||
|
||||
class B:
|
||||
def __init__(self, a: A):
|
||||
self.a = a
|
||||
|
||||
@classmethod
|
||||
def create(cls, request: Request, a: A):
|
||||
next(counter)
|
||||
return cls(a)
|
||||
|
||||
|
||||
class C:
|
||||
def __init__(self, b: B):
|
||||
self.b = b
|
||||
|
||||
@classmethod
|
||||
def create(cls, request: Request, b: B):
|
||||
next(counter)
|
||||
return cls(b)
|
||||
|
||||
|
||||
class D:
|
||||
def __init__(self, a: A):
|
||||
self.a = a
|
||||
|
||||
@classmethod
|
||||
def create(cls, request: Request, a: A):
|
||||
next(counter)
|
||||
return cls(a)
|
||||
|
||||
|
||||
class E:
|
||||
def __init__(self, c: C, d: D):
|
||||
self.c = c
|
||||
self.d = d
|
||||
|
||||
@classmethod
|
||||
def create(cls, request: Request, c: C, d: D):
|
||||
next(counter)
|
||||
return cls(c, d)
|
||||
|
||||
|
||||
class Alpha: ...
|
||||
|
||||
|
||||
class Beta:
|
||||
def __init__(self, alpha: Alpha) -> None:
|
||||
self.alpha = alpha
|
||||
|
||||
|
||||
class Gamma:
|
||||
def __init__(self, beta: Beta, request: Optional[Request] = None) -> None:
|
||||
self.beta = beta
|
||||
self.request = request
|
||||
|
||||
|
||||
class Delta:
|
||||
def __init__(self, gamma: Gamma, request: Request) -> None:
|
||||
self.gamma = gamma
|
||||
self.request = request
|
||||
|
||||
|
||||
@dataclass
|
||||
class Foo:
|
||||
ident: UUID
|
||||
|
||||
|
||||
def make_gamma_with_request(beta: Beta, request: Request):
|
||||
return Gamma(beta, request)
|
||||
|
||||
|
||||
def make_gamma_without_request(beta: Beta):
|
||||
return Gamma(beta)
|
||||
|
||||
|
||||
def test_injection_not_allowed_when_ext_disabled(bare_app):
|
||||
ext = Extend(bare_app, built_in_extensions=False)
|
||||
|
||||
with pytest.raises(
|
||||
SanicException, match="Injection extension not enabled"
|
||||
):
|
||||
ext.add_dependency(1, 2)
|
||||
|
||||
|
||||
def test_injection_of_matched_object(app):
|
||||
@app.get("/person/<name:str>")
|
||||
def handler(request, name: Name):
|
||||
request.ctx.name = name
|
||||
return text(name.name)
|
||||
|
||||
app.ext.add_dependency(Name)
|
||||
|
||||
request, response = app.test_client.get("/person/george")
|
||||
|
||||
assert response.body == b"george"
|
||||
assert isinstance(request.ctx.name, Name)
|
||||
assert request.ctx.name.name == "george"
|
||||
|
||||
|
||||
def test_injection_of_matched_object_as_deprecated_injection(app):
|
||||
@app.get("/person/<name:str>")
|
||||
def handler(request, name: Name):
|
||||
request.ctx.name = name
|
||||
return text(name.name)
|
||||
|
||||
message = (
|
||||
"The 'ext.injection' method has been deprecated and will be removed "
|
||||
"in v22.6. Please use 'ext.add_dependency' instead."
|
||||
)
|
||||
with pytest.warns(DeprecationWarning, match=message):
|
||||
app.ext.injection(Name)
|
||||
|
||||
request, response = app.test_client.get("/person/george")
|
||||
|
||||
assert response.body == b"george"
|
||||
assert isinstance(request.ctx.name, Name)
|
||||
assert request.ctx.name.name == "george"
|
||||
|
||||
|
||||
def test_injection_of_simple_object(app):
|
||||
@app.get("/person/<name>")
|
||||
def handler(request, name: str, person: NamedPerson):
|
||||
request.ctx.person = person
|
||||
return text(person.name)
|
||||
|
||||
app.ext.add_dependency(NamedPerson)
|
||||
|
||||
request, response = app.test_client.get("/person/george")
|
||||
|
||||
assert response.body == b"george"
|
||||
assert isinstance(request.ctx.person, NamedPerson)
|
||||
assert request.ctx.person.name == "george"
|
||||
|
||||
|
||||
def test_injection_of_object_with_constructor(app):
|
||||
@app.get("/person/<person_id:int>")
|
||||
async def person_details(request, person_id: PersonID, person: Person):
|
||||
request.ctx.person_id = person_id
|
||||
request.ctx.person = person
|
||||
return text(
|
||||
f"{person.person_id.person_id}\n{person.name}\n{person.age}"
|
||||
)
|
||||
|
||||
app.ext.add_dependency(Person, Person.create)
|
||||
app.ext.add_dependency(PersonID)
|
||||
|
||||
request, response = app.test_client.get("/person/999")
|
||||
|
||||
assert response.body == b"999\nnoname\n111"
|
||||
assert isinstance(request.ctx.person_id, PersonID)
|
||||
assert isinstance(request.ctx.person, Person)
|
||||
assert request.ctx.person.person_id == request.ctx.person_id
|
||||
assert request.ctx.person.person_id.person_id == 999
|
||||
assert request.ctx.person.name == "noname"
|
||||
assert request.ctx.person.age == 111
|
||||
|
||||
|
||||
def test_injection_on_cbv(app):
|
||||
class View(HTTPMethodView, attach=app, uri="/person/<name:str>"):
|
||||
async def get(self, request, name: Name):
|
||||
request.ctx.name = name
|
||||
return text(name.name)
|
||||
|
||||
@staticmethod
|
||||
async def post(request, name: Name):
|
||||
request.ctx.name = name
|
||||
return text(name.name)
|
||||
|
||||
app.ext.add_dependency(Name)
|
||||
|
||||
for client in (app.test_client.get, app.test_client.post):
|
||||
request, response = client("/person/george")
|
||||
|
||||
assert response.body == b"george"
|
||||
assert isinstance(request.ctx.name, Name)
|
||||
assert request.ctx.name.name == "george"
|
||||
|
||||
|
||||
def test_nested_dependencies(app):
|
||||
app.ext.add_dependency(A, A.create)
|
||||
app.ext.add_dependency(B, B.create)
|
||||
app.ext.add_dependency(C, C.create)
|
||||
app.ext.add_dependency(D, D.create)
|
||||
app.ext.add_dependency(E, E.create)
|
||||
|
||||
@app.get("/")
|
||||
async def nested(request: Request, c: C, e: E):
|
||||
return json(
|
||||
[
|
||||
isinstance(c, C),
|
||||
isinstance(c.b, B),
|
||||
isinstance(c.b.a, A),
|
||||
isinstance(e, E),
|
||||
isinstance(e.c, C),
|
||||
isinstance(e.d, D),
|
||||
isinstance(e.d.a, A),
|
||||
]
|
||||
)
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
|
||||
assert all(response.json)
|
||||
# TODO:
|
||||
# - After implementing https://github.com/sanic-org/sanic-ext/issues/77
|
||||
# this should be == 5
|
||||
assert next(counter) == 9
|
||||
|
||||
|
||||
def test_injection_on_websocket(app):
|
||||
ev = asyncio.Event()
|
||||
|
||||
app.ext.dependency(A())
|
||||
|
||||
@app.websocket("/foo")
|
||||
async def handler(request, ws, foo: A):
|
||||
assert isinstance(foo, A)
|
||||
if isinstance(foo, A):
|
||||
ev.set()
|
||||
|
||||
request, response = app.test_client.websocket("/foo")
|
||||
assert ev.is_set()
|
||||
|
||||
|
||||
def test_injection_of_awaitable_variable_in_do_cast(app):
|
||||
"""Test for do_cast() iscoroutine() check"""
|
||||
|
||||
@app.get("/person/<name:str>")
|
||||
def handler(request, name: AsyncName):
|
||||
request.ctx.name = name
|
||||
return text(name.name)
|
||||
|
||||
app.ext.add_dependency(AsyncName)
|
||||
|
||||
request, response = app.test_client.get("/person/george")
|
||||
|
||||
assert response.body == b"george"
|
||||
assert isinstance(request.ctx.name, AsyncName)
|
||||
assert request.ctx.name.name == "george"
|
||||
|
||||
|
||||
def test_injection_of_awaitable_variable_in_call(app):
|
||||
"""Test for __call__() iscoroutine() check"""
|
||||
|
||||
@app.get("/person/<name:str>")
|
||||
def handler(request, name: AsyncName):
|
||||
request.ctx.name = name
|
||||
return text(name.name)
|
||||
|
||||
def test():
|
||||
return AsyncName("george")
|
||||
|
||||
app.ext.dependency(test())
|
||||
|
||||
request, response = app.test_client.get("/person/george")
|
||||
|
||||
assert response.body == b"george"
|
||||
assert isinstance(request.ctx.name, AsyncName)
|
||||
assert request.ctx.name.name == "george"
|
||||
|
||||
|
||||
def test_injection_class_constructors(app):
|
||||
app.ext.add_dependency(Alpha)
|
||||
app.ext.add_dependency(Beta)
|
||||
|
||||
@app.get("/")
|
||||
def handler(request: Request, beta: Beta):
|
||||
return json({"is_beta": isinstance(beta, Beta)})
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
assert response.json == {"is_beta": True}
|
||||
|
||||
|
||||
def test_injection_class_constructors_with_optional_request(app):
|
||||
app.ext.add_dependency(Alpha)
|
||||
app.ext.add_dependency(Beta)
|
||||
app.ext.add_dependency(Gamma)
|
||||
|
||||
@app.get("/")
|
||||
def handler(request: Request, gamma: Gamma):
|
||||
return json(
|
||||
{
|
||||
"is_gamma": isinstance(gamma, Gamma),
|
||||
"has_request": bool(gamma.request),
|
||||
}
|
||||
)
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
assert response.json == {"is_gamma": True, "has_request": True}
|
||||
|
||||
|
||||
def test_injection_class_constructors_with_request(app):
|
||||
app.ext.add_dependency(Alpha)
|
||||
app.ext.add_dependency(Beta)
|
||||
app.ext.add_dependency(Gamma)
|
||||
app.ext.add_dependency(Delta)
|
||||
|
||||
@app.get("/")
|
||||
def handler(request: Request, delta: Delta):
|
||||
return json(
|
||||
{
|
||||
"is_delta": isinstance(delta, Delta),
|
||||
"has_request": bool(delta.request),
|
||||
}
|
||||
)
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
assert response.json == {"is_delta": True, "has_request": True}
|
||||
|
||||
|
||||
def test_injection_class_constructors_with_func_and_request(app):
|
||||
app.ext.add_dependency(Alpha)
|
||||
app.ext.add_dependency(Beta)
|
||||
app.ext.add_dependency(Gamma, make_gamma_with_request)
|
||||
|
||||
@app.get("/")
|
||||
def handler(request: Request, gamma: Gamma):
|
||||
return json(
|
||||
{
|
||||
"is_gamma": isinstance(gamma, Gamma),
|
||||
"has_request": bool(gamma.request),
|
||||
}
|
||||
)
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
assert response.json == {"is_gamma": True, "has_request": True}
|
||||
|
||||
|
||||
def test_injection_class_constructors_with_func_and_no_request(app):
|
||||
app.ext.add_dependency(Alpha)
|
||||
app.ext.add_dependency(Beta)
|
||||
app.ext.add_dependency(Gamma, make_gamma_without_request)
|
||||
|
||||
@app.get("/")
|
||||
def handler(request: Request, gamma: Gamma):
|
||||
return json(
|
||||
{
|
||||
"is_gamma": isinstance(gamma, Gamma),
|
||||
"has_request": bool(gamma.request),
|
||||
}
|
||||
)
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
assert response.json == {"is_gamma": True, "has_request": False}
|
||||
|
||||
|
||||
def test_injection_of_lambda_properties(app):
|
||||
@app.get("/foo")
|
||||
def handler(request, foo: Foo):
|
||||
return json(request.id == foo.ident and isinstance(foo.ident, UUID))
|
||||
|
||||
app.ext.add_dependency(Foo, lambda request: Foo(request.id), "request")
|
||||
|
||||
_, response = app.test_client.get("/foo")
|
||||
|
||||
assert response.body == b"true"
|
||||
@@ -0,0 +1,69 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic, json
|
||||
|
||||
|
||||
@dataclass
|
||||
class Foo:
|
||||
bar: int
|
||||
|
||||
|
||||
def test_constant_in_registry(app: Sanic):
|
||||
assert len(app.ext._constant_registry._registry) == 0
|
||||
app.ext.add_constant("bar", 999)
|
||||
assert len(app.ext._constant_registry._registry) == 1
|
||||
assert "bar" in app.ext._constant_registry
|
||||
|
||||
|
||||
def test_constant_is_injected(app: Sanic):
|
||||
app.ext.add_constant("bar", 999)
|
||||
|
||||
@app.get("/")
|
||||
async def handler(_, bar: int):
|
||||
return json({"bar": bar})
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
|
||||
assert response.json["bar"] == 999
|
||||
assert app.config.BAR == 999
|
||||
|
||||
|
||||
def test_constant_is_injected_into_constructor(app: Sanic):
|
||||
app.ext.add_constant("bar", 999)
|
||||
app.ext.add_dependency(Foo)
|
||||
|
||||
@app.get("/")
|
||||
async def handler(_, foo: Foo):
|
||||
return json({"foo": foo.bar})
|
||||
|
||||
_, response = app.test_client.get("/")
|
||||
|
||||
assert response.json["foo"] == 999
|
||||
assert app.config.BAR == 999
|
||||
|
||||
|
||||
def test_load_config(app: Sanic):
|
||||
app.ext.load_constants()
|
||||
assert len(app.ext._constant_registry._registry) == 0
|
||||
app.ext.load_constants({**app.config}, overwrite=True)
|
||||
assert len(app.ext._constant_registry._registry) == len(
|
||||
[k for k in app.config.keys() if k.isupper()]
|
||||
)
|
||||
assert "request_buffer_size" in app.ext._constant_registry
|
||||
|
||||
|
||||
def test_load_custom(app: Sanic):
|
||||
app.ext.load_constants({"foo": "bar"})
|
||||
assert len(app.ext._constant_registry._registry) == 1
|
||||
assert app.ext._constant_registry.get("foo") == "bar"
|
||||
|
||||
|
||||
def test_load_overwrite(app: Sanic):
|
||||
app.ext.load_constants({"foo": "bar"})
|
||||
with pytest.raises(
|
||||
ValueError, match="A value for FOO has already been assigned"
|
||||
):
|
||||
app.ext.load_constants({"foo": "bar"})
|
||||
app.ext.load_constants({"foo": "bar"}, True)
|
||||
@@ -0,0 +1,45 @@
|
||||
from sanic import Request, text
|
||||
|
||||
|
||||
class Foo:
|
||||
def bar(self):
|
||||
return "foobar"
|
||||
|
||||
|
||||
def test_dependency_added(app):
|
||||
foo = Foo()
|
||||
foobar = Foo()
|
||||
|
||||
app.ext.dependency(foo)
|
||||
app.ext.dependency(foobar, name="something")
|
||||
|
||||
assert app.ctx._dependencies.foo is foo
|
||||
assert app.ctx._dependencies.something is foobar
|
||||
|
||||
|
||||
def test_dependency_injection(app):
|
||||
foo = Foo()
|
||||
|
||||
app.ext.dependency(foo)
|
||||
|
||||
@app.get("/getfoo")
|
||||
async def getfoo(request: Request, foo: Foo):
|
||||
return text(foo.bar())
|
||||
|
||||
_, response = app.test_client.get("/getfoo")
|
||||
|
||||
assert response.text == "foobar"
|
||||
|
||||
|
||||
def test_dependency_injection_head(app):
|
||||
foo = Foo()
|
||||
|
||||
app.ext.dependency(foo)
|
||||
|
||||
@app.get("/getfoo")
|
||||
async def getfoo(request: Request, foo: Foo):
|
||||
return text(foo.bar())
|
||||
|
||||
_, response = app.test_client.head("/getfoo")
|
||||
|
||||
assert int(response.headers.get("content-length", 0)) == 6
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.signals import Event
|
||||
|
||||
from sanic_ext.config import Config
|
||||
from sanic_ext.extensions.injection.injector import add_injection
|
||||
|
||||
|
||||
def test_default_config_signal():
|
||||
config = Config()
|
||||
assert config.INJECTION_SIGNAL == Event.HTTP_ROUTING_AFTER
|
||||
|
||||
|
||||
def test_not_allowed_signals_error():
|
||||
with pytest.raises(SanicException):
|
||||
Config(injection_signal=Event.HTTP_LIFECYCLE_REQUEST)
|
||||
|
||||
|
||||
def test_http_routing_after_succeeds():
|
||||
Config(injection_signal=Event.HTTP_ROUTING_AFTER)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not hasattr(Event, "HTTP_HANDLER_BEFORE"),
|
||||
reason="Sanic version does not support HTTP_HANDLER_BEFORE",
|
||||
)
|
||||
def test_http_handler_before_succeeds():
|
||||
Config(injection_signal=Event.HTTP_HANDLER_BEFORE)
|
||||
|
||||
|
||||
def test_add_injection_uses_signal_config():
|
||||
app = Mock()
|
||||
|
||||
app.signal = Mock(return_value=Mock())
|
||||
app.ext.config.INJECTION_SIGNAL = "random_string"
|
||||
app.ext.config.INJECTION_PRIORITY = 99999
|
||||
add_injection(app, Mock(), Mock())
|
||||
|
||||
app.signal.assert_called_once_with("random_string", priority=99999)
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, call
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Request
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
from sanic_ext.exceptions import InitError
|
||||
from sanic_ext.extensions.injection.constructor import Constructor, gather_args
|
||||
from sanic_ext.extensions.injection.registry import (
|
||||
ConstantRegistry,
|
||||
InjectionRegistry,
|
||||
)
|
||||
|
||||
|
||||
class Foo:
|
||||
def __init__(self, num: int):
|
||||
self.num = num
|
||||
self.mock = AsyncMock()
|
||||
|
||||
@classmethod
|
||||
async def create(cls, request: Request, bar: int):
|
||||
instance = cls(bar)
|
||||
await instance.mock(request, bar)
|
||||
return instance
|
||||
|
||||
|
||||
class Bar:
|
||||
def __init__(self, foo: Foo):
|
||||
self.foo = foo
|
||||
|
||||
@classmethod
|
||||
async def create(cls, request: Request, foo: Foo):
|
||||
return cls(foo)
|
||||
|
||||
|
||||
class Chicken:
|
||||
@classmethod
|
||||
def create(cls, egg: Egg):
|
||||
return cls()
|
||||
|
||||
|
||||
class Egg:
|
||||
@classmethod
|
||||
def create(cls, chicken: Chicken):
|
||||
return cls()
|
||||
|
||||
|
||||
class InheritedRequest(Request): ...
|
||||
|
||||
|
||||
class Baz:
|
||||
@classmethod
|
||||
async def create(cls, request: InheritedRequest):
|
||||
return cls()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gather_args():
|
||||
func = AsyncMock()
|
||||
func.return_value = 999
|
||||
injections = {"some_foo": (Foo, func)}
|
||||
request = object()
|
||||
kwargs = {"zero": 0, "one": True, "two": "2", "three": None}
|
||||
|
||||
args = await gather_args(injections, request, **kwargs)
|
||||
assert args == {"some_foo": 999}
|
||||
assert func.call_args == call(request, **kwargs)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_circular_refs():
|
||||
injections = InjectionRegistry()
|
||||
injections.register(Chicken, Chicken.create)
|
||||
injections.register(Egg, Egg.create)
|
||||
|
||||
assert isinstance(injections[Chicken], Constructor)
|
||||
assert isinstance(injections[Egg], Constructor)
|
||||
|
||||
message = (
|
||||
"Circular dependency injection detected on 'create'. Check "
|
||||
"dependencies of 'create' which may contain circular dependency "
|
||||
f"chain with {Chicken}."
|
||||
)
|
||||
with pytest.raises(InitError, match=message):
|
||||
injections.finalize(Mock(), ConstantRegistry({}), [])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("raises,allowed", ((False, (int,)), (True, [])))
|
||||
async def test_finalize_allowed_types(raises, allowed):
|
||||
injections = InjectionRegistry()
|
||||
injections.register(Foo, Foo.create)
|
||||
|
||||
if raises:
|
||||
with pytest.raises(
|
||||
InitError, match=".Could not find the following dependencies."
|
||||
):
|
||||
injections.finalize(Mock(), ConstantRegistry({}), allowed)
|
||||
else:
|
||||
injections.finalize(Mock(), ConstantRegistry({}), allowed)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_constructor():
|
||||
injections = InjectionRegistry()
|
||||
injections.register(Foo, Foo.create)
|
||||
injections.finalize(Mock(), ConstantRegistry({}), {int})
|
||||
request = object()
|
||||
constructor = injections[Foo]
|
||||
|
||||
foo = await constructor(request, bar=999)
|
||||
|
||||
assert isinstance(foo, Foo)
|
||||
assert foo.num == 999
|
||||
assert constructor.pass_kwargs
|
||||
foo.mock.assert_awaited_once_with(request, 999)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_constructor_nested():
|
||||
injections = InjectionRegistry()
|
||||
injections.register(Foo, Foo.create)
|
||||
injections.register(Bar, Bar.create)
|
||||
injections.finalize(Mock(), ConstantRegistry({}), {int})
|
||||
request = object()
|
||||
constructor_bar = injections[Bar]
|
||||
|
||||
bar = await constructor_bar(request, bar=999)
|
||||
|
||||
assert isinstance(bar, Bar)
|
||||
assert isinstance(bar.foo, Foo)
|
||||
assert bar.foo.num == 999
|
||||
assert not constructor_bar.pass_kwargs
|
||||
bar.foo.mock.assert_awaited_once_with(request, 999)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_constructor_failure_kwargs():
|
||||
injections = InjectionRegistry()
|
||||
injections.register(Foo, Foo.create)
|
||||
injections.finalize(Mock(), ConstantRegistry({}), {int})
|
||||
request = object()
|
||||
constructor = injections[Foo]
|
||||
|
||||
message = (
|
||||
"Failure to inject dependencies. Make sure that all dependencies "
|
||||
"for 'create' have been registered."
|
||||
)
|
||||
with pytest.raises(ServerError, match=message):
|
||||
await constructor(request)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_constructor_failure_nested():
|
||||
injections = InjectionRegistry()
|
||||
injections.register(Foo, Foo.create)
|
||||
injections.register(Bar, Bar.create)
|
||||
injections.finalize(Mock(), ConstantRegistry({}), {int})
|
||||
request = object()
|
||||
constructor: Bar = injections[Bar]
|
||||
|
||||
message = (
|
||||
"Failure to inject dependencies. Make sure that all dependencies "
|
||||
"for 'create' have been registered."
|
||||
)
|
||||
with pytest.raises(ServerError, match=message):
|
||||
await constructor(request)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_constructor_with_inherited_request():
|
||||
injections = InjectionRegistry()
|
||||
injections.register(Baz, Baz.create)
|
||||
injections.finalize(Mock(), ConstantRegistry({}), [])
|
||||
|
||||
request = object()
|
||||
constructor_baz = injections[Baz]
|
||||
|
||||
baz = await constructor_baz(request)
|
||||
|
||||
assert isinstance(baz, Baz)
|
||||
assert not constructor_baz.pass_kwargs
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from sanic import Sanic
|
||||
|
||||
from sanic_ext.extensions.logging.logger import Logger
|
||||
|
||||
|
||||
def test_defult_config(app: Sanic):
|
||||
assert hasattr(app.config, "LOGGERS")
|
||||
|
||||
|
||||
def test_custom_background_logger(app: Sanic):
|
||||
assert Logger.LOGGERS == []
|
||||
Logger.prepare(app)
|
||||
assert hasattr(app.shared_ctx, "logger_queue")
|
||||
assert Logger.LOGGERS == app.config.LOGGERS
|
||||
assert "sanic.root" in Logger.LOGGERS
|
||||
|
||||
logger = Logger()
|
||||
assert (
|
||||
len(logger.loggers) == len(Logger.LOGGERS) == len(app.config.LOGGERS)
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
from sanic_ext.extensions.openapi.autodoc import YamlStyleParametersParser
|
||||
|
||||
|
||||
tests = []
|
||||
|
||||
_ = ""
|
||||
|
||||
tests.append({"doc": _, "expects": {}})
|
||||
|
||||
_ = "one line docstring"
|
||||
|
||||
tests.append({"doc": _, "expects": {"summary": "one line docstring"}})
|
||||
|
||||
_ = """
|
||||
first line
|
||||
|
||||
more lines
|
||||
"""
|
||||
|
||||
tests.append(
|
||||
{
|
||||
"doc": _,
|
||||
"expects": {"summary": "first line", "description": "more lines"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
_ = """
|
||||
first line
|
||||
|
||||
more lines
|
||||
|
||||
openapi:
|
||||
---
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
"""
|
||||
|
||||
tests.append(
|
||||
{
|
||||
"doc": _,
|
||||
"expects": {
|
||||
"summary": "first line",
|
||||
"description": "more lines",
|
||||
"responses": {"200": {"description": "OK"}},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_autodoc():
|
||||
for t in tests:
|
||||
parser = YamlStyleParametersParser(t["doc"])
|
||||
assert parser.to_openAPI_2() == t["expects"]
|
||||
assert parser.to_openAPI_3() == t["expects"]
|
||||
@@ -0,0 +1,83 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sanic import Request, Sanic
|
||||
from sanic.response import text
|
||||
|
||||
from sanic_ext import openapi
|
||||
from sanic_ext.extensions.openapi.definitions import RequestBody
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserProfile:
|
||||
name: str
|
||||
age: int
|
||||
email: str
|
||||
|
||||
|
||||
def test_body(app: Sanic):
|
||||
@app.route("/test0")
|
||||
async def handler0(request: Request):
|
||||
"""
|
||||
openapi:
|
||||
---
|
||||
summary: Updates a pet in the store with form data
|
||||
description: some description
|
||||
operationId: someId
|
||||
requestBody:
|
||||
description: user to add to the system
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- age
|
||||
- email
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
age:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
"""
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test1")
|
||||
@openapi.body({"application/json": UserProfile})
|
||||
async def handler1(request: Request):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test2")
|
||||
@openapi.body(RequestBody({"application/json": UserProfile}))
|
||||
async def handler2(request: Request):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test3")
|
||||
@openapi.body(RequestBody(UserProfile))
|
||||
async def handler3(request: Request):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test4")
|
||||
@openapi.body(UserProfile)
|
||||
async def handler4(request: Request):
|
||||
return text("ok")
|
||||
|
||||
spec = get_spec(app)
|
||||
for i in range(5):
|
||||
assert f"/test{i}" in spec["paths"]
|
||||
content = spec["paths"][f"/test{i}"]["get"]["requestBody"]["content"]
|
||||
media_type = next(iter(content.keys()))
|
||||
if i <= 2:
|
||||
assert media_type == "application/json"
|
||||
else:
|
||||
assert media_type == "*/*"
|
||||
properties = content[media_type]["schema"]["properties"]
|
||||
assert properties["name"]["type"] == "string"
|
||||
assert properties["age"]["type"] == "integer"
|
||||
assert properties["email"]["type"] == "string"
|
||||
@@ -0,0 +1,34 @@
|
||||
import pytest
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"schemes,expected",
|
||||
(
|
||||
(None, ("http://example.com/",)),
|
||||
("https", ("https://example.com/",)),
|
||||
(
|
||||
"http,https",
|
||||
(
|
||||
"http://example.com/",
|
||||
"https://example.com/",
|
||||
),
|
||||
),
|
||||
(
|
||||
["http", "https"],
|
||||
(
|
||||
"http://example.com/",
|
||||
"https://example.com/",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_config_scheme(app, schemes, expected):
|
||||
app.config.API_SCHEMES = schemes
|
||||
app.config.API_HOST = "example.com"
|
||||
|
||||
spec = get_spec(app)
|
||||
urls = {s["url"] for s in spec["servers"]}
|
||||
|
||||
assert urls == set(expected)
|
||||
@@ -0,0 +1,890 @@
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.views import HTTPMethodView
|
||||
|
||||
from sanic_ext import openapi
|
||||
|
||||
from .utils import get_path, get_spec
|
||||
|
||||
|
||||
class Choice(Enum):
|
||||
ONE = 1
|
||||
TWO = 2
|
||||
|
||||
|
||||
class Bar:
|
||||
name: str
|
||||
|
||||
|
||||
class LittleFoo:
|
||||
iid: int
|
||||
bar: Bar
|
||||
|
||||
|
||||
class BigFoo:
|
||||
iid: int
|
||||
uid: UUID
|
||||
created: date
|
||||
updated: datetime
|
||||
multi: Union[str, int]
|
||||
nullable_single: Optional[bool]
|
||||
nullable_multi: Optional[Union[str, int]]
|
||||
bar: Bar
|
||||
adict: dict[str, Any]
|
||||
bdict: dict[str, bool]
|
||||
anything: Any
|
||||
choice: Choice
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args,content_type",
|
||||
(
|
||||
((LittleFoo,), "*/*"),
|
||||
(({"application/json": LittleFoo},), "application/json"),
|
||||
((openapi.definitions.RequestBody(LittleFoo),), "*/*"),
|
||||
(
|
||||
(
|
||||
openapi.definitions.RequestBody(
|
||||
{"application/json": LittleFoo}
|
||||
),
|
||||
),
|
||||
"application/json",
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_body_decorator(app, args, content_type):
|
||||
@app.route("/")
|
||||
@openapi.body(*args, description="something")
|
||||
async def handler(_): ...
|
||||
|
||||
spec = get_path(app, "/")
|
||||
|
||||
assert spec["requestBody"]["description"] == "something"
|
||||
|
||||
content = spec["requestBody"]["content"]
|
||||
|
||||
assert content_type in content
|
||||
|
||||
schema = content[content_type]["schema"]
|
||||
|
||||
assert schema["type"] == "object"
|
||||
assert schema["properties"]["bar"]["type"] == "object"
|
||||
assert (
|
||||
schema["properties"]["bar"]["properties"]["name"]["type"] == "string"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"decorator", (openapi.deprecated(), openapi.deprecated)
|
||||
)
|
||||
def test_deprecated_decorator(app, decorator):
|
||||
@app.route("/")
|
||||
@decorator
|
||||
async def handler(_): ...
|
||||
|
||||
spec = get_path(app, "/")
|
||||
assert spec["deprecated"]
|
||||
|
||||
|
||||
def test_description_decorator(app):
|
||||
@app.route("/")
|
||||
@openapi.description("foo")
|
||||
async def handler(_): ...
|
||||
|
||||
spec = get_path(app, "/")
|
||||
assert spec["description"] == "foo"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args",
|
||||
(
|
||||
("http://example.com/docs",),
|
||||
(
|
||||
openapi.definitions.ExternalDocumentation(
|
||||
"http://example.com/docs"
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_document_decorator(app, args):
|
||||
@app.route("/")
|
||||
@openapi.document(*args)
|
||||
async def handler(_): ...
|
||||
|
||||
spec = get_path(app, "/")
|
||||
assert spec["externalDocs"]["url"] == "http://example.com/docs"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"decorator,excluded",
|
||||
(
|
||||
(openapi.exclude(), True),
|
||||
(openapi.exclude(True), True),
|
||||
(openapi.exclude(False), False),
|
||||
),
|
||||
)
|
||||
def test_exclude_decorator(app, decorator, excluded):
|
||||
@app.route("/")
|
||||
@decorator
|
||||
async def handler(_): ...
|
||||
|
||||
if excluded:
|
||||
with pytest.raises(KeyError):
|
||||
get_path(app, "/")
|
||||
else:
|
||||
get_path(app, "/")
|
||||
|
||||
|
||||
def test_operation_decorator(app):
|
||||
@app.route("/")
|
||||
@openapi.operation("foo")
|
||||
async def handler(_): ...
|
||||
|
||||
spec = get_path(app, "/")
|
||||
assert spec["operationId"] == "foo"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"decorator,expected",
|
||||
(
|
||||
(
|
||||
openapi.parameter("thing"),
|
||||
{
|
||||
"name": "thing",
|
||||
"schema": {"type": "string"},
|
||||
"in": "query",
|
||||
},
|
||||
),
|
||||
(
|
||||
openapi.parameter("Authorization", str, "header"),
|
||||
{
|
||||
"name": "Authorization",
|
||||
"schema": {"type": "string"},
|
||||
"in": "header",
|
||||
},
|
||||
),
|
||||
(
|
||||
openapi.parameter(
|
||||
parameter=openapi.definitions.Parameter(
|
||||
"foobar", deprecated=True
|
||||
)
|
||||
),
|
||||
{
|
||||
"name": "foobar",
|
||||
"schema": {"type": "string"},
|
||||
"deprecated": True,
|
||||
"in": "query",
|
||||
},
|
||||
),
|
||||
(
|
||||
openapi.parameter("thing", required=True),
|
||||
{
|
||||
"name": "thing",
|
||||
"schema": {"type": "string"},
|
||||
"required": True,
|
||||
"in": "query",
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_parameter_decorator(app, decorator, expected):
|
||||
@app.route("/")
|
||||
@decorator
|
||||
def handler_one(_): ...
|
||||
|
||||
parameters = get_path(app, "/")["parameters"]
|
||||
assert parameters[0] == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"decorator,expected",
|
||||
(
|
||||
(
|
||||
openapi.response(),
|
||||
{
|
||||
"default": {
|
||||
"content": {"*/*": {"schema": {"type": "string"}}},
|
||||
"description": "Default Response",
|
||||
}
|
||||
},
|
||||
),
|
||||
(
|
||||
openapi.response(200, str, "foobar"),
|
||||
{
|
||||
"200": {
|
||||
"content": {"*/*": {"schema": {"type": "string"}}},
|
||||
"description": "foobar",
|
||||
}
|
||||
},
|
||||
),
|
||||
(
|
||||
openapi.response(200, Bar, "..."),
|
||||
{
|
||||
"200": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "...",
|
||||
}
|
||||
},
|
||||
),
|
||||
(
|
||||
openapi.response(
|
||||
content={"application/json": Bar}, description="..."
|
||||
),
|
||||
{
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "...",
|
||||
}
|
||||
},
|
||||
),
|
||||
(
|
||||
openapi.response(
|
||||
response=openapi.definitions.Response(
|
||||
{"application/json": Bar}, description="...", status=201
|
||||
)
|
||||
),
|
||||
{
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "...",
|
||||
}
|
||||
},
|
||||
),
|
||||
(
|
||||
openapi.response(content={"application/json": BigFoo}),
|
||||
{
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"iid": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
},
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
},
|
||||
"updated": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
},
|
||||
"multi": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
},
|
||||
]
|
||||
},
|
||||
"nullable_single": {
|
||||
"type": "boolean",
|
||||
"nullable": True,
|
||||
},
|
||||
"nullable_multi": {
|
||||
"nullable": True,
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
},
|
||||
],
|
||||
},
|
||||
"bar": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
},
|
||||
},
|
||||
"adict": {
|
||||
"type": "object",
|
||||
"additionalProperties": {},
|
||||
},
|
||||
"bdict": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
},
|
||||
},
|
||||
"anything": {},
|
||||
"choice": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"enum": [1, 2],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default Response",
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_response_decorator(app, decorator, expected):
|
||||
@app.route("/")
|
||||
@decorator
|
||||
def handler_one(_): ...
|
||||
|
||||
responses = get_path(app, "/")["responses"]
|
||||
assert responses == expected
|
||||
|
||||
|
||||
def test_summary_decorator(app):
|
||||
@app.route("/")
|
||||
@openapi.summary("foo")
|
||||
async def handler(_): ...
|
||||
|
||||
spec = get_path(app, "/")
|
||||
assert spec["summary"] == "foo"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"decorator,tags",
|
||||
(
|
||||
(openapi.tag("foo"), ("foo",)),
|
||||
(openapi.tag("foo", openapi.definitions.Tag("bar")), ("foo", "bar")),
|
||||
),
|
||||
)
|
||||
def test_tag_decorator(app, decorator, tags):
|
||||
@app.route("/")
|
||||
@decorator
|
||||
def handler_one(_): ...
|
||||
|
||||
spec = get_spec(app)
|
||||
for tag in tags:
|
||||
assert {"name": tag} in spec["tags"]
|
||||
|
||||
tagged = get_path(app, "/")["tags"]
|
||||
assert list(tagged) == list(tags)
|
||||
|
||||
|
||||
def test_definition_decorator_body_dict_w_obj(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(body={"application/json": Bar})
|
||||
async def handler(_): ...
|
||||
|
||||
body = get_path(app, "/")["requestBody"]
|
||||
assert body == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_body_dict_only_schema_head(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
body={
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
body = get_path(app, "/")["requestBody"]
|
||||
assert body == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_body_dict_types_schema_head(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(body={"application/json": {"schema": {"name": str}}})
|
||||
async def handler(_): ...
|
||||
|
||||
body = get_path(app, "/")["requestBody"]
|
||||
assert body == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_body_dict_only_schema_root(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
body={
|
||||
"application/json": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
body = get_path(app, "/")["requestBody"]
|
||||
assert body == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_body_dict_types_schema_root(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(body={"application/json": {"name": str}})
|
||||
async def handler(_): ...
|
||||
|
||||
body = get_path(app, "/")["requestBody"]
|
||||
assert body == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_httpmethodview(app):
|
||||
class View(HTTPMethodView, uri="/", attach=app):
|
||||
@openapi.definition(body={"application/json": Bar})
|
||||
async def get(self, request): ...
|
||||
|
||||
body = get_path(app, "/")["requestBody"]
|
||||
assert body == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_body_dict_multi(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(body={"application/json": Bar, "text/plain": str})
|
||||
async def handler(_): ...
|
||||
|
||||
body = get_path(app, "/")["requestBody"]
|
||||
assert body == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
},
|
||||
"text/plain": {"schema": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_body_request_body(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
body=openapi.definitions.RequestBody(str, required=True)
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
body = get_path(app, "/")["requestBody"]
|
||||
assert body == {
|
||||
"content": {"*/*": {"schema": {"type": "string"}}},
|
||||
"required": True,
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_body_model(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(body=Bar)
|
||||
async def handler(_): ...
|
||||
|
||||
body = get_path(app, "/")["requestBody"]
|
||||
assert body == {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_deprecated(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(deprecated=True)
|
||||
async def handler(_): ...
|
||||
|
||||
assert get_path(app, "/")["deprecated"]
|
||||
|
||||
|
||||
def test_definition_decorator_description(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(description="foo")
|
||||
async def handler(_): ...
|
||||
|
||||
assert get_path(app, "/")["description"] == "foo"
|
||||
|
||||
|
||||
def test_definition_decorator_document_string(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(document="foo")
|
||||
async def handler(_): ...
|
||||
|
||||
assert get_path(app, "/")["externalDocs"] == {"url": "foo"}
|
||||
|
||||
|
||||
def test_definition_decorator_document_obj(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
document=openapi.definitions.ExternalDocumentation("foo", "bar")
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
assert get_path(app, "/")["externalDocs"] == {
|
||||
"url": "foo",
|
||||
"description": "bar",
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_operation(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(operation="foo")
|
||||
async def handler(_): ...
|
||||
|
||||
assert get_path(app, "/")["operationId"] == "foo"
|
||||
|
||||
|
||||
def test_definition_decorator_parameter_string(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(parameter="foo")
|
||||
async def handler(_): ...
|
||||
|
||||
parameters = get_path(app, "/")["parameters"]
|
||||
assert {
|
||||
"name": "foo",
|
||||
"schema": {"type": "string"},
|
||||
"in": "query",
|
||||
} in parameters
|
||||
|
||||
|
||||
def test_definition_decorator_parameter_dict(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(parameter={"name": "foo"})
|
||||
async def handler(_): ...
|
||||
|
||||
parameters = get_path(app, "/")["parameters"]
|
||||
assert {
|
||||
"name": "foo",
|
||||
"schema": {"type": "string"},
|
||||
"in": "query",
|
||||
} in parameters
|
||||
|
||||
|
||||
def test_definition_decorator_parameter_obj(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
parameter=openapi.definitions.Parameter("foo", description="bar")
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
parameters = get_path(app, "/")["parameters"]
|
||||
assert {
|
||||
"name": "foo",
|
||||
"schema": {"type": "string"},
|
||||
"in": "query",
|
||||
"description": "bar",
|
||||
} in parameters
|
||||
|
||||
|
||||
def test_definition_decorator_parameter_string_multi(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(parameter=["foo", "bar"])
|
||||
async def handler(_): ...
|
||||
|
||||
parameters = get_path(app, "/")["parameters"]
|
||||
assert {
|
||||
"name": "foo",
|
||||
"schema": {"type": "string"},
|
||||
"in": "query",
|
||||
} in parameters
|
||||
assert {
|
||||
"name": "bar",
|
||||
"schema": {"type": "string"},
|
||||
"in": "query",
|
||||
} in parameters
|
||||
|
||||
|
||||
def test_definition_decorator_parameter_dict_multi(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
parameter=[{"name": "foo"}, {"name": "bar", "location": "header"}]
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
parameters = get_path(app, "/")["parameters"]
|
||||
assert {
|
||||
"name": "foo",
|
||||
"schema": {"type": "string"},
|
||||
"in": "query",
|
||||
} in parameters
|
||||
assert {
|
||||
"name": "bar",
|
||||
"schema": {"type": "string"},
|
||||
"in": "header",
|
||||
} in parameters
|
||||
|
||||
|
||||
def test_definition_decorator_parameter_obj_multi(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
parameter=[
|
||||
openapi.definitions.Parameter("foo"),
|
||||
openapi.definitions.Parameter("bar"),
|
||||
]
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
parameters = get_path(app, "/")["parameters"]
|
||||
assert {
|
||||
"name": "foo",
|
||||
"schema": {"type": "string"},
|
||||
"in": "query",
|
||||
} in parameters
|
||||
assert {
|
||||
"name": "bar",
|
||||
"schema": {"type": "string"},
|
||||
"in": "query",
|
||||
} in parameters
|
||||
|
||||
|
||||
def test_definition_decorator_response_dict(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(response={"application/json": Bar})
|
||||
async def handler(_): ...
|
||||
|
||||
responses = get_path(app, "/")["responses"]
|
||||
assert responses["default"] == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default Response",
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_response_obj(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
response=openapi.definitions.Response(
|
||||
{"application/json": Bar}, status=201
|
||||
)
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
responses = get_path(app, "/")["responses"]
|
||||
assert responses["201"] == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default Response",
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_response_model(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(response=Bar)
|
||||
async def handler(_): ...
|
||||
|
||||
responses = get_path(app, "/")["responses"]
|
||||
assert responses["default"] == {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default Response",
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_response_dict_multi(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
response=[
|
||||
{"application/json": Bar, "text/plain": str},
|
||||
{"text/html": str},
|
||||
]
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
responses = get_path(app, "/")["responses"]
|
||||
|
||||
assert responses["default"] == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
},
|
||||
"text/plain": {"schema": {"type": "string"}},
|
||||
"text/html": {"schema": {"type": "string"}},
|
||||
},
|
||||
"description": "Default Response",
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_response_obj_multi(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
response=[
|
||||
openapi.definitions.Response(
|
||||
{"application/json": Bar}, status=201
|
||||
),
|
||||
openapi.definitions.Response(
|
||||
{"*/*": str}, status=400, description="Something bad"
|
||||
),
|
||||
]
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
responses = get_path(app, "/")["responses"]
|
||||
assert responses["201"] == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default Response",
|
||||
}
|
||||
assert responses["400"] == {
|
||||
"content": {"*/*": {"schema": {"type": "string"}}},
|
||||
"description": "Something bad",
|
||||
}
|
||||
|
||||
|
||||
def test_definition_decorator_response_model_multi(app):
|
||||
with pytest.raises(SanicException):
|
||||
|
||||
@app.route("/")
|
||||
@openapi.definition(response=[Bar, LittleFoo])
|
||||
async def handler(_): ...
|
||||
|
||||
|
||||
def test_definition_decorator_summary(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(summary="foo")
|
||||
async def handler(_): ...
|
||||
|
||||
assert get_path(app, "/")["summary"] == "foo"
|
||||
|
||||
|
||||
def test_definition_decorator_tag_string(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(tag="foo")
|
||||
async def handler(_): ...
|
||||
|
||||
assert list(get_path(app, "/")["tags"]) == ["foo"]
|
||||
|
||||
|
||||
def test_definition_decorator_tag_obj(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(tag=openapi.definitions.Tag("foo"))
|
||||
async def handler(_): ...
|
||||
|
||||
assert list(get_path(app, "/")["tags"]) == ["foo"]
|
||||
|
||||
|
||||
def test_definition_decorator_tag_string_multi(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(tag=["foo", "bar"])
|
||||
async def handler(_): ...
|
||||
|
||||
assert list(get_path(app, "/")["tags"]) == ["foo", "bar"]
|
||||
|
||||
|
||||
def test_definition_decorator_tag_obj_multi(app):
|
||||
@app.route("/")
|
||||
@openapi.definition(
|
||||
tag=[openapi.definitions.Tag("foo"), openapi.definitions.Tag("bar")]
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
assert list(get_path(app, "/")["tags"]) == ["foo", "bar"]
|
||||
@@ -0,0 +1,56 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic_ext.extensions.openapi.definitions import Server
|
||||
from sanic_ext.extensions.openapi.types import Definition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def Thing():
|
||||
class Thing(Definition):
|
||||
name: str
|
||||
foo: Optional[bool]
|
||||
bar: Optional[bool]
|
||||
|
||||
def __init__(self, name, foo=None, bar=None):
|
||||
super().__init__(name=name, foo=foo, bar=bar)
|
||||
|
||||
return Thing
|
||||
|
||||
|
||||
def test_serialize_null_show_by_default(Thing):
|
||||
thing = Thing(name="ok")
|
||||
serialized = thing.serialize()
|
||||
|
||||
assert serialized["name"] == "ok"
|
||||
assert serialized["foo"] is None
|
||||
assert serialized["bar"] is None
|
||||
|
||||
|
||||
def test_serialize_some_nullable(Thing):
|
||||
Thing.__nullable__ = ["foo"]
|
||||
thing = Thing(name="ok")
|
||||
serialized = thing.serialize()
|
||||
|
||||
assert serialized["name"] == "ok"
|
||||
assert serialized["foo"] is None
|
||||
assert "bar" not in serialized
|
||||
|
||||
|
||||
def test_serialize_no_nullable(Thing):
|
||||
Thing.__nullable__ = False
|
||||
thing = Thing(name="ok")
|
||||
serialized = thing.serialize()
|
||||
|
||||
assert serialized["name"] == "ok"
|
||||
assert "foo" not in serialized
|
||||
assert "bar" not in serialized
|
||||
|
||||
|
||||
def test_server_definitions_defaults():
|
||||
assert Server(url="url").fields == {
|
||||
"url": "url",
|
||||
"description": None,
|
||||
"variables": {},
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
from sanic import Request, Sanic
|
||||
from sanic.response import text
|
||||
|
||||
from sanic_ext import openapi
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
def test_deprecated(app: Sanic):
|
||||
@app.route("/test0")
|
||||
@openapi.deprecated()
|
||||
async def handler0(request: Request):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test1")
|
||||
@openapi.deprecated
|
||||
async def handler1(request: Request):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test2")
|
||||
async def handler2(request: Request):
|
||||
"""
|
||||
openapi:
|
||||
---
|
||||
summary: This is a summary.
|
||||
deprecated: true
|
||||
"""
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test3")
|
||||
@openapi.definition(deprecated=True)
|
||||
async def handler3(request: Request):
|
||||
return text("ok")
|
||||
|
||||
spec = get_spec(app)
|
||||
paths = spec["paths"]
|
||||
assert len(paths) == 4
|
||||
for i in range(4):
|
||||
assert paths[f"/test{i}"]["get"]["deprecated"]
|
||||
@@ -0,0 +1,52 @@
|
||||
from sanic import Blueprint, Request, Sanic, text
|
||||
|
||||
from sanic_ext.extensions.openapi import openapi
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
def test_exclude_decorator(app: Sanic):
|
||||
@app.route("/test0")
|
||||
@openapi.exclude()
|
||||
async def handler0(request: Request):
|
||||
"""
|
||||
openapi:
|
||||
---
|
||||
summary: This is a summary.
|
||||
"""
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test1")
|
||||
@openapi.definition(summary="This is a summary.", exclude=True)
|
||||
async def handler1(request: Request):
|
||||
return text("ok")
|
||||
|
||||
spec = get_spec(app)
|
||||
paths = spec["paths"]
|
||||
assert len(paths) == 0
|
||||
|
||||
|
||||
def test_exclude_bp(app: Sanic):
|
||||
bp1 = Blueprint("blueprint1")
|
||||
bp2 = Blueprint("blueprint2")
|
||||
|
||||
@bp1.route("/op1")
|
||||
@openapi.summary("handler 1")
|
||||
async def handler1(request: Request):
|
||||
return text("bp1, ok")
|
||||
|
||||
@bp2.route("/op2")
|
||||
@openapi.summary("handler 2")
|
||||
async def handler2(request: Request):
|
||||
return text("bp2, ok")
|
||||
|
||||
app.blueprint(bp1)
|
||||
app.blueprint(bp2)
|
||||
|
||||
openapi.exclude(bp=bp1)
|
||||
spec = get_spec(app)
|
||||
paths = spec["paths"]
|
||||
assert len(paths) == 1
|
||||
assert "/op2" in paths
|
||||
assert "/op1" not in paths
|
||||
assert paths["/op2"]["get"]["summary"] == "handler 2"
|
||||
@@ -0,0 +1,56 @@
|
||||
from sanic import Request, Sanic
|
||||
from sanic.response import text
|
||||
|
||||
from sanic_ext import openapi
|
||||
from sanic_ext.extensions.openapi.definitions import ExternalDocumentation
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
def test_external_docs(app: Sanic):
|
||||
@app.route("/test0")
|
||||
@openapi.document("http://example.com/more", "Find more info here")
|
||||
async def handler0(request: Request):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test1")
|
||||
@openapi.definition(
|
||||
document=ExternalDocumentation(
|
||||
"http://example.com/more", "Find more info here"
|
||||
)
|
||||
)
|
||||
async def handler1(request: Request):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test2")
|
||||
@openapi.definition(document="http://example.com/more")
|
||||
async def handler2(request: Request):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test3")
|
||||
async def handler3(request: Request):
|
||||
"""
|
||||
openapi:
|
||||
---
|
||||
summary: This is a summary.
|
||||
externalDocs:
|
||||
description: Find more info here
|
||||
url: http://example.com/more
|
||||
"""
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test4")
|
||||
@openapi.document(
|
||||
ExternalDocumentation("http://example.com/more", "Find more info here")
|
||||
)
|
||||
async def handler4(request: Request):
|
||||
return text("ok")
|
||||
|
||||
spec = get_spec(app)
|
||||
paths = spec["paths"]
|
||||
assert len(paths) == 5
|
||||
for i in range(5):
|
||||
doc_obj = paths[f"/test{i}"]["get"]["externalDocs"]
|
||||
assert doc_obj["url"] == "http://example.com/more"
|
||||
if i != 2:
|
||||
assert doc_obj["description"] == "Find more info here"
|
||||
@@ -0,0 +1,51 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
|
||||
from sanic_ext import openapi
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
def test_func_handlers(app: Sanic):
|
||||
class TestClass:
|
||||
@staticmethod
|
||||
@openapi.definition(
|
||||
summary="staticmethod custom summary",
|
||||
tag="Test",
|
||||
)
|
||||
async def staticmethod_handler(request):
|
||||
return text("...")
|
||||
|
||||
@classmethod
|
||||
@openapi.definition(
|
||||
summary="classmethod custom summary",
|
||||
tag="Test",
|
||||
)
|
||||
async def classmethod_handler(cls, request):
|
||||
return text("...")
|
||||
|
||||
@openapi.definition(
|
||||
summary="instance method custom summary",
|
||||
tag="Test",
|
||||
)
|
||||
async def instance_method_handler(self, request):
|
||||
return text("...")
|
||||
|
||||
app.add_route(TestClass.staticmethod_handler, "/staticmethod")
|
||||
app.add_route(TestClass.classmethod_handler, "/classmethod")
|
||||
app.add_route(TestClass().instance_method_handler, "/instance_method")
|
||||
|
||||
spec = get_spec(app)
|
||||
paths = spec["paths"]
|
||||
assert len(paths) == 3
|
||||
assert (
|
||||
paths["/staticmethod"]["get"]["summary"]
|
||||
== "staticmethod custom summary"
|
||||
)
|
||||
assert (
|
||||
paths["/classmethod"]["get"]["summary"] == "classmethod custom summary"
|
||||
)
|
||||
assert (
|
||||
paths["/instance_method"]["get"]["summary"]
|
||||
== "instance method custom summary"
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
import attrs
|
||||
import pytest
|
||||
|
||||
from msgspec import Meta, Struct
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.dataclasses import dataclass as pydataclass
|
||||
|
||||
from sanic_ext import openapi
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
@dataclass
|
||||
class FooDataclass:
|
||||
links: list[UUID]
|
||||
priority: int = field(
|
||||
metadata={"openapi": {"exclusiveMinimum": 1, "exclusiveMaximum": 10}}
|
||||
)
|
||||
ident: str = field(
|
||||
default="XXXX", metadata={"openapi": {"example": "ABC123"}}
|
||||
)
|
||||
|
||||
|
||||
@attrs.define
|
||||
class FooAttrs:
|
||||
links: list[UUID]
|
||||
priority: int = attrs.field(
|
||||
metadata={"openapi": {"exclusiveMinimum": 1, "exclusiveMaximum": 10}}
|
||||
)
|
||||
ident: str = attrs.field(
|
||||
default="XXXX", metadata={"openapi": {"example": "ABC123"}}
|
||||
)
|
||||
|
||||
|
||||
class FooPydanticBaseModel(BaseModel):
|
||||
links: list[UUID]
|
||||
priority: int = Field(gt=1, lt=10)
|
||||
ident: str = Field("XXXX", example="ABC123")
|
||||
|
||||
|
||||
@pydataclass
|
||||
class FooPydanticDataclass:
|
||||
links: list[UUID]
|
||||
priority: int = Field(gt=1, lt=10)
|
||||
ident: str = Field("XXXX", example="ABC123")
|
||||
|
||||
|
||||
class FooStruct(Struct):
|
||||
links: list[UUID]
|
||||
priority: Annotated[
|
||||
int,
|
||||
Meta(
|
||||
extra={"openapi": {"exclusiveMinimum": 1, "exclusiveMaximum": 10}}
|
||||
),
|
||||
]
|
||||
ident: Annotated[str, Meta(extra={"openapi": {"example": "ABC123"}})] = (
|
||||
"XXXX"
|
||||
)
|
||||
|
||||
|
||||
models = [
|
||||
FooDataclass,
|
||||
FooAttrs,
|
||||
FooPydanticBaseModel,
|
||||
FooPydanticDataclass,
|
||||
]
|
||||
|
||||
models.append(FooStruct)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("Foo", models)
|
||||
def test_models(app, Foo):
|
||||
@app.get("/")
|
||||
@openapi.definition(body={"application/json": Foo})
|
||||
async def handler(_): ...
|
||||
|
||||
spec = get_spec(app)
|
||||
foo_props = spec["paths"]["/"]["get"]["requestBody"]["content"][
|
||||
"application/json"
|
||||
]["schema"]["properties"]
|
||||
|
||||
assert foo_props["links"] == {
|
||||
"title": "Links",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "uuid"},
|
||||
}
|
||||
assert foo_props["ident"] == {
|
||||
"title": "Ident",
|
||||
"type": "string",
|
||||
"default": "XXXX",
|
||||
"example": "ABC123",
|
||||
}
|
||||
assert foo_props["priority"] == {
|
||||
"title": "Priority",
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"exclusiveMinimum": 1,
|
||||
"exclusiveMaximum": 10,
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
import attrs
|
||||
import pytest
|
||||
|
||||
from msgspec import Struct
|
||||
from pydantic import BaseModel
|
||||
from pydantic.dataclasses import dataclass as pydataclass
|
||||
|
||||
from sanic_ext import openapi
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlertDataclass:
|
||||
hit: dict[str, int]
|
||||
last_updated: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlertResponseDataclass:
|
||||
alert: AlertDataclass
|
||||
rule_id: str
|
||||
|
||||
|
||||
class AlertPydanticBaseModel(BaseModel):
|
||||
hit: dict[str, int]
|
||||
last_updated: datetime
|
||||
|
||||
|
||||
class AlertResponsePydanticBaseModel(BaseModel):
|
||||
alert: AlertPydanticBaseModel
|
||||
rule_id: str
|
||||
|
||||
|
||||
class AlertMsgspecBaseModel(Struct):
|
||||
hit: dict[str, int]
|
||||
last_updated: datetime
|
||||
|
||||
|
||||
class AlertResponseMsgspecBaseModel(Struct):
|
||||
alert: AlertMsgspecBaseModel
|
||||
rule_id: str
|
||||
|
||||
|
||||
@pydataclass
|
||||
class AlertPydanticDataclass:
|
||||
hit: dict[str, int]
|
||||
last_updated: datetime
|
||||
|
||||
|
||||
@pydataclass
|
||||
class AlertResponsePydanticDataclass:
|
||||
alert: AlertPydanticDataclass
|
||||
rule_id: str
|
||||
|
||||
|
||||
@attrs.define
|
||||
class AlertAttrs:
|
||||
hit: dict[str, int]
|
||||
last_updated: datetime
|
||||
|
||||
|
||||
@attrs.define
|
||||
class AlertResponseAttrs:
|
||||
alert: AlertAttrs
|
||||
rule_id: str
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"AlertResponse,check_alert",
|
||||
(
|
||||
(AlertResponseDataclass, False),
|
||||
(AlertResponseAttrs, False),
|
||||
(AlertResponseMsgspecBaseModel, True),
|
||||
(AlertResponsePydanticBaseModel, True),
|
||||
(AlertResponsePydanticDataclass, True),
|
||||
),
|
||||
)
|
||||
def test_pydantic_base_model(app, AlertResponse, check_alert):
|
||||
@app.get("/")
|
||||
@openapi.definition(
|
||||
body={"application/json": openapi.Component(AlertResponse)}
|
||||
)
|
||||
async def handler(_): ...
|
||||
|
||||
spec = get_spec(app)
|
||||
alert_response_name = AlertResponse.__name__
|
||||
alert_name = "Alert" + alert_response_name[13:]
|
||||
|
||||
assert spec["paths"]["/"]["get"]["requestBody"] == {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": f"#/components/schemas/{alert_response_name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert alert_response_name in spec["components"]["schemas"]
|
||||
|
||||
if check_alert:
|
||||
assert alert_name in spec["components"]["schemas"]
|
||||
assert spec["components"]["schemas"][alert_response_name][
|
||||
"properties"
|
||||
]["alert"] == {"$ref": f"#/components/schemas/{alert_name}"}
|
||||
@@ -0,0 +1,149 @@
|
||||
from sanic import Request, Sanic, text
|
||||
|
||||
from sanic_ext.extensions.openapi import openapi
|
||||
from sanic_ext.extensions.openapi.definitions import Parameter
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
def test_parameter(app: Sanic):
|
||||
DESCRIPTION = "val1 path param"
|
||||
NAME = "val1"
|
||||
LOCATION = "path"
|
||||
TYPE = "integer"
|
||||
|
||||
@app.route("/test1/<val1>")
|
||||
async def handler1(request: Request, val1: int):
|
||||
"""
|
||||
openapi:
|
||||
---
|
||||
operationId: get.test1
|
||||
parameters:
|
||||
- name: val1
|
||||
in: path
|
||||
description: val1 path param
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
"""
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test2/<val1>")
|
||||
@openapi.parameter(
|
||||
parameter=Parameter(
|
||||
name="val1",
|
||||
schema=int,
|
||||
location=LOCATION,
|
||||
description=DESCRIPTION,
|
||||
required=True,
|
||||
)
|
||||
)
|
||||
async def handler2(request: Request, val1: int):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test3/<val1>")
|
||||
@openapi.parameter(
|
||||
"val1",
|
||||
description=DESCRIPTION,
|
||||
required=True,
|
||||
schema=int,
|
||||
location=LOCATION,
|
||||
)
|
||||
async def handler3(request: Request, val1: int):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test4/<val1>")
|
||||
@openapi.definition(
|
||||
parameter={
|
||||
"name": "val1",
|
||||
"description": DESCRIPTION,
|
||||
"required": True,
|
||||
"schema": int,
|
||||
"location": LOCATION,
|
||||
}
|
||||
)
|
||||
async def handler4(request: Request, val1: int):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test5/<val1>")
|
||||
@openapi.definition(
|
||||
parameter=Parameter(
|
||||
name="val1",
|
||||
schema=int,
|
||||
location=LOCATION,
|
||||
description=DESCRIPTION,
|
||||
required=True,
|
||||
)
|
||||
)
|
||||
async def handler5(request: Request, val1: int):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test6/<val1:strorempty>")
|
||||
async def handler6(request: Request, val1: str):
|
||||
"""
|
||||
openapi:
|
||||
---
|
||||
operationId: get.test1
|
||||
parameters:
|
||||
- name: val1
|
||||
in: path
|
||||
description: val1 path param
|
||||
required: false
|
||||
"""
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test7/<val1:ext>")
|
||||
@openapi.parameter(
|
||||
parameter=Parameter(
|
||||
name="val1",
|
||||
location=LOCATION,
|
||||
description=DESCRIPTION,
|
||||
required=True,
|
||||
)
|
||||
)
|
||||
async def handler7(request: Request, val1: int):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test8/<val1:alpha>")
|
||||
@openapi.parameter(
|
||||
parameter=Parameter(
|
||||
name="val1",
|
||||
location=LOCATION,
|
||||
description=DESCRIPTION,
|
||||
required=True,
|
||||
)
|
||||
)
|
||||
async def handler8(request: Request, val1: int):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test9/<val1:slug>")
|
||||
@openapi.parameter(
|
||||
parameter=Parameter(
|
||||
name="val1",
|
||||
location=LOCATION,
|
||||
description=DESCRIPTION,
|
||||
required=True,
|
||||
)
|
||||
)
|
||||
async def handler9(request: Request, val1: int):
|
||||
return text("ok")
|
||||
|
||||
spec = get_spec(app)
|
||||
for i in range(1, 6):
|
||||
assert f"/test{i}/{{val1}}" in spec["paths"]
|
||||
parameter = spec["paths"][f"/test{i}/{{val1}}"]["get"]["parameters"][0]
|
||||
assert parameter["name"] == NAME
|
||||
assert parameter["in"] == LOCATION
|
||||
assert parameter["required"] is True
|
||||
assert parameter["schema"]["type"] == TYPE
|
||||
assert parameter["description"] == DESCRIPTION
|
||||
|
||||
for i in range(6, 10):
|
||||
assert f"/test{i}/{{val1}}" in spec["paths"]
|
||||
parameter = spec["paths"][f"/test{i}/{{val1}}"]["get"]["parameters"][0]
|
||||
assert parameter["name"] == NAME
|
||||
assert parameter["in"] == LOCATION
|
||||
assert parameter["required"] is not (i == 6)
|
||||
assert parameter["schema"]["type"] == "string"
|
||||
assert parameter["description"] == DESCRIPTION
|
||||
@@ -0,0 +1,58 @@
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def handlers(app: Sanic):
|
||||
@app.route("/test1")
|
||||
async def handler1(_): ...
|
||||
|
||||
@app.route("/test2/1")
|
||||
async def handler2(_): ...
|
||||
|
||||
@app.route("/test3/")
|
||||
async def handler3(_): ...
|
||||
|
||||
@app.route("/test4/2/")
|
||||
async def handler4(_): ...
|
||||
|
||||
|
||||
def test_paths_with_all_uri_filter(app: Sanic):
|
||||
app.config.API_URI_FILTER = "all"
|
||||
|
||||
spec = get_spec(app)
|
||||
assert list(spec["paths"].keys()) == [
|
||||
"/test1",
|
||||
"/test1/",
|
||||
"/test2/1",
|
||||
"/test2/1/",
|
||||
"/test3",
|
||||
"/test3/",
|
||||
"/test4/2",
|
||||
"/test4/2/",
|
||||
]
|
||||
|
||||
|
||||
def test_paths_with_slash_uri_filter(app: Sanic):
|
||||
app.config.API_URI_FILTER = "slash"
|
||||
spec = get_spec(app)
|
||||
|
||||
assert list(spec["paths"].keys()) == [
|
||||
"/test1/",
|
||||
"/test2/1/",
|
||||
"/test3/",
|
||||
"/test4/2/",
|
||||
]
|
||||
|
||||
|
||||
def test_paths_without_uri_filter(app: Sanic):
|
||||
spec = get_spec(app)
|
||||
assert list(spec["paths"].keys()) == [
|
||||
"/test1",
|
||||
"/test2/1",
|
||||
"/test3",
|
||||
"/test4/2",
|
||||
]
|
||||
@@ -0,0 +1,75 @@
|
||||
from sys import version_info
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic_ext.extensions.openapi.types import Schema, String
|
||||
|
||||
|
||||
@pytest.mark.skipif(version_info < (3, 9), reason="Not needed on 3.8")
|
||||
def test_schema_list():
|
||||
class Foo:
|
||||
list1: list[int]
|
||||
list2: list[int]
|
||||
|
||||
@property
|
||||
def show(self) -> bool:
|
||||
return True
|
||||
|
||||
def no_show_method(self) -> None: ...
|
||||
|
||||
@classmethod
|
||||
def no_show_classmethod(self) -> None: ...
|
||||
|
||||
@staticmethod
|
||||
def no_show_staticmethod() -> None: ...
|
||||
|
||||
schema = Schema.make(Foo)
|
||||
serialized = schema.serialize()
|
||||
|
||||
assert "no_show_method" not in serialized
|
||||
assert "no_show_classmethod" not in serialized
|
||||
assert "no_show_staticmethod" not in serialized
|
||||
assert serialized == {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list1": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer", "format": "int32"},
|
||||
},
|
||||
"list2": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer", "format": "int32"},
|
||||
},
|
||||
"show": {"type": "boolean"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_schema_fields():
|
||||
class Pet:
|
||||
name = String(example="Snoopy")
|
||||
|
||||
class Single:
|
||||
pet = Pet
|
||||
|
||||
class Ignore: ...
|
||||
|
||||
class Multiple:
|
||||
pets = [Pet]
|
||||
|
||||
pet_schema = Schema.make(Pet).serialize()
|
||||
single_schema = Schema.make(Single).serialize()
|
||||
multiple_schema = Schema.make(Multiple).serialize()
|
||||
|
||||
assert pet_schema == {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string", "example": "Snoopy"}},
|
||||
}
|
||||
assert single_schema == {
|
||||
"type": "object",
|
||||
"properties": {"pet": pet_schema},
|
||||
}
|
||||
assert multiple_schema == {
|
||||
"type": "object",
|
||||
"properties": {"pets": {"type": "array", "items": pet_schema}},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
from sanic import Sanic
|
||||
|
||||
from sanic_ext import openapi
|
||||
from tests.extensions.openapi.utils import get_path, get_spec
|
||||
|
||||
|
||||
def test_secured(app: Sanic):
|
||||
@app.route("/one")
|
||||
async def handler1(request):
|
||||
"""
|
||||
openapi:
|
||||
---
|
||||
security:
|
||||
- foo: []
|
||||
"""
|
||||
|
||||
@app.route("/two")
|
||||
@openapi.secured("foo")
|
||||
@openapi.secured({"bar": []})
|
||||
@openapi.secured(baz=[])
|
||||
async def handler2(request): ...
|
||||
|
||||
@app.route("/three")
|
||||
@openapi.definition(secured="foo")
|
||||
@openapi.definition(secured={"bar": []})
|
||||
async def handler3(request): ...
|
||||
|
||||
spec = get_path(app, "/one")
|
||||
assert {"foo": []} in spec["security"]
|
||||
|
||||
spec = get_path(app, "/two")
|
||||
assert {"foo": []} in spec["security"]
|
||||
assert {"bar": []} in spec["security"]
|
||||
assert {"baz": []} in spec["security"]
|
||||
|
||||
spec = get_path(app, "/three")
|
||||
assert {"foo": []} in spec["security"]
|
||||
assert {"bar": []} in spec["security"]
|
||||
|
||||
|
||||
def test_security_scheme(app: Sanic):
|
||||
app.ext.openapi.add_security_scheme("api_key", "apiKey")
|
||||
app.ext.openapi.add_security_scheme("token1", "http")
|
||||
app.ext.openapi.add_security_scheme(
|
||||
"token2", "http", scheme="bearer", bearer_format="JWT"
|
||||
)
|
||||
app.ext.openapi.add_security_scheme("oldschool", "http", scheme="basic")
|
||||
app.ext.openapi.add_security_scheme(
|
||||
"oa2",
|
||||
"oauth2",
|
||||
flows={
|
||||
"implicit": {
|
||||
"authorizationUrl": "http://example.com/auth",
|
||||
"scopes": {
|
||||
"one:two": "something",
|
||||
"three:four": "something else",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
spec = get_spec(app)
|
||||
schemes = spec["components"]["securitySchemes"]
|
||||
|
||||
assert schemes["api_key"] == {
|
||||
"type": "apiKey",
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
}
|
||||
assert schemes["token1"] == {
|
||||
"type": "http",
|
||||
"scheme": "Bearer",
|
||||
}
|
||||
assert schemes["token2"] == {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT",
|
||||
}
|
||||
assert schemes["oa2"] == {
|
||||
"type": "oauth2",
|
||||
"flows": {
|
||||
"implicit": {
|
||||
"authorizationUrl": "http://example.com/auth",
|
||||
"scopes": {
|
||||
"one:two": "something",
|
||||
"three:four": "something else",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_raw(app: Sanic):
|
||||
app.ext.openapi.raw({"security": [{}, {"foo": []}]})
|
||||
|
||||
spec = get_spec(app)
|
||||
assert {} in spec["security"]
|
||||
assert {"foo": []} in spec["security"]
|
||||
@@ -0,0 +1,37 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sanic_ext.extensions.openapi.builders import SpecificationBuilder
|
||||
|
||||
|
||||
def test_specification_singleton():
|
||||
spec = SpecificationBuilder()
|
||||
spec.tag("foo")
|
||||
|
||||
new_spec = SpecificationBuilder()
|
||||
|
||||
assert spec is new_spec
|
||||
assert spec.tags == new_spec.tags
|
||||
|
||||
|
||||
def test_specification_reset(get_docs):
|
||||
spec = SpecificationBuilder()
|
||||
|
||||
docs = get_docs()
|
||||
assert len(docs["tags"]) == 0
|
||||
|
||||
spec.tag("foo")
|
||||
docs = get_docs()
|
||||
assert len(docs["tags"]) == 1
|
||||
|
||||
spec.reset()
|
||||
docs = get_docs()
|
||||
assert len(docs["tags"]) == 0
|
||||
|
||||
|
||||
def test_custom_specification(app):
|
||||
petstore_file = Path(__file__).parent / "static" / "petstore.json"
|
||||
petstore_data = petstore_file.read_text()
|
||||
app.config.OAS_CUSTOM_FILE = petstore_file
|
||||
|
||||
_, response = app.test_client.get("/docs/openapi.json")
|
||||
assert response.text == petstore_data
|
||||
@@ -0,0 +1,38 @@
|
||||
from sanic import Request, Sanic
|
||||
from sanic.response import text
|
||||
|
||||
from sanic_ext import openapi
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
def test_summary(app: Sanic):
|
||||
@app.route("/test0")
|
||||
async def handler0(request: Request):
|
||||
"""This is a summary."""
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test1")
|
||||
@openapi.summary("This is a summary.")
|
||||
async def handler1(request: Request):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test2")
|
||||
@openapi.definition(summary="This is a summary.")
|
||||
async def handler2(request: Request):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test3")
|
||||
async def handler3(request: Request):
|
||||
"""
|
||||
openapi:
|
||||
---
|
||||
summary: This is a summary.
|
||||
"""
|
||||
return text("ok")
|
||||
|
||||
spec = get_spec(app)
|
||||
paths = spec["paths"]
|
||||
assert len(paths) == 4
|
||||
for i in range(4):
|
||||
assert paths[f"/test{i}"]["get"]["summary"] == "This is a summary."
|
||||
@@ -0,0 +1,35 @@
|
||||
from sanic import Request, Sanic, text
|
||||
|
||||
from sanic_ext.extensions.openapi import openapi
|
||||
|
||||
from .utils import get_spec
|
||||
|
||||
|
||||
def test_tag(app: Sanic):
|
||||
TAG_NAME = "test-tag-1"
|
||||
|
||||
@app.route("/test0")
|
||||
async def handler0(request: Request, val1: int):
|
||||
"""
|
||||
openapi:
|
||||
---
|
||||
tags:
|
||||
- test-tag-1
|
||||
"""
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test1")
|
||||
@openapi.tag(TAG_NAME)
|
||||
async def handler1(request: Request, val1: int):
|
||||
return text("ok")
|
||||
|
||||
@app.route("/test2")
|
||||
@openapi.definition(tag=TAG_NAME)
|
||||
async def handler2(request: Request, val1: int):
|
||||
return text("ok")
|
||||
|
||||
spec = get_spec(app)
|
||||
for i in range(3):
|
||||
assert f"/test{i}" in spec["paths"]
|
||||
tag = spec["paths"][f"/test{i}"]["get"]["tags"][0]
|
||||
assert tag == TAG_NAME
|
||||
@@ -0,0 +1,49 @@
|
||||
import sys
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic_ext.utils.typing import contains_annotations, flat_values
|
||||
|
||||
|
||||
class Foo: ...
|
||||
|
||||
|
||||
def test_dict_values_nested():
|
||||
values = flat_values(
|
||||
{
|
||||
"level1": {
|
||||
"level2": {
|
||||
"level3": {
|
||||
"level4": {"item": 999, "items": [888, 777]},
|
||||
"item": 666,
|
||||
},
|
||||
"item": True,
|
||||
},
|
||||
"multiple": [
|
||||
{"nested": {"object": None}},
|
||||
{"nested": {"object": None}},
|
||||
],
|
||||
},
|
||||
"item": True,
|
||||
}
|
||||
)
|
||||
assert values == {999, 888, 777, 666, None, True}
|
||||
|
||||
|
||||
params = [
|
||||
({"foo": str}, True),
|
||||
({"foo": list[str]}, True),
|
||||
({"foo": Optional[str]}, True),
|
||||
({"foo": Foo}, True),
|
||||
({"foo": "str"}, False),
|
||||
]
|
||||
params.append(({"foo": list[str]}, True))
|
||||
if sys.version_info >= (3, 10):
|
||||
params.append(({"foo": str | None}, True))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("item,expected", params)
|
||||
def test_contains_annotations(item, expected):
|
||||
assert contains_annotations(item) == expected
|
||||
@@ -0,0 +1,14 @@
|
||||
from typing import Any
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def get_spec(app: Sanic) -> dict[str, Any]:
|
||||
test_client = app.test_client
|
||||
_, response = test_client.get("/docs/openapi.json")
|
||||
return response.json
|
||||
|
||||
|
||||
def get_path(app: Sanic, path: str, method: str = "get") -> dict[str, Any]:
|
||||
spec = get_spec(app)
|
||||
return spec["paths"][path][method.lower()]
|
||||
@@ -0,0 +1,153 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
from sanic_ext import render
|
||||
|
||||
|
||||
def test_default_templates():
|
||||
app = Sanic("templating")
|
||||
app.extend(
|
||||
config={
|
||||
"templating_path_to_templates": Path(__file__).parent / "templates"
|
||||
}
|
||||
)
|
||||
|
||||
@app.get("/1")
|
||||
@app.ext.template("foo.html")
|
||||
async def handler(_):
|
||||
return {"seq": ["one", "two"]}
|
||||
|
||||
@app.get("/2")
|
||||
async def handler2(_):
|
||||
return await render(
|
||||
"foo.html", context={"seq": ["three", "four"]}, app=app
|
||||
)
|
||||
|
||||
@app.get("/3")
|
||||
@app.ext.template("foo.html")
|
||||
async def handler3(_):
|
||||
return await render(
|
||||
context={"seq": ["five", "six"]}, status=201, app=app
|
||||
)
|
||||
|
||||
_, response = app.test_client.get("/1")
|
||||
assert response.content_type == "text/html; charset=utf-8"
|
||||
assert "<li>one</li>" in response.text
|
||||
assert "<li>two</li>" in response.text
|
||||
|
||||
_, response = app.test_client.get("/2")
|
||||
assert response.content_type == "text/html; charset=utf-8"
|
||||
assert "<li>three</li>" in response.text
|
||||
assert "<li>four</li>" in response.text
|
||||
|
||||
_, response = app.test_client.get("/3")
|
||||
assert response.content_type == "text/html; charset=utf-8"
|
||||
assert "<li>five</li>" in response.text
|
||||
assert "<li>six</li>" in response.text
|
||||
assert response.status == 201
|
||||
|
||||
|
||||
def test_render_from_string():
|
||||
app = Sanic("templating-from-string")
|
||||
app.extend()
|
||||
|
||||
template = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>My Webpage</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Hello, world!!!!</h1>
|
||||
<ul>
|
||||
{% for item in seq %}
|
||||
<li>{{ item }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
"""
|
||||
|
||||
@app.get("/2")
|
||||
async def handler2(_):
|
||||
return await render(
|
||||
template_source=template,
|
||||
context={"seq": ["three", "four"]},
|
||||
app=app,
|
||||
)
|
||||
|
||||
_, response = app.test_client.get("/2")
|
||||
assert response.content_type == "text/html; charset=utf-8"
|
||||
assert "<li>three</li>" in response.text
|
||||
assert "<li>four</li>" in response.text
|
||||
|
||||
|
||||
def test_config_templating_dir():
|
||||
app = Sanic("templating")
|
||||
app.config.TEMPLATING_PATH_TO_TEMPLATES = (
|
||||
Path(__file__).parent / "templates"
|
||||
)
|
||||
|
||||
assert app.ext.templating.environment.get_template(
|
||||
"foo.html"
|
||||
).filename == str(Path(__file__).parent / "templates" / "foo.html")
|
||||
|
||||
|
||||
def test_url_for():
|
||||
app = Sanic("templating-from-string")
|
||||
app.extend()
|
||||
|
||||
template = r"url: {{ url_for('handler') }}"
|
||||
|
||||
@app.get("/one/two/three")
|
||||
async def handler(_):
|
||||
return await render(template_source=template)
|
||||
|
||||
_, response = app.test_client.get("/one/two/three")
|
||||
assert response.text == "url: /one/two/three"
|
||||
|
||||
|
||||
def test_default_context():
|
||||
app = Sanic("templating-from-string")
|
||||
app.extend(
|
||||
config={
|
||||
"templating_path_to_templates": Path(__file__).parent / "templates"
|
||||
}
|
||||
)
|
||||
|
||||
template = r"{{ request.args.get('test') }}"
|
||||
|
||||
@app.get("/1")
|
||||
async def handler1(_):
|
||||
return await render(template_source=template)
|
||||
|
||||
@app.get("/2")
|
||||
@app.ext.template("request_test.html")
|
||||
async def handler2(_):
|
||||
return {}
|
||||
|
||||
@app.get("/3")
|
||||
async def handler3(_):
|
||||
return await render("request_test.html", context={}, app=app)
|
||||
|
||||
@app.get("/4")
|
||||
@app.ext.template("request_test.html")
|
||||
async def handler4(_):
|
||||
return await render(context={}, app=app)
|
||||
|
||||
_, response = app.test_client.get("/1?test=passing")
|
||||
assert response.text == "passing"
|
||||
|
||||
_, response = app.test_client.get("/2?test=passing")
|
||||
assert response.text == "passing"
|
||||
|
||||
_, response = app.test_client.get("/3?test=passing")
|
||||
assert response.text == "passing"
|
||||
|
||||
_, response = app.test_client.get("/4?test=passing")
|
||||
assert response.text == "passing"
|
||||
@@ -0,0 +1,63 @@
|
||||
from typing import Union
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
from sanic_ext import Extend, Extension
|
||||
|
||||
|
||||
def test_multiple_extensions(bare_app: Sanic):
|
||||
mock = Mock()
|
||||
|
||||
class FooExtension(Extension):
|
||||
name = "foo"
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def startup(self, _) -> None:
|
||||
mock()
|
||||
|
||||
class BarExtension(Extension):
|
||||
name = "bar"
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def startup(self, _) -> None:
|
||||
mock()
|
||||
|
||||
bare_app.extend(extensions=[FooExtension, BarExtension, BarExtension])
|
||||
|
||||
assert mock.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("instance", (True, False))
|
||||
def test_multiple_extensions_pre_register(bare_app: Sanic, instance: bool):
|
||||
mock = Mock()
|
||||
|
||||
class FooExtension(Extension):
|
||||
name = "foo"
|
||||
|
||||
def startup(self, _) -> None:
|
||||
mock()
|
||||
|
||||
class BarExtension(Extension):
|
||||
name = "bar"
|
||||
|
||||
def startup(self, _) -> None:
|
||||
mock()
|
||||
|
||||
foo: Union[type[Extension], Extension] = FooExtension
|
||||
bar: Union[type[Extension], Extension] = BarExtension
|
||||
if instance:
|
||||
foo = foo() # type: ignore
|
||||
bar = bar() # type: ignore
|
||||
|
||||
Extend.register(foo)
|
||||
Extend.register(bar)
|
||||
bare_app.extend()
|
||||
|
||||
assert mock.call_count == 2
|
||||
Reference in New Issue
Block a user