Changeset View
Changeset View
Standalone View
Standalone View
swh/web/tests/api/test_apidoc.py
# Copyright (C) 2015-2018 The Software Heritage developers | # Copyright (C) 2015-2019 The Software Heritage developers | ||||
# See the AUTHORS file at the top-level directory of this distribution | # See the AUTHORS file at the top-level directory of this distribution | ||||
# License: GNU Affero General Public License version 3, or any later version | # License: GNU Affero General Public License version 3, or any later version | ||||
# See top-level LICENSE file for more information | # See top-level LICENSE file for more information | ||||
from rest_framework.test import APITestCase | import pytest | ||||
from rest_framework.response import Response | from rest_framework.response import Response | ||||
from swh.storage.exc import StorageDBError, StorageAPIError | from swh.storage.exc import StorageDBError, StorageAPIError | ||||
from swh.web.api.apidoc import api_doc, _parse_httpdomain_doc | from swh.web.api.apidoc import api_doc, _parse_httpdomain_doc | ||||
from swh.web.api.apiurls import api_route | from swh.web.api.apiurls import api_route | ||||
from swh.web.common.exc import BadInputExc, ForbiddenExc, NotFoundExc | from swh.web.common.exc import BadInputExc, ForbiddenExc, NotFoundExc | ||||
from swh.web.tests.testcase import WebTestCase | from swh.web.tests.django_asserts import assert_template_used | ||||
# flake8: noqa | |||||
httpdomain_doc = """ | httpdomain_doc = """ | ||||
.. http:get:: /api/1/revision/(sha1_git)/ | .. http:get:: /api/1/revision/(sha1_git)/ | ||||
Get information about a revision in the archive. | Get information about a revision in the archive. | ||||
Revisions are identified by **sha1** checksums, compatible with Git commit identifiers. | Revisions are identified by **sha1** checksums, compatible with Git commit | ||||
See :func:`swh.model.identifiers.revision_identifier` in our data model module for details | identifiers. | ||||
about how they are computed. | See :func:`swh.model.identifiers.revision_identifier` in our data model | ||||
module for details about how they are computed. | |||||
:param string sha1_git: hexadecimal representation of the revision **sha1_git** identifier | :param string sha1_git: hexadecimal representation of the revision | ||||
**sha1_git** identifier | |||||
:reqheader Accept: the requested response content type, | :reqheader Accept: the requested response content type, | ||||
either ``application/json`` (default) or ``application/yaml`` | either ``application/json`` (default) or ``application/yaml`` | ||||
:resheader Content-Type: this depends on :http:header:`Accept` header of request | :resheader Content-Type: this depends on :http:header:`Accept` header | ||||
of request | |||||
:>json object author: information about the author of the revision | :>json object author: information about the author of the revision | ||||
:>json object committer: information about the committer of the revision | :>json object committer: information about the committer of the revision | ||||
:>json string committer_date: ISO representation of the commit date (in UTC) | :>json string committer_date: ISO representation of the commit date | ||||
(in UTC) | |||||
:>json string date: ISO representation of the revision date (in UTC) | :>json string date: ISO representation of the revision date (in UTC) | ||||
:>json string directory: the unique identifier that revision points to | :>json string directory: the unique identifier that revision points to | ||||
:>json string directory_url: link to :http:get:`/api/1/directory/(sha1_git)/[(path)/]` | :>json string directory_url: link to | ||||
to get information about the directory associated to the revision | :http:get:`/api/1/directory/(sha1_git)/[(path)/]` to get information | ||||
about the directory associated to the revision | |||||
:>json string id: the revision unique identifier | :>json string id: the revision unique identifier | ||||
:>json boolean merge: whether or not the revision corresponds to a merge commit | :>json boolean merge: whether or not the revision corresponds to a merge | ||||
commit | |||||
:>json string message: the message associated to the revision | :>json string message: the message associated to the revision | ||||
:>json array parents: the parents of the revision, i.e. the previous revisions | :>json array parents: the parents of the revision, i.e. the previous | ||||
that head directly to it, each entry of that array contains an unique parent | revisions that head directly to it, each entry of that array contains | ||||
revision identifier but also a link to :http:get:`/api/1/revision/(sha1_git)/` | an unique parent revision identifier but also a link to | ||||
to get more information about it | :http:get:`/api/1/revision/(sha1_git)/` to get more information | ||||
about it | |||||
:>json string type: the type of the revision | :>json string type: the type of the revision | ||||
**Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` | **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head` | ||||
:statuscode 200: no error | :statuscode 200: no error | ||||
:statuscode 400: an invalid **sha1_git** value has been provided | :statuscode 400: an invalid **sha1_git** value has been provided | ||||
:statuscode 404: requested revision can not be found in the archive | :statuscode 404: requested revision can not be found in the archive | ||||
**Request:** | **Request:** | ||||
.. parsed-literal:: | .. parsed-literal:: | ||||
$ curl -i :swh_web_api:`revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/` | :swh_web_api:`revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/` | ||||
""" | """ | ||||
vlorentz: why is this changed? | |||||
Done Inline ActionsI tried to better respect flake8 formatting when refactoring the tests. Nevertheless, as the api endpoint docstrings are parsed by the swh.web.api.apidoc module I managed to improve that docstring in order to remove the #noqa marker, this will anlambert: I tried to better respect `flake8` formatting when refactoring the tests.
Nevertheless, as the… | |||||
class APIDocTestCase(WebTestCase, APITestCase): | |||||
exception_http_code = { | exception_http_code = { | ||||
BadInputExc: 400, | BadInputExc: 400, | ||||
ForbiddenExc: 403, | ForbiddenExc: 403, | ||||
NotFoundExc: 404, | NotFoundExc: 404, | ||||
Exception: 500, | Exception: 500, | ||||
StorageAPIError: 503, | StorageAPIError: 503, | ||||
StorageDBError: 503, | StorageDBError: 503, | ||||
} | } | ||||
def test_apidoc_nodoc_failure(self): | |||||
with self.assertRaises(Exception): | def test_apidoc_nodoc_failure(): | ||||
with pytest.raises(Exception): | |||||
@api_doc('/my/nodoc/url/') | @api_doc('/my/nodoc/url/') | ||||
def apidoc_nodoc_tester(request, arga=0, argb=0): | def apidoc_nodoc_tester(request, arga=0, argb=0): | ||||
return Response(arga + argb) | return Response(arga + argb) | ||||
@staticmethod | |||||
@api_route(r'/some/(?P<myarg>[0-9]+)/(?P<myotherarg>[0-9]+)/', | @api_route(r'/some/(?P<myarg>[0-9]+)/(?P<myotherarg>[0-9]+)/', | ||||
'some-doc-route') | 'some-doc-route') | ||||
@api_doc('/some/doc/route/') | @api_doc('/some/doc/route/') | ||||
def apidoc_route(request, myarg, myotherarg, akw=0): | def apidoc_route(request, myarg, myotherarg, akw=0): | ||||
""" | """ | ||||
Sample doc | Sample doc | ||||
""" | """ | ||||
return {'result': int(myarg) + int(myotherarg) + akw} | return {'result': int(myarg) + int(myotherarg) + akw} | ||||
def test_apidoc_route_doc(self): | # remove deprecation warnings related to docutils | ||||
# when | @pytest.mark.filterwarnings( | ||||
rv = self.client.get('/api/1/some/doc/route/') | 'ignore:.*U.*mode is deprecated:DeprecationWarning') | ||||
def test_apidoc_route_doc(client): | |||||
rv = client.get('/api/1/some/doc/route/', HTTP_ACCEPT='text/html') | |||||
# then | assert rv.status_code == 200, rv.content | ||||
self.assertEqual(rv.status_code, 200, rv.data) | assert_template_used(rv, 'api/apidoc.html') | ||||
self.assertTemplateUsed('api/apidoc.html') | |||||
def test_apidoc_route_fn(self): | |||||
# when | def test_apidoc_route_fn(api_client): | ||||
rv = self.client.get('/api/1/some/1/1/') | rv = api_client.get('/api/1/some/1/1/') | ||||
# then | assert rv.status_code == 200, rv.data | ||||
self.assertEqual(rv.status_code, 200, rv.data) | |||||
@staticmethod | |||||
@api_route(r'/test/error/(?P<exc_name>.+)/', | @api_route(r'/test/error/(?P<exc_name>.+)/', 'test-error') | ||||
'test-error') | |||||
@api_doc('/test/error/') | @api_doc('/test/error/') | ||||
def apidoc_test_error_route(request, exc_name): | def apidoc_test_error_route(request, exc_name): | ||||
""" | """ | ||||
Sample doc | Sample doc | ||||
""" | """ | ||||
for e in APIDocTestCase.exception_http_code.keys(): | for e in exception_http_code.keys(): | ||||
if e.__name__ == exc_name: | if e.__name__ == exc_name: | ||||
raise e('Error') | raise e('Error') | ||||
def test_apidoc_error(self): | |||||
for exc, code in self.exception_http_code.items(): | |||||
# when | |||||
rv = self.client.get('/api/1/test/error/%s/' % exc.__name__) | |||||
# then | def test_apidoc_error(api_client): | ||||
self.assertEqual(rv.status_code, code) | for exc, code in exception_http_code.items(): | ||||
rv = api_client.get('/api/1/test/error/%s/' % exc.__name__) | |||||
assert rv.status_code == code, rv.data | |||||
@staticmethod | |||||
@api_route(r'/some/full/(?P<myarg>[0-9]+)/(?P<myotherarg>[0-9]+)/', | @api_route(r'/some/full/(?P<myarg>[0-9]+)/(?P<myotherarg>[0-9]+)/', | ||||
'some-complete-doc-route') | 'some-complete-doc-route') | ||||
@api_doc('/some/complete/doc/route/') | @api_doc('/some/complete/doc/route/') | ||||
def apidoc_full_stack(request, myarg, myotherarg, akw=0): | def apidoc_full_stack(request, myarg, myotherarg, akw=0): | ||||
""" | """ | ||||
Sample doc | Sample doc | ||||
""" | """ | ||||
return {'result': int(myarg) + int(myotherarg) + akw} | return {'result': int(myarg) + int(myotherarg) + akw} | ||||
def test_apidoc_full_stack_doc(self): | |||||
# when | |||||
rv = self.client.get('/api/1/some/complete/doc/route/') | |||||
# then | |||||
self.assertEqual(rv.status_code, 200, rv.data) | |||||
self.assertTemplateUsed('api/apidoc.html') | |||||
def test_apidoc_full_stack_fn(self): | |||||
# when | |||||
rv = self.client.get('/api/1/some/full/1/1/') | |||||
# then | # remove deprecation warnings related to docutils | ||||
self.assertEqual(rv.status_code, 200, rv.data) | @pytest.mark.filterwarnings( | ||||
'ignore:.*U.*mode is deprecated:DeprecationWarning') | |||||
def test_apidoc_full_stack_doc(client): | |||||
rv = client.get('/api/1/some/complete/doc/route/', HTTP_ACCEPT='text/html') | |||||
assert rv.status_code == 200, rv.content | |||||
assert_template_used(rv, 'api/apidoc.html') | |||||
def test_api_doc_parse_httpdomain(self): | |||||
def test_apidoc_full_stack_fn(api_client): | |||||
rv = api_client.get('/api/1/some/full/1/1/') | |||||
assert rv.status_code == 200, rv.data | |||||
def test_api_doc_parse_httpdomain(): | |||||
doc_data = { | doc_data = { | ||||
'description': '', | 'description': '', | ||||
'urls': [], | 'urls': [], | ||||
'args': [], | 'args': [], | ||||
'params': [], | 'params': [], | ||||
'resheaders': [], | 'resheaders': [], | ||||
'reqheaders': [], | 'reqheaders': [], | ||||
'return_type': '', | 'return_type': '', | ||||
'returns': [], | 'returns': [], | ||||
'status_codes': [], | 'status_codes': [], | ||||
'examples': [] | 'examples': [] | ||||
} | } | ||||
_parse_httpdomain_doc(httpdomain_doc, doc_data) | _parse_httpdomain_doc(httpdomain_doc, doc_data) | ||||
expected_urls = [{ | expected_urls = [{ | ||||
'rule': '/api/1/revision/ **\\(sha1_git\\)** /', | 'rule': '/api/1/revision/ **\\(sha1_git\\)** /', | ||||
'methods': ['GET', 'HEAD', 'OPTIONS'] | 'methods': ['GET', 'HEAD'] | ||||
}] | }] | ||||
self.assertIn('urls', doc_data) | assert 'urls' in doc_data | ||||
self.assertEqual(doc_data['urls'], expected_urls) | assert doc_data['urls'] == expected_urls | ||||
expected_description = ('Get information about a revision in the archive. ' | |||||
'Revisions are identified by **sha1** checksums, ' | |||||
'compatible with Git commit identifiers. See ' | |||||
'**swh.model.identifiers.revision_identifier** in ' | |||||
'our data model module for details about how they ' | |||||
'are computed.') | |||||
expected_description = 'Get information about a revision in the archive. \ | assert 'description' in doc_data | ||||
Revisions are identified by **sha1** checksums, compatible with Git commit \ | assert doc_data['description'] == expected_description | ||||
identifiers. See **swh.model.identifiers.revision_identifier** in our data \ | |||||
model module for details about how they are computed.' | |||||
self.assertIn('description', doc_data) | |||||
self.assertEqual(doc_data['description'], expected_description) | |||||
expected_args = [{ | expected_args = [{ | ||||
'name': 'sha1_git', | 'name': 'sha1_git', | ||||
'type': 'string', | 'type': 'string', | ||||
'doc': 'hexadecimal representation of the revision **sha1_git** identifier' | 'doc': ('hexadecimal representation of the revision ' | ||||
'**sha1_git** identifier') | |||||
}] | }] | ||||
self.assertIn('args', doc_data) | assert 'args' in doc_data | ||||
self.assertEqual(doc_data['args'], expected_args) | assert doc_data['args'] == expected_args | ||||
expected_params = [] | expected_params = [] | ||||
self.assertIn('params', doc_data) | assert 'params' in doc_data | ||||
self.assertEqual(doc_data['params'], expected_params) | assert doc_data['params'] == expected_params | ||||
expected_reqheaders = [{ | expected_reqheaders = [{ | ||||
'doc': 'the requested response content type, either ``application/json`` or ``application/yaml``', | 'doc': ('the requested response content type, either ' | ||||
'``application/json`` or ``application/yaml``'), | |||||
'name': 'Accept' | 'name': 'Accept' | ||||
}] | }] | ||||
self.assertIn('reqheaders', doc_data) | assert 'reqheaders' in doc_data | ||||
self.assertEqual(doc_data['reqheaders'], expected_reqheaders) | assert doc_data['reqheaders'] == expected_reqheaders | ||||
expected_resheaders = [{ | expected_resheaders = [{ | ||||
'doc': 'this depends on **Accept** header of request', | 'doc': 'this depends on **Accept** header of request', | ||||
'name': 'Content-Type' | 'name': 'Content-Type' | ||||
}] | }] | ||||
self.assertIn('resheaders', doc_data) | assert 'resheaders' in doc_data | ||||
self.assertEqual(doc_data['resheaders'], expected_resheaders) | assert doc_data['resheaders'] == expected_resheaders | ||||
expected_statuscodes = [ | expected_statuscodes = [ | ||||
{ | { | ||||
'code': '200', | 'code': '200', | ||||
'doc': 'no error' | 'doc': 'no error' | ||||
}, | }, | ||||
{ | { | ||||
'code': '400', | 'code': '400', | ||||
'doc': 'an invalid **sha1_git** value has been provided' | 'doc': 'an invalid **sha1_git** value has been provided' | ||||
}, | }, | ||||
{ | { | ||||
'code': '404', | 'code': '404', | ||||
'doc': 'requested revision can not be found in the archive' | 'doc': 'requested revision can not be found in the archive' | ||||
} | } | ||||
] | ] | ||||
self.assertIn('status_codes', doc_data) | assert 'status_codes' in doc_data | ||||
self.assertEqual(doc_data['status_codes'], expected_statuscodes) | assert doc_data['status_codes'] == expected_statuscodes | ||||
expected_return_type = 'object' | expected_return_type = 'object' | ||||
self.assertIn('return_type', doc_data) | assert 'return_type' in doc_data | ||||
self.assertEqual(doc_data['return_type'], expected_return_type) | assert doc_data['return_type'] in expected_return_type | ||||
expected_returns = [ | expected_returns = [ | ||||
{ | { | ||||
'name': 'author', | 'name': 'author', | ||||
'type': 'object', | 'type': 'object', | ||||
'doc': 'information about the author of the revision' | 'doc': 'information about the author of the revision' | ||||
}, | }, | ||||
{ | { | ||||
'name': 'committer', | 'name': 'committer', | ||||
'type': 'object', | 'type': 'object', | ||||
'doc': 'information about the committer of the revision' | 'doc': 'information about the committer of the revision' | ||||
}, | }, | ||||
{ | { | ||||
'name': 'committer_date', | 'name': 'committer_date', | ||||
'type': 'string', | 'type': 'string', | ||||
'doc': 'ISO representation of the commit date (in UTC)' | 'doc': 'ISO representation of the commit date (in UTC)' | ||||
}, | }, | ||||
{ | { | ||||
'name': 'date', | 'name': 'date', | ||||
'type': 'string', | 'type': 'string', | ||||
'doc': 'ISO representation of the revision date (in UTC)' | 'doc': 'ISO representation of the revision date (in UTC)' | ||||
}, | }, | ||||
{ | { | ||||
'name': 'directory', | 'name': 'directory', | ||||
'type': 'string', | 'type': 'string', | ||||
'doc': 'the unique identifier that revision points to' | 'doc': 'the unique identifier that revision points to' | ||||
}, | }, | ||||
{ | { | ||||
'name': 'directory_url', | 'name': 'directory_url', | ||||
'type': 'string', | 'type': 'string', | ||||
'doc': 'link to `</api/1/directory/>`_ to get information about the directory associated to the revision' | 'doc': ('link to `</api/1/directory/>`_ to get information about ' | ||||
'the directory associated to the revision') | |||||
}, | }, | ||||
{ | { | ||||
'name': 'id', | 'name': 'id', | ||||
'type': 'string', | 'type': 'string', | ||||
'doc': 'the revision unique identifier' | 'doc': 'the revision unique identifier' | ||||
}, | }, | ||||
{ | { | ||||
'name': 'merge', | 'name': 'merge', | ||||
'type': 'boolean', | 'type': 'boolean', | ||||
'doc': 'whether or not the revision corresponds to a merge commit' | 'doc': 'whether or not the revision corresponds to a merge commit' | ||||
}, | }, | ||||
{ | { | ||||
'name': 'message', | 'name': 'message', | ||||
'type': 'string', | 'type': 'string', | ||||
'doc': 'the message associated to the revision' | 'doc': 'the message associated to the revision' | ||||
}, | }, | ||||
{ | { | ||||
'name': 'parents', | 'name': 'parents', | ||||
'type': 'array', | 'type': 'array', | ||||
'doc': 'the parents of the revision, i.e. the previous revisions that head directly to it, each entry of that array contains an unique parent revision identifier but also a link to `</api/1/revision/>`_ to get more information about it' | 'doc': ('the parents of the revision, i.e. the previous revisions ' | ||||
'that head directly to it, each entry of that array ' | |||||
'contains an unique parent revision identifier but also a ' | |||||
'link to `</api/1/revision/>`_ to get more information ' | |||||
'about it') | |||||
}, | }, | ||||
{ | { | ||||
'name': 'type', | 'name': 'type', | ||||
'type': 'string', | 'type': 'string', | ||||
'doc': 'the type of the revision' | 'doc': 'the type of the revision' | ||||
} | } | ||||
] | ] | ||||
self.assertIn('returns', doc_data) | assert 'returns' in doc_data | ||||
self.assertEqual(doc_data['returns'], expected_returns) | assert doc_data['returns'] == expected_returns | ||||
expected_examples = ['/api/1/revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/'] | expected_examples = [ | ||||
'/api/1/revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/' | |||||
] | |||||
self.assertIn('examples', doc_data) | assert 'examples' in doc_data | ||||
self.assertEqual(doc_data['examples'], expected_examples) | assert doc_data['examples'] == expected_examples |
why is this changed?