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,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()]
|
||||
Reference in New Issue
Block a user