From b5e15fc741e83a383ff6f02b8146f8ec74d8dcce Mon Sep 17 00:00:00 2001 From: Vaz Allen Date: Tue, 6 Oct 2015 23:31:57 -0700 Subject: [PATCH 1/9] Use /api in the RAML baseUri, as used in local.ini --- example.raml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example.raml b/example.raml index 6550b63..bf53634 100644 --- a/example.raml +++ b/example.raml @@ -5,7 +5,7 @@ documentation: - title: Home content: | Welcome to the example API. -baseUri: http://{host}:{port}/{version} +baseUri: http://{host}:{port}/api version: v1 mediaType: application/json protocols: [HTTP, HTTPS] From c7c42da6e1ca389680fb4c7564ba36a9fff23f59 Mon Sep 17 00:00:00 2001 From: Vaz Allen Date: Tue, 6 Oct 2015 23:33:57 -0700 Subject: [PATCH 2/9] Update RAML with example values, add Ra functional tests --- .gitignore | 1 + example.raml | 179 ++++++++++++++++++++++++++++++++++++++++++++- local.ini.template | 5 ++ requirements.dev | 1 + requirements.txt | 1 + schemas/user.json | 4 +- tests/test_api.py | 145 ++++++++++++++++++++++++++++++++++++ tox.ini | 22 ++++++ 8 files changed, 352 insertions(+), 6 deletions(-) create mode 100644 tests/test_api.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 62d7a88..2e47d73 100644 --- a/.gitignore +++ b/.gitignore @@ -57,5 +57,6 @@ docs/_build/ target/ local.ini +test.ini mock/ mock.sh diff --git a/example.raml b/example.raml index bf53634..4754480 100644 --- a/example.raml +++ b/example.raml @@ -56,8 +56,33 @@ securedBy: [x_ticket_auth] body: application/json: schema: !include schemas/user.json + example: | + { + "username": "rick", + "email": "RICK@example.com", + "password": "megatrees", + "first_name": "Rick" + } + responses: + 201: + description: Created user + body: + application/json: + schema: !include schemas/user.json + headers: + Location: + description: The URL where the created user is available + type: string + pattern: "http.*" + example: http://localhost:6543/api/users/rick + patch: description: Update multiple users + body: + application/json: + example: | + { "last_name": "Sanchez" } + head: description: Determine whether a given resource is available options: @@ -65,11 +90,59 @@ securedBy: [x_ticket_auth] /{username}: displayName: One user + + uriParameters: + username: + type: string + maxLength: 50 + example: rick + get: description: Get a particular user - patch: + responses: + 200: + body: + application/json: + schema: !include schemas/user.json + put: + description: Replace a particular user + body: + application/json: + example: | + { + "username": "morty", + "email": "morty@example.com", + "first_name": "Mortimer", + "last_name": "Smith", + "password": "$2a$10$RrAZgBWzCXaBR.uM83AOg.YzYfnhxujau7JuQ2enP1ota3lgyt/9S", + "status": "active", + "profile": null, + "groups": ["user"], + "settings": {}, + "stories": [], + "assigned_stories": [], + "last_login": null, + "created_at": "2015-09-11T02:13:29Z", + "updated_at": "2015-09-11T03:48:55Z" + } + responses: + 200: + body: + application/json: + schema: !include schemas/user.json + + patch: description: Update a particular user + body: + application/json: + example: { "username": "rickC137" } + responses: + 200: + body: + application/json: + schema: !include schemas/user.json + delete: description: Delete a particular user @@ -79,6 +152,9 @@ securedBy: [x_ticket_auth] description: Get all settings of a particular user post: description: Change a user's settings + body: + application/json: + example: { "language": "en" } /groups: displayName: User groups @@ -86,6 +162,9 @@ securedBy: [x_ticket_auth] description: Get all groups of a particular user post: description: Change a user's groups + body: + application/json: + example: { "admin": null } /profile: displayName: User profile @@ -96,21 +175,56 @@ securedBy: [x_ticket_auth] body: application/json: schema: !include schemas/profile.json + example: { "address": "123 Fake St" } + responses: + 200: + body: + application/json: + schema: !include schemas/profile.json patch: description: Update a user's profile + body: + application/json: + example: { "address": "124 Pretend Rd" } + responses: + 200: + body: + application/json: + schema: !include schemas/profile.json /stories: securedBy: [item_owner_acl] displayName: All stories + get: description: Get all stories + post: description: Create a new story body: application/json: schema: !include schemas/story.json + example: | + { + "owner_id": "rick", + "due_date": "2020-11-11T11:11:11Z", + "name": "do science", + "description": "real sciency stuff" + } + responses: + 201: + description: Created story + body: + application/json: + + schema: !include schemas/story.json + patch: description: Update multiple stories + body: + application/json: + example: { "assignee_id": "rick" } + delete: description: Delete multiple stories head: @@ -120,13 +234,70 @@ securedBy: [x_ticket_auth] /{id}: displayName: One story + + uriParameters: + id: + description: story ID + type: integer + minimum: 1 + example: 1 + get: description: Get a particular story + responses: + 200: + body: + application/json: + schema: !include schemas/story.json + + put: + description: Replace a particular story + body: + application/json: + example: | + { + "owner_id": "rick", + "due_date": "2020-11-11T11:11:11Z", + "name": "watch TV", + "description": "not very sciency", + "assignee_id": "rick", + "arbitrary_object": null, + "attachment": null, + "available_for": null, + "completed": false, + "created_at": "2015-09-11T05:01:27Z", + "id": 516, + "price": null, + "progress": 0.0, + "rating": null, + "reads": 0, + "signs_number": null, + "start_date": null, + "unicode_description": null, + "unicode_name": null, + "updated_at": null, + "valid_date": null, + "valid_time": null + } + responses: + 200: + body: + application/json: + schemas: !include schemas/story.json + + patch: + description: Update a story + body: + application/json: + example: { "completed": true } + responses: + 200: + body: + application/json: + schemas: !include schemas/story.json + delete: description: Delete a particular story - patch: - put: - description: Update a particular story head: description: Determine whether a given resource is available options: diff --git a/local.ini.template b/local.ini.template index 9fc2ac0..fd5f13f 100644 --- a/local.ini.template +++ b/local.ini.template @@ -15,6 +15,11 @@ system.user = system system.password = 123456 system.email = user@domain.com +# Database configuration: +# +# Note on testing: copy this template to test.ini and use a different database +# and elasticsearch index, as they will be wiped on each test run. + # SQLA sqlalchemy.url = postgresql://localhost:5432/ramses_example diff --git a/requirements.dev b/requirements.dev index 792751b..8240f88 100644 --- a/requirements.dev +++ b/requirements.dev @@ -6,4 +6,5 @@ waitress==0.8.9 -e ../nefertari-mongodb -e ../nefertari-sqla -e ../ramses +-e ../ra -e . diff --git a/requirements.txt b/requirements.txt index cd89d47..437a755 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,6 @@ nefertari==0.5.1 nefertari-mongodb==0.3.3 nefertari-sqla==0.3.3 ramses==0.4.1 +ra -e . diff --git a/schemas/user.json b/schemas/user.json index 7f7b972..a8b994f 100644 --- a/schemas/user.json +++ b/schemas/user.json @@ -26,7 +26,7 @@ } }, "profile": { - "type": ["integer", "string"], + "type": ["integer", "string", "null"], "_db_settings": { "type": "relationship", "document": "Profile", @@ -160,4 +160,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..77b326e --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,145 @@ +import os +import ra +import pytest +import webtest +import ramses + + +appdir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +ramlfile = os.path.join(appdir, 'example.raml') + +if not os.path.exists(os.path.join(appdir, 'test.ini')): + raise Exception("Could not find test.ini in root of project: " + "Create a test.ini configured with testing DB and ES index") + +testapp = webtest.TestApp('config:test.ini', relative_to=appdir) + +# ra entry point: instantiate the API test suite +api = ra.api(ramlfile, testapp) + +User = ramses.models.get_existing_model('User') +Story = ramses.models.get_existing_model('Story') +Profile = ramses.models.get_existing_model('Profile') + + +@api.hooks.before_each +def delete_resources(): + Profile._delete_many(Profile.get_collection()) + Story._delete_many(Story.get_collection()) + User._delete_many(User.get_collection()) + import transaction + transaction.commit() + + +@api.hooks.before_each(exclude=['POST /users']) +def create_user(): + User(**api.examples.build('user')).save() + # XXX: it takes some time for the object to be propagated to ES. + # This is not ideal at all. + import time; time.sleep(2) + + +@api.hooks.before_each(only=['PATCH /users/{username}/profile']) +def create_profile(): + Profile(**api.examples.build('user.profile', user_id='rick')).save() + import time; time.sleep(2) + + +@api.hooks.before_each(only=['/stories*'], exclude=['POST']) +def create_story(): + story = Story(**api.examples.build('story', id=1)).save() + import time; time.sleep(2) + +@api.hooks.before_each +def commit(): + import transaction + transaction.commit() + + +# defining a resource scope: + +@api.resource('/users') +def users_resource(users): + + # scope-local pytest fixtures + # + # a resource scope acts just like a regular module scope + # with respect to pytest fixtures: + + @pytest.fixture + def two_hundred(): + return 200 + + # defining tests for methods in this resource: + + @users.get + def get(req, two_hundred): + # ``req`` is a callable request object that is pre-bound to the app + # that was passed into ``ra.api`` as well as the URI derived from + # the resource (test scope) and method (test) decorators. + # + # This example uses the other scope-local fixture defined above. + response = req() + assert response.status_code == two_hundred + assert 'rick' in response + + @users.post + def post_using_example(req): + # By default, when JSON data needs to be sent in the request body, + # Ra will look for an ``example`` property in the RAML definition + # of the resource method's body and use that. + # + # As in WebTest request methods, you can specify the expected + # status code(s), which will be test the response status. + response = req(status=201) + # assert lowercase validator in schema is working: + assert response.json['email'] == 'rick@example.com' + + # defining a custom user factory; underscored functions are not + # considered tests (but better to import factories from another module) + def _user_factory(): + import string + import random + name = ''.join(random.choice(string.ascii_lowercase) for _ in range(10)) + email = "{}@example.com".format(name) + return dict(username=name, email=email, password=name) + + # using the factory: + + @users.post(factory=_user_factory) + def post_using_factory(req): + response = req() + username = req.data['username'] + assert username in response + + # defining a sub-resource: + + @users.resource('/{username}') + def user_resource(user): + + # this resource will be requested at /users/{username} + # + # By default, Ra will look at the ``example`` property for + # URI parameters as defined in the RAML, and fill the URI + # template with that. In this case it will use 'rick', which + # we created in the before-hook. + + @user.get + def get(req): + # This is equivalent to the default test for a resource + # and method: + req() + + +@api.resource('/stories') +def stories_resource(stories): + + @stories.resource('/{id}') + def story_resource(story): + + @story.get + def get(req): + response = req() + assert 'do science' in response + +api.autotest() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..3bfaec7 --- /dev/null +++ b/tox.ini @@ -0,0 +1,22 @@ +[tox] +envlist = + py27, + py33,py34, + +[testenv] +setenv = + PYTHONHASHSEED=0 +deps = -rrequirements.dev +commands = py.test + +[testenv:flake8] +deps = + flake8==2.3.0 + pep8==1.6.2 +commands = + flake8 ra + +[pytest] +addopts = -x -v +norecursedirs = env .tox +testpaths = tests From 91b35d5c4a741bf368b870cc70c7fc93cdea9d43 Mon Sep 17 00:00:00 2001 From: Jonathan Stoikovitch Date: Thu, 8 Oct 2015 22:26:21 -0400 Subject: [PATCH 3/9] _validators -> _processors --- schemas/user.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/schemas/user.json b/schemas/user.json index a8b994f..8fd7256 100644 --- a/schemas/user.json +++ b/schemas/user.json @@ -80,7 +80,7 @@ "unique": true, "primary_key": true }, - "_validators": ["lowercase"] + "_processors": ["lowercase"] }, "email": { "type": "string", @@ -90,7 +90,7 @@ "required": true, "unique": true }, - "_validators": ["lowercase"] + "_processors": ["lowercase"] }, "password": { "type": "string", @@ -100,7 +100,7 @@ "required": true, "min_length": 3 }, - "_validators": ["encrypt"] + "_processors": ["encrypt"] }, "first_name": { "type": ["string", "null"], From 21b64dff046bef75aded2919aa0712c201c6e75a Mon Sep 17 00:00:00 2001 From: Vaz Allen Date: Sun, 22 Nov 2015 22:42:25 -0800 Subject: [PATCH 4/9] comment in RAML about example for testing lowercase processor --- example.raml | 1 + 1 file changed, 1 insertion(+) diff --git a/example.raml b/example.raml index 4754480..92891e9 100644 --- a/example.raml +++ b/example.raml @@ -56,6 +56,7 @@ securedBy: [x_ticket_auth] body: application/json: schema: !include schemas/user.json + # uppercase used in email to test use of lowercase processor example: | { "username": "rick", From a6554bd218559a942246ac93814da26d27910002 Mon Sep 17 00:00:00 2001 From: Vaz Allen Date: Tue, 1 Dec 2015 17:23:33 -0800 Subject: [PATCH 5/9] Remove hooks, use filters; simplified ra.api() call --- tests/test_api.py | 81 +++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 77b326e..da5902f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,57 +1,64 @@ -import os import ra import pytest -import webtest -import ramses -appdir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -ramlfile = os.path.join(appdir, 'example.raml') - -if not os.path.exists(os.path.join(appdir, 'test.ini')): - raise Exception("Could not find test.ini in root of project: " - "Create a test.ini configured with testing DB and ES index") +# ra entry point: instantiate the API test suite +api = ra.api('example.raml') -testapp = webtest.TestApp('config:test.ini', relative_to=appdir) -# ra entry point: instantiate the API test suite -api = ra.api(ramlfile, testapp) +@pytest.fixture(scope='session') +def models(): + import ramses.models + return dict( + User=ramses.models.get_existing_model('User'), + Profile=ramses.models.get_existing_model('Profile'), + Story=ramses.models.get_existing_model('Story')) -User = ramses.models.get_existing_model('User') -Story = ramses.models.get_existing_model('Story') -Profile = ramses.models.get_existing_model('Profile') +# perform any necessary test setup +@pytest.fixture(autouse=True) +def setup(req, examples, models): + User = models['User'] + Story = models['Story'] + Profile = models['Profile'] -@api.hooks.before_each -def delete_resources(): - Profile._delete_many(Profile.get_collection()) - Story._delete_many(Story.get_collection()) - User._delete_many(User.get_collection()) import transaction - transaction.commit() + import time + + def delete_data(): + Profile._delete_many(Profile.get_collection()) + Story._delete_many(Story.get_collection()) + User._delete_many(User.get_collection()) + transaction.commit() + + def create_user(): + example = examples.build('user') + user = User(**example).save() + # XXX: it takes some time for the object to be propagated to ES. + # This is not ideal at all. + time.sleep(2) + return user + def create_profile(user_id): + example = examples.build('user.profile', user_id=user_id) + Profile(**example).save() + time.sleep(2) -@api.hooks.before_each(exclude=['POST /users']) -def create_user(): - User(**api.examples.build('user')).save() - # XXX: it takes some time for the object to be propagated to ES. - # This is not ideal at all. - import time; time.sleep(2) + def create_story(): + example = examples.build('story', id=1) + Story(**example).save() + delete_data() -@api.hooks.before_each(only=['PATCH /users/{username}/profile']) -def create_profile(): - Profile(**api.examples.build('user.profile', user_id='rick')).save() - import time; time.sleep(2) + if req.match(exclude='POST /users'): + user = create_user() + if req.match('PATCH /users/{username}/profile'): + create_profile(user.username) -@api.hooks.before_each(only=['/stories*'], exclude=['POST']) -def create_story(): - story = Story(**api.examples.build('story', id=1)).save() - import time; time.sleep(2) + if req.match('/stories*', exclude='POST'): + create_story() -@api.hooks.before_each -def commit(): import transaction transaction.commit() From 686191949dd7ec32d9f3f6e6c1807240eefdb56f Mon Sep 17 00:00:00 2001 From: Artem Kostiuk Date: Wed, 17 Feb 2016 15:16:43 +0200 Subject: [PATCH 6/9] Few style fixes --- requirements.txt | 2 +- tests/test_api.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 995ef29..5219b7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ waitress==0.8.9 nefertari==0.6.0 nefertari-mongodb==0.4.0 nefertari-sqla==0.4.0 -ra==0.1.0 +ra ramses==0.5.0 -e . diff --git a/tests/test_api.py b/tests/test_api.py index 77b326e..46dab79 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,16 +1,17 @@ import os -import ra + import pytest import webtest import ramses - +import ra appdir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) ramlfile = os.path.join(appdir, 'example.raml') if not os.path.exists(os.path.join(appdir, 'test.ini')): - raise Exception("Could not find test.ini in root of project: " - "Create a test.ini configured with testing DB and ES index") + raise Exception( + "Could not find test.ini in root of project: " + "Create a test.ini configured with testing DB and ES index") testapp = webtest.TestApp('config:test.ini', relative_to=appdir) @@ -47,9 +48,10 @@ def create_profile(): @api.hooks.before_each(only=['/stories*'], exclude=['POST']) def create_story(): - story = Story(**api.examples.build('story', id=1)).save() + Story(**api.examples.build('story', id=1)).save() import time; time.sleep(2) + @api.hooks.before_each def commit(): import transaction @@ -100,7 +102,8 @@ def post_using_example(req): def _user_factory(): import string import random - name = ''.join(random.choice(string.ascii_lowercase) for _ in range(10)) + name = ''.join(random.choice(string.ascii_lowercase) + for _ in range(10)) email = "{}@example.com".format(name) return dict(username=name, email=email, password=name) From 166ee12e8b496a14a34d447a57f1753be283297d Mon Sep 17 00:00:00 2001 From: Artem Kostiuk Date: Fri, 19 Feb 2016 12:36:15 +0200 Subject: [PATCH 7/9] Drop ES after tests. Move story ID in raml --- example.raml | 1 + tests/test_api.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/example.raml b/example.raml index c1e2c19..a3e802c 100644 --- a/example.raml +++ b/example.raml @@ -217,6 +217,7 @@ securedBy: [x_ticket_auth] schema: !include schemas/story.json example: | { + "id": 1, "owner_id": "rick", "due_date": "2020-11-11T11:11:11Z", "name": "do science", diff --git a/tests/test_api.py b/tests/test_api.py index 31f59d3..d515ded 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -15,6 +15,14 @@ def models(): Story=ramses.models.get_existing_model('Story')) +@pytest.fixture(scope='module', autouse=True) +def drop_storages(request): + def _drop_es(): + from nefertari.elasticsearch import ES + ES.delete_index() + request.addfinalizer(_drop_es) + + # perform any necessary test setup @pytest.fixture(autouse=True) def setup(req, examples, models): @@ -45,7 +53,7 @@ def create_profile(user_id): time.sleep(2) def create_story(): - example = examples.build('story', id=1) + example = examples.build('story') Story(**example).save() delete_data() From ba8f2891c6b259ca89b88216abd5a7f7679a2ea1 Mon Sep 17 00:00:00 2001 From: Artem Kostiuk Date: Fri, 19 Feb 2016 13:10:38 +0200 Subject: [PATCH 8/9] Fix Profile creation in tests --- tests/test_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index d515ded..4f57d8f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -47,8 +47,8 @@ def create_user(): time.sleep(2) return user - def create_profile(user_id): - example = examples.build('user.profile', user_id=user_id) + def create_profile(user): + example = examples.build('user.profile', user=user) Profile(**example).save() time.sleep(2) @@ -62,7 +62,7 @@ def create_story(): user = create_user() if req.match('PATCH /users/{username}/profile'): - create_profile(user.username) + create_profile(user) if req.match('/stories*', exclude='POST'): create_story() From 9a4cbd59fbaf926e2ed4cdcf203a784badac4a72 Mon Sep 17 00:00:00 2001 From: Artem Kostiuk Date: Fri, 19 Feb 2016 14:01:32 +0200 Subject: [PATCH 9/9] Dont use ramses to get models --- tests/test_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 4f57d8f..0b93740 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -8,11 +8,11 @@ @pytest.fixture(scope='session') def models(): - import ramses.models + from nefertari import engine return dict( - User=ramses.models.get_existing_model('User'), - Profile=ramses.models.get_existing_model('Profile'), - Story=ramses.models.get_existing_model('Story')) + User=engine.get_document_cls('User'), + Profile=engine.get_document_cls('Profile'), + Story=engine.get_document_cls('Story')) @pytest.fixture(scope='module', autouse=True)