diff --git a/.gitsecrets b/.gitsecrets new file mode 100644 index 0000000..548e1b6 --- /dev/null +++ b/.gitsecrets @@ -0,0 +1 @@ +[0-9A-Za-z]{32} diff --git a/dev/bootstrap.sh b/dev/bootstrap.sh new file mode 100755 index 0000000..5ac0d73 --- /dev/null +++ b/dev/bootstrap.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Inspired by RJ Zaworski's blog post at +# https://rjzaworski.com/2018/01/keeping-git-hooks-in-sync + +set -e + +install_hooks() { + rm -rf ./.git/hooks + cp -r ./dev/hooks ./.git/hooks +} + +log() { + echo "$1" +} + +if [ ! -d ".git" ]; then { + log "Aborting because you are not at the root of a git repository" + exit 1 +} +fi + +log "Configuring git hooks" +install_hooks diff --git a/dev/hooks/pre-commit b/dev/hooks/pre-commit new file mode 100755 index 0000000..a17b960 --- /dev/null +++ b/dev/hooks/pre-commit @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# Pre-commit hook to run before each commit + +# Check that no secrets are about to be committed +git secrets scan diff --git a/requirements.txt b/requirements.txt index dbd23d3..7f0528a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,15 @@ alabaster==0.7.11 +atomicwrites==1.2.1 +attrs==18.2.0 Babel==2.6.0 blinker==1.4 certifi==2018.8.13 chardet==3.0.4 -click==6.7 +Click==7.0 colorama==0.3.9 +coverage==4.5.2 docutils==0.14 -Flask==0.12.2 +Flask==1.0.2 Flask-BabelEx==0.9.3 Flask-Login==0.4.1 Flask-Mail==0.9.1 @@ -22,13 +25,22 @@ itsdangerous==0.24 Jinja2==2.10 MarkupSafe==1.0 mccabe==0.6.1 +mock==2.0.0 +more-itertools==5.0.0 nose==1.3.7 packaging==17.1 passlib==1.7.1 +pbr==5.1.1 +pluggy==0.8.0 +py==1.7.0 Pygments==2.2.0 pyparsing==2.2.0 +pytest==4.1.0 +pytest-cov==2.6.1 +pytest-mock==1.10.0 python-dateutil==2.7.3 pytz==2018.3 +qpp-git-secrets==1.1.1 requests==2.20.1 six==1.11.0 snowballstemmer==1.2.1 @@ -36,6 +48,7 @@ speaklater==1.3 Sphinx==1.7.6 sphinx-rtd-theme==0.4.1 sphinxcontrib-websupport==1.1.0 +unidiff==0.5.5 urllib3==1.24.1 -Werkzeug==0.12.2 +Werkzeug==0.14.1 WTForms==2.1 diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..8380330 --- /dev/null +++ b/test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +export CULTUREMESH_API_KEY=1234 +export WTF_CSRF_SECRET_KEY=1234 +export CULTUREMESH_API_BASE_ENDPOINT=https://www.culturemesh.com/api-dev/v1 + +python -m pytest \ No newline at end of file diff --git a/test/unit/webapp/__init__.py b/test/unit/webapp/__init__.py new file mode 100644 index 0000000..989ea25 --- /dev/null +++ b/test/unit/webapp/__init__.py @@ -0,0 +1,57 @@ +import pytest +from culturemesh import app + +"""Initialize the testing environment + +Creates an app for testing that has the configuration flag ``TESTING`` set to +``True``. + +""" + +# The following function is derived from an example in the Flask documentation +# found at the following URL: http://flask.pocoo.org/docs/1.0/testing/. The +# Flask license statement has been included below as attribution. +# +# Copyright (c) 2010 by the Pallets team. +# +# Some rights reserved. +# +# Redistribution and use in source and binary forms of the software as well as +# documentation, with or without modification, are permitted provided that the +# following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND DOCUMENTATION, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +@pytest.fixture +def client(): + """Configures the app for testing + + Sets app config variable ``TESTING`` to ``True`` + + :return: App for testing + """ + + #app.config['TESTING'] = True + client = app.test_client() + + yield client diff --git a/test/unit/webapp/test_posts.py b/test/unit/webapp/test_posts.py new file mode 100644 index 0000000..817ad8b --- /dev/null +++ b/test/unit/webapp/test_posts.py @@ -0,0 +1,114 @@ +from test.unit.webapp import client +import mock +from mock import call + + +view_post_post = {'id': 626, 'id_network': 1, 'id_user': 157, 'img_link': None, + 'post_class': 'o', + 'post_date': 'Sun, 26 Aug 2018 22:31:04 GMT', + 'post_original': None, + 'post_text': "Hi everyone! I'm hoping to move here soon, but " + "I'd like to get a better sense of the local " + "community. Would anyone be willing to take a " + "few minutes to talk with me about there " + "experiences living here, particularly after " + "leaving home? Thanks!\n", 'vid_link': None} +view_post_net = {'city_cur': 'Palo Alto', 'city_origin': None, + 'country_cur': 'United States', + 'country_origin': 'United States', + 'date_added': 'Tue, 12 Jan 2016 05:51:19 GMT', 'id': 1, + 'id_city_cur': 332851, 'id_city_origin': None, + 'id_country_cur': 47228, 'id_country_origin': 47228, + 'id_language_origin': None, 'id_region_cur': 55833, + 'id_region_origin': 56020, 'img_link': None, + 'language_origin': None, 'network_class': 'rc', + 'region_cur': 'California', 'region_origin': 'Michigan', + 'twitter_query_level': 'A'} +view_post_replies = [{'id': 465, 'id_network': 1, 'id_parent': 626, + 'id_user': 157, + 'reply_date': 'Sun, 02 Dec 2018 18:20:40 GMT', + 'reply_text': "This is a test reply, but I'd be happy " + "to talk to you. "}, + {'id': 461, 'id_network': 1, 'id_parent': 626, + 'id_user': 172, + 'reply_date': 'Tue, 18 Sep 2018 16:09:13 GMT', + 'reply_text': 'This is another test reply. Do not mind ' + 'me, but welcome to Palo Alto! Hope you ' + 'like it here'}, + {'id': 460, 'id_network': 1, 'id_parent': 626, + 'id_user': 171, + 'reply_date': 'Tue, 18 Sep 2018 16:07:16 GMT', + 'reply_text': 'This is only a test reply. But I am sure ' + 'someone else here can help you out.'}] + + +def mock_client_get_user(id): + users = [ + {'about_me': "I'm from Michigan", + 'act_code': '764efa883dda1e11db47671c4a3bbd9e', + 'company_news': None, 'confirmed': 0, + 'events_interested_in': None, 'events_upcoming': None, + 'first_name': 'c', 'fp_code': None, 'gender': 'n', 'id': 157, + 'img_link': 'https://www.culturemesh.com/user_images/null', + 'last_login': '0000-00-00 00:00:00', 'last_name': 's', + 'network_activity': None, + 'register_date': 'Sun, 02 Dec 2018 16:33:20 GMT', 'role': 0, + 'username': 'cs'}, + {'about_me': 'I like to cook and watch movies. I recently made some ' + 'clam chowder and it was amazing :D. Originally from ' + 'Mexico, now living in the bay area.', + 'act_code': '', 'company_news': None, 'confirmed': 0, + 'events_interested_in': None, 'events_upcoming': None, + 'first_name': 'Alan', 'fp_code': None, 'gender': None, 'id': 171, + 'img_link': None, 'last_login': '0000-00-00 00:00:00', + 'last_name': 'Last name', 'network_activity': None, + 'register_date': 'Thu, 20 Sep 2018 10:30:04 GMT', 'role': 0, + 'username': 'aefl'}, + {'about_me': 'Live and learn', 'act_code': '', 'company_news': None, + 'confirmed': 0, 'events_interested_in': None, 'events_upcoming': None, + 'first_name': 'Alan 2.0', 'fp_code': None, 'gender': None, 'id': 172, + 'img_link': None, 'last_login': '0000-00-00 00:00:00', + 'last_name': 'Lastname', 'network_activity': None, + 'register_date': 'Wed, 19 Sep 2018 22:15:15 GMT', 'role': 0, + 'username': 'aefl2'} + ] + for user in users: + if user['id'] == id: + return user + raise ValueError("User ID {} is unknown to mock_client_get_user".format(id)) + + +@mock.patch('culturemesh.blueprints.posts.controllers.Client.get_post', + return_value=view_post_post) +@mock.patch('culturemesh.blueprints.posts.controllers.Client.get_network', + return_value=view_post_net) +@mock.patch('culturemesh.blueprints.posts.controllers.Client.get_user', + side_effect=mock_client_get_user) +@mock.patch('culturemesh.blueprints.posts.controllers.Client.get_post_replies', + return_value=view_post_replies) +def test_view_post(replies, user, net, post, client): + result = client.get('/post/?id=626') + html = result.data.decode() + + # Check that replies are displayed + assert "This is another test reply. Do not mind me, " in html + assert 'This is only a test reply.' in html + + # Check that reply author username displayed + assert 'aefl' in html + + # Check that post text displayed + assert 'Hi everyone!' in html + + # Check that post author username displayed + assert 'cs' in html + + # Check that network name displayed + assert 'From Michigan, United States in Palo Alto, California, ' \ + 'United States' in html + + replies.assert_called_with(626, 100) + user.assert_has_calls([call(157), call(171), call(172), call(157)], + any_order=False) + net.assert_called_with(1) + post.assert_called_with('626') diff --git a/test/unit/webapp/test_root.py b/test/unit/webapp/test_root.py new file mode 100644 index 0000000..f7ea2ce --- /dev/null +++ b/test/unit/webapp/test_root.py @@ -0,0 +1,23 @@ +from test.unit.webapp import client + + +def test_landing(client): + landing = client.get("/") + html = landing.data.decode() + + # Check that links to `about` and `login` pages exist + assert "About" in html + assert " Login" in html + + # Spot check important text + assert "At CultureMesh, we're building networks to match these " \ + "real-world dynamics and knit the diverse fabrics of our world " \ + "together." in html + assert "1. Join a network you belong to." in html + + assert landing.status_code == 200 + + +def test_landing_aliases(client): + landing = client.get("/") + assert client.get("/index/").data == landing.data