@@ -13,6 +13,7 @@ export default BaseView.extend({
<div class="settings-menu no-display">
<ul>
<li class="user-name" data-hook='user-name'></li>
<li><a data-l10n-id="menuWiki" data-hook="wiki" href="https://wiki.mozilla.org/Test_Pilot" target="_blank">Test Pilot Wiki</a></li>
<li><a data-l10n-id="menuFileIssue" data-hook="issue" href="https://github.com/mozilla/testpilot/issues/new" target="_blank">File an Issue</a></li>
<li><a data-l10n-id="menuLogout" data-hook="logout">Logout</a></li>
@@ -30,6 +31,16 @@ export default BaseView.extend({
'click [data-hook=settings-button]': 'toggleSettings'
},

props: {
'userName': {
'type': 'string'
}
},

bindings: {
'userName': '[data-hook=user-name]'
},

afterRender() {
document.body.addEventListener('click', this.close.bind(this));
},
@@ -99,9 +99,10 @@
@include respond-to('not-small') {
font-size: $font-unit * 2;
height: $grid-unit * 3;
line-height: $grid-unit * 3;
padding: 0 $grid-unit * 1.5;
}

box-shadow: 0 2px 0 0 $transparent-black-2;
}

@@ -242,6 +242,7 @@

.tour-next,
.tour-back {
cursor: pointer;
height: 40px;
width: 40px;
}
@@ -51,18 +51,26 @@
background: $white;
border-radius: $small-border-radius;
box-shadow: 1px 1px 3px 1px $transparent-black-2;
left: -$grid-unit * 5.7;
left: -$grid-unit * 6.7;
position: absolute;
top: $grid-unit * 2.25;
width: $grid-unit * 8;
width: $grid-unit * 9;
z-index: 999;

.user-name {
color: $black;
font-size: $font-unit;
margin-bottom: $grid-unit * .25;
overflow: hidden;
text-overflow: ellipsis;
}

&:after {
border-bottom: $grid-unit * .5 solid $white;
border-left: ($grid-unit * .5) + 1 solid transparent;
border-right: ($grid-unit * .5) + 1 solid transparent;
content: '';
left: 126px;
left: 146px;
position: absolute;
top: -($grid-unit * .5);
}
@@ -88,4 +96,8 @@
li {
list-style: none;
}

hr {
color: $transparent-black-2;
}
}
@@ -9,13 +9,14 @@
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<meta name="viewport" content="width=device-width">
<link rel="localization" href="{{ static('locales/{locale}/app.l20n') }}">
<link rel="localization" href="{{ settings.STATIC_URL }}locales/{locale}/app.l20n">

</head>
<body>

<div data-hook="page-container"></div>

<script src="{{ url('wafflejs') }}"></script>
<script src="{{ static('app/app.js') }}"></script>
<script src="https://pontoon.mozilla.org/pontoon.js"></script>
<script>
@@ -15,6 +15,7 @@
from os.path import abspath
from django.utils.functional import lazy
from pathlib import Path
from django_jinja.builtins import DEFAULT_EXTENSIONS as DJANGO_JINJA_DEFAULT_EXTENSIONS

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
@@ -80,6 +81,9 @@ def path(*args):
'markupfield',
'product_details',
'hvad',
'waffle',
'constance.backends.database',
'constance',

# FxA auth handling
'allauth',
@@ -107,6 +111,7 @@ def path(*args):
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'waffle.middleware.WaffleMiddleware',
'testpilot.base.middleware.RequestSummaryLogger',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
@@ -166,17 +171,6 @@ def path(*args):

ACCOUNT_EMAIL_VERIFICATION = False

ACCOUNT_INVITE_ONLY_MODE = config('ACCOUNT_INVITE_ONLY_MODE', default=True, cast=bool)

ACCOUNT_AUTOACTIVATION_DOMAINS = config(
'ACCOUNT_AUTOACTIVATION_DOMAINS', default='mozilla.com',
cast=lambda domains: tuple('@{0}'.format(s.strip()) for s in domains.split(',')))


MOZILLIANS_API_KEY = config('MOZILLIANS_API_KEY', default=None)

MOZILLIANS_API_BASE_URL = config('MOZILLIANS_API_BASE_URL',
default='https://mozillians.org/api/v2')

DATADOG_KEYS = {
'api_key': config('DATADOG_API_KEY', default=None),
@@ -319,6 +313,8 @@ def lazy_langs():
AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID', default=None)
AWS_SECRET_ACCESS_KEY = config('AWS_SECRET_ACCESS_KEY', default=None)
AWS_STORAGE_BUCKET_NAME = config('AWS_STORAGE_BUCKET_NAME')
AWS_QUERYSTRING_AUTH = False
AWS_S3_CUSTOM_DOMAIN = config('AWS_S3_CUSTOM_DOMAIN', default=None)
MEDIA_URL = config('MEDIA_URL')
else:
MEDIA_ROOT = config('MEDIA_ROOT', default=os.path.join(BASE_DIR, 'media'))
@@ -337,6 +333,9 @@ def lazy_langs():
'match_regex': r'^(?!(admin|registration|rest_framework)/.*)',
'match_extension': '.html',
'newstyle_gettext': True,
'extensions': DJANGO_JINJA_DEFAULT_EXTENSIONS + [
'waffle.jinja.WaffleExtension',
],
'context_processors': [
'testpilot.base.context_processors.settings',
'testpilot.base.context_processors.i18n',
@@ -465,3 +464,7 @@ def lazy_langs():
},
}
}

CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
CONSTANCE_CONFIG = {
}
@@ -22,5 +22,6 @@
url(r'^api/experiments/', include('testpilot.experiments.urls')),
url(r'^api/', include(router.urls)),
# Catch-all fallback to frontend client view
url(r'^', include('waffle.urls')),
url(r'', include('testpilot.frontend.urls')),
)
@@ -4,6 +4,3 @@
class TestPilotUsersAppConfig(AppConfig):
name = 'testpilot.users'
verbose_name = 'Test Pilot Users'

def ready(self):
import testpilot.users.signals # noqa

This file was deleted.

@@ -1,6 +1,5 @@
from unittest.mock import patch, Mock

import requests
from requests.exceptions import Timeout
import json

@@ -12,19 +11,15 @@

from rest_framework import fields

from allauth.account.signals import user_signed_up
from allauth.account.models import EmailAddress
from allauth.socialaccount.signals import pre_social_login
from allauth.socialaccount.models import (SocialLogin, SocialApp,
SocialAccount, SocialToken)
from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken

from .providers.fxa.provider import FirefoxAccountsProvider

from ..utils import gravatar_url
from ..experiments.models import (Experiment, UserInstallation)

from .models import UserProfile
from .signals import is_vouched_on_mozillians_org

import logging
logger = logging.getLogger(__name__)
@@ -299,258 +294,6 @@ def test_get_logged_in(self):
)


class InviteOnlyModeTests(TestCase):

def setUp(self):
self.username = 'newuserdoe2'
self.password = 'trustno1'
self.email = '%s@example.com' % self.username

self.user = User.objects.create_user(
username=self.username,
email=self.email,
password=self.password)

UserProfile.objects.filter(user=self.user).delete()

@patch('testpilot.users.signals.is_vouched_on_mozillians_org')
@override_settings(ACCOUNT_INVITE_ONLY_MODE=True)
def test_invite_only_signup(self, mock_vouched):
"""User.is_active should be False on signup with invite mode"""
mock_vouched.return_value = False
self.user.is_active = True
user_signed_up.send(sender=self.user.__class__,
request=None,
user=self.user)

self.assertEqual(False, self.user.is_active)

profile = UserProfile.objects.get_profile(self.user)
self.assertEqual(True, profile.invite_pending)

@patch('testpilot.users.signals.is_vouched_on_mozillians_org')
@override_settings(ACCOUNT_INVITE_ONLY_MODE=True,
ACCOUNT_AUTOACTIVATION_DOMAINS=('mozilla.com', 'hy.fr'))
def test_invite_only_account_autoactivation_domains(self, mock_vouched):
"""Users with valid email address domains should be auto-activated"""
mock_vouched.return_value = False
self.user.is_active = True

for email in ['someone@mozilla.com', 'someone@hy.fr']:

self.user.email = email

user_signed_up.send(sender=self.user.__class__,
request=None,
user=self.user)

self.assertEqual(True, self.user.is_active)

profile = UserProfile.objects.get_profile(self.user)
self.assertEqual(False, profile.invite_pending,
'Invite not expected for {0}'.format(email))

mock_vouched.assert_not_called()
mock_vouched.reset_mock()

for email in ['someone@aol.com', 'someone@mozilla.com@aol.com']:

self.user.email = email

user_signed_up.send(sender=self.user.__class__,
request=None,
user=self.user)

self.assertEqual(False, self.user.is_active)

profile = UserProfile.objects.get_profile(self.user)
self.assertEqual(True, profile.invite_pending,
'Invite expected for {0}'.format(email))

mock_vouched.assert_called_once_with(self.user)
mock_vouched.reset_mock()

@patch('testpilot.users.signals.is_vouched_on_mozillians_org')
@override_settings(ACCOUNT_INVITE_ONLY_MODE=True)
def test_invite_only_mozillians_autoactivation(self, mock_vouched):
"""Users vouched on mozillians.org should be auto-activated"""
mock_vouched.return_value = True
self.user.is_active = True
self.user.email = 'job@bluth.com'

user_signed_up.send(sender=self.user.__class__,
request=None,
user=self.user)

self.assertEqual(True, self.user.is_active)

profile = UserProfile.objects.get_profile(self.user)
self.assertEqual(False, profile.invite_pending)

@patch('testpilot.users.signals.is_vouched_on_mozillians_org')
@override_settings(ACCOUNT_INVITE_ONLY_MODE=False)
def test_open_signup(self, mock_vouched):
"""User.is_active should be True on signup without invite mode"""
self.user.is_active = True
user_signed_up.send(sender=self.user.__class__,
request=None,
user=self.user)
self.assertEqual(True, self.user.is_active)

profile = UserProfile.objects.get_profile(self.user)
self.assertEqual(False, profile.invite_pending)

mock_vouched.assert_not_called()

@patch('requests.get')
@override_settings(MOZILLIANS_API_KEY='8675309',
MOZILLIANS_API_BASE_URL='https://example.com')
def test_is_vouched_on_mozillians_org_exception(self, mock_requests_get):
"""Check for vouched Mozillian membership should yield False on HTTP error"""
email = 'bob@loblaw.com'
mock_requests_get.side_effect = requests.exceptions.RequestException()
result_vouched = is_vouched_on_mozillians_org(User(email=email))
self.assertEqual(False, result_vouched)

def test_is_vouched_on_mozillians_org(self):
"""Check for vouched Mozillian membership should use the API"""
for (email, expected_vouched) in (('user1@exmaple.com', False),
('user2@example.com', True)):
self._assert_is_vouched_on_mozillians(
expected_vouched,
email,
{
'count': 1,
'next': None,
'previous': None,
'results': [
{
'_url': 'https://mozillians.org/api/v2/users/12345/',
'is_vouched': expected_vouched,
'username': 'user'
}
]
}
)

def test_is_vouched_on_mozillians_org_unknown_user(self):
"""Check for vouched Mozillian membership should properly handle unknown user"""
self._assert_is_vouched_on_mozillians(
False,
'user3@example.com',
{'count': 0, 'next': None, 'previous': None, 'results': []}
)

@patch('requests.get')
@override_settings(MOZILLIANS_API_KEY='8675309',
MOZILLIANS_API_BASE_URL='https://example.com')
def _assert_is_vouched_on_mozillians(self, expected_vouched, email,
mock_api_data, mock_requests_get):

expected_api_url = '%(base_url)s/users/?api-key=%(api_key)s&email=%(email)s' % dict(
base_url='https://example.com',
api_key='8675309',
email=email
)

mock_json = Mock(return_value={
'count': 1,
'next': None,
'previous': None,
'results': [
{
'_url': 'https://mozillians.org/api/v2/users/12345/',
'is_vouched': expected_vouched,
'username': 'user'
}
]
})

mock_response = Mock()
mock_response.json = mock_json

mock_requests_get.return_value = mock_response

result_vouched = is_vouched_on_mozillians_org(User(email=email))
self.assertEqual(expected_vouched, result_vouched)

mock_json.assert_called_with()
mock_requests_get.assert_called_with(expected_api_url)

@patch('testpilot.users.signals.is_vouched_on_mozillians_org')
@override_settings(ACCOUNT_INVITE_ONLY_MODE=True)
def test_auto_activiate_if_qualified_after_signup(self, mock_vouched):
"""Users qualified after sign-up should be auto-activated on sign-in"""
mock_vouched.return_value = False

sociallogin = SocialLogin(user=self.user)

self.user.is_active = True
user_signed_up.send(sender=self.user.__class__,
request=None,
user=self.user)

self.assertEqual(False, self.user.is_active)

pre_social_login.send(sender=SocialLogin,
request=None,
sociallogin=sociallogin)

self.assertEqual(False, self.user.is_active)

profile = UserProfile.objects.get_profile(self.user)
self.assertEqual(True, profile.invite_pending)

mock_vouched.return_value = True

pre_social_login.send(sender=SocialLogin,
request=None,
sociallogin=sociallogin)

self.assertEqual(True, self.user.is_active)

profile = UserProfile.objects.get_profile(self.user)
self.assertEqual(False, profile.invite_pending)

@patch('testpilot.users.signals.is_vouched_on_mozillians_org')
def test_auto_activate_after_settings_change(self, mock_vouched):
"""Pending invitations should be auto-activated on sign-in after invite mode turned off"""
mock_vouched.return_value = False
sociallogin = SocialLogin(user=self.user)

with self.settings(ACCOUNT_INVITE_ONLY_MODE=True):

self.user.is_active = True
user_signed_up.send(sender=self.user.__class__,
request=None,
user=self.user)

self.assertEqual(False, self.user.is_active)

profile = UserProfile.objects.get_profile(self.user)
self.assertEqual(True, profile.invite_pending)

pre_social_login.send(sender=SocialLogin,
request=None,
sociallogin=sociallogin)

self.assertEqual(False, self.user.is_active)

profile = UserProfile.objects.get_profile(self.user)
self.assertEqual(True, profile.invite_pending)

with self.settings(ACCOUNT_INVITE_ONLY_MODE=False):

pre_social_login.send(sender=SocialLogin,
request=None,
sociallogin=sociallogin)

self.assertEqual(True, self.user.is_active)

profile = UserProfile.objects.get_profile(self.user)
self.assertEqual(False, profile.invite_pending)


class UserRetirementTests(TestCase):

@classmethod