Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Start of ui testing (bug 919543)

Add some basic tests for the testing include urls
  • Loading branch information...
commit 2234eb70c1d1f17ec97365008af67d28901e7e2c 1 parent c6a4276
@muffinresearch muffinresearch authored
View
33 Makefile
@@ -0,0 +1,33 @@
+VENV_DIR=$(HOME)/venv
+SUPERVISORD_PIDFILE=tmp/supervisord.pid
+
+setup:
+ mkdir -p tmp
+ mkdir -p uitests/captures
+
+test-ui: setup clean start nap run-ui-tests stop
+
+nap:
+ @echo Waiting for start-up to complete
+ bin/backoff.sh curl -I http://localhost:9765/include.js
+
+run-ui-tests:
+ @/bin/bash -c 'pushd uitests > /dev/null; \
+ casperjs test tests ; \
+ popd > /dev/null || \
+ $(MAKE) stop'
+
+clean:
+ find uitests/captures -type f -exec rm {} \;
+
+start:
+ @if [ ! -f "$(SUPERVISORD_PIDFILE)" ]; then \
+ VENV_DIR=$(VENV_DIR) supervisord -c ./supervisord.conf && echo supervisord started; \
+ else echo supervisord already started; fi
+
+stop:
+ @if [ -f "$(SUPERVISORD_PIDFILE)" ]; then \
+ kill `cat $(SUPERVISORD_PIDFILE)` && echo supervisord stopped; \
+ else echo supervisord pidfile not found; fi
+
+.PHONY: setup test-ui nap run-ui-tests clean start stop
View
36 bin/backoff.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+# Based on: http://stackoverflow.com/questions/8350942/how-to-re-run-the-curl-command-automatically-when-the-error-occurs
+# Retries a command a configurable number of times with backoff.
+#
+# The retry count is given by ATTEMPTS (default 5), the initial backoff
+# timeout is given by TIMEOUT in seconds (default 1.)
+#
+# Successive backoffs double the timeout.
+
+max_attempts=${ATTEMPTS-5}
+timeout=${TIMEOUT-1}
+attempt=0
+exitCode=0
+
+while [[ $attempt < $max_attempts ]]; do
+ set +e
+ "$@" > /dev/null 2>&1
+ exitCode=$?
+ set -e
+
+ if [[ $exitCode == 0 ]]
+ then
+ echo Success!
+ break
+ fi
+
+ echo "Failure! Retrying in $timeout.." 1>&2
+ sleep $timeout
+ attempt=$(( attempt + 1 ))
+ timeout=$(( timeout * 2 ))
+done
+
+if [[ $exitCode != 0 ]]; then
+ echo "You've failed me for the last time! ($@)" 1>&2
+fi
View
2  requirements/test.txt
@@ -3,3 +3,5 @@
nose-blockage==0.1.2
pyquery==1.2.4
cssselect==0.8
+supervisor==3.0
+meld3==0.6.10
View
13 supervisord.conf
@@ -0,0 +1,13 @@
+[supervisord]
+logfile=tmp/supervisord.log
+pidfile=tmp/supervisord.pid
+
+[program:webpay]
+directory=.
+command=%(ENV_VENV_DIR)s/webpay/bin/python manage.py runserver --settings=webpay.settings.uitests 0.0.0.0:9765
+autostart=true
+redirect_stderr=true
+stdout_logfile=tmp/webpay.log
+stdout_logfile_maxbytes=1MB
+stopsignal=KILL
+stopasgroup=true ; This is needed to also take out the child process that runserver creates.
View
24 uitests/helpers.js
@@ -0,0 +1,24 @@
+var settings = require('./settings');
+
+if (settings.showClientConsole) {
+ casper.on('remote.message', function(message) {
+ this.echo(message);
+ });
+}
+
+// Clear localStorage when the page object is initialised.
+casper.on('page.initialized', function() {
+ casper.echo('Clearing localStorage', 'INFO');
+ casper.evaluate(function(){ localStorage.clear(); });
+});
+
+exports.setLoginFilter = function(emailAddress) {
+
+ casper.setFilter("page.prompt", function(msg, value) {
+ if (msg === "Enter email address") {
+ this.echo('Entering email address: ' + emailAddress, "INFO");
+ return emailAddress;
+ }
+ });
+
+};
View
9 uitests/settings.js
@@ -0,0 +1,9 @@
+
+var settings = {
+ testServer: 'http://localhost:9765',
+ showClientConsole: false,
+};
+
+Object.keys(settings).forEach(function(key) {
+ exports[key] = settings[key];
+});
View
86 uitests/tests/pin.js
@@ -0,0 +1,86 @@
+var settings = require('./settings');
+var helpers = require('./helpers');
+
+
+casper.test.begin('Test Basic Pin Entry', {
+
+ setUp: function(test) {
+ // Sets the filter so we always login as this user.
+ helpers.setLoginFilter("tester@fakepersona.mozilla.org");
+ },
+
+ tearDown: function(test) {
+
+ },
+
+ test: function(test) {
+
+ casper.start(settings.testServer + '/mozpay/');
+
+ casper.waitFor(function check() {
+ return this.visible('#signin');
+ }, function then() {
+ test.assertVisible('#signin', 'Check signin element is present.');
+ this.click('#signin');
+ });
+
+ casper.waitFor(function check() {
+ return this.visible('#pin') && !this.visible('#progress');
+ }, function then() {
+ test.assertVisible('#pin', 'Check pin entry is visible.');
+ }, function timeout() {
+ this.capture('captures/progress-still-visible.png');
+ test.fail('#pin element for Pin Entry is not visible before timeout.');
+ }, 10000);
+
+ casper.then(function testPinIncorrectData(){
+ this.sendKeys('#id_pin', 'a');
+ test.assertVisible('.error-msg', 'Check error message is shown for bad input.');
+ test.assertSelectorHasText('.error-msg', 'Pin can only contain digits', 'Check error message text is present.');
+ test.assertNotVisible('#forgot-pin', 'Check #forgot-pin is hidden when error message is shown.');
+ this.click('.pinbox');
+ test.assertNotVisible('.error-msg', 'Check .error-msg is no longer visible pin entry.');
+ });
+
+ casper.then(function testConditionallyForgotPin(){
+ // Only do this if Enter Pin instead of Create Pin.
+ if (this.fetchText('h2') == 'Enter Pin') {
+ casper.waitFor(function check() {
+ return this.visible('#forgot-pin');
+ }, function then() {
+ test.assertVisible('#forgot-pin', 'Check #forgot-pin is now visible on focus of pin entry.');
+ }, function timeout() {
+ this.capture('captures/forgot-pin-visble.png');
+ test.fail('#forgot-pin is not visible.');
+ }, 5000);
+ }
+ });
+
+ casper.then(function testContinueIsDisabledUntilFilled(){
+ test.assertExists('button[type="submit"]:disabled', 'Check submit button is disabled prior to pin entry');
+ this.sendKeys('#id_pin', '1234', {keepFocus: true});
+ }).waitFor(function check() {
+ return this.exists('button[type="submit"]:not(:disabled)');
+ }, function then() {
+ test.assertExists('button[type="submit"]:not(:disabled)', 'Check submit button is not disabled');
+ }, function timeout() {
+ this.capture('captures/pin-continue-not-enabled.png');
+ test.fail('button[type="submit"] is still disabled and should be enabled');
+ }, 5000);
+
+ casper.then(function testEnterPin(){
+ // Note: Sending keys focuses the input so we can't do this one at a time.
+ this.sendKeys('#id_pin', '1234', {keepFocus: true});
+ }).waitFor(function check() {
+ return this.exists('.display span:first-child.filled') && this.exists('.display span:last-child.filled');
+ }, function then() {
+ test.assertExists('.display span:first-child.filled', 'Check first item is filled');
+ test.assertExists('.display span:last-child.filled', 'Check last item is filled');
+ });
+
+ casper.run(function() {
+ test.done();
+ });
+ }
+
+});
View
22 webpay/settings/uitests.py
@@ -0,0 +1,22 @@
+from .base import *
+
+BROWSERID_DOMAIN = 'localhost:9765'
+BROWSERID_JS_URL = '/include.js'
+BROWSERID_VERIFICATION_URL = 'http://%s/verify' % BROWSERID_DOMAIN
+DATABASES = { 'default': {} }
+DEBUG = DEV = TEMPLATE_DEBUG = True
+FAKE_PAYMENTS = True
+HMAC_KEYS = { '2012-06-06': 'some secret', }
+MARKETPLACE_URL = 'http://localhost:9765'
+MEDIA_URL = '/mozpay/media/'
+SECRET_KEY = 'FAKE'
+SESSION_COOKIE_SECURE = False
+SITE_URL = ''
+SOLITUDE_URL = 'https://mock-solitude.paas.allizom.org/'
+TEST_PIN_UI = True
+
+
+try:
+ from .uitestslocal import *
+except ImportError, exc:
+ pass
View
0  webpay/testing/__init__.py
No changes.
View
54 webpay/testing/persona.py
@@ -0,0 +1,54 @@
+from os.path import abspath
+from time import time, sleep
+
+from django import http
+from django.conf import settings
+from django.views.decorators.csrf import csrf_exempt
+
+from webpay.base.decorators import json_view
+
+OK_USER = 'tester@fakepersona.mozilla.org'
+TIMEOUT_USER = 'timeout@fakepersona.mozilla.org'
+FAILED_LOGIN = 'fail@fakepersona.mozilla.org'
+ERROR_LOGIN = '500@fakepersona.mozilla.org'
+
+
+def fake_include(request):
+ """Serve up stubbyid.js for testing."""
+ if not settings.DEV or not settings.TEST_PIN_UI:
+ return http.HttpResponseForbidden()
+
+ with open(abspath('webpay/testing/stubbyid.js')) as fh:
+ stub_js = fh.read()
+ return http.HttpResponse(stub_js,
+ content_type='text/javascript')
+
+
+@csrf_exempt
+@json_view
+def fake_verify(request):
+ """Fake verification for testing"""
+
+ if not settings.DEV or not settings.TEST_PIN_UI:
+ return http.HttpResponseForbidden()
+
+ assertion = request.POST.get('assertion')
+ success = {
+ 'status': 'okay',
+ 'audience': 'http://localhost:9765',
+ 'expires': int(time()),
+ 'issuer': 'fake-persona'
+ }
+
+ if assertion == OK_USER:
+ success['email'] = OK_USER
+ return success
+ elif assertion == TIMEOUT_USER:
+ sleep(10)
+ success['email'] = TIMEOUT_USER
+ return success
+ elif assertion == ERROR_LOGIN:
+ return http.HttpResponseServerError()
+ elif assertion == FAILED_LOGIN:
+ request.session.clear()
+ return http.HttpResponseBadRequest()
View
222 webpay/testing/stubbyid.js
@@ -0,0 +1,222 @@
+// stubbyid.js v0.2
+// A simple client-side "simulator" for the Persona login service.
+// https://github.com/toolness/stubbyid
+
+(function() {
+ "use strict";
+
+ var LOGIN_STATE_KEY = "STUBBYID_LOGIN_STATE";
+ var USE_WIDGET = false;
+ var widget = {
+ el: document.createElement('div'),
+ update: function() {
+ if (!USE_WIDGET) {
+ return;
+ }
+ var state = getLoginState();
+ if (state) {
+ widget.el.innerHTML = 'Persona simulator thinks you want to ' +
+ 'be logged in as <strong>' + escapeHtml(state) +
+ '</strong>. <button>logout</button>';
+ } else {
+ widget.el.innerHTML = 'Persona simulator thinks you want to ' +
+ 'be logged out. <button>login</button>';
+ }
+ },
+ init: function() {
+ if (!USE_WIDGET) {
+ return;
+ }
+ widget.el.style.position = "fixed";
+ widget.el.style.bottom = "0";
+ widget.el.style.right = "0";
+ widget.el.style.color = "white";
+ widget.el.style.fontFamily = "Helvetica, Arial, sans-serif";
+ widget.el.style.fontSize = "12px";
+ widget.el.style.backgroundColor = "rgba(0, 0, 0, 0.75)";
+ widget.el.style.padding = "4px";
+ widget.el.style.zIndex = "100000";
+ attach(widget.el, "click", function(event) {
+ if (target(event).nodeName == "BUTTON") {
+ if (getLoginState())
+ setLoginState(null, false);
+ else
+ setLoginState(window.prompt("Enter email address") || null,
+ false);
+ }
+ });
+ document.body.appendChild(widget.el);
+ widget.update();
+ }
+ };
+ var escapeHtml = function(string) {
+ var entityMap = {
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;",
+ '"': '&quot;',
+ "'": '&#39;',
+ "/": '&#x2F;'
+ };
+
+ return String(string).replace(/[&<>"'\/]/g, function (s) {
+ return entityMap[s];
+ });
+ };
+ var target = function(event) {
+ return event.target || event.srcElement;
+ };
+ var attach = function(element, eventName, cb) {
+ if (element.addEventListener)
+ element.addEventListener(eventName, cb, false);
+ else
+ element.attachEvent('on' + eventName, cb);
+ };
+ var setLoginState = function(state, notifyWatcher) {
+ if (typeof(notifyWatcher) == "undefined") notifyWatcher = true;
+ state = state || null;
+ if (getLoginState() === state)
+ return;
+ if (state) {
+ window.localStorage.setItem(LOGIN_STATE_KEY, state);
+ widget.update();
+ if (notifyWatcher) watchOptions.onlogin(state);
+ } else {
+ window.localStorage.removeItem(LOGIN_STATE_KEY);
+ widget.update();
+ if (notifyWatcher) watchOptions.onlogout();
+ }
+ };
+ var getLoginState = function() {
+ return window.localStorage.getItem(LOGIN_STATE_KEY) || null;
+ };
+ var fail = function(msg) {
+ log(msg);
+ throw new Error(msg);
+ };
+ var log = function(msg) {
+ if (window.console && window.console.log)
+ window.console.log("STUBBYID: " + msg);
+ if (navigator.id.stubby.onlog)
+ navigator.id.stubby.onlog(msg);
+ };
+ var watchOptions = {
+ _onlogin: null,
+ _onlogout: null,
+ onlogin: function(assertion) {
+ if (watchOptions._onlogin) {
+ log("Calling onlogin().");
+ watchOptions._onlogin(assertion);
+ }
+ },
+ onlogout: function() {
+ if (watchOptions._onlogout) {
+ log("Calling onlogout().");
+ watchOptions._onlogout();
+ }
+ },
+ onready: function() {
+ if (watchOptions._onready) {
+ log("Calling onready().");
+ watchOptions._onready();
+ }
+ }
+
+ };
+
+ window.navigator.id = {
+ stubby: {
+ reset: function() {
+ watchOptions._onlogin = null;
+ watchOptions._onlogout = null;
+ watchOptions._onready = null;
+ setLoginState(null);
+ },
+ onlog: null,
+ setPersonaState: setLoginState,
+ getPersonaState: getLoginState,
+ widgetElement: widget.el
+ },
+ // https://developer.mozilla.org/en-US/docs/DOM/navigator.id.watch
+ watch: function navigator_id_watch(options) {
+ if (!(typeof(options.loggedInUser) == "undefined" ||
+ typeof(options.loggedInUser) == "string" ||
+ options.loggedInUser === null))
+ fail("loggedInUser must be null, undefined, or string");
+ if (typeof(options.onlogin) != "function")
+ fail("onlogin must be a function");
+ if (typeof(options.onlogout) != "function")
+ fail("onlogout must be a function");
+
+ var personaState = getLoginState();
+ var loggedInUser = options.loggedInUser;
+ var reasoning = "";
+
+ watchOptions._onlogin = options.onlogin;
+ watchOptions._onlogout = options.onlogout;
+ watchOptions._onready = options.onready;
+
+ if (typeof(loggedInUser) == "undefined") {
+ reasoning = "Client doesn't know if user is logged in or not ";
+ if (personaState) {
+ log(reasoning + "and they want to be logged in as " +
+ personaState + ".");
+ watchOptions.onlogin(personaState);
+ } else {
+ log(reasoning + "and they want to be logged out. ");
+ watchOptions.onlogout();
+ }
+ } else if (typeof(loggedInUser) == "string") {
+ reasoning = "Client thinks the user is logged in as " +
+ loggedInUser + " ";
+
+ if (personaState) {
+ if (personaState == loggedInUser) {
+ log(reasoning + "and they want to be, so doing nothing.");
+ watchOptions.onready(personaState);
+ } else {
+ log(reasoning + "but they want to be logged in as " +
+ personaState + ".");
+ watchOptions.onlogin(personaState);
+ }
+ } else {
+ log(reasoning + "but they want to be logged out.");
+ watchOptions.onlogout();
+ }
+ } else if (loggedInUser === null) {
+ reasoning = "Client thinks the user is logged out ";
+ if (personaState) {
+ log(reasoning + "but they want to be logged in as " +
+ personaState + ".");
+ watchOptions.onlogin(personaState);
+ } else {
+ log(reasoning + "and they want to be, so doing nothing.");
+ }
+ }
+ },
+ request: function navigator_id_request(options) {
+ options = options || {};
+ var response = window.prompt("Enter email address");
+ if (!response) {
+ if (options.oncancel)
+ options.oncancel();
+ return;
+ }
+ setLoginState(response);
+ },
+ logout: function navigator_id_logout() {
+ setLoginState(null);
+ },
+ get: function navigator_id_get(gotAssertion) {
+ var email = window.prompt("Enter email address") || null;
+ window.setTimeout(function() { gotAssertion(email); }, 1);
+ }
+ };
+
+ window.navigator.id.getVerifiedEmail = window.navigator.id.get;
+
+ if (document.readyState == "complete")
+ widget.init();
+ else
+ attach(window, "load", widget.init);
+})();
View
0  webpay/testing/tests/__init__.py
No changes.
View
37 webpay/testing/tests/test_testing_views.py
@@ -0,0 +1,37 @@
+from django import test
+from django.conf import settings
+from django.core.urlresolvers import reverse, NoReverseMatch
+
+import mock
+from nose.tools import eq_
+
+@mock.patch.object(settings, 'TEMPLATE_DEBUG', True)
+class TestTestingViews(test.TestCase):
+
+ def setUp(self):
+ super(TestTestingViews, self).setUp()
+ self.client = test.Client()
+
+ @mock.patch.object(settings, 'DEV', False)
+ @mock.patch.object(settings, 'TEST_PIN_UI', True)
+ def test_403_include(self):
+ url = reverse('fake_include')
+ eq_(self.client.get(url).status_code, 403)
+
+ @mock.patch.object(settings, 'DEV', False)
+ @mock.patch.object(settings, 'TEST_PIN_UI', True)
+ def test_403_verify(self):
+ url = reverse('fake_verify')
+ eq_(self.client.get(url).status_code, 403)
+
+ @mock.patch.object(settings, 'DEV', True)
+ @mock.patch.object(settings, 'TEST_PIN_UI', False)
+ def test_403_include_2(self):
+ url = reverse('fake_include')
+ eq_(self.client.get(url).status_code, 403)
+
+ @mock.patch.object(settings, 'DEV', True)
+ @mock.patch.object(settings, 'TEST_PIN_UI', False)
+ def test_403_verify_2(self):
+ url = reverse('fake_verify')
+ eq_(self.client.get(url).status_code, 403)
View
24 webpay/testing/urls.py
@@ -0,0 +1,24 @@
+from django.conf import settings
+from django.conf.urls.defaults import patterns, include, url
+from django.views.defaults import page_not_found, server_error
+from django.views.generic.base import TemplateView
+from urlparse import urlparse
+
+from webpay.testing.persona import fake_include, fake_verify
+
+# Remove leading and trailing slashes so the regex matches.
+path = urlparse(settings.MEDIA_URL).path
+media_url = path.lstrip('/').rstrip('/')
+
+urlpatterns = patterns('',
+ url(r'^404$', page_not_found, name="error_404"),
+ url(r'^500$', server_error, name="error_500"),
+ url(r'^include.js$', fake_include, name="fake_include"),
+ url(r'^verify$', fake_verify, name="fake_verify"),
+ (r'^was-locked/$', TemplateView.as_view(
+ template_name='pin/pin_was_locked.html')),
+ (r'^is-locked/$', TemplateView.as_view(
+ template_name='pin/pin_is_locked.html')),
+ (r'^%s/(?P<path>.*)$' % media_url, 'django.views.static.serve',
+ {'document_root': settings.MEDIA_ROOT}),
+)
View
18 webpay/urls.py
@@ -37,22 +37,8 @@
# (r'^admin/', include(admin.site.urls)),
)
+# Test/Development only urls.
if settings.TEMPLATE_DEBUG:
-
- from django.views.defaults import page_not_found, server_error
- from django.views.generic.base import TemplateView
- from urlparse import urlparse
-
- # Remove leading and trailing slashes so the regex matches.
- path = urlparse(settings.MEDIA_URL).path
- media_url = path.lstrip('/').rstrip('/')
urlpatterns += patterns('',
- url(r'^404$', page_not_found, name="error_404"),
- url(r'^500$', server_error, name="error_500"),
- (r'^was-locked/$', TemplateView.as_view(
- template_name='pin/pin_was_locked.html')),
- (r'^is-locked/$', TemplateView.as_view(
- template_name='pin/pin_is_locked.html')),
- (r'^%s/(?P<path>.*)$' % media_url, 'django.views.static.serve',
- {'document_root': settings.MEDIA_ROOT}),
+ url(r'^', include('webpay.testing.urls')),
)
Please sign in to comment.
Something went wrong with that request. Please try again.