Skip to content

Commit ed53141

Browse files
author
Jon Wayne Parrott
committed
Refactoring 4-auth tests to use py.test
Also: * Fixing bug in model_cloudsql.from_sql that caused None to be cast to literal string 'None'. * Fixing bug where flaky_filter wasn't working as expected.
1 parent e97d8ff commit ed53141

File tree

20 files changed

+299
-217
lines changed

20 files changed

+299
-217
lines changed

2-structured-data/bookshelf/model_cloudsql.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from flask import Flask
1616
from flask.ext.sqlalchemy import SQLAlchemy
17-
import six
1817

1918

2019
builtin_list = list
@@ -29,11 +28,10 @@ def init_app(app):
2928

3029
def from_sql(row):
3130
"""Translates a SQLAlchemy model instance into a dictionary"""
32-
d = {}
33-
for column in row.__table__.columns:
34-
d[column.name] = six.text_type(getattr(row, column.name))
35-
36-
return d
31+
data = row.__dict__.copy()
32+
data['id'] = row.id
33+
data.pop('_sa_instance_state')
34+
return data
3735

3836

3937
# [START model]

2-structured-data/tests/test_crud.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ def tracking_create(*args, **kwargs):
7575
list(map(model.delete, ids_to_delete))
7676

7777

78-
def flaky_filter(e, *args):
78+
def flaky_filter(info, *args):
7979
"""Used by flaky to determine when to re-run a test case."""
80+
_, e, _ = info
8081
return isinstance(e, ServiceUnavailable)
8182

8283

3-binary-data/bookshelf/model_cloudsql.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from flask import Flask
1616
from flask.ext.sqlalchemy import SQLAlchemy
17-
import six
1817

1918

2019
builtin_list = list
@@ -29,11 +28,10 @@ def init_app(app):
2928

3029
def from_sql(row):
3130
"""Translates a SQLAlchemy model instance into a dictionary"""
32-
d = {}
33-
for column in row.__table__.columns:
34-
d[column.name] = six.text_type(getattr(row, column.name))
35-
36-
return d
31+
data = row.__dict__.copy()
32+
data['id'] = row.id
33+
data.pop('_sa_instance_state')
34+
return data
3735

3836

3937
class Book(db.Model):

3-binary-data/requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
Flask==0.10.1
2-
gcloud==0.9.0
2+
gcloud==0.10.1
33
gunicorn==19.4.5
4-
oauth2client==1.5.2
4+
oauth2client==2.0.0.post1
55
Flask-SQLAlchemy==2.1
6-
PyMySQL==0.7.1
6+
PyMySQL==0.7.2
77
Flask-PyMongo==0.4.0
88
PyMongo==3.2.1
99
six==1.10.0

3-binary-data/tests/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def tracking_create(*args, **kwargs):
7676
list(map(model.delete, ids_to_delete))
7777

7878

79-
def flaky_filter(e, *args):
79+
def flaky_filter(info, *args):
8080
"""Used by flaky to determine when to re-run a test case."""
81+
_, e, _ = info
8182
return isinstance(e, ServiceUnavailable)

3-binary-data/tests/test_storage.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
import re
1616

17-
import httplib2
1817
from conftest import flaky_filter
1918
from flaky import flaky
20-
from six import BytesIO
19+
import httplib2
2120
import pytest
21+
from six import BytesIO
2222

2323

2424
# Mark all test cases in this class as flaky, so that if errors occur they
@@ -64,7 +64,7 @@ def test_upload_bad_file(self, app):
6464
}
6565

6666
with app.test_client() as c:
67-
rv = c.post('/books/add', data=data,
68-
follow_redirects=True)
67+
rv = c.post('/books/add', data=data, follow_redirects=True)
68+
6969
# check we weren't pwned
7070
assert rv.status == '400 BAD REQUEST'

4-auth/bookshelf/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import httplib2
2020

2121
# [START include]
22-
from oauth2client.flask_util import UserOAuth2
22+
from oauth2client.contrib.flask_util import UserOAuth2
2323

2424
oauth2 = UserOAuth2()
2525
# [END include]

4-auth/bookshelf/model_cloudsql.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from flask import Flask
1616
from flask.ext.sqlalchemy import SQLAlchemy
17-
import six
1817

1918

2019
builtin_list = list
@@ -29,11 +28,10 @@ def init_app(app):
2928

3029
def from_sql(row):
3130
"""Translates a SQLAlchemy model instance into a dictionary"""
32-
d = {}
33-
for column in row.__table__.columns:
34-
d[column.name] = six.text_type(getattr(row, column.name))
35-
36-
return d
31+
data = row.__dict__.copy()
32+
data['id'] = row.id
33+
data.pop('_sa_instance_state')
34+
return data
3735

3836

3937
class Book(db.Model):
@@ -85,7 +83,9 @@ def read(id):
8583

8684

8785
def create(data):
86+
print(data)
8887
book = Book(**data)
88+
print(book)
8989
db.session.add(book)
9090
db.session.commit()
9191
return from_sql(book)

4-auth/bookshelf/templates/view.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ <h3>Book</h3>
4040
{% endif %}
4141
</div>
4242
<div class="media-body">
43+
{{book|pprint}}
4344
<h4 class="book-title">
4445
{{book.title}}
4546
<small>{{book.publishedDate}}</small>
4647
</h4>
4748
<h5 class="book-author">By {{book.author|default('Unknown', True)}}</h5>
4849
<p class="book-description">{{book.description}}</p>
49-
<small class="book-added-by">Added by {{book.createdBy|default('Anonymous', True)}}</small>
50+
<small class="book-added-by">Added by {{book.get('createdBy')|default('Anonymous', True)}}</small>
5051
</div>
5152
</div>
5253

4-auth/requirements-dev.txt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
tox==2.3.1
2-
unittest2==1.1.0
3-
nose==1.3.7
42
flake8==2.5.4
5-
coverage==4.1b2
6-
BeautifulSoup4==4.4.1
7-
mock==1.3.0
8-
flaky==3.0.3
3+
flaky==3.1.0
4+
pytest==2.8.7
5+
pytest-cov==2.2.1

4-auth/requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
Flask==0.10.1
2-
gcloud==0.9.0
2+
gcloud==0.10.1
33
gunicorn==19.4.5
4-
oauth2client==1.5.2
4+
oauth2client==2.0.0.post1
55
Flask-SQLAlchemy==2.1
6-
PyMySQL==0.7.1
6+
PyMySQL==0.7.2
77
Flask-PyMongo==0.4.0
88
PyMongo==3.2.1
99
six==1.10.0

4-auth/tests/conftest.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright 2015 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""conftest.py is used to define common test fixtures for pytest."""
16+
17+
import bookshelf
18+
import config
19+
from gcloud.exceptions import ServiceUnavailable
20+
import pytest
21+
22+
23+
@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb'])
24+
def app(request):
25+
"""This fixtures provides a Flask app instance configured for testing.
26+
27+
Because it's parametric, it will cause every test that uses this fixture
28+
to run three times: one time for each backend (datastore, cloudsql, and
29+
mongodb).
30+
31+
It also ensures the tests run within a request context, allowing
32+
any calls to flask.request, flask.current_app, etc. to work."""
33+
app = bookshelf.create_app(
34+
config,
35+
testing=True,
36+
config_overrides={
37+
'DATA_BACKEND': request.param
38+
})
39+
40+
with app.test_request_context():
41+
yield app
42+
43+
44+
@pytest.yield_fixture
45+
def model(monkeypatch, app):
46+
"""This fixture provides a modified version of the app's model that tracks
47+
all created items and deletes them at the end of the test.
48+
49+
Any tests that directly or indirectly interact with the database should use
50+
this to ensure that resources are properly cleaned up.
51+
52+
Monkeypatch is provided by pytest and used to patch the model's create
53+
method.
54+
55+
The app fixture is needed to provide the configuration and context needed
56+
to get the proper model object.
57+
"""
58+
model = bookshelf.get_model()
59+
60+
ids_to_delete = []
61+
62+
# Monkey-patch create so we can track the IDs of every item
63+
# created and delete them after the test case.
64+
original_create = model.create
65+
66+
def tracking_create(*args, **kwargs):
67+
res = original_create(*args, **kwargs)
68+
ids_to_delete.append(res['id'])
69+
return res
70+
71+
monkeypatch.setattr(model, 'create', tracking_create)
72+
73+
yield model
74+
75+
# Delete all items that we created during tests.
76+
list(map(model.delete, ids_to_delete))
77+
78+
79+
def flaky_filter(info, *args):
80+
"""Used by flaky to determine when to re-run a test case."""
81+
_, e, _ = info
82+
return isinstance(e, ServiceUnavailable)

4-auth/tests/test_auth.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Copyright 2015 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import contextlib
16+
17+
from conftest import flaky_filter
18+
from flaky import flaky
19+
from oauth2client.client import OAuth2Credentials
20+
import pytest
21+
22+
23+
@pytest.fixture
24+
def client_with_credentials(app):
25+
"""This fixture provides a Flask app test client that has a session
26+
pre-configured with use credentials."""
27+
credentials = OAuth2Credentials(
28+
'access_token',
29+
'client_id',
30+
'client_secret',
31+
'refresh_token',
32+
'3600',
33+
None,
34+
'Test',
35+
id_token={'sub': '123', 'email': 'user@example.com'},
36+
scopes=('email', 'profile'))
37+
38+
@contextlib.contextmanager
39+
def inner():
40+
with app.test_client() as client:
41+
with client.session_transaction() as session:
42+
session['profile'] = {'id': 'abc', 'displayName': 'Test User'}
43+
session['google_oauth2_credentials'] = credentials.to_json()
44+
yield client
45+
46+
return inner
47+
48+
49+
# Mark all test cases in this class as flaky, so that if errors occur they
50+
# can be retried. This is useful when databases are temporarily unavailable.
51+
@flaky(rerun_filter=flaky_filter)
52+
# Tell pytest to use both the app and model fixtures for all test cases.
53+
# This ensures that configuration is properly applied and that all database
54+
# resources created during tests are cleaned up. These fixtures are defined
55+
# in conftest.py
56+
@pytest.mark.usefixtures('app', 'model')
57+
class TestAuth(object):
58+
59+
def test_not_logged_in(self, app):
60+
with app.test_client() as c:
61+
rv = c.get('/books/')
62+
63+
assert rv.status == '200 OK'
64+
body = rv.data.decode('utf-8')
65+
assert 'Login' in body
66+
67+
def test_logged_in(self, client_with_credentials):
68+
with client_with_credentials() as c:
69+
rv = c.get('/books/')
70+
71+
assert rv.status == '200 OK'
72+
body = rv.data.decode('utf-8')
73+
assert 'Test User' in body
74+
75+
def test_add_anonymous(self, app):
76+
data = {
77+
'title': 'Test Book',
78+
}
79+
80+
with app.test_client() as c:
81+
rv = c.post('/books/add', data=data, follow_redirects=True)
82+
83+
assert rv.status == '200 OK'
84+
body = rv.data.decode('utf-8')
85+
assert 'Test Book' in body
86+
assert 'Added by Anonymous' in body
87+
88+
def test_add_logged_in(self, client_with_credentials):
89+
data = {
90+
'title': 'Test Book',
91+
}
92+
93+
with client_with_credentials() as c:
94+
rv = c.post('/books/add', data=data, follow_redirects=True)
95+
96+
assert rv.status == '200 OK'
97+
body = rv.data.decode('utf-8')
98+
assert 'Test Book' in body
99+
assert 'Added by Test User' in body
100+
101+
def test_mine(self, model, client_with_credentials):
102+
# Create two books, one created by the logged in user and one
103+
# created by another user.
104+
model.create({
105+
'title': 'Book 1',
106+
'createdById': 'abc'
107+
})
108+
109+
model.create({
110+
'title': 'Book 2',
111+
'createdById': 'def'
112+
})
113+
114+
# Check the "My Books" page and make sure only one of the books
115+
# appears.
116+
with client_with_credentials() as c:
117+
rv = c.get('/books/mine')
118+
119+
assert rv.status == '200 OK'
120+
body = rv.data.decode('utf-8')
121+
assert 'Book 1' in body
122+
assert 'Book 2' not in body

0 commit comments

Comments
 (0)