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()