test_aiohttp.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. # coding: utf-8
  2. from aiohttp import web
  3. import marshmallow
  4. from hapic import Hapic
  5. from hapic import HapicData
  6. from hapic.ext.aiohttp.context import AiohttpContext
  7. class TestAiohttpExt(object):
  8. async def test_aiohttp_only__ok__nominal_case(
  9. self,
  10. aiohttp_client,
  11. loop,
  12. ):
  13. async def hello(request):
  14. return web.Response(text='Hello, world')
  15. app = web.Application(debug=True)
  16. app.router.add_get('/', hello)
  17. client = await aiohttp_client(app)
  18. resp = await client.get('/')
  19. assert resp.status == 200
  20. text = await resp.text()
  21. assert 'Hello, world' in text
  22. async def test_aiohttp_input_path__ok__nominal_case(
  23. self,
  24. aiohttp_client,
  25. loop,
  26. ):
  27. hapic = Hapic(async_=True)
  28. class InputPathSchema(marshmallow.Schema):
  29. name = marshmallow.fields.String()
  30. @hapic.input_path(InputPathSchema())
  31. async def hello(request, hapic_data: HapicData):
  32. name = hapic_data.path.get('name')
  33. return web.Response(text='Hello, {}'.format(name))
  34. app = web.Application(debug=True)
  35. app.router.add_get('/{name}', hello)
  36. hapic.set_context(AiohttpContext(app))
  37. client = await aiohttp_client(app)
  38. resp = await client.get('/bob')
  39. assert resp.status == 200
  40. text = await resp.text()
  41. assert 'Hello, bob' in text
  42. async def test_aiohttp_input_path__error_wrong_input_parameter(
  43. self,
  44. aiohttp_client,
  45. loop,
  46. ):
  47. hapic = Hapic(async_=True)
  48. class InputPathSchema(marshmallow.Schema):
  49. i = marshmallow.fields.Integer()
  50. @hapic.input_path(InputPathSchema())
  51. async def hello(request, hapic_data: HapicData):
  52. i = hapic_data.path.get('i')
  53. return web.Response(text='integer: {}'.format(str(i)))
  54. app = web.Application(debug=True)
  55. app.router.add_get('/{i}', hello)
  56. hapic.set_context(AiohttpContext(app))
  57. client = await aiohttp_client(app)
  58. resp = await client.get('/bob') # NOTE: should be integer here
  59. assert resp.status == 400
  60. error = await resp.json()
  61. assert 'Validation error of input data' in error.get('message')
  62. assert {'i': ['Not a valid integer.']} == error.get('details')
  63. async def test_aiohttp_input_body__ok_nominal_case(
  64. self,
  65. aiohttp_client,
  66. loop,
  67. ):
  68. hapic = Hapic(async_=True)
  69. class InputBodySchema(marshmallow.Schema):
  70. name = marshmallow.fields.String()
  71. @hapic.input_body(InputBodySchema())
  72. async def hello(request, hapic_data: HapicData):
  73. name = hapic_data.body.get('name')
  74. return web.Response(text='Hello, {}'.format(name))
  75. app = web.Application(debug=True)
  76. app.router.add_post('/', hello)
  77. hapic.set_context(AiohttpContext(app))
  78. client = await aiohttp_client(app)
  79. resp = await client.post('/', data={'name': 'bob'})
  80. assert resp.status == 200
  81. text = await resp.text()
  82. assert 'Hello, bob' in text
  83. async def test_aiohttp_input_body__error__incorrect_input_body(
  84. self,
  85. aiohttp_client,
  86. loop,
  87. ):
  88. hapic = Hapic(async_=True)
  89. class InputBodySchema(marshmallow.Schema):
  90. i = marshmallow.fields.Integer()
  91. @hapic.input_body(InputBodySchema())
  92. async def hello(request, hapic_data: HapicData):
  93. i = hapic_data.body.get('i')
  94. return web.Response(text='integer, {}'.format(i))
  95. app = web.Application(debug=True)
  96. app.router.add_post('/', hello)
  97. hapic.set_context(AiohttpContext(app))
  98. client = await aiohttp_client(app)
  99. resp = await client.post('/', data={'i': 'bob'}) # NOTE: should be int
  100. assert resp.status == 400
  101. error = await resp.json()
  102. assert 'Validation error of input data' in error.get('message')
  103. assert {'i': ['Not a valid integer.']} == error.get('details')
  104. async def test_aiohttp_output_body__ok__nominal_case(
  105. self,
  106. aiohttp_client,
  107. loop,
  108. ):
  109. hapic = Hapic(async_=True)
  110. class OuputBodySchema(marshmallow.Schema):
  111. name = marshmallow.fields.String()
  112. @hapic.output_body(OuputBodySchema())
  113. async def hello(request):
  114. return {
  115. 'name': 'bob',
  116. }
  117. app = web.Application(debug=True)
  118. app.router.add_get('/', hello)
  119. hapic.set_context(AiohttpContext(app))
  120. client = await aiohttp_client(app)
  121. resp = await client.get('/')
  122. assert resp.status == 200
  123. data = await resp.json()
  124. assert 'bob' == data.get('name')
  125. async def test_aiohttp_output_body__error__incorrect_output_body(
  126. self,
  127. aiohttp_client,
  128. loop,
  129. ):
  130. hapic = Hapic(async_=True)
  131. class OuputBodySchema(marshmallow.Schema):
  132. i = marshmallow.fields.Integer(required=True)
  133. @hapic.output_body(OuputBodySchema())
  134. async def hello(request):
  135. return {
  136. 'i': 'bob', # NOTE: should be integer
  137. }
  138. app = web.Application(debug=True)
  139. app.router.add_get('/', hello)
  140. hapic.set_context(AiohttpContext(app))
  141. client = await aiohttp_client(app)
  142. resp = await client.get('/')
  143. assert resp.status == 500
  144. data = await resp.json()
  145. assert 'Validation error of output data' == data.get('message')
  146. assert {
  147. 'i': ['Missing data for required field.'],
  148. } == data.get('details')
  149. async def test_aiohttp_handle_excpetion__ok__nominal_case(
  150. self,
  151. aiohttp_client,
  152. loop,
  153. ):
  154. hapic = Hapic(async_=True)
  155. @hapic.handle_exception(ZeroDivisionError, http_code=400)
  156. async def hello(request):
  157. 1 / 0
  158. app = web.Application(debug=True)
  159. app.router.add_get('/', hello)
  160. hapic.set_context(AiohttpContext(app))
  161. client = await aiohttp_client(app)
  162. resp = await client.get('/')
  163. assert resp.status == 400
  164. data = await resp.json()
  165. assert 'division by zero' == data.get('message')
  166. async def test_aiohttp_output_stream__ok__nominal_case(
  167. self,
  168. aiohttp_client,
  169. loop,
  170. ):
  171. hapic = Hapic(async_=True)
  172. class AsyncGenerator:
  173. def __init__(self):
  174. self._iterator = iter([
  175. {'name': 'Hello, bob'},
  176. {'name': 'Hello, franck'},
  177. ])
  178. async def __aiter__(self):
  179. return self
  180. async def __anext__(self):
  181. return next(self._iterator)
  182. class OuputStreamItemSchema(marshmallow.Schema):
  183. name = marshmallow.fields.String()
  184. @hapic.output_stream(OuputStreamItemSchema())
  185. async def hello(request):
  186. return AsyncGenerator()
  187. app = web.Application(debug=True)
  188. app.router.add_get('/', hello)
  189. hapic.set_context(AiohttpContext(app))
  190. client = await aiohttp_client(app)
  191. resp = await client.get('/')
  192. assert resp.status == 200
  193. line = await resp.content.readline()
  194. assert b'{"name": "Hello, bob"}\n' == line
  195. line = await resp.content.readline()
  196. assert b'{"name": "Hello, franck"}\n' == line
  197. async def test_aiohttp_output_stream__error__ignore(
  198. self,
  199. aiohttp_client,
  200. loop,
  201. ):
  202. hapic = Hapic(async_=True)
  203. class AsyncGenerator:
  204. def __init__(self):
  205. self._iterator = iter([
  206. {'name': 'Hello, bob'},
  207. {'nameZ': 'Hello, Z'}, # This line is incorrect
  208. {'name': 'Hello, franck'},
  209. ])
  210. async def __aiter__(self):
  211. return self
  212. async def __anext__(self):
  213. return next(self._iterator)
  214. class OuputStreamItemSchema(marshmallow.Schema):
  215. name = marshmallow.fields.String(required=True)
  216. @hapic.output_stream(OuputStreamItemSchema(), ignore_on_error=True)
  217. async def hello(request):
  218. return AsyncGenerator()
  219. app = web.Application(debug=True)
  220. app.router.add_get('/', hello)
  221. hapic.set_context(AiohttpContext(app))
  222. client = await aiohttp_client(app)
  223. resp = await client.get('/')
  224. assert resp.status == 200
  225. line = await resp.content.readline()
  226. assert b'{"name": "Hello, bob"}\n' == line
  227. line = await resp.content.readline()
  228. assert b'{"name": "Hello, franck"}\n' == line
  229. async def test_aiohttp_output_stream__error__interrupt(
  230. self,
  231. aiohttp_client,
  232. loop,
  233. ):
  234. hapic = Hapic(async_=True)
  235. class AsyncGenerator:
  236. def __init__(self):
  237. self._iterator = iter([
  238. {'name': 'Hello, bob'},
  239. {'nameZ': 'Hello, Z'}, # This line is incorrect
  240. {'name': 'Hello, franck'}, # This line must not be reached
  241. ])
  242. async def __aiter__(self):
  243. return self
  244. async def __anext__(self):
  245. return next(self._iterator)
  246. class OuputStreamItemSchema(marshmallow.Schema):
  247. name = marshmallow.fields.String(required=True)
  248. @hapic.output_stream(OuputStreamItemSchema(), ignore_on_error=False)
  249. async def hello(request):
  250. return AsyncGenerator()
  251. app = web.Application(debug=True)
  252. app.router.add_get('/', hello)
  253. hapic.set_context(AiohttpContext(app))
  254. client = await aiohttp_client(app)
  255. resp = await client.get('/')
  256. assert resp.status == 200
  257. line = await resp.content.readline()
  258. assert b'{"name": "Hello, bob"}\n' == line
  259. line = await resp.content.readline()
  260. assert b'' == line
  261. def test_unit__generate_doc__ok__nominal_case(
  262. self,
  263. aiohttp_client,
  264. loop,
  265. ):
  266. hapic = Hapic(async_=True)
  267. class InputPathSchema(marshmallow.Schema):
  268. username = marshmallow.fields.String(required=True)
  269. class InputQuerySchema(marshmallow.Schema):
  270. show_deleted = marshmallow.fields.Boolean(required=False)
  271. class UserSchema(marshmallow.Schema):
  272. name = marshmallow.fields.String(required=True)
  273. @hapic.with_api_doc()
  274. @hapic.input_path(InputPathSchema())
  275. @hapic.input_query(InputQuerySchema())
  276. @hapic.output_body(UserSchema())
  277. async def get_user(request, hapic_data):
  278. pass
  279. @hapic.with_api_doc()
  280. @hapic.input_path(InputPathSchema())
  281. @hapic.output_body(UserSchema())
  282. async def post_user(request, hapic_data):
  283. pass
  284. app = web.Application(debug=True)
  285. app.router.add_get('/{username}', get_user)
  286. app.router.add_post('/{username}', post_user)
  287. hapic.set_context(AiohttpContext(app))
  288. doc = hapic.generate_doc('aiohttp', 'testing')
  289. assert 'UserSchema' in doc.get('definitions')
  290. assert {
  291. 'name': {'type': 'string'}
  292. } == doc['definitions']['UserSchema'].get('properties')
  293. assert '/{username}' in doc.get('paths')
  294. assert 'get' in doc['paths']['/{username}']
  295. assert 'post' in doc['paths']['/{username}']
  296. assert [
  297. {
  298. 'name': 'username',
  299. 'in': 'path',
  300. 'required': True,
  301. 'type': 'string',
  302. },
  303. {
  304. 'name': 'show_deleted',
  305. 'in': 'query',
  306. 'required': False,
  307. 'type': 'boolean',
  308. }
  309. ] == doc['paths']['/{username}']['get']['parameters']
  310. assert {
  311. 200: {
  312. 'schema': {
  313. '$ref': '#/definitions/UserSchema',
  314. },
  315. 'description': '200',
  316. }
  317. } == doc['paths']['/{username}']['get']['responses']
  318. assert [
  319. {
  320. 'name': 'username',
  321. 'in': 'path',
  322. 'required': True,
  323. 'type': 'string',
  324. }
  325. ] == doc['paths']['/{username}']['post']['parameters']
  326. assert {
  327. 200: {
  328. 'schema': {
  329. '$ref': '#/definitions/UserSchema',
  330. },
  331. 'description': '200',
  332. }
  333. } == doc['paths']['/{username}']['get']['responses']
  334. def test_unit__generate_output_stream_doc__ok__nominal_case(
  335. self,
  336. aiohttp_client,
  337. loop,
  338. ):
  339. hapic = Hapic(async_=True)
  340. class OuputStreamItemSchema(marshmallow.Schema):
  341. name = marshmallow.fields.String(required=True)
  342. @hapic.with_api_doc()
  343. @hapic.output_stream(OuputStreamItemSchema())
  344. async def get_users(request, hapic_data):
  345. pass
  346. app = web.Application(debug=True)
  347. app.router.add_get('/', get_users)
  348. hapic.set_context(AiohttpContext(app))
  349. doc = hapic.generate_doc('aiohttp', 'testing')
  350. assert '/' in doc.get('paths')
  351. assert 'get' in doc['paths']['/']
  352. assert 200 in doc['paths']['/']['get'].get('responses', {})
  353. assert {
  354. 'items': {
  355. '$ref': '#/definitions/OuputStreamItemSchema'
  356. },
  357. 'type': 'array',
  358. } == doc['paths']['/']['get']['responses'][200]['schema']