Get team invite confirmation working and fully tested
This commit is contained in:
parent
eefb7e1ec9
commit
43b6695f9c
12 changed files with 458 additions and 43 deletions
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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'];
|
||||
}
|
13
static/partials/confirm-team-invite.html
Normal file
13
static/partials/confirm-team-invite.html
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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>
|
||||
|
|
Reference in a new issue