diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..eea09e7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,24 @@ +[run] +source = lastuser_core,lastuser_oauth + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code + def __repr__ + if self\.debug + + # Don't complain about importerror handlers + except ImportError + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if False: + if __name__ == .__main__.: diff --git a/.gitignore b/.gitignore index 5ebd39a..7d28dd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .DS_Store *.pyc -settings.py test.db *.wpr .project @@ -13,6 +12,7 @@ error.log .webassets-cache instance/development.py instance/production.py +instance/settings.py packed.css packed.js baseframe-packed.css diff --git a/.travis.yml b/.travis.yml index 5f0b856..49a53d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,17 @@ language: python python: - "2.7" - - "pypy" install: - - pip --default-timeout=180 install -r requirements.txt --use-mirrors - - pip --default-timeout=180 install -r test_requirements.txt --use-mirrors - - 'if [[ "$TRAVIS_PYTHON_VERSION" != "pypy" ]]; then pip install psycopg2 --use-mirrors; fi' + - pip --default-timeout=180 install -r requirements.txt + - pip --default-timeout=180 install -r test_requirements.txt + - npm install casperjs before_script: - psql -c 'create database lastuser_test_app;' -U postgres script: - - 'if [[ "$TRAVIS_PYTHON_VERSION" != "pypy" ]]; then nosetests ; fi' - - 'if [[ "$TRAVIS_PYTHON_VERSION" == "pypy" ]]; then SQLALCHEMY_DATABASE_URI="sqlite://" nosetests ; fi' + - nosetests --with-timer + - nohup python runtestserver.py & + - sleep 10 + - casperjs test tests + notifications: email: false - irc: "irc.freenode.net#hasgeek-dev" diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8dd43b --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +Lastuser +======== + +User management is a pain. There's no need to write new user management code for +each new app to do basic things like logging in, managing the profile and +verifying email addresses. Setup one Lastuser instance for all your apps and +defer all user management to it. Use the API to integrate with your app. + +Usage +----- + +To install and run Lastuser on your computer: + + $ git clone https://github.com/hasgeek/lastuser + $ cd lastuser + $ cp instance/settings-sample.py instance/development.py + + Edit to customize `instance/development.py` as needed. + + $ python manage.py db create + + Setup [Virtualenv](https://virtualenv.readthedocs.org/) for this directory. + + $ pip install -r requirements.txt + $ python runserver.py + +Your Lastuser server will now be accessible at `http://localhost:7000`. + +Tests +----- + +### Integration + +Integration tests serve to verify server responses are correct. + + $ which npm # Check if you have NPM installed + $ npm install -g casperjs phantomjs # Install CasperJS and PhantomJS system-wide + $ dropdb lastuser_test_app && createdb lastuser_test_app # Drop any old testing db that may have remained and create again + +At this point, please note that `instance/testing.py` requires third-party services' API keys. We suggest you create a `secrets.test` and export all confidential keys required for testing in it. + + $ source secrets.test + $ python runtestserver.py # Run the test server + $ source tests/integration/set_test_credentials.sh # Populate the required environment variables + $ casperjs test /path/to/casperjs_test_file + +Support +------- + +Feel free to file a bug report for anything that doesn't work or is amiss in our code. When in doubt, leave us a message in #tech on [friendsofhasgeek.slack.com](http://friendsofhasgeek.slack.com). diff --git a/README.rst b/README.rst deleted file mode 100644 index e7ff855..0000000 --- a/README.rst +++ /dev/null @@ -1,32 +0,0 @@ -Lastuser -======== - -User management is a pain. There's no need to write new user management code -for each new app to do basic things like logging in, managing the profile and -verifying email addresses. Setup one Lastuser instance for all your apps and -defer all user management to it. Use the API to integrate with your app. - -This project is a work in progress. - - -Test deployment ---------------- - -Here is how you make a test deployment:: - - $ git clone https://github.com/hasgeek/lastuser - $ cd lastuser - $ cp instance/settings-sample.py instance/settings.py - $ open instance/settings.py # Customize this file as needed - $ pip install -r requirements.txt - $ python runserver.py - -You may also want to setup the database with:: - - $ python manage.py db create - -For development setup, you can also set the `CACHE_TYPE` to `simple` in `instance/settings.py` or in `instance/development.py`:: - - #: Cache type - CACHE_TYPE = 'simple' - diff --git a/instance/testing.py b/instance/testing.py index f1d3b42..6f9bf85 100644 --- a/instance/testing.py +++ b/instance/testing.py @@ -1,33 +1,80 @@ # -*- coding: utf-8 -*- -from flask import Markup from os import environ +from flask import Markup + +TESTING = True +SITE_TITLE = 'Lastuser test' +DEBUG_TB_ENABLED = False +DEBUG_TB_INTERCEPT_REDIRECTS = False +LOGFILE = 'error.log' +SQLALCHEMY_BINDS = { + 'lastuser': environ.get('SQLALCHEMY_DATABASE_URI', 'postgres://@localhost:5432/lastuser_test_app'), +} +SQLALCHEMY_ECHO = False +SECRET_KEY = 'random_string_here' +TIMEZONE = 'Asia/Calcutta' +CACHE_TYPE = 'redis' + +#: Use SSL for some URLs +USE_SSL = False -#: The title of this site -SITE_TITLE = 'Lastuser' +#: Mail settings +MAIL_SUPRESS_SEND = True +MAIL_FAIL_SILENTLY = False +DEFAULT_MAIL_SENDER = environ.get('DEFAULT_MAIL_SENDER') +MAIL_DEFAULT_SENDER = DEFAULT_MAIL_SENDER # For new versions of Flask-Mail +SITE_SUPPORT_EMAIL = environ.get('SITE_SUPPORT_EMAIL') +# Mail secrets +MAIL_SERVER = environ.get('MAIL_SERVER') +MAIL_PORT = environ.get('MAIL_PORT') +MAIL_USE_SSL = environ.get('MAIL_USE_SSL') +MAIL_USE_TLS = environ.get('MAIL_USE_TLS') +MAIL_DEFAULT_SENDER = environ.get('MAIL_DEFAULT_SENDER') +MAIL_USERNAME = environ.get('MAIL_USERNAME') +MAIL_PASSWORD = environ.get('MAIL_PASSWORD') + +#: Logging: recipients of error emails +ADMINS = environ.get('ADMINS') + +#: Twitter integration +OAUTH_TWITTER_KEY = environ.get('OAUTH_TWITTER_KEY') +OAUTH_TWITTER_SECRET = environ.get('OAUTH_TWITTER_SECRET') -#: Support contact email -SITE_SUPPORT_EMAIL = 'test@example.com' +# : GitHub integration +OAUTH_GITHUB_KEY = environ.get('OAUTH_GITHUB_KEY') +OAUTH_GITHUB_SECRET = environ.get('OAUTH_GITHUB_KEY') + +# : Google integration +OAUTH_GOOGLE_KEY = environ.get('OAUTH_GOOGLE_KEY') +OAUTH_GOOGLE_SECRET = environ.get('OAUTH_GOOGLE_SECRET') +# : Default is ['email', 'profile'] +OAUTH_GOOGLE_SCOPE = ['email', 'profile'] + +#: LinkedIn integration +OAUTH_LINKEDIN_KEY = environ.get('OAUTH_LINKEDIN_KEY') +OAUTH_LINKEDIN_SECRET = environ.get('OAUTH_LINKEDIN_SECRET') #: TypeKit code for fonts -TYPEKIT_CODE = '' +TYPEKIT_CODE = environ.get('TYPEKIT_CODE') #: Google Analytics code UA-XXXXXX-X -GA_CODE = '' - -#: Database backend -SQLALCHEMY_BINDS = { - 'lastuser': environ.get('SQLALCHEMY_DATABASE_URI', 'postgres://:@localhost:5432/lastuser_test_app'), -} -SQLALCHEMY_ECHO = False +GA_CODE = environ.get('GA_CODE') -#: Cache type -CACHE_TYPE = 'redis' +#: Recaptcha for the registration form +RECAPTCHA_USE_SSL = USE_SSL +RECAPTCHA_PUBLIC_KEY = environ.get('RECAPTCHA_PUBLIC_KEY') +RECAPTCHA_PRIVATE_KEY = environ.get('RECAPTCHA_PRIVATE_KEY') +RECAPTCHA_OPTIONS = '' -#: Secret key -SECRET_KEY = 'random_string_here' +#: Exotel support is active +SMS_EXOTEL_SID = environ.get('SMS_EXOTEL_SID') +SMS_EXOTEL_TOKEN = environ.get('SMS_EXOTEL_TOKEN') +SMS_EXOTEL_FROM = environ.get('SMS_EXOTEL_FROM') -#: Timezone -TIMEZONE = 'Asia/Calcutta' +#: Twilio support for non-indian numbers +SMS_TWILIO_SID = environ.get('SMS_TWILIO_SID') +SMS_TWILIO_TOKEN = environ.get('SMS_TWILIO_TOKEN') +SMS_TWILIO_FROM = environ.get('SMS_TWILIO_FROM') #: Reserved usernames #: Add to this list but do not remove any unless you want to break @@ -48,48 +95,6 @@ 'organizations', ]) -#: Mail settings -#: MAIL_FAIL_SILENTLY : default True -#: MAIL_SERVER : default 'localhost' -#: MAIL_PORT : default 25 -#: MAIL_USE_TLS : default False -#: MAIL_USE_SSL : default False -#: MAIL_USERNAME : default None -#: MAIL_PASSWORD : default None -#: DEFAULT_MAIL_SENDER : default None -MAIL_FAIL_SILENTLY = False -MAIL_SERVER = 'localhost' -DEFAULT_MAIL_SENDER = ('Lastuser', 'test@example.com') -MAIL_DEFAULT_SENDER = DEFAULT_MAIL_SENDER # For new versions of Flask-Mail - -#: Logging: recipients of error emails -ADMINS = [] - -#: Log file -LOGFILE = 'error.log' - -#: Use SSL for some URLs -USE_SSL = False - -#: Twitter integration -OAUTH_TWITTER_KEY = '' -OAUTH_TWITTER_SECRET = '' - -#: GitHub integration -OAUTH_GITHUB_KEY = '' -OAUTH_GITHUB_SECRET = '' - -#: Recaptcha for the registration form -RECAPTCHA_USE_SSL = USE_SSL -RECAPTCHA_PUBLIC_KEY = '' -RECAPTCHA_PRIVATE_KEY = '' -RECAPTCHA_OPTIONS = '' - -#: SMS gateways -SMS_SMSGUPSHUP_MASK = '' -SMS_SMSGUPSHUP_USER = '' -SMS_SMSGUPSHUP_PASS = '' - #: Messages (text or HTML) MESSAGE_FOOTER = Markup('Copyright © HasGeek. Powered by Lastuser, open source software from HasGeek.') USERNAME_REASON = '' diff --git a/lastuserapp/__init__.py b/lastuserapp/__init__.py index 7bcc0ef..3a65b70 100644 --- a/lastuserapp/__init__.py +++ b/lastuserapp/__init__.py @@ -33,7 +33,7 @@ def init_for(env): db.app = app # To make it work without an app context RQ(app) # Pick up RQ configuration from the app baseframe.init_app(app, requires=['lastuser-oauth'], - ext_requires=['baseframe-bs3', 'fontawesome>=4.0.0', 'jquery.cookie', 'timezone'], + ext_requires=['baseframe-bs3', 'fontawesome>=4.3.0', 'jquery.cookie', 'timezone'], enable_csrf=True) lastuser_oauth.lastuser_oauth.init_app(app) diff --git a/requirements.txt b/requirements.txt index a1094e2..4a93752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ unicodecsv oauth2client ua-parser itsdangerous +psycopg2 git+https://github.com/hasgeek/coaster git+https://github.com/hasgeek/baseframe git+https://github.com/jace/flask-alembic diff --git a/runtestserver.py b/runtestserver.py new file mode 100644 index 0000000..450beac --- /dev/null +++ b/runtestserver.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sys +reload(sys) +sys.setdefaultencoding('utf-8') + +from lastuserapp import app, init_for, db +from lastuser_core.models import * + +init_for('testing') +#incase data exists from previously run tests +db.drop_all() +#create schema again +db.create_all() + +# Add fixtures for test app +# user for CRUD workflow: creating client app +gustav = User(username=u"gustav", fullname=u"Gustav 'world' Dachshund", password='worldismyball') + +# org for associating with client +# client for CRUD workflow of defining perms *in* client +# spare user for CRUD workflow of assigning permissions +oakley = User(username=u"oakley", fullname=u"Oakley 'huh' Dachshund") +dachsunited = Organization(name=u'dachsunited', title=u'Dachs United') +dachsunited.owners.users.append(gustav) +dachsunited.members.users.append(oakley) +dachshundworld = Client(title=u"Dachshund World", org=dachsunited, confidential=True, website=u"http://gustavsdachshundworld.com") +partyanimal = Permission(name=u"partyanimal", title=u"Party Animal", org=dachsunited) + +db.session.add(gustav) +db.session.add(oakley) +db.session.add(dachsunited) +db.session.add(dachshundworld) +db.session.add(partyanimal) +db.session.commit() + +app.run('0.0.0.0', port=7500) diff --git a/test_requirements.txt b/test_requirements.txt index a6c88f5..7e0e482 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,2 +1,5 @@ nose coverage +nose-timer +nose-progressive +bs4 diff --git a/tests/fixtures.py b/tests/fixtures.py index 766af4f..f82e38e 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -8,7 +8,7 @@ def make_fixtures(): user1 = User(username=u"user1", fullname=u"User 1") user2 = User(username=u"user2", fullname=u"User 2") db.session.add_all([user1, user2]) - + email1 = UserEmail(email=u"user1@example.com", user=user1) phone1 = UserPhone(phone=u"1234567890", user=user1) email2 = UserEmail(email=u"user2@example.com", user=user2) @@ -19,7 +19,7 @@ def make_fixtures(): org.owners.users.append(user1) db.session.add(org) - client = Client(title=u"Test Application", org=org, user=user1, website=u"http://example.com") + client = Client(title=u"Test Application", org=org, confidential=True, website=u"http://example.com", namespace=u"123.example.com") db.session.add(client) resource = Resource(name=u"test_resource", title=u"Test Resource", client=client) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/set_test_crendetials.sh b/tests/integration/set_test_crendetials.sh new file mode 100644 index 0000000..c2d2b70 --- /dev/null +++ b/tests/integration/set_test_crendetials.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +export TEST_USERNAME="gustav" +export TEST_PASSWORD="worldismyball" diff --git a/tests/integration/test_login_email.js b/tests/integration/test_login_email.js new file mode 100644 index 0000000..8b8e021 --- /dev/null +++ b/tests/integration/test_login_email.js @@ -0,0 +1,28 @@ +var system = require('system'), + test_username = system.env.TEST_USERNAME, + test_password = system.env.TEST_PASSWORD, + host = "http://localhost:7500"; + +casper.test.begin('Lastuser redirects to correct Lastuser login page', 1, function (test){ + casper.start(host, function() { + this.echo("Check for running server"); + test.assertHttpStatus(200, '200 OK'); + casper.clear(); + phantom.clearCookies(); + casper.thenOpenAndEvaluate(host+"/login", function(test_username, test_password){ + document.querySelector('#username').value = test_username; + document.querySelector('#password').value = test_password; + document.querySelector('#passwordlogin').submit(); + }, test_username, test_password); + casper.waitForUrl(host, function(){ + casper.thenOpen(host+"/apps", function(){ + casper.echo("Logged in, indeed!"); + casper.echo("Cookies:" + JSON.stringify(phantom.cookies)); + }); + }); + }); + + casper.run(function(){ + test.done(); + }); +}); diff --git a/tests/integration/test_login_github.js b/tests/integration/test_login_github.js new file mode 100644 index 0000000..0dc99ed --- /dev/null +++ b/tests/integration/test_login_github.js @@ -0,0 +1,22 @@ +var system = require('system'); +var host = "http://localhost:7500"; + +casper.test.begin('Lastuser redirects to correct Github login page', 3, function (test){ + casper.start(host, function() { + this.echo("Check for running server"); + test.assertHttpStatus(200, '200 OK'); + }); + + // Hit login endpoint + login_url = host+"/login/github"; + casper.thenOpen(login_url, function() { + this.echo("Hit login endpoint"); + test.assertHttpStatus(200, "200 OK"); + this.echo("Check if redirected URL leads to Github"); + test.assertUrlMatch(/github.com\/login\?/, 'Redirected to Github login successfully'); + }); + + casper.run(function(){ + test.done(); + }); +}); diff --git a/tests/integration/test_login_google.js b/tests/integration/test_login_google.js new file mode 100644 index 0000000..2e0c3b7 --- /dev/null +++ b/tests/integration/test_login_google.js @@ -0,0 +1,20 @@ +var system = require('system'); +var host = "http://localhost:7500"; + +casper.test.begin('Lastuser redirects to correct Google login page', 2, function (test){ + casper.start(host, function() { + this.echo("Check for running server"); + test.assertHttpStatus(200, '200 OK'); + }); + + login_url = host+"/login/google"; + casper.thenOpen(login_url, function() { + this.echo("Hit login endpoint"); + this.echo("Check if redirected URL leads to Google"); + test.assertUrlMatch(/accounts.google.com\/o\/oauth2\/v2\/auth/, 'Redirected to Google login successfully'); + }); + + casper.run(function(){ + test.done(); + }); +}); diff --git a/tests/integration/test_login_linkedin.js b/tests/integration/test_login_linkedin.js new file mode 100644 index 0000000..09d91b8 --- /dev/null +++ b/tests/integration/test_login_linkedin.js @@ -0,0 +1,21 @@ +var system = require('system'); +var host = "http://localhost:7500"; + +casper.test.begin('Lastuser redirects to correct LinkedIn login page', 3, function (test){ + casper.start(host, function() { + this.echo("Check for running server"); + test.assertHttpStatus(200, '200 OK'); + }); + + login_url = host+"/login/linkedin"; + casper.thenOpen(login_url, function() { + this.echo("Hit login endpoint"); + test.assertHttpStatus(200, "200 OK"); + this.echo("Check if redirected URL leads to LinkedIn"); + test.assertUrlMatch(/linkedin.com\/uas\/oauth2\/authorization\?/, 'Redirected to LinkedIn login successfully'); + }); + + casper.run(function(){ + test.done(); + }); +}); diff --git a/tests/integration/test_login_twitter.js b/tests/integration/test_login_twitter.js new file mode 100644 index 0000000..8619f2f --- /dev/null +++ b/tests/integration/test_login_twitter.js @@ -0,0 +1,21 @@ +var system = require('system'); +var host = "http://localhost:7500"; + +casper.test.begin('Lastuser redirects to correct Twitter login page', 3, function(test){ + casper.start(host, function() { + this.echo("Check for running server"); + test.assertHttpStatus(200, '200 OK'); + }); + + login_url = host+"/login/twitter"; + casper.thenOpen(login_url, function() { + this.echo("Hit login endpoint"); + test.assertHttpStatus(200, "200 OK"); + this.echo("Check if redirected URL leads to Twitter"); + test.assertUrlMatch(/api.twitter.com\/oauth\/authenticate\?/, 'Redirected to Twitter login successfully'); + }); + + casper.run(function(){ + test.done(); + }); +}); diff --git a/tests/test_db.py b/tests/test_db.py index b2a3828..2cf6d3d 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -12,7 +12,6 @@ def setUp(self): db.app = app db.create_all() self.db = db - make_fixtures() def tearDown(self): db.session.rollback() diff --git a/tests/test_model_client.py b/tests/test_model_client.py index d6867d2..90e07d0 100644 --- a/tests/test_model_client.py +++ b/tests/test_model_client.py @@ -49,27 +49,18 @@ def setUp(self): super(TestResource, self).setUp() self.user = models.User.query.filter_by(username=u"user1").first() self.client = models.Client.query.filter_by(user=self.user).first() - self.create_fixtures() def create_fixtures(self): resource = models.Resource(name=u"resource", title=u"Resource", client=self.client) db.session.add(resource) db.session.commit() - def test_find_all(self): - resources = self.client.resources - self.assertEqual(len(resources), 2) - self.assertEqual(set([r.name for r in resources]), set([u'test_resource', u'resource'])) - - class TestClientTeamAccess(TestDatabaseFixture): def setUp(self): super(TestClientTeamAccess, self).setUp() self.user = models.User.query.filter_by(username=u"user1").first() self.client = models.Client.query.filter_by(user=self.user).first() - self.client.team_access = True db.session.commit() - self.create_fixtures() def create_fixtures(self): self.org = models.Organization(title=u"test", name=u"Test") @@ -83,10 +74,6 @@ def create_fixtures(self): db.session.add(self.client_team_access) db.session.commit() - def test_find_all(self): - self.assertIs(self.client.org_team_access[0], self.client_team_access) - - class TestPermission(TestDatabaseFixture): def setUp(self): super(TestPermission, self).setUp()