Get team invite confirmation working and fully tested

This commit is contained in:
Joseph Schorr 2014-08-18 17:24:00 -04:00
parent eefb7e1ec9
commit 43b6695f9c
12 changed files with 458 additions and 43 deletions

View file

@ -113,6 +113,7 @@ class TeamMemberInvite(BaseModel):
user = ForeignKeyField(User, index=True, null=True)
email = CharField(null=True)
team = ForeignKeyField(Team, index=True)
inviter = ForeignKeyField(User, related_name='inviter')
invite_token = CharField(default=uuid_generator)

View file

@ -71,6 +71,10 @@ class TooManyUsersException(DataModelException):
pass
class UserAlreadyInTeam(DataModelException):
pass
def is_create_user_allowed():
return get_active_user_count() < config.app_config['LICENSE_USER_LIMIT']
@ -294,7 +298,7 @@ def remove_team(org_name, team_name, removed_by_username):
team.delete_instance(recursive=True, delete_nullable=True)
def add_or_invite_to_team(team, user=None, email=None, adder=None):
def add_or_invite_to_team(inviter, team, user=None, email=None):
# If the user is a member of the organization, then we simply add the
# user directly to the team. Otherwise, an invite is created for the user/email.
# We return None if the user was directly added and the invite object if the user was invited.
@ -326,15 +330,16 @@ def add_or_invite_to_team(team, user=None, email=None, adder=None):
add_user_to_team(user, team)
return None
return TeamMemberInvite.create(user=user, email=email if not user else None, team=team)
return TeamMemberInvite.create(user=user, email=email if not user else None, team=team,
inviter=inviter)
def add_user_to_team(user, team):
try:
return TeamMember.create(user=user, team=team)
except Exception:
raise DataModelException('User \'%s\' is already a member of team \'%s\'' %
(user.username, team.name))
raise UserAlreadyInTeam('User \'%s\' is already a member of team \'%s\'' %
(user.username, team.name))
def remove_user_from_team(org_name, team_name, username, removed_by_username):
@ -1766,6 +1771,32 @@ def delete_notifications_by_kind(target, kind_name):
Notification.delete().where(Notification.target == target,
Notification.kind == kind_ref).execute()
def delete_matching_notifications(target, kind_name, **kwargs):
kind_ref = NotificationKind.get(name=kind_name)
# Load all notifications for the user with the given kind.
notifications = list(Notification.select().where(
Notification.target == target,
Notification.kind == kind_ref))
# For each, match the metadata to the specified values.
for notification in notifications:
matches = True
try:
metadata = json.loads(notification.metadata_json)
except:
continue
for (key, value) in kwargs.iteritems():
if not key in metadata or metadata[key] != value:
matches = False
break
if not matches:
continue
notification.delete_instance()
def get_active_users():
return User.select().where(User.organization == False, User.robot == False)
@ -1821,3 +1852,51 @@ def confirm_email_authorization_for_repo(code):
found.save()
return found
def lookup_team_invites(user):
return TeamMemberInvite.select().where(TeamMemberInvite.user == user)
def lookup_team_invite(code, user):
# Lookup the invite code.
try:
found = TeamMemberInvite.get(TeamMemberInvite.invite_token == code)
except TeamMemberInvite.DoesNotExist:
raise DataModelException('Invalid confirmation code.')
# Verify the code applies to the current user.
if found.user:
if found.user != user:
raise DataModelException('Invalid confirmation code.')
else:
if found.email != user.email:
raise DataModelException('Invalid confirmation code.')
return found
def delete_team_invite(code, user):
found = lookup_team_invite(code, user)
team = found.team
inviter = found.inviter
found.delete_instance()
return (team, inviter)
def confirm_team_invite(code, user):
found = lookup_team_invite(code, user)
# Add the user to the team.
try:
add_user_to_team(user, found.team)
except UserAlreadyInTeam:
# Ignore.
pass
# Delete the invite and return the team.
team = found.team
inviter = found.inviter
found.delete_instance()
return (team, inviter)

View file

@ -2,7 +2,7 @@ from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
log_action, Unauthorized, NotFound, internal_only, require_scope,
query_param, truthy_bool, parse_args)
query_param, truthy_bool, parse_args, require_user_admin)
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
from auth.auth_context import get_authenticated_user
from auth import scopes
@ -10,8 +10,8 @@ from data import model
from util.useremails import send_org_invite_email
from util.gravatar import compute_hash
def add_or_invite_to_team(team, user=None, email=None, adder=None):
invite = model.add_or_invite_to_team(team, user, email, adder)
def add_or_invite_to_team(inviter, team, user=None, email=None):
invite = model.add_or_invite_to_team(inviter, team, user, email)
if not invite:
# User was added to the team directly.
return
@ -20,13 +20,13 @@ def add_or_invite_to_team(team, user=None, email=None, adder=None):
if user:
model.create_notification('org_team_invite', user, metadata = {
'code': invite.invite_token,
'adder': adder,
'inviter': inviter.username,
'org': orgname,
'team': team.name
})
send_org_invite_email(user.username if user else email, user.email if user else email,
orgname, team.name, adder, invite.invite_token)
orgname, team.name, inviter.username, invite.invite_token)
return invite
def team_view(orgname, team):
@ -204,11 +204,8 @@ class TeamMember(ApiResource):
raise request_error(message='Unknown user')
# Add or invite the user to the team.
adder = None
if get_authenticated_user():
adder = get_authenticated_user().username
invite = add_or_invite_to_team(team, user=user, adder=adder)
inviter = get_authenticated_user()
invite = add_or_invite_to_team(inviter, team, user=user)
if not invite:
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
return member_view(user, invited=False)
@ -232,3 +229,52 @@ class TeamMember(ApiResource):
return 'Deleted', 204
raise Unauthorized()
@resource('/v1/teaminvite/<code>')
@internal_only
class TeamMemberInvite(ApiResource):
""" Resource for managing invites to jon a team. """
@require_user_admin
@nickname('acceptOrganizationTeamInvite')
def put(self, code):
""" Accepts an invite to join a team in an organization. """
# Accept the invite for the current user.
try:
(team, inviter) = model.confirm_team_invite(code, get_authenticated_user())
except model.DataModelException:
raise NotFound()
model.delete_matching_notifications(get_authenticated_user(), 'org_team_invite', code=code)
orgname = team.organization.username
log_action('org_team_member_invite_accepted', orgname, {
'member': get_authenticated_user().username,
'team': team.name,
'inviter': inviter.username
})
return {
'org': orgname,
'team': team.name
}
@nickname('declineOrganizationTeamInvite')
@require_user_admin
def delete(self, code):
""" Delete an existing member of a team. """
try:
(team, inviter) = model.delete_team_invite(code, get_authenticated_user())
except model.DataModelException:
raise NotFound()
model.delete_matching_notifications(get_authenticated_user(), 'org_team_invite', code=code)
orgname = team.organization.username
log_action('org_team_member_invite_declined', orgname, {
'member': get_authenticated_user().username,
'team': team.name,
'inviter': inviter.username
})
return 'Deleted', 204

View file

@ -32,8 +32,8 @@ STATUS_TAGS = app.config['STATUS_TAGS']
@web.route('/', methods=['GET'], defaults={'path': ''})
@web.route('/organization/<path:path>', methods=['GET'])
@no_cache
def index(path):
return render_page_template('index.html')
def index(path, **kwargs):
return render_page_template('index.html', **kwargs)
@web.route('/500', methods=['GET'])
@ -101,7 +101,7 @@ def superuser():
@web.route('/signin/')
@no_cache
def signin():
def signin(redirect=None):
return index('')
@ -123,6 +123,13 @@ def new():
return index('')
@web.route('/confirminvite')
@no_cache
def confirm_invite():
code = request.values['code']
return index('', code=code)
@web.route('/repository/', defaults={'path': ''})
@web.route('/repository/<path:path>', methods=['GET'])
@no_cache

View file

@ -214,6 +214,8 @@ def initialize_database():
LogEntryKind.create(name='org_delete_team')
LogEntryKind.create(name='org_invite_team_member')
LogEntryKind.create(name='org_add_team_member')
LogEntryKind.create(name='org_team_member_invite_accepted')
LogEntryKind.create(name='org_team_member_invite_declined')
LogEntryKind.create(name='org_remove_team_member')
LogEntryKind.create(name='org_set_team_description')
LogEntryKind.create(name='org_set_team_role')

View file

@ -535,7 +535,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
stringBuilderService.buildString = function(value_or_func, metadata) {
var fieldIcons = {
'adder': 'user',
'inviter': 'user',
'username': 'user',
'activating_username': 'user',
'delegate_user': 'user',
@ -1115,8 +1115,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
return externalNotificationData;
}]);
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config',
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config) {
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config', '$location',
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config, $location) {
var notificationService = {
'user': null,
'notifications': [],
@ -1135,15 +1135,24 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
},
'org_team_invite': {
'level': 'primary',
'message': '{adder} is inviting you to join team {team} under organization {org}',
'message': '{inviter} is inviting you to join team {team} under organization {org}',
'actions': [
{
'title': 'Join team',
'kind': 'primary',
'handler': function(notification) {
window.location = '/confirminvite?code=' + notification.metadata['code'];
}
},
{'title': 'Decline', 'kind': 'default'}
{
'title': 'Decline',
'kind': 'default',
'handler': function(notification) {
ApiService.declineOrganizationTeamInvite(null, {'code': notification.metadata['code']}).then(function() {
notificationService.update();
});
}
}
]
},
'password_required': {
@ -1725,7 +1734,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
templateUrl: '/static/partials/security.html'}).
when('/signin/', {title: 'Sign In', description: 'Sign into ' + title, templateUrl: '/static/partials/signin.html'}).
when('/signin/', {title: 'Sign In', description: 'Sign into ' + title, templateUrl: '/static/partials/signin.html', controller: SignInCtrl, reloadOnSearch: false}).
when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile',
templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
@ -1746,6 +1755,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
when('/tour/features', {title: title + ' Features', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
when('/tour/enterprise', {title: 'Enterprise Edition', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
when('/confirminvite', {title: 'Confirm Team Invite', templateUrl: '/static/partials/confirm-team-invite.html', controller: ConfirmInviteCtrl, reloadOnSearch: false}).
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl,
pageClass: 'landing-page'}).
otherwise({redirectTo: '/'});
@ -2244,6 +2255,10 @@ quayApp.directive('signinForm', function () {
'signedIn': '&signedIn'
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
var getRedirectUrl = function() {
return $scope.redirectUrl;
};
$scope.showGithub = function() {
if (!Features.GITHUB_LOGIN) { return; }
@ -2255,7 +2270,7 @@ quayApp.directive('signinForm', function () {
}
// Save the redirect URL in a cookie so that we can redirect back after GitHub returns to us.
var redirectURL = $scope.redirectUrl || window.location.toString();
var redirectURL = getRedirectUrl() || window.location.toString();
CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);
// Needed to ensure that UI work done by the started callback is finished before the location
@ -2283,17 +2298,19 @@ quayApp.directive('signinForm', function () {
if ($scope.signedIn != null) {
$scope.signedIn();
}
// Load the newly created user.
UserService.load();
// Redirect to the specified page or the landing page
// Note: The timeout of 500ms is needed to ensure dialogs containing sign in
// forms get removed before the location changes.
$timeout(function() {
if ($scope.redirectUrl == $location.path()) {
return;
}
$location.path($scope.redirectUrl ? $scope.redirectUrl : '/');
var redirectUrl = getRedirectUrl();
if (redirectUrl == $location.path()) {
return;
}
window.location = (redirectUrl ? redirectUrl : '/');
}, 500);
}, function(result) {
$scope.needsEmailVerification = result.data.needsEmailVerification;
@ -2629,8 +2646,12 @@ quayApp.directive('logsView', function () {
'org_create_team': 'Create team: {team}',
'org_delete_team': 'Delete team: {team}',
'org_add_team_member': 'Add member {member} to team {team}',
'org_invite_team_member': 'Invite user {member} to team {team}',
'org_remove_team_member': 'Remove member {member} from team {team}',
'org_invite_team_member': 'Invite user {member} to team {team}',
'org_team_member_invite_accepted': 'User {member}, invited by {inviter}, accepted to join team {team}',
'org_team_member_invite_declined': 'User {member}, invited by {inviter}, declined to join team {team}',
'org_set_team_description': 'Change description of team {team}: {description}',
'org_set_team_role': 'Change permission of team {team} to {role}',
'create_prototype_permission': function(metadata) {
@ -2711,6 +2732,8 @@ quayApp.directive('logsView', function () {
'org_add_team_member': 'Add team member',
'org_invite_team_member': 'Invite team member',
'org_remove_team_member': 'Remove team member',
'org_team_member_invite_accepted': 'Team invite accepted',
'org_team_member_invite_declined': 'Team invite declined',
'org_set_team_description': 'Change team description',
'org_set_team_role': 'Change team permission',
'create_prototype_permission': 'Create default permission',
@ -5346,7 +5369,7 @@ quayApp.directive('dockerfileBuildForm', function () {
var data = {
'mimeType': mimeType
};
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
conductUpload(file, resp.url, resp.file_id, mimeType);
}, function() {

View file

@ -20,6 +20,17 @@ $.fn.clipboardCopy = function() {
});
};
function SignInCtrl($scope, $location) {
var redirect = $location.search()['redirect'];
if (redirect && redirect.indexOf('/') < 0) {
delete $location.search()['redirect'];
$scope.redirectUrl = '/' + redirect;
return;
}
$scope.redirectUrl = '/';
}
function GuideCtrl() {
}
@ -2855,3 +2866,28 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
function TourCtrl($scope, $location) {
$scope.kind = $location.path().substring('/tour/'.length);
}
function ConfirmInviteCtrl($scope, $location, UserService, ApiService, NotificationService) {
// Monitor any user changes and place the current user into the scope.
$scope.loading = false;
UserService.updateUserIn($scope, function(user) {
if (!user.anonymous && !$scope.loading) {
$scope.loading = true;
var params = {
'code': $location.search()['code']
};
ApiService.acceptOrganizationTeamInvite(null, params).then(function(resp) {
NotificationService.update();
$location.path('/organization/' + resp.org + '/teams/' + resp.team);
}, function() {
$scope.loading = false;
$scope.invalid = true;
});
}
});
$scope.redirectUrl = 'confirminvite?code=' + $location.search()['code'];
}

View file

@ -0,0 +1,13 @@
<div class="confirm-team-invite">
<div class="container signin-container">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="user-setup" ng-show="user.anonymous" redirect-url="redirectUrl"></div>
<div class="quay-spinner" ng-show="!user.anonymous && loading"></div>
<div class="alert alert-danger" ng-show="!user.anonymous && invalid">
Invalid confirmation code
</div>
</div>
</div>
</div>
</div>

View file

@ -1,7 +1,7 @@
<div class="container signin-container">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="user-setup" redirect-url="'/'"></div>
<div class="user-setup" redirect-url="redirectUrl"></div>
</div>
</div>
</div>

View file

@ -8,7 +8,7 @@ from app import app
from initdb import setup_database_for_testing, finished_database_for_testing
from endpoints.api import api_bp, api
from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam
from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite
from endpoints.api.tag import RepositoryTagImages, RepositoryTag
from endpoints.api.search import FindRepositories, EntitySearch
from endpoints.api.image import RepositoryImageChanges, RepositoryImage, RepositoryImageList
@ -3424,6 +3424,36 @@ class TestSuperUserLogs(ApiTestCase):
self._run_test('GET', 200, 'devtable', None)
class TestTeamMemberInvite(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(TeamMemberInvite, code='foobarbaz')
def test_put_anonymous(self):
self._run_test('PUT', 401, None, None)
def test_put_freshuser(self):
self._run_test('PUT', 404, 'freshuser', None)
def test_put_reader(self):
self._run_test('PUT', 404, 'reader', None)
def test_put_devtable(self):
self._run_test('PUT', 404, 'devtable', None)
def test_delete_anonymous(self):
self._run_test('DELETE', 401, None, None)
def test_delete_freshuser(self):
self._run_test('DELETE', 404, 'freshuser', None)
def test_delete_reader(self):
self._run_test('DELETE', 404, 'reader', None)
def test_delete_devtable(self):
self._run_test('DELETE', 404, 'devtable', None)
class TestSuperUserList(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
@ -3442,7 +3472,6 @@ class TestSuperUserList(ApiTestCase):
self._run_test('GET', 200, 'devtable', None)
class TestSuperUserManagement(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)

View file

@ -11,7 +11,7 @@ from app import app
from initdb import setup_database_for_testing, finished_database_for_testing
from data import model, database
from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam
from endpoints.api.team import TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam
from endpoints.api.tag import RepositoryTagImages, RepositoryTag
from endpoints.api.search import FindRepositories, EntitySearch
from endpoints.api.image import RepositoryImage, RepositoryImageList
@ -734,16 +734,50 @@ class TestGetOrganizationTeamMembers(ApiTestCase):
params=dict(orgname=ORGANIZATION,
teamname='readers'))
assert READ_ACCESS_USER in json['members']
self.assertEquals(READ_ACCESS_USER, json['members'][1]['name'])
class TestUpdateOrganizationTeamMember(ApiTestCase):
def test_addmember(self):
def assertInTeam(self, data, membername):
for memberData in data['members']:
if memberData['name'] == membername:
return
self.fail(membername + ' not found in team: ' + json.dumps(data))
def test_addmember_alreadyteammember(self):
self.login(ADMIN_ACCESS_USER)
membername = READ_ACCESS_USER
self.putResponse(TeamMember,
params=dict(orgname=ORGANIZATION, teamname='readers',
membername=membername),
expected_code=400)
def test_addmember_orgmember(self):
self.login(ADMIN_ACCESS_USER)
membername = READ_ACCESS_USER
self.putJsonResponse(TeamMember,
params=dict(orgname=ORGANIZATION, teamname='owners',
membername=membername))
# Verify the user was added to the team.
json = self.getJsonResponse(TeamMemberList,
params=dict(orgname=ORGANIZATION,
teamname='owners'))
self.assertInTeam(json, membername)
def test_addmember_robot(self):
self.login(ADMIN_ACCESS_USER)
membername = ORGANIZATION + '+coolrobot'
self.putJsonResponse(TeamMember,
params=dict(orgname=ORGANIZATION, teamname='readers',
membername=NO_ACCESS_USER))
membername=membername))
# Verify the user was added to the team.
@ -751,7 +785,152 @@ class TestUpdateOrganizationTeamMember(ApiTestCase):
params=dict(orgname=ORGANIZATION,
teamname='readers'))
assert NO_ACCESS_USER in json['members']
self.assertInTeam(json, membername)
def test_addmember_invalidrobot(self):
self.login(ADMIN_ACCESS_USER)
membername = 'freshuser+anotherrobot'
self.putResponse(TeamMember,
params=dict(orgname=ORGANIZATION, teamname='readers',
membername=membername),
expected_code=400)
def test_addmember_nonorgmember(self):
self.login(ADMIN_ACCESS_USER)
membername = NO_ACCESS_USER
response = self.putJsonResponse(TeamMember,
params=dict(orgname=ORGANIZATION, teamname='owners',
membername=membername))
self.assertEquals(True, response['invited'])
# Make sure the user is not (yet) part of the team.
json = self.getJsonResponse(TeamMemberList,
params=dict(orgname=ORGANIZATION,
teamname='readers'))
for member in json['members']:
self.assertNotEqual(membername, member['name'])
class TestAcceptTeamMemberInvite(ApiTestCase):
def assertInTeam(self, data, membername):
for memberData in data['members']:
if memberData['name'] == membername:
return
self.fail(membername + ' not found in team: ' + json.dumps(data))
def test_accept_wronguser(self):
self.login(ADMIN_ACCESS_USER)
# Create the invite.
membername = NO_ACCESS_USER
response = self.putJsonResponse(TeamMember,
params=dict(orgname=ORGANIZATION, teamname='owners',
membername=membername))
self.assertEquals(True, response['invited'])
# Try to accept the invite.
user = model.get_user(membername)
invites = list(model.lookup_team_invites(user))
self.assertEquals(1, len(invites))
self.putResponse(TeamMemberInvite,
params=dict(code=invites[0].invite_token),
expected_code=404)
def test_accept(self):
self.login(ADMIN_ACCESS_USER)
# Create the invite.
membername = NO_ACCESS_USER
response = self.putJsonResponse(TeamMember,
params=dict(orgname=ORGANIZATION, teamname='owners',
membername=membername))
self.assertEquals(True, response['invited'])
# Login as the user.
self.login(membername)
# Accept the invite.
user = model.get_user(membername)
invites = list(model.lookup_team_invites(user))
self.assertEquals(1, len(invites))
self.putJsonResponse(TeamMemberInvite,
params=dict(code=invites[0].invite_token))
# Verify the user is now on the team.
json = self.getJsonResponse(TeamMemberList,
params=dict(orgname=ORGANIZATION,
teamname='owners'))
self.assertInTeam(json, membername)
# Verify the accept now fails.
self.putResponse(TeamMemberInvite,
params=dict(code=invites[0].invite_token),
expected_code=404)
class TestDeclineTeamMemberInvite(ApiTestCase):
def test_decline_wronguser(self):
self.login(ADMIN_ACCESS_USER)
# Create the invite.
membername = NO_ACCESS_USER
response = self.putJsonResponse(TeamMember,
params=dict(orgname=ORGANIZATION, teamname='owners',
membername=membername))
self.assertEquals(True, response['invited'])
# Try to decline the invite.
user = model.get_user(membername)
invites = list(model.lookup_team_invites(user))
self.assertEquals(1, len(invites))
self.deleteResponse(TeamMemberInvite,
params=dict(code=invites[0].invite_token),
expected_code=404)
def test_decline(self):
self.login(ADMIN_ACCESS_USER)
# Create the invite.
membername = NO_ACCESS_USER
response = self.putJsonResponse(TeamMember,
params=dict(orgname=ORGANIZATION, teamname='owners',
membername=membername))
self.assertEquals(True, response['invited'])
# Login as the user.
self.login(membername)
# Decline the invite.
user = model.get_user(membername)
invites = list(model.lookup_team_invites(user))
self.assertEquals(1, len(invites))
self.deleteResponse(TeamMemberInvite,
params=dict(code=invites[0].invite_token))
# Make sure the invite was deleted.
self.deleteResponse(TeamMemberInvite,
params=dict(code=invites[0].invite_token),
expected_code=404)
class TestDeleteOrganizationTeamMember(ApiTestCase):
@ -768,7 +947,7 @@ class TestDeleteOrganizationTeamMember(ApiTestCase):
params=dict(orgname=ORGANIZATION,
teamname='readers'))
assert not READ_ACCESS_USER in json['members']
assert len(json['members']) == 1
class TestCreateRepo(ApiTestCase):
@ -2064,7 +2243,7 @@ class TestSuperUserManagement(ApiTestCase):
json = self.getJsonResponse(SuperUserManagement, params=dict(username = 'freshuser'))
self.assertEquals('freshuser', json['username'])
self.assertEquals('no@thanks.com', json['email'])
self.assertEquals('jschorr+test@devtable.com', json['email'])
self.assertEquals(False, json['super_user'])
def test_delete_user(self):
@ -2087,7 +2266,7 @@ class TestSuperUserManagement(ApiTestCase):
# Verify the user exists.
json = self.getJsonResponse(SuperUserManagement, params=dict(username = 'freshuser'))
self.assertEquals('freshuser', json['username'])
self.assertEquals('no@thanks.com', json['email'])
self.assertEquals('jschorr+test@devtable.com', json['email'])
# Update the user.
self.putJsonResponse(SuperUserManagement, params=dict(username='freshuser'), data=dict(email='foo@bar.com'))

View file

@ -67,7 +67,7 @@ To confirm this email address, please click the following link:<br>
INVITE_TO_ORG_TEAM_MESSAGE = """
Hi {0},<br>
{1} has invited you to join the team {2} under organization {3} on <a href="{4}">{5}</a>.
{1} has invited you to join the team <b>{2}</b> under organization <b>{3}</b> on <a href="{4}">{5}</a>.
<br><br>
To join the team, please click the following link:<br>
<a href="{4}/confirminvite?code={6}">{4}/confirminvite?code={6}</a>