Skip to content

Commit

Permalink
Merge pull request #10 from codeforamerica/add_user_logins
Browse files Browse the repository at this point in the history
Add user logins
  • Loading branch information
bengolder committed Jan 1, 2016
2 parents 236da88 + ee78109 commit 4567019
Show file tree
Hide file tree
Showing 69 changed files with 1,636 additions and 381 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.DS_Store
.env
migrations
node_modules
__pycache__
*.sw[op]
*.pyc
.coverage
.coverage
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ install: make install.travis
script: make test.travis
addons:
postgresql: "9.4"
env:
- TEST_DATABASE_URL="postgresql+psycopg2://postgres@localhost/test_typeseam"
before_script:
- psql -c 'create database test_typeseam;' -U postgres
after_success:
- coveralls
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,20 @@ test:
dropdb test_$(DB_NAME) --if-exists
createdb test_$(DB_NAME)
nosetests \
--eval-attr "not slow" \
--eval-attr "not selenium" \
--verbose \
--nocapture \
--with-coverage \
--cover-package=./typeseam \
--cover-erase
dropdb test_$(DB_NAME)

test.specific:
dropdb test_$(DB_NAME) --if-exists
createdb test_$(DB_NAME)
nosetests \
$(CURRENT_TESTS) \
--verbose \
--nocapture

test.full:
$(info This test requires the server to be running locally)
Expand All @@ -53,7 +60,6 @@ test.full:
--with-coverage \
--cover-package=./typeseam \
--cover-erase
dropdb test_$(DB_NAME)

test.travis:
nosetests \
Expand Down
32 changes: 25 additions & 7 deletions frontend/js/main.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
$( document ).ready(function() {
addCSRFTokenToRequests()
listenToEvents();
getNewResponses();
});

var PDF_LOADING_STATES = [
["sending", 2000],
["generating", 13000],
["retrieving", 5000]
["retrieving", 5000],
];

function addCSRFTokenToRequests(){
// Taken directly from
// http://flask-wtf.readthedocs.org/en/latest/csrf.html#ajax
var csrftoken = $('meta[name=csrf-token]').attr('content');
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
}
});
}

function listenToEvents(){
$('.responses-header').on('click', '.load_new_responses', getNewResponses);
$('.responses').on('click', '.pdf_cell', getPDF);
$('.container').on('click', '.pdf_button', getPDF);
}

function getNewResponses(e){
$('button.load_new_responses').addClass('loading');
$.ajax({
url: "/api/new_responses",
url: API_ENDPOINTS.new_responses,
success: handleNewResponses,
timeout: 10000
});
Expand All @@ -29,6 +43,8 @@ function stateTransitionChain(target, stateStack, index){
target.removeClass(prevStateClassName);
}
if( index == stateStack.length ){
target.removeClass("loading");
target.addClass('default');
return;
}
var stateClassName = stateStack[index][0];
Expand All @@ -41,13 +57,15 @@ function stateTransitionChain(target, stateStack, index){

function getPDF(e){
var target = $(this);
target.removeClass("untouched");
console.log("clicked to get pdf on", target);
target.removeClass("default");
target.addClass('loading');
var responseId = target.parent('.response').attr('id');
var responseId = target.parents('.response').attr('id');
responseId = responseId.split("-")[1]
stateTransitionChain($(this), PDF_LOADING_STATES, 0);
stateTransitionChain(target, PDF_LOADING_STATES, 0);
$.ajax({
url: "/api/get_pdf/" + responseId,
method: "POST",
url: target.attr("data-apiendpoint"),
success: handleNewPDF(responseId),
timeout: 20000
});
Expand Down
8 changes: 4 additions & 4 deletions frontend/less/custom.less
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@

// states
.stateful .state { display: none; }
.untouched .state.content_default { display: inline; }
.sending .state.content_sending { display: inline; }
.generating .state.content_generating { display: inline; }
.retrieving .state.content_retrieving { display: inline; }
.stateful.default .state.content_default { display: inline; }
.stateful.sending .state.content_sending { display: inline; }
.stateful.generating .state.content_generating { display: inline; }
.stateful.retrieving .state.content_retrieving { display: inline; }

.btn .glyphicon {
margin-right: .5em;
Expand Down
3 changes: 2 additions & 1 deletion requirements/ci.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ nose==1.3.6
coverage==3.7.1
-e git+https://github.com/jarus/flask-testing.git@c969b41b31f60a5a8bacd44b3eb63d1642f2d8bf#egg=Flask_Testing-master
factory-boy==2.6.0
mock==1.3.0
mock==1.3.0
beautifulsoup4==4.4.1
7 changes: 7 additions & 0 deletions requirements/prod.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ requests==2.8.1

# Remote server
gunicorn==19.4.1

# User Accounts (includes Flask-mail as dependency)
Flask-User==0.6.8
Flask-SSLify==0.1.5

# Email
sendgrid==1.5.19
51 changes: 51 additions & 0 deletions tests/functional/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
import os
from pprint import pprint
from nose.plugins.attrib import attr

from tests.selenium_test_base import SeleniumBaseTestCase

class TestAuthTasks(SeleniumBaseTestCase):

user = {
'email': os.environ.get('DEFAULT_ADMIN_EMAIL', '[email protected]'),
'password': os.environ.get('DEFAULT_ADMIN_PASSWORD', 'this-sketch-is-too-silly'),
}

def open_email_inbox(self):
self.get_email()
self.wait(1)
email_input = self.xpath("//input[@name='Email']")
email_input.send_keys(self.user['email'])
email_input.send_keys(self.keys.ENTER)
self.wait(1)
password_input = self.xpath("//input[@name='Passwd']")
password_input.send_keys(self.user['password'])
password_input.send_keys(self.keys.ENTER)
self.wait(30)

def test_redirect_to_login(self):
self.get('/')
self.assertIn('Log in', self.browser.title)
email_input = self.browser.find_element_by_xpath('//input[@name=email]')
print(email_input)

def test_click_on_forgot_password_gets_email_form(self):
self.get('/')
self.assertIn('Log in', self.browser.title)
email_input = self.browser.find_element_by_name('email')
# find email

def test_get_login_page(self):
self.get('/login')
self.assertIn('Log in', self.browser.title)
self.screenshot('login.png')

def test_able_to_login(self):
self.get('/login')
email_input = self.browser.find_element_by_name('email')
email_input.send_keys(self.user['email'])
password_input = self.browser.find_element_by_name('password')
password_input.send_keys(self.user['password'])
self.screenshot('login-filled.png')
password_input.submit()
8 changes: 8 additions & 0 deletions tests/functional/test_intake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from tests.selenium_test_base import SeleniumBaseTestCase

class TestFormFillerTasks(SeleniumBaseTestCase):

def test_index_get(self):
self.get('/')
self.assertIn('Clean Slate', self.browser.title)
self.screenshot('index.png')
15 changes: 0 additions & 15 deletions tests/functional/test_selenium.py

This file was deleted.

Empty file added tests/integration/__init__.py
Empty file.
116 changes: 116 additions & 0 deletions tests/integration/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from flask import url_for, request, session
from flask.ext.login import current_user
from sqlalchemy import func, distinct
from pprint import pprint

from typeseam.app import db
from typeseam.auth.models import User
from typeseam.auth.queries import create_user, get_user_by_email

from tests.test_base import BaseTestCase


class TestAuthViews(BaseTestCase):

sample_user_data = dict(
email="[email protected]",
password="Hell0"
)

def setUp(self):
BaseTestCase.setUp(self)
self.client = self.app.test_client()
create_user(**self.sample_user_data)

def get_user(self):
return get_user_by_email(self.sample_user_data['email'])

def test_wrong_password_returns_to_login(self):
response = self.login(password="not hello")
self.assertIn('Sign in', response.data.decode('utf-8'))
self.assertFalse(current_user.is_authenticated)

def login(self, **kwargs):
login_data = dict(**self.sample_user_data)
login_data.update(**kwargs)
get_response = self.client.get('/', follow_redirects=True)
csrf_token = self.get_input_value('csrf_token', get_response)
return self.client.post(
url_for('user.login'),
data=dict(csrf_token=csrf_token, **login_data),
follow_redirects=True)

def logout(self):
return self.client.get(url_for('user.logout'), follow_redirects=True)

def test_new_user_password_is_properly_encrypted(self):
# check that the password was hashed with bcrypt
raw_password = self.sample_user_data['password']
user = self.get_user()
encoded_raw_password = raw_password.encode('utf-8')
presumably_hashed_password = user.password.encode('utf-8')
import bcrypt
self.assertEqual(
bcrypt.hashpw(encoded_raw_password, presumably_hashed_password),
presumably_hashed_password)

def test_unauthenticated_home_page_resolves_to_login_view(self):
r = self.client.get('/')
self.assertEqual(r.status_code, 302) # is it a redirect?
r = self.client.get('/', follow_redirects=True)
self.assertIn('Sign in', r.data.decode('utf-8')) # did it redirect to Log in?

def test_login_form_includes_csrf_token(self):
r = self.client.get(url_for('user.login'))
self.assertIn('csrf_token', r.data.decode('utf-8'))

def test_can_login(self):
response = self.login()
self.assertEqual(response.status_code, 200)

def test_can_logout(self):
self.login()
response = self.logout()
self.assertFalse(current_user.is_authenticated)

def test_successful_login_has_message(self):
response = self.login()
self.assertIn('signed in successfully', response.data.decode('utf-8'))

def test_login_fails_without_csrf(self):
response = self.client.post(
url_for('user.login'),
data=dict(**self.sample_user_data),
follow_redirects=True)
self.assertEqual(response.status_code, 400)

# def test_login_warns_about_http_and_links_to_https(self):
# raise NotImplementedError

# def test_login_has_forgot_password_link(self):
# raise NotImplementedError

# def test_forgot_password_post_sends_email(self):
# raise NotImplementedError

# def test_forgot_password_view_errors_on_unused_email(self):
# raise NotImplementedError

# def test_password_reset_email_contains_proper_link(self):
# raise NotImplementedError

# def test_password_reset_link_expires(self):
# raise NotImplementedError

# def test_password_reset_form_looks_sufficient(self):
# raise NotImplementedError

# def test_password_reset_has_csrf_and_https_warning(self):
# raise NotImplementedError

# def test_password_reset_fails_without_csrf(self):
# raise NotImplementedError

# def test_successful_password_reset_goes_to_next_with_message(self):
# raise NotImplementedError

26 changes: 26 additions & 0 deletions tests/integration/test_mail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from tests.test_base import BaseTestCase
from typeseam.utils.sendgrid_mailer import email_dispatched
from typeseam.auth.tasks import sendgrid_email


class TestMail(BaseTestCase):

def setUp(self):
BaseTestCase.setUp(self)
self.body = """Hey there, this is an email message."""
self.subject = "Hello from mail tests"

def send_mail(self):
sendgrid_email(
subject="testing mail again",
recipients=['[email protected]'],
text_message="What is up?"
)

def test_can_send_mail(self):
messages = []
def fire(app, message, **extra):
messages.append(message)
email_dispatched.connect(fire)
self.send_mail()
self.assertEqual(len(messages), 1)
Loading

0 comments on commit 4567019

Please sign in to comment.