takahe/hatchway/tests/test_view.py

245 lines
6.6 KiB
Python
Raw Normal View History

import json
import pytest
from django.core import files
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http import QueryDict
from django.test import RequestFactory
from django.test.client import MULTIPART_CONTENT
from pydantic import BaseModel
from hatchway import ApiError, Body, QueryOrBody, api_view
from hatchway.view import ApiView
def test_basic_view():
"""
Tests that a view with simple types works correctly
"""
@api_view
def test_view(
request,
a: int,
b: QueryOrBody[int | None] = None,
c: str = "x",
) -> str:
if b is None:
return c * a
else:
return c * (a - b)
# Call it with a few different patterns to verify it's type coercing right
factory = RequestFactory()
# Implicit query param
response = test_view(factory.get("/test/?a=4"))
assert json.loads(response.content) == "xxxx"
# QueryOrBody pulling from query
response = test_view(factory.get("/test/?a=4&b=2"))
assert json.loads(response.content) == "xx"
# QueryOrBody pulling from formdata body
response = test_view(factory.post("/test/?a=4", {"b": "3"}))
assert json.loads(response.content) == "x"
# QueryOrBody pulling from JSON body
response = test_view(
factory.post(
"/test/?a=4", json.dumps({"b": 3}), content_type="application/json"
)
)
assert json.loads(response.content) == "x"
# Implicit Query not pulling from body
with pytest.raises(TypeError):
test_view(factory.post("/test/", {"a": 4, "b": 3}))
def test_body_direct():
"""
Tests that a Pydantic model with BodyDirect gets its fields from the top level
"""
class TestModel(BaseModel):
number: int
name: str
@api_view
def test_view(request, data: TestModel) -> int:
return data.number
factory = RequestFactory()
# formdata version
response = test_view(factory.post("/test/", {"number": "123", "name": "Andrew"}))
assert json.loads(response.content) == 123
# JSON body version
response = test_view(
factory.post(
"/test/",
json.dumps({"number": "123", "name": "Andrew"}),
content_type="application/json",
)
)
assert json.loads(response.content) == 123
def test_list_response():
"""
Tests that a view with a list response type works correctly with both
dicts and pydantic model instances.
"""
class TestModel(BaseModel):
number: int
name: str
@api_view
def test_view_dict(request) -> list[TestModel]:
return [
{"name": "Andrew", "number": 1}, # type:ignore
{"name": "Alice", "number": 0}, # type:ignore
]
@api_view
def test_view_model(request) -> list[TestModel]:
return [TestModel(name="Andrew", number=1), TestModel(name="Alice", number=0)]
response = test_view_dict(RequestFactory().get("/test/"))
assert json.loads(response.content) == [
{"name": "Andrew", "number": 1},
{"name": "Alice", "number": 0},
]
response = test_view_model(RequestFactory().get("/test/"))
assert json.loads(response.content) == [
{"name": "Andrew", "number": 1},
{"name": "Alice", "number": 0},
]
def test_patch_body():
"""
Tests that PATCH also gets its body parsed
"""
@api_view.patch
def test_view(request, a: Body[int]):
return a
factory = RequestFactory()
response = test_view(
factory.patch(
"/test/",
content_type=MULTIPART_CONTENT,
data=factory._encode_data({"a": "42"}, MULTIPART_CONTENT),
)
)
assert json.loads(response.content) == 42
def test_file_body():
"""
Tests that file uploads work right
"""
@api_view.post
def test_view(request, a: Body[int], b: files.File) -> str:
return str(a) + b.read().decode("ascii")
factory = RequestFactory()
uploaded_file = SimpleUploadedFile(
"file.txt",
b"MY FILE IS AMAZING",
content_type="text/plain",
)
response = test_view(
factory.post(
"/test/",
data={"a": 42, "b": uploaded_file},
)
)
assert json.loads(response.content) == "42MY FILE IS AMAZING"
def test_no_response():
"""
Tests that a view with no response type returns the contents verbatim
"""
@api_view
def test_view(request):
return [1, "woooooo"]
response = test_view(RequestFactory().get("/test/"))
assert json.loads(response.content) == [1, "woooooo"]
def test_wrong_method():
"""
Tests that a view with a method limiter works
"""
@api_view.get
def test_view(request):
return "yay"
response = test_view(RequestFactory().get("/test/"))
assert json.loads(response.content) == "yay"
response = test_view(RequestFactory().post("/test/"))
assert response.status_code == 405
def test_api_error():
"""
Tests that ApiError propagates right
"""
@api_view.get
def test_view(request):
raise ApiError(401, "you did a bad thing")
response = test_view(RequestFactory().get("/test/"))
assert json.loads(response.content) == {"error": "you did a bad thing"}
assert response.status_code == 401
def test_unusable_type():
"""
Tests that you get a nice error when you use a type on an input that
Pydantic doesn't understand.
"""
with pytest.raises(ValueError):
@api_view.get
def test_view(request, a: RequestFactory):
pass
def test_get_values():
"""
Tests that ApiView.get_values correctly handles lists
"""
assert ApiView.get_values({"a": 2, "b": [3, 4]}) == {"a": 2, "b": [3, 4]}
assert ApiView.get_values({"a": 2, "b[]": [3, 4]}) == {"a": 2, "b": [3, 4]}
assert ApiView.get_values(QueryDict("a=2&b=3&b=4")) == {"a": "2", "b": ["3", "4"]}
assert ApiView.get_values(QueryDict("a=2&b[]=3&b[]=4")) == {
"a": "2",
"b": ["3", "4"],
}
assert ApiView.get_values(QueryDict("a=2&b=3")) == {"a": "2", "b": "3"}
assert ApiView.get_values(QueryDict("a=2&b[]=3")) == {"a": "2", "b": ["3"]}
assert ApiView.get_values(QueryDict("a[b]=1")) == {"a": {"b": "1"}}
assert ApiView.get_values(QueryDict("a[b]=1&a[c]=2")) == {"a": {"b": "1", "c": "2"}}
assert ApiView.get_values(QueryDict("a[b][c]=1")) == {"a": {"b": {"c": "1"}}}
assert ApiView.get_values(QueryDict("a[b][c][]=1")) == {"a": {"b": {"c": ["1"]}}}
assert ApiView.get_values(QueryDict("a[b][]=1&a[b][]=2")) == {
"a": {"b": ["1", "2"]}
}