From 900ccd4c4772f547638e1e5ac0814a43723e7aaa Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 31 Jan 2014 16:19:29 -0500 Subject: [PATCH 01/17] Start on unit tests for the API endpoint --- endpoints/api.py | 2 + test/test_api_usage.py | 85 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 test/test_api_usage.py diff --git a/endpoints/api.py b/endpoints/api.py index 2b31e4700..b4cec5ca4 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -314,6 +314,8 @@ def create_new_user(): @internal_api_call def signin_user(): signin_data = request.get_json() + if not signin_data: + abort(404) username = signin_data['username'] password = signin_data['password'] diff --git a/test/test_api_usage.py b/test/test_api_usage.py new file mode 100644 index 000000000..23c1c1d33 --- /dev/null +++ b/test/test_api_usage.py @@ -0,0 +1,85 @@ +import unittest +import json + +from flask import url_for +from endpoints.api import api +from app import app +from initdb import setup_database_for_testing, finished_database_for_testing +from specs import build_specs + +app.register_blueprint(api, url_prefix='/api') + +NO_ACCESS_USER = 'freshuser' +READ_ACCESS_USER = 'reader' +ADMIN_ACCESS_USER = 'devtable' +ORGANIZATION = 'buynlarge' + +class ApiTestCase(unittest.TestCase): + def setUp(self): + setup_database_for_testing(self) + self.app = app.test_client() + self.ctx = app.test_request_context() + self.ctx.__enter__() + + def tearDown(self): + finished_database_for_testing(self) + self.ctx.__exit__(True, None, None) + + def getJsonResponse(self, method_name): + rv = self.app.get(url_for(method_name)) + assert rv.status_code == 200 + data = rv.data + parsed = json.loads(data) + return parsed + + def login(self, username): + self.app.post(url_for('api.signin_user'), + data=json.dumps(dict(username=username, password='password')), + headers={"Content-Type": "application/json"}) + +class TestDiscovery(ApiTestCase): + def test_discovery(self): + """ Basic sanity check that discovery returns valid JSON in the expected format. """ + json = self.getJsonResponse('api.discovery') + found = set([]) + for method_info in json['endpoints']: + found.add(method_info['name']) + + assert 'discovery' in found + +class TestPlans(ApiTestCase): + def test_plans(self): + """ Basic sanity check that the plans are returned in the expected format. """ + json = self.getJsonResponse('api.list_plans') + found = set([]) + for method_info in json['plans']: + found.add(method_info['stripeId']) + + assert 'free' in found + +class TestLoggedInUser(ApiTestCase): + def test_guest(self): + json = self.getJsonResponse('api.get_logged_in_user') + assert json['anonymous'] == True + + def test_user(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.get_logged_in_user') + assert json['anonymous'] == False + assert json['username'] == READ_ACCESS_USER + +class TestGetUserPrivateCount(ApiTestCase): + def test_nonallowed(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.get_user_private_count') + assert json['privateCount'] == 0 + assert json['reposAllowed'] == 0 + + def test_allowed(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_user_private_count') + assert json['privateCount'] == 6 + assert json['reposAllowed'] == 125 + +if __name__ == '__main__': + unittest.main() From 05b33dced44f0e82a6c4d567edb4aa48e8184aa2 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 31 Jan 2014 17:54:01 -0500 Subject: [PATCH 02/17] Continue on API unit tests --- endpoints/api.py | 21 ++-- test/test_api_usage.py | 241 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 248 insertions(+), 14 deletions(-) diff --git a/endpoints/api.py b/endpoints/api.py index b4cec5ca4..a1b3ba51a 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -261,7 +261,6 @@ def convert_user_to_organization(): @internal_api_call def change_user_details(): user = current_user.db_user() - user_data = request.get_json() try: @@ -422,6 +421,7 @@ def get_matching_entities(prefix): team_data = [entity_team_view(team) for team in teams] user_data = [user_view(user) for user in users] + return jsonify({ 'results': team_data + user_data }) @@ -447,11 +447,16 @@ def create_organization(): existing = None try: - existing = (model.get_organization(org_data['name']) or - model.get_user(org_data['name'])) + existing = model.get_organization(org_data['name']) except model.InvalidOrganizationException: pass + if not existing: + try: + existing = model.get_user(org_data['name']) + except model.InvalidUserException: + pass + if existing: msg = 'A user or organization with this name already exists' return request_error(message=msg) @@ -550,7 +555,7 @@ def prototype_view(proto, org_members): 'id': proto.uuid, } -@api.route('/api/organization//prototypes', methods=['GET']) +@api.route('/organization//prototypes', methods=['GET']) @api_login_required def get_organization_prototype_permissions(orgname): permission = AdministerOrganizationPermission(orgname) @@ -588,7 +593,7 @@ def log_prototype_action(action_kind, orgname, prototype, **kwargs): log_action(action_kind, orgname, log_params) -@api.route('/api/organization//prototypes', methods=['POST']) +@api.route('/organization//prototypes', methods=['POST']) @api_login_required def create_organization_prototype_permission(orgname): permission = AdministerOrganizationPermission(orgname) @@ -636,7 +641,7 @@ def create_organization_prototype_permission(orgname): abort(403) -@api.route('/api/organization//prototypes/', +@api.route('/organization//prototypes/', methods=['DELETE']) @api_login_required def delete_organization_prototype_permission(orgname, prototypeid): @@ -658,7 +663,7 @@ def delete_organization_prototype_permission(orgname, prototypeid): abort(403) -@api.route('/api/organization//prototypes/', +@api.route('/organization//prototypes/', methods=['PUT']) @api_login_required def update_organization_prototype_permission(orgname, prototypeid): @@ -1363,7 +1368,7 @@ def get_image_changes(namespace, repository, image_id): abort(403) -@api.route('/api/repository//tag/', +@api.route('/repository//tag/', methods=['DELETE']) @parse_repository_name def delete_full_tag(namespace, repository, tag): diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 23c1c1d33..67d8deb1b 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -6,6 +6,7 @@ from endpoints.api import api from app import app from initdb import setup_database_for_testing, finished_database_for_testing from specs import build_specs +from data import model app.register_blueprint(api, url_prefix='/api') @@ -14,6 +15,12 @@ READ_ACCESS_USER = 'reader' ADMIN_ACCESS_USER = 'devtable' ORGANIZATION = 'buynlarge' +NEW_USER_DETAILS = { + 'username': 'bobby', + 'password': 'password', + 'email': 'bobby@tables.com', +} + class ApiTestCase(unittest.TestCase): def setUp(self): setup_database_for_testing(self) @@ -25,17 +32,50 @@ class ApiTestCase(unittest.TestCase): finished_database_for_testing(self) self.ctx.__exit__(True, None, None) - def getJsonResponse(self, method_name): - rv = self.app.get(url_for(method_name)) - assert rv.status_code == 200 + def getJsonResponse(self, method_name, params={}): + rv = self.app.get(url_for(method_name, **params)) + self.assertEquals(200, rv.status_code) data = rv.data parsed = json.loads(data) return parsed - def login(self, username): - self.app.post(url_for('api.signin_user'), - data=json.dumps(dict(username=username, password='password')), + def postResponse(self, method_name, params={}, data={}, expected_code=200): + rv = self.app.post(url_for(method_name, **params), data=json.dumps(data), headers={"Content-Type": "application/json"}) + self.assertEquals(rv.status_code, expected_code) + return rv.data + + def getResponse(self, method_name, params={}, expected_code=200): + rv = self.app.get(url_for(method_name, **params)) + self.assertEquals(rv.status_code, expected_code) + return rv.data + + def postJsonResponse(self, method_name, params={}, data={}, expected_code=200): + rv = self.app.post(url_for(method_name, **params), data=json.dumps(data), + headers={"Content-Type": "application/json"}) + + if rv.status_code != expected_code: + print 'Mismatch data for method %s: %s' % (method_name, rv.data) + + self.assertEquals(rv.status_code, expected_code) + data = rv.data + parsed = json.loads(data) + return parsed + + def putJsonResponse(self, method_name, params={}, data={}, expected_code=200): + rv = self.app.put(url_for(method_name, **params), data=json.dumps(data), + headers={"Content-Type": "application/json"}) + + if rv.status_code != expected_code: + print 'Mismatch data for method %s: %s' % (method_name, rv.data) + + self.assertEquals(rv.status_code, expected_code) + data = rv.data + parsed = json.loads(data) + return parsed + + def login(self, username, password='password'): + return self.postJsonResponse('api.signin_user', data=dict(username=username, password=password)) class TestDiscovery(ApiTestCase): def test_discovery(self): @@ -81,5 +121,194 @@ class TestGetUserPrivateCount(ApiTestCase): assert json['privateCount'] == 6 assert json['reposAllowed'] == 125 +class TestConvertToOrganization(ApiTestCase): + def test_sameadminuser(self): + self.login(READ_ACCESS_USER) + json = self.postJsonResponse('api.convert_user_to_organization', + data={'adminUser': READ_ACCESS_USER, 'adminPassword': 'password'}, + expected_code=400) + + self.assertEqual('The admin user is not valid', json['message']) + + def test_invalidadminuser(self): + self.login(READ_ACCESS_USER) + json = self.postJsonResponse('api.convert_user_to_organization', + data={'adminUser': 'unknownuser', 'adminPassword': 'password'}, + expected_code=400) + + self.assertEqual('The admin user credentials are not valid', json['message']) + + def test_invalidadminpassword(self): + self.login(READ_ACCESS_USER) + json = self.postJsonResponse('api.convert_user_to_organization', + data={'adminUser': ADMIN_ACCESS_USER, 'adminPassword': 'invalidpass'}, + expected_code=400) + + self.assertEqual('The admin user credentials are not valid', json['message']) + + def test_convert(self): + self.login(READ_ACCESS_USER) + json = self.postJsonResponse('api.convert_user_to_organization', + data={'adminUser': ADMIN_ACCESS_USER, + 'adminPassword': 'password', + 'plan': 'free'}) + + self.assertEqual(True, json['success']) + + # Verify the organization exists. + organization = model.get_organization(READ_ACCESS_USER) + assert organization is not None + + +class TestChangeUserDetails(ApiTestCase): + def test_changepassword(self): + self.login(READ_ACCESS_USER) + self.putJsonResponse('api.change_user_details', data=dict(password='newpasswordiscool')) + self.login(READ_ACCESS_USER, password='newpasswordiscool') + + def test_changeinvoiceemail(self): + self.login(READ_ACCESS_USER) + + json = self.putJsonResponse('api.change_user_details', data=dict(invoice_email=True)) + self.assertEquals(True, json['invoice_email']) + + json = self.putJsonResponse('api.change_user_details', data=dict(invoice_email=False)) + self.assertEquals(False, json['invoice_email']) + + +class TestCreateNewUser(ApiTestCase): + def test_existingusername(self): + json = self.postJsonResponse('api.create_new_user', + data=dict(username=READ_ACCESS_USER, + password='password', + email='test@example.com'), + expected_code=400) + + self.assertEquals('The username already exists', json['message']) + + def test_createuser(self): + data = self.postResponse('api.create_new_user', + data=NEW_USER_DETAILS, + expected_code=201) + self.assertEquals('Created', data) + + +class TestSignout(ApiTestCase): + def test_signout(self): + self.login(READ_ACCESS_USER) + + json = self.getJsonResponse('api.get_logged_in_user') + assert json['username'] == READ_ACCESS_USER + + self.postResponse('api.logout') + + json = self.getJsonResponse('api.get_logged_in_user') + assert json['anonymous'] == True + + +class TestGetMatchingEntities(ApiTestCase): + def test_notinorg(self): + self.login(NO_ACCESS_USER) + + json = self.getJsonResponse('api.get_matching_entities', + params=dict(prefix='o', namespace=ORGANIZATION, includeTeams=True)) + + names = set([r['name'] for r in json['results']]) + assert 'outsideorg' in names + assert not 'owners' in names + + def test_inorg(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_matching_entities', + params=dict(prefix='o', namespace=ORGANIZATION, includeTeams=True)) + + names = set([r['name'] for r in json['results']]) + assert 'outsideorg' in names + assert 'owners' in names + + +class TestCreateOrganization(ApiTestCase): + def test_existinguser(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_organization', data=dict(name=ADMIN_ACCESS_USER), + expected_code=400) + + self.assertEquals('A user or organization with this name already exists', json['message']) + + def test_existingorg(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_organization', data=dict(name=ORGANIZATION), + expected_code=400) + + self.assertEquals('A user or organization with this name already exists', json['message']) + + def test_createorg(self): + self.login(ADMIN_ACCESS_USER) + + data = self.postResponse('api.create_organization', + data=dict(name='neworg', email='test@example.com'), + expected_code=201) + + self.assertEquals('Created', data) + + # Ensure the org was created. + organization = model.get_organization('neworg') + assert organization is not None + + +class TestGetOrganization(ApiTestCase): + def test_unknownorg(self): + self.login(ADMIN_ACCESS_USER) + self.getResponse('api.get_organization', params=dict(orgname='notvalid'), + expected_code=403) + + def test_cannotaccess(self): + self.login(NO_ACCESS_USER) + self.getResponse('api.get_organization', params=dict(orgname=ORGANIZATION), + expected_code=403) + + def test_getorganization(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.get_organization', params=dict(orgname=ORGANIZATION)) + + self.assertEquals(ORGANIZATION, json['name']) + self.assertEquals(False, json['is_admin']) + + def test_getorganization_asadmin(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_organization', params=dict(orgname=ORGANIZATION)) + + self.assertEquals(ORGANIZATION, json['name']) + self.assertEquals(True, json['is_admin']) + +class TestChangeOrganizationDetails(ApiTestCase): + def test_changeinvoiceemail(self): + self.login(ADMIN_ACCESS_USER) + + json = self.putJsonResponse('api.change_organization_details', + params=dict(orgname=ORGANIZATION), + data=dict(invoice_email=True)) + + self.assertEquals(True, json['invoice_email']) + + json = self.putJsonResponse('api.change_organization_details', + params=dict(orgname=ORGANIZATION), + data=dict(invoice_email=False)) + self.assertEquals(False, json['invoice_email']) + + + def test_changemail(self): + self.login(ADMIN_ACCESS_USER) + + json = self.putJsonResponse('api.change_organization_details', + params=dict(orgname=ORGANIZATION), + data=dict(email='newemail@example.com')) + + self.assertEquals('newemail@example.com', json['email']) + + if __name__ == '__main__': unittest.main() From 36d37e839bfe367d63e58bf953669638322f16fe Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 31 Jan 2014 18:54:31 -0500 Subject: [PATCH 03/17] Continue on API unit tests: Now 50% (or so) coverage --- endpoints/api.py | 8 +- test/test_api_usage.py | 262 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 265 insertions(+), 5 deletions(-) diff --git a/endpoints/api.py b/endpoints/api.py index a1b3ba51a..6dfc52dc1 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -610,9 +610,9 @@ def create_organization_prototype_permission(orgname): 'name' in details['activating_user']): activating_username = details['activating_user']['name'] - delegate = details['delegate'] - delegate_kind = delegate['kind'] - delegate_name = delegate['name'] + delegate = details['delegate'] if 'delegate' in details else {} + delegate_kind = delegate.get('kind', None) + delegate_name = delegate.get('name', None) delegate_username = delegate_name if delegate_kind == 'user' else None delegate_teamname = delegate_name if delegate_kind == 'team' else None @@ -628,7 +628,7 @@ def create_organization_prototype_permission(orgname): return request_error(message='Unknown activating user') if not delegate_user and not delegate_team: - return request_error(message='Missing delagate user or team') + return request_error(message='Missing delegate user or team') role_name = details['role'] diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 67d8deb1b..81837d590 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -50,6 +50,11 @@ class ApiTestCase(unittest.TestCase): self.assertEquals(rv.status_code, expected_code) return rv.data + def deleteResponse(self, method_name, params={}, expected_code=204): + rv = self.app.delete(url_for(method_name, **params)) + self.assertEquals(rv.status_code, expected_code) + return rv.data + def postJsonResponse(self, method_name, params={}, data={}, expected_code=200): rv = self.app.post(url_for(method_name, **params), data=json.dumps(data), headers={"Content-Type": "application/json"}) @@ -158,6 +163,13 @@ class TestConvertToOrganization(ApiTestCase): # Verify the organization exists. organization = model.get_organization(READ_ACCESS_USER) assert organization is not None + + # Verify the admin user is the org's admin. + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_organization', params=dict(orgname=READ_ACCESS_USER)) + + self.assertEquals(READ_ACCESS_USER, json['name']) + self.assertEquals(True, json['is_admin']) class TestChangeUserDetails(ApiTestCase): @@ -257,7 +269,12 @@ class TestCreateOrganization(ApiTestCase): # Ensure the org was created. organization = model.get_organization('neworg') assert organization is not None - + + # Verify the admin user is the org's admin. + json = self.getJsonResponse('api.get_organization', params=dict(orgname='neworg')) + self.assertEquals('neworg', json['name']) + self.assertEquals(True, json['is_admin']) + class TestGetOrganization(ApiTestCase): def test_unknownorg(self): @@ -284,6 +301,7 @@ class TestGetOrganization(ApiTestCase): self.assertEquals(ORGANIZATION, json['name']) self.assertEquals(True, json['is_admin']) + class TestChangeOrganizationDetails(ApiTestCase): def test_changeinvoiceemail(self): self.login(ADMIN_ACCESS_USER) @@ -310,5 +328,247 @@ class TestChangeOrganizationDetails(ApiTestCase): self.assertEquals('newemail@example.com', json['email']) +class TestGetOrganizationPrototypes(ApiTestCase): + def test_getprototypes(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_organization_prototype_permissions', + params=dict(orgname=ORGANIZATION)) + + assert len(json['prototypes']) > 0 + + +class TestCreateOrganizationPrototypes(ApiTestCase): + def test_invaliduser(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_organization_prototype_permission', + params=dict(orgname=ORGANIZATION), + data=dict(activating_user={'name': 'unknownuser'}, + role='read', + delegate={'kind': 'team', 'name': 'owners'}), + expected_code=400) + + self.assertEquals('Unknown activating user', json['message']) + + + def test_missingdelegate(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_organization_prototype_permission', + params=dict(orgname=ORGANIZATION), + data=dict(role='read'), + expected_code=400) + + self.assertEquals('Missing delegate user or team', json['message']) + + def test_createprototype(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_organization_prototype_permission', + params=dict(orgname=ORGANIZATION), + data=dict(role='read', delegate={'kind': 'team', 'name': 'readers'})) + + self.assertEquals('read', json['role']) + pid = json['id'] + + # Verify the prototype exists. + json = self.getJsonResponse('api.get_organization_prototype_permissions', + params=dict(orgname=ORGANIZATION)) + + ids = set([p['id'] for p in json['prototypes']]) + assert pid in ids + + +class TestDeleteOrganizationPrototypes(ApiTestCase): + def test_deleteprototype(self): + self.login(ADMIN_ACCESS_USER) + + # Get the existing prototypes + json = self.getJsonResponse('api.get_organization_prototype_permissions', + params=dict(orgname=ORGANIZATION)) + + ids = [p['id'] for p in json['prototypes']] + pid = ids[0] + + # Delete a prototype. + self.deleteResponse('api.delete_organization_prototype_permission', + params=dict(orgname=ORGANIZATION, prototypeid=pid)) + + # Verify the prototype no longer exists. + json = self.getJsonResponse('api.get_organization_prototype_permissions', + params=dict(orgname=ORGANIZATION)) + + newids = [p['id'] for p in json['prototypes']] + assert not pid in newids + + +class TestUpdateOrganizationPrototypes(ApiTestCase): + def test_updateprototype(self): + self.login(ADMIN_ACCESS_USER) + + # Get the existing prototypes + json = self.getJsonResponse('api.get_organization_prototype_permissions', + params=dict(orgname=ORGANIZATION)) + + ids = [p['id'] for p in json['prototypes']] + pid = ids[0] + + # Update a prototype. + json = self.putJsonResponse('api.delete_organization_prototype_permission', + params=dict(orgname=ORGANIZATION, prototypeid=pid), + data=dict(role='admin')) + + self.assertEquals('admin', json['role']) + + + +class TestGetOrganiaztionMembers(ApiTestCase): + def test_getmembers(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_organization_members', + params=dict(orgname=ORGANIZATION)) + + assert ADMIN_ACCESS_USER in json['members'] + assert READ_ACCESS_USER in json['members'] + assert not NO_ACCESS_USER in json['members'] + + def test_getspecificmember(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_organization_member', + params=dict(orgname=ORGANIZATION, membername=ADMIN_ACCESS_USER)) + + self.assertEquals(ADMIN_ACCESS_USER, json['member']['name']) + self.assertEquals('user', json['member']['kind']) + + assert 'owners' in json['member']['teams'] + + +class TestGetOrganizationPrivateAllowed(ApiTestCase): + def test_existingorg(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_organization_private_allowed', + params=dict(orgname=ORGANIZATION)) + + self.assertEquals(True, json['privateAllowed']) + assert not 'reposAllowed' in json + + + def test_neworg(self): + self.login(ADMIN_ACCESS_USER) + + data = self.postResponse('api.create_organization', + data=dict(name='neworg', email='test@example.com'), + expected_code=201) + + json = self.getJsonResponse('api.get_organization_private_allowed', + params=dict(orgname='neworg')) + + self.assertEquals(False, json['privateAllowed']) + + +class TestUpdateOrganizationTeam(ApiTestCase): + def test_updateexisting(self): + self.login(ADMIN_ACCESS_USER) + + data = self.postJsonResponse('api.update_organization_team', + params=dict(orgname=ORGANIZATION, teamname='readers'), + data=dict(description = 'My cool team', role = 'creator')) + + self.assertEquals('My cool team', data['description']) + self.assertEquals('creator', data['role']) + + def test_attemptchangeroleonowners(self): + self.login(ADMIN_ACCESS_USER) + + self.postResponse('api.update_organization_team', + params=dict(orgname=ORGANIZATION, teamname='owners'), + data=dict(role = 'creator'), + expected_code=400) + + def test_createnewteam(self): + self.login(ADMIN_ACCESS_USER) + + data = self.putJsonResponse('api.update_organization_team', + params=dict(orgname=ORGANIZATION, teamname='newteam'), + data=dict(description = 'My cool team', role = 'member'), + expected_code=201) + + self.assertEquals('My cool team', data['description']) + self.assertEquals('member', data['role']) + + # Verify the team was created. + json = self.getJsonResponse('api.get_organization', params=dict(orgname=ORGANIZATION)) + assert 'newteam' in json['teams'] + + +class TestDeleteOrganizationTeam(ApiTestCase): + def test_deleteteam(self): + self.login(ADMIN_ACCESS_USER) + + self.deleteResponse('api.delete_organization_team', + params=dict(orgname=ORGANIZATION, teamname='readers')) + + # Make sure the team was deleted + json = self.getJsonResponse('api.get_organization', params=dict(orgname=ORGANIZATION)) + assert not 'readers' in json['teams'] + + def test_attemptdeleteowners(self): + self.login(ADMIN_ACCESS_USER) + + self.deleteResponse('api.delete_organization_team', + params=dict(orgname=ORGANIZATION, teamname='owners'), + expected_code=400) + + +class TestGetOrganizationTeamMembers(ApiTestCase): + def test_invalidteam(self): + self.login(ADMIN_ACCESS_USER) + + self.getResponse('api.get_organization_team_members', + params=dict(orgname=ORGANIZATION, teamname='notvalid'), + expected_code=404) + + def test_getmembers(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_organization_team_members', + params=dict(orgname=ORGANIZATION, teamname='readers')) + + assert READ_ACCESS_USER in json['members'] + + +class TestUpdateOrganizationTeamMember(ApiTestCase): + def test_addmember(self): + self.login(ADMIN_ACCESS_USER) + + self.postJsonResponse('api.update_organization_team_member', + params=dict(orgname=ORGANIZATION, teamname='readers', membername=NO_ACCESS_USER)) + + + # Verify the user was added to the team. + json = self.getJsonResponse('api.get_organization_team_members', + params=dict(orgname=ORGANIZATION, teamname='readers')) + + assert NO_ACCESS_USER in json['members'] + + +class TestDeleteOrganizationTeamMember(ApiTestCase): + def test_deletemember(self): + self.login(ADMIN_ACCESS_USER) + + self.deleteResponse('api.delete_organization_team_member', + params=dict(orgname=ORGANIZATION, teamname='readers', membername=READ_ACCESS_USER)) + + + # Verify the user was removed from the team. + json = self.getJsonResponse('api.get_organization_team_members', + params=dict(orgname=ORGANIZATION, teamname='readers')) + + assert not READ_ACCESS_USER in json['members'] + + if __name__ == '__main__': unittest.main() From e3eee958a4cd91a89ae5b76a85f18b4e7c6b5907 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 31 Jan 2014 19:45:44 -0500 Subject: [PATCH 04/17] Continue on API unit tests: Now 60% (or so) coverage --- test/test_api_usage.py | 138 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 81837d590..205828c91 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -570,5 +570,143 @@ class TestDeleteOrganizationTeamMember(ApiTestCase): assert not READ_ACCESS_USER in json['members'] +class TestCreateRepo(ApiTestCase): + def test_duplicaterepo(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_repo', + data=dict(repository='simple', visibility='public'), + expected_code=400) + + self.assertEquals('Repository already exists', json['message']) + + + def test_createrepo(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_repo', + data=dict(repository='newrepo', visibility='public', description='')) + + + self.assertEquals(ADMIN_ACCESS_USER, json['namespace']) + self.assertEquals('newrepo', json['name']) + + + def test_createrepo_underorg(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_repo', + data=dict(namespace=ORGANIZATION, + repository='newrepo', + visibility='private', + description='')) + + self.assertEquals(ORGANIZATION, json['namespace']) + self.assertEquals('newrepo', json['name']) + + +class TestFindRepos(ApiTestCase): + def test_findrepos_asguest(self): + json = self.getJsonResponse('api.find_repos', params=dict(query='p')) + assert len(json['repositories']) == 1 + + self.assertEquals(json['repositories'][0]['namespace'], 'public') + self.assertEquals(json['repositories'][0]['name'], 'publicrepo') + + def test_findrepos_asuser(self): + self.login(NO_ACCESS_USER) + + json = self.getJsonResponse('api.find_repos', params=dict(query='p')) + assert len(json['repositories']) == 1 + + self.assertEquals(json['repositories'][0]['namespace'], 'public') + self.assertEquals(json['repositories'][0]['name'], 'publicrepo') + + def test_findrepos_orgmember(self): + self.login(READ_ACCESS_USER) + + json = self.getJsonResponse('api.find_repos', params=dict(query='p')) + assert len(json['repositories']) > 1 + + +class TestListRepos(ApiTestCase): + def test_listrepos_asguest(self): + json = self.getJsonResponse('api.list_repos', params=dict(public=True)) + assert len(json['repositories']) == 0 + + def test_listrepos_orgmember(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.list_repos', params=dict(public=True)) + assert len(json['repositories']) > 1 + + def test_listrepos_filter(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.list_repos', params=dict(namespace=ORGANIZATION, public=False)) + + for repo in json['repositories']: + self.assertEquals(ORGANIZATION, repo['namespace']) + + def test_listrepos_limit(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.list_repos', params=dict(limit=2)) + + assert len(json['repositories']) == 2 + + +class TestUpdateRepo(ApiTestCase): + def test_updatedescription(self): + self.login(ADMIN_ACCESS_USER) + + self.putJsonResponse('api.update_repo', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(description='Some cool repo')) + + # Verify the repo description was updated. + json = self.getJsonResponse('api.get_repo', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + self.assertEquals('Some cool repo', json['description']) + + +class TestChangeRepoVisibility(ApiTestCase): + def test_changevisibility(self): + self.login(ADMIN_ACCESS_USER) + + # Make public. + self.postJsonResponse('api.change_repo_visibility', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(visibility='public')) + + # Verify the visibility. + json = self.getJsonResponse('api.get_repo', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + self.assertEquals(True, json['is_public']) + + # Make private. + self.postJsonResponse('api.change_repo_visibility', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(visibility='private')) + + # Verify the visibility. + json = self.getJsonResponse('api.get_repo', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + self.assertEquals(False, json['is_public']) + + +class TestDeleteRepository(ApiTestCase): + def test_deleterepo(self): + self.login(ADMIN_ACCESS_USER) + + self.deleteResponse('api.delete_repository', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + # Verify the repo was deleted. + self.getResponse('api.get_repo', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + expected_code=404) + + if __name__ == '__main__': unittest.main() From 08160afddeaf357fea80e072d4d99e8a68b1b248 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 3 Feb 2014 18:18:33 -0500 Subject: [PATCH 05/17] Finish API endpoint unit tests --- endpoints/api.py | 14 +- static/js/app.js | 7 +- test/data/test.db | Bin 141312 -> 143360 bytes test/test_api_usage.py | 569 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 586 insertions(+), 4 deletions(-) diff --git a/endpoints/api.py b/endpoints/api.py index 6dfc52dc1..e1c8710fa 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -1248,7 +1248,11 @@ def create_webhook(namespace, repository): def get_webhook(namespace, repository, public_id): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): - webhook = model.get_webhook(namespace, repository, public_id) + try: + webhook = model.get_webhook(namespace, repository, public_id) + except model.InvalidWebhookException: + abort(404) + return jsonify(webhook_view(webhook)) abort(403) # Permission denied @@ -1643,7 +1647,11 @@ def list_repo_tokens(namespace, repository): def get_tokens(namespace, repository, code): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): - perm = model.get_repo_delegate_token(namespace, repository, code) + try: + perm = model.get_repo_delegate_token(namespace, repository, code) + except model.InvalidTokenException: + abort(404) + return jsonify(token_view(perm)) abort(403) # Permission denied @@ -1780,6 +1788,8 @@ def set_card(user, token): cus.save() except stripe.CardError as e: return carderror_response(e) + except stripe.InvalidRequestError as e: + return carderror_response(e) return get_card(user) diff --git a/static/js/app.js b/static/js/app.js index 8d7d08231..8c2d6a4b4 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -427,6 +427,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest var planService = {}; var listeners = []; + var previousSubscribeFailure = false; + planService.getFreePlan = function() { return 'free'; }; @@ -616,12 +618,15 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest if (orgname && !planService.isOrgCompatible(plan)) { return; } planService.getCardInfo(orgname, function(cardInfo) { - if (plan.price > 0 && !cardInfo.last4) { + if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) { planService.showSubscribeDialog($scope, orgname, planId, callbacks); return; } + previousSubscribeFailure = false; + planService.setSubscription(orgname, planId, callbacks['success'], function(resp) { + previousSubscribeFailure = true; planService.handleCardError(resp); callbacks['failure'](resp); }); diff --git a/test/data/test.db b/test/data/test.db index be6291cdaf3680fcaa53c5ac81823dcc8be9fd92..ef37436e49c58b2480d56f94c74fafc38a630553 100644 GIT binary patch delta 696 zcmZvZPiPZC6vpT6w;|bdGex{9)MXJG(GB}2yK7gdmehI?di1O$1;G}z^i+!V;6*Da zdq@w0A|6XA)pnr~+9FnHz&{|0prZAlR*O<79@2xeMnX+wV3>LDec$&RX8d_-{6p%- z-ozwhY~rSNR2+=8{g;b#MY`3=6QW9}iJeWL7V#`?Ag*0&n?FRdtZ_^;ETD!h{J>Xy z!aKai3smtKi+F(P`@7Vu2!Gy|GfAERQ8FV#A+@}GguHO;NxLwJm=LUB8^5rL&sf7U z9wNebyu~Y2g+YjL>6_ffW947@icj5~W0w7q#pAP)dYJB@T?QjD$>K>?4=RX|N_}z@ z8~7q9-{TEl;u%&2=MpL+q>SB7()b!at$AsVnfQ%!n2A!y=x!^`Vu6lpT(Y?aFES2F<*7bOpRx_p(JVi3oxn14o@aJ>4(~t@*`}2@%tqS| zTtDafj_LTB|F)fc&h{P8$*Xa@eBl7?iB|i_`#7H!pOXe$h6+aEittWc>g8#skiuBc zzOH0b)(#ZQ_H(9K)~J5|-02}b(DXAS!^U8#cy?q^AM4IET#ROy9mK6JC-s4grIJ2Y UP=$d~$7rIkgT|9u_5V=(2BER3MgRZ+ delta 260 zcmZp8z|nAlV}dm6R0alyu89ivK!)xk&Bm0iDU5T}w`nl3uqs}({?@w#zhDN4m&eCN^vu-VPJm5e3*F&b1QQi lvpcf}Gbht4rqfJofClt2P4{ 0 class TestConvertToOrganization(ApiTestCase): def test_sameadminuser(self): @@ -707,6 +711,569 @@ class TestDeleteRepository(ApiTestCase): params=dict(repository=ADMIN_ACCESS_USER + '/simple'), expected_code=404) +class TestGetRepository(ApiTestCase): + def test_getrepo_public_asguest(self): + json = self.getJsonResponse('api.get_repo', + params=dict(repository=PUBLIC_USER + '/publicrepo')) + + self.assertEquals(PUBLIC_USER, json['namespace']) + self.assertEquals('publicrepo', json['name']) + + self.assertEquals(True, json['is_public']) + self.assertEquals(False, json['is_organization']) + self.assertEquals(False, json['is_building']) + + self.assertEquals(False, json['can_write']) + self.assertEquals(False, json['can_admin']) + + assert 'latest' in json['tags'] + + def test_getrepo_public_asowner(self): + self.login(PUBLIC_USER) + + json = self.getJsonResponse('api.get_repo', + params=dict(repository=PUBLIC_USER + '/publicrepo')) + + self.assertEquals(False, json['is_organization']) + self.assertEquals(True, json['can_write']) + self.assertEquals(True, json['can_admin']) + + def test_getrepo_building(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_repo', + params=dict(repository=ADMIN_ACCESS_USER + '/building')) + + self.assertEquals(True, json['can_write']) + self.assertEquals(True, json['can_admin']) + self.assertEquals(True, json['is_building']) + self.assertEquals(False, json['is_organization']) + + def test_getrepo_org_asnonmember(self): + self.getResponse('api.get_repo', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO), + expected_code=403) + + def test_getrepo_org_asreader(self): + self.login(READ_ACCESS_USER) + + json = self.getJsonResponse('api.get_repo', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO)) + + self.assertEquals(ORGANIZATION, json['namespace']) + self.assertEquals(ORG_REPO, json['name']) + + self.assertEquals(False, json['can_write']) + self.assertEquals(False, json['can_admin']) + + self.assertEquals(True, json['is_organization']) + + def test_getrepo_org_asadmin(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_repo', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO)) + + self.assertEquals(True, json['can_write']) + self.assertEquals(True, json['can_admin']) + + self.assertEquals(True, json['is_organization']) + + +class TestGetRepoBuilds(ApiTestCase): + def test_getrepo_nobuilds(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_repo_builds', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + assert len(json['builds']) == 0 + + def test_getrepobuilds(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_repo_builds', + params=dict(repository=ADMIN_ACCESS_USER + '/building')) + + assert len(json['builds']) > 0 + build = json['builds'][0] + + assert 'id' in build + assert 'status' in build + assert 'message' in build + + +class TestRequearRepoBuild(ApiTestCase): + def test_requestrepobuild(self): + self.login(ADMIN_ACCESS_USER) + + # Ensure where not yet building. + json = self.getJsonResponse('api.get_repo_builds', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + assert len(json['builds']) == 0 + + # Request a (fake) build. + self.postResponse('api.request_repo_build', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(file_id = 'foobarbaz'), + expected_code=201) + + # Check for the build. + json = self.getJsonResponse('api.get_repo_builds', + params=dict(repository=ADMIN_ACCESS_USER + '/building')) + + assert len(json['builds']) > 0 + + +class TestWebhooks(ApiTestCase): + def test_webhooks(self): + self.login(ADMIN_ACCESS_USER) + + # Add a webhook. + json = self.postJsonResponse('api.create_webhook', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(url='http://example.com')) + + self.assertEquals('http://example.com', json['parameters']['url']) + wid = json['public_id'] + + # Get the webhook. + json = self.getJsonResponse('api.get_webhook', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', public_id=wid)) + + self.assertEquals(wid, json['public_id']) + self.assertEquals('http://example.com', json['parameters']['url']) + + # Verify the webhook is listed. + json = self.getJsonResponse('api.list_webhooks', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + ids = [w['public_id'] for w in json['webhooks']] + assert wid in ids + + # Delete the webhook. + self.deleteResponse('api.delete_webhook', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', public_id=wid), + expected_code=204) + + # Verify the webhook is gone. + self.getResponse('api.get_webhook', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', public_id=wid), + expected_code=404) + + +class TestListAndGetImage(ApiTestCase): + def test_listandgetimages(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.list_repository_images', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + assert len(json['images']) > 0 + for image in json['images']: + assert 'id' in image + assert 'tags' in image + assert 'created' in image + assert 'comment' in image + assert 'command' in image + assert 'ancestors' in image + assert 'dbid' in image + assert 'size' in image + + ijson = self.getJsonResponse('api.get_image', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', + image_id=image['id'])) + + self.assertEquals(image['id'], ijson['id']) + +class TestGetImageChanges(ApiTestCase): + def test_getimagechanges(self): + self.login(ADMIN_ACCESS_USER) + + # Find an image to check. + json = self.getJsonResponse('api.list_repository_images', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + image_id = json['images'][0]['id'] + + # Lookup the image's changes. + # TODO: Fix me once we can get fake changes into the test data + #self.getJsonResponse('api.get_image_changes', + # params=dict(repository=ADMIN_ACCESS_USER + '/simple', + # image_id=image_id)) + + +class TestListAndDeleteTag(ApiTestCase): + def test_listtagimagesanddeletetag(self): + self.login(ADMIN_ACCESS_USER) + + # List the images for prod. + json = self.getJsonResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='prod')) + + prod_images = json['images'] + assert len(prod_images) > 0 + + # List the images for staging. + json = self.getJsonResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='staging')) + + staging_images = json['images'] + assert len(prod_images) == len(staging_images) + 1 + + # Delete prod. + self.deleteResponse('api.delete_full_tag', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='prod'), + expected_code=204) + + # Make sure the tag is gone. + self.getResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='prod'), + expected_code=404) + + # Make the sure the staging images are still there. + json = self.getJsonResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='staging')) + + self.assertEquals(staging_images, json['images']) + + + def test_deletesubtag(self): + self.login(ADMIN_ACCESS_USER) + + # List the images for prod. + json = self.getJsonResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='prod')) + + prod_images = json['images'] + assert len(prod_images) > 0 + + # Delete staging. + self.deleteResponse('api.delete_full_tag', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='staging'), + expected_code=204) + + # Make sure the prod images are still around. + json = self.getJsonResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='prod')) + + self.assertEquals(prod_images, json['images']) + + +class TestRepoPermissions(ApiTestCase): + def listUserPermissions(self): + return self.getJsonResponse('api.list_repo_user_permissions', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'))['permissions'] + + def listTeamPermissions(self): + return self.getJsonResponse('api.list_repo_team_permissions', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO))['permissions'] + + def test_userpermissions(self): + self.login(ADMIN_ACCESS_USER) + + # The repo should start with just the admin as a user perm. + permissions = self.listUserPermissions() + + self.assertEquals(1, len(permissions)) + assert ADMIN_ACCESS_USER in permissions + self.assertEquals('admin', permissions[ADMIN_ACCESS_USER]['role']) + + # Add another user. + self.putJsonResponse('api.change_user_permissions', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', username=NO_ACCESS_USER), + data=dict(role='read')) + + # Verify the user is present. + permissions = self.listUserPermissions() + + self.assertEquals(2, len(permissions)) + assert NO_ACCESS_USER in permissions + self.assertEquals('read', permissions[NO_ACCESS_USER]['role']) + + json = self.getJsonResponse('api.get_user_permissions', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', username=NO_ACCESS_USER)) + self.assertEquals('read', json['role']) + + # Change the user's permissions. + self.putJsonResponse('api.change_user_permissions', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', username=NO_ACCESS_USER), + data=dict(role='admin')) + + # Verify. + permissions = self.listUserPermissions() + + self.assertEquals(2, len(permissions)) + assert NO_ACCESS_USER in permissions + self.assertEquals('admin', permissions[NO_ACCESS_USER]['role']) + + # Delete the user's permission. + self.deleteResponse('api.delete_user_permissions', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', username=NO_ACCESS_USER)) + + # Verify. + permissions = self.listUserPermissions() + + self.assertEquals(1, len(permissions)) + assert not NO_ACCESS_USER in permissions + + + def test_teampermissions(self): + self.login(ADMIN_ACCESS_USER) + + # The repo should start with just the readers as a team perm. + permissions = self.listTeamPermissions() + + self.assertEquals(1, len(permissions)) + assert 'readers' in permissions + self.assertEquals('read', permissions['readers']['role']) + + # Add another team. + self.putJsonResponse('api.change_team_permissions', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO, teamname='owners'), + data=dict(role='write')) + + # Verify the team is present. + permissions = self.listTeamPermissions() + + self.assertEquals(2, len(permissions)) + assert 'owners' in permissions + self.assertEquals('write', permissions['owners']['role']) + + json = self.getJsonResponse('api.get_team_permissions', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO, teamname='owners')) + self.assertEquals('write', json['role']) + + # Change the team's permissions. + self.putJsonResponse('api.change_team_permissions', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO, teamname='owners'), + data=dict(role='admin')) + + # Verify. + permissions = self.listTeamPermissions() + + self.assertEquals(2, len(permissions)) + assert 'owners' in permissions + self.assertEquals('admin', permissions['owners']['role']) + + # Delete the team's permission. + self.deleteResponse('api.delete_team_permissions', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO, teamname='owners')) + + # Verify. + permissions = self.listTeamPermissions() + + self.assertEquals(1, len(permissions)) + assert not 'owners' in permissions + +class TestApiTokens(ApiTestCase): + def listTokens(self): + return self.getJsonResponse('api.list_repo_tokens', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'))['tokens'] + + def test_tokens(self): + self.login(ADMIN_ACCESS_USER) + + # Create a new token. + json = self.postJsonResponse('api.create_token', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(role='read', friendlyName='mytoken'), + expected_code=201) + + self.assertEquals('mytoken', json['friendlyName']) + self.assertEquals('read', json['role']) + token_code = json['code'] + + # Verify. + tokens = self.listTokens() + assert token_code in tokens + self.assertEquals('mytoken', tokens[token_code]['friendlyName']) + + json = self.getJsonResponse('api.get_tokens', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', code=token_code)) + self.assertEquals(tokens[token_code], json) + + # Change the token's permission. + self.putJsonResponse('api.change_token', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', code=token_code), + data=dict(role='write')) + + # Verify. + json = self.getJsonResponse('api.get_tokens', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', code=token_code)) + self.assertEquals('write', json['role']) + + # Delete the token. + self.deleteResponse('api.delete_token', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', code=token_code)) + + # Verify. + self.getResponse('api.get_tokens', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', code=token_code), + expected_code=404) + + +class TestUserCard(ApiTestCase): + def test_getusercard(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_user_card') + + self.assertEquals('4242', json['card']['last4']) + self.assertEquals('Visa', json['card']['type']) + + def test_setusercard_error(self): + self.login(ADMIN_ACCESS_USER) + json = self.postJsonResponse('api.set_user_card', + data=dict(token='sometoken'), + expected_code=402) + assert 'carderror' in json + + +class TestOrgCard(ApiTestCase): + def test_getorgcard(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_org_card', + params=dict(orgname=ORGANIZATION)) + + self.assertEquals('4242', json['card']['last4']) + self.assertEquals('Visa', json['card']['type']) + + +class TestUserSubscription(ApiTestCase): + def getSubscription(self): + return self.getJsonResponse('api.get_user_subscription') + + def test_updateplan(self): + self.login(ADMIN_ACCESS_USER) + + # Change the plan. + self.putJsonResponse('api.update_user_subscription', + data=dict(plan='free')) + + # Verify + sub = self.getSubscription() + self.assertEquals('free', sub['plan']) + + # Change the plan. + self.putJsonResponse('api.update_user_subscription', + data=dict(plan='bus-large')) + + # Verify + sub = self.getSubscription() + self.assertEquals('bus-large', sub['plan']) + + +class TestOrgSubscription(ApiTestCase): + def getSubscription(self): + return self.getJsonResponse('api.get_org_subscription', params=dict(orgname=ORGANIZATION)) + + def test_updateplan(self): + self.login(ADMIN_ACCESS_USER) + + # Change the plan. + self.putJsonResponse('api.update_org_subscription', + params=dict(orgname=ORGANIZATION), + data=dict(plan='free')) + + # Verify + sub = self.getSubscription() + self.assertEquals('free', sub['plan']) + + # Change the plan. + self.putJsonResponse('api.update_org_subscription', + params=dict(orgname=ORGANIZATION), + data=dict(plan='bus-large')) + + # Verify + sub = self.getSubscription() + self.assertEquals('bus-large', sub['plan']) + + +class TestUserRobots(ApiTestCase): + def getRobotNames(self): + return [r['name'] for r in self.getJsonResponse('api.get_user_robots')['robots']] + + def test_robots(self): + self.login(NO_ACCESS_USER) + + # Create a robot. + json = self.putJsonResponse('api.create_user_robot', + params=dict(robot_shortname='bender'), + expected_code=201) + + self.assertEquals(NO_ACCESS_USER + '+bender', json['name']) + + # Verify. + robots = self.getRobotNames() + assert NO_ACCESS_USER + '+bender' in robots + + # Delete the robot. + self.deleteResponse('api.delete_user_robot', + params=dict(robot_shortname='bender')) + + # Verify. + robots = self.getRobotNames() + assert not NO_ACCESS_USER + '+bender' in robots + + +class TestOrgRobots(ApiTestCase): + def getRobotNames(self): + return [r['name'] for r in self.getJsonResponse('api.get_org_robots', + params=dict(orgname=ORGANIZATION))['robots']] + + def test_robots(self): + self.login(ADMIN_ACCESS_USER) + + # Create a robot. + json = self.putJsonResponse('api.create_org_robot', + params=dict(orgname=ORGANIZATION, robot_shortname='bender'), + expected_code=201) + + self.assertEquals(ORGANIZATION + '+bender', json['name']) + + # Verify. + robots = self.getRobotNames() + assert ORGANIZATION + '+bender' in robots + + # Delete the robot. + self.deleteResponse('api.delete_org_robot', + params=dict(orgname=ORGANIZATION, robot_shortname='bender')) + + # Verify. + robots = self.getRobotNames() + assert not ORGANIZATION + '+bender' in robots + + +class TestLogs(ApiTestCase): + def test_user_logs(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.list_user_logs') + assert 'logs' in json + assert 'start_time' in json + assert 'end_time' in json + + def test_org_logs(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.list_org_logs', params=dict(orgname=ORGANIZATION)) + assert 'logs' in json + assert 'start_time' in json + assert 'end_time' in json + + def test_performer(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.list_org_logs', params=dict(orgname=ORGANIZATION)) + all_logs = json['logs'] + + json = self.getJsonResponse('api.list_org_logs', + params=dict(performer=READ_ACCESS_USER, orgname=ORGANIZATION)) + + assert len(json['logs']) < len(all_logs) + for log in json['logs']: + self.assertEquals(READ_ACCESS_USER, log['performer']['name']) if __name__ == '__main__': unittest.main() From 83a34c0ef3be44248c096a7bfcab8af3965163f1 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 3 Feb 2014 18:30:06 -0500 Subject: [PATCH 06/17] Fix the few broken security tests after the error code fixes --- test/specs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/specs.py b/test/specs.py index e0a037655..0c44903b3 100644 --- a/test/specs.py +++ b/test/specs.py @@ -231,9 +231,9 @@ def build_specs(): TestSpec(url_for('api.get_webhook', repository=PUBLIC_REPO, public_id=FAKE_WEBHOOK), admin_code=403), TestSpec(url_for('api.get_webhook', repository=ORG_REPO, - public_id=FAKE_WEBHOOK), admin_code=400), + public_id=FAKE_WEBHOOK), admin_code=404), TestSpec(url_for('api.get_webhook', repository=PRIVATE_REPO, - public_id=FAKE_WEBHOOK), admin_code=400), + public_id=FAKE_WEBHOOK), admin_code=404), TestSpec(url_for('api.list_webhooks', repository=PUBLIC_REPO), admin_code=403), @@ -382,9 +382,9 @@ def build_specs(): TestSpec(url_for('api.get_tokens', repository=PUBLIC_REPO, code=FAKE_TOKEN), admin_code=403), TestSpec(url_for('api.get_tokens', repository=ORG_REPO, code=FAKE_TOKEN), - admin_code=400), + admin_code=404), TestSpec(url_for('api.get_tokens', repository=PRIVATE_REPO, - code=FAKE_TOKEN), admin_code=400), + code=FAKE_TOKEN), admin_code=404), TestSpec(url_for('api.create_token', repository=PUBLIC_REPO), admin_code=403).set_method('POST'), From 4c9a1c1621f80c85e0256b854602d9146e41b740 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 5 Feb 2014 17:00:33 -0500 Subject: [PATCH 07/17] Disable pretty printing of JSON, since it can break Docker CLI --- application.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application.py b/application.py index 80411d50f..f15925b7e 100644 --- a/application.py +++ b/application.py @@ -4,6 +4,8 @@ import os from app import app as application from data.model import db as model_db +# Turn off pretty printing of JSON responses since it can break Docker. +application.config['JSONIFY_PRETTYPRINT_REGULAR'] = False # Initialize logging application.config['LOGGING_CONFIG']() From 732ce83795390074f5ade4ebf44b25e164d292fd Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 5 Feb 2014 17:05:06 -0500 Subject: [PATCH 08/17] Move JSON pretty print config flag to the proper place --- application.py | 3 --- config.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/application.py b/application.py index f15925b7e..5cfb29108 100644 --- a/application.py +++ b/application.py @@ -4,9 +4,6 @@ import os from app import app as application from data.model import db as model_db -# Turn off pretty printing of JSON responses since it can break Docker. -application.config['JSONIFY_PRETTYPRINT_REGULAR'] = False - # Initialize logging application.config['LOGGING_CONFIG']() diff --git a/config.py b/config.py index f9837357c..bf5afb96d 100644 --- a/config.py +++ b/config.py @@ -14,7 +14,7 @@ from test import analytics as fake_analytics class FlaskConfig(object): SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884' - + JSONIFY_PRETTYPRINT_REGULAR = False class FlaskProdConfig(FlaskConfig): SESSION_COOKIE_SECURE = True From ef68982728a670652d6d95736c90c275064d51ec Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 6 Feb 2014 14:13:35 -0500 Subject: [PATCH 09/17] Garbage collect repositories on push and on tag deletion. --- data/model.py | 33 +++++++++++++++++++++++++++++++++ endpoints/index.py | 5 ++++- endpoints/tags.py | 16 +--------------- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/data/model.py b/data/model.py index c047e1602..336fc1f32 100644 --- a/data/model.py +++ b/data/model.py @@ -973,6 +973,39 @@ def delete_tag_and_images(namespace_name, repository_name, tag_name): store.remove(repository_path) +def garbage_collect_repository(namespace_name, repository_name): + # Get a list of all images used by tags in the repository + tag_query = (RepositoryTag + .select(RepositoryTag, Image) + .join(Image) + .switch(RepositoryTag) + .join(Repository) + .where(Repository.name == repository_name, + Repository.namespace == namespace_name)) + + referenced_anscestors = set() + for tag in tag_query: + ancestor_list = [int(img) for img in tag.image.ancestors.split('/')[1:-1]] + referenced_anscestors = referenced_anscestors.union(set(ancestor_list)) + referenced_anscestors.add(tag.image.id) + + all_repo_images = get_repository_images(namespace_name, repository_name) + all_images = {int(img.id):img for img in all_repo_images} + to_remove = set(all_images.keys()).difference(referenced_anscestors) + + logger.info('Cleaning up unreferenced images: %s', to_remove) + + for image_id_to_remove in to_remove: + image_to_remove = all_images[image_id_to_remove] + image_path = store.image_path(namespace_name, repository_name, + image_to_remove.docker_image_id) + logger.debug('Recursively deleting image path: %s' % image_path) + image_to_remove.delete_instance() + store.remove(image_path) + + return len(to_remove) + + def get_tag_image(namespace_name, repository_name, tag_name): joined = Image.select().join(RepositoryTag).join(Repository) fetched = list(joined.where(Repository.name == repository_name, diff --git a/endpoints/index.py b/endpoints/index.py index 998a0f94d..41064d805 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -222,6 +222,8 @@ def update_images(namespace, repository): updated_tags[image['Tag']] = image['id'] model.set_image_checksum(image['id'], repo, image['checksum']) + num_removed = model.garbage_collect_repository(namespace, repository) + # Generate a job for each webhook that has been added to this repo webhooks = model.list_webhooks(namespace, repository) for webhook in webhooks: @@ -237,7 +239,8 @@ def update_images(namespace, repository): 'homepage': 'https://quay.io/repository/%s' % repo_string, 'visibility': repo.visibility.name, 'updated_tags': updated_tags, - 'pushed_image_count': len(image_with_checksums), + 'pushed_image_count': len(image_with_checksums), + 'pruned_image_count': num_removed, } webhook_queue.put(json.dumps(webhook_data)) diff --git a/endpoints/tags.py b/endpoints/tags.py index f6b0e1163..cbfefda9f 100644 --- a/endpoints/tags.py +++ b/endpoints/tags.py @@ -73,21 +73,7 @@ def delete_tag(namespace, repository, tag): if permission.can(): model.delete_tag(namespace, repository, tag) - - return make_response('Deleted', 204) - - abort(403) - - -@tags.route('/repositories//tags', - methods=['DELETE']) -@process_auth -@parse_repository_name -def delete_repository_tags(namespace, repository): - permission = ModifyRepositoryPermission(namespace, repository) - - if permission.can(): - model.delete_all_repository_tags(namespace, repository) + model.garbage_collect_repository(namespace, repository) return make_response('Deleted', 204) From 1012daf3031c53342baff58d7932c9e7db05dbd6 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 6 Feb 2014 14:15:59 -0500 Subject: [PATCH 10/17] Remove the tests for the now removed method: delete all repository tags. --- test/specs.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/specs.py b/test/specs.py index e0a037655..0a523f0fe 100644 --- a/test/specs.py +++ b/test/specs.py @@ -587,13 +587,4 @@ def build_index_specs(): IndexTestSpec(url_for('tags.delete_tag', repository=ORG_REPO, tag=FAKE_TAG_NAME), NO_REPO, 403, 403, 403, 400).set_method('DELETE'), - - IndexTestSpec(url_for('tags.delete_repository_tags', - repository=PUBLIC_REPO), - NO_REPO, 403, 403, 403, 403).set_method('DELETE'), - IndexTestSpec(url_for('tags.delete_repository_tags', - repository=PRIVATE_REPO), - NO_REPO, 403, 403, 403, 204).set_method('DELETE'), - IndexTestSpec(url_for('tags.delete_repository_tags', repository=ORG_REPO), - NO_REPO, 403, 403, 403, 204).set_method('DELETE'), ] From 2a89c2bb35f7c8fa4e47cf5f77bf1b38b2a262a7 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 6 Feb 2014 14:22:13 -0500 Subject: [PATCH 11/17] Refactor the garbage collection code a tiny bit. --- data/model.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/model.py b/data/model.py index 336fc1f32..66cb37821 100644 --- a/data/model.py +++ b/data/model.py @@ -985,7 +985,9 @@ def garbage_collect_repository(namespace_name, repository_name): referenced_anscestors = set() for tag in tag_query: - ancestor_list = [int(img) for img in tag.image.ancestors.split('/')[1:-1]] + # The anscestor list is in the format '/1/2/3/', extract just the ids + anscestor_id_strings = tag.image.ancestors.split('/')[1:-1] + ancestor_list = [int(img_id_str) for img_id_str in anscestor_id_strings] referenced_anscestors = referenced_anscestors.union(set(ancestor_list)) referenced_anscestors.add(tag.image.id) @@ -999,8 +1001,8 @@ def garbage_collect_repository(namespace_name, repository_name): image_to_remove = all_images[image_id_to_remove] image_path = store.image_path(namespace_name, repository_name, image_to_remove.docker_image_id) - logger.debug('Recursively deleting image path: %s' % image_path) image_to_remove.delete_instance() + logger.debug('Deleting image storage: %s' % image_path) store.remove(image_path) return len(to_remove) From ec8f599a110a0977e6376516e38df82b01862163 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 6 Feb 2014 14:40:36 -0500 Subject: [PATCH 12/17] First half of pylint fixes for the unit tests. --- test/test_api_usage.py | 194 ++++++++++++++++++++++++++--------------- 1 file changed, 126 insertions(+), 68 deletions(-) diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 7f586a614..fca9f2eed 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -1,11 +1,10 @@ import unittest -import json +import json as py_json from flask import url_for from endpoints.api import api from app import app from initdb import setup_database_for_testing, finished_database_for_testing -from specs import build_specs from data import model app.register_blueprint(api, url_prefix='/api') @@ -25,6 +24,7 @@ NEW_USER_DETAILS = { 'email': 'bobby@tables.com', } + class ApiTestCase(unittest.TestCase): def setUp(self): setup_database_for_testing(self) @@ -40,12 +40,13 @@ class ApiTestCase(unittest.TestCase): rv = self.app.get(url_for(method_name, **params)) self.assertEquals(200, rv.status_code) data = rv.data - parsed = json.loads(data) + parsed = py_json.loads(data) return parsed def postResponse(self, method_name, params={}, data={}, expected_code=200): - rv = self.app.post(url_for(method_name, **params), data=json.dumps(data), - headers={"Content-Type": "application/json"}) + rv = self.app.post(url_for(method_name, **params), + data=py_json.dumps(data), + headers={"Content-Type": "application/json"}) self.assertEquals(rv.status_code, expected_code) return rv.data @@ -59,36 +60,43 @@ class ApiTestCase(unittest.TestCase): self.assertEquals(rv.status_code, expected_code) return rv.data - def postJsonResponse(self, method_name, params={}, data={}, expected_code=200): - rv = self.app.post(url_for(method_name, **params), data=json.dumps(data), - headers={"Content-Type": "application/json"}) + def postJsonResponse(self, method_name, params={}, data={}, + expected_code=200): + rv = self.app.post(url_for(method_name, **params), + data=py_json.dumps(data), + headers={"Content-Type": "application/json"}) if rv.status_code != expected_code: print 'Mismatch data for method %s: %s' % (method_name, rv.data) self.assertEquals(rv.status_code, expected_code) data = rv.data - parsed = json.loads(data) + parsed = py_json.loads(data) return parsed - def putJsonResponse(self, method_name, params={}, data={}, expected_code=200): - rv = self.app.put(url_for(method_name, **params), data=json.dumps(data), - headers={"Content-Type": "application/json"}) + def putJsonResponse(self, method_name, params={}, data={}, + expected_code=200): + rv = self.app.put(url_for(method_name, **params), data=py_json.dumps(data), + headers={"Content-Type": "application/json"}) if rv.status_code != expected_code: print 'Mismatch data for method %s: %s' % (method_name, rv.data) self.assertEquals(rv.status_code, expected_code) data = rv.data - parsed = json.loads(data) + parsed = py_json.loads(data) return parsed def login(self, username, password='password'): - return self.postJsonResponse('api.signin_user', data=dict(username=username, password=password)) + return self.postJsonResponse('api.signin_user', + data=dict(username=username, + password=password)) + class TestDiscovery(ApiTestCase): def test_discovery(self): - """ Basic sanity check that discovery returns valid JSON in the expected format. """ + """ Basic sanity check that discovery returns valid JSON in the expected + format. """ json = self.getJsonResponse('api.discovery') found = set([]) for method_info in json['endpoints']: @@ -96,9 +104,11 @@ class TestDiscovery(ApiTestCase): assert 'discovery' in found + class TestPlans(ApiTestCase): def test_plans(self): - """ Basic sanity check that the plans are returned in the expected format. """ + """ Basic sanity check that the plans are returned in the expectedformat. + """ json = self.getJsonResponse('api.list_plans') found = set([]) for method_info in json['plans']: @@ -106,6 +116,7 @@ class TestPlans(ApiTestCase): assert 'free' in found + class TestLoggedInUser(ApiTestCase): def test_guest(self): json = self.getJsonResponse('api.get_logged_in_user') @@ -117,6 +128,7 @@ class TestLoggedInUser(ApiTestCase): assert json['anonymous'] == False assert json['username'] == READ_ACCESS_USER + class TestGetUserPrivateCount(ApiTestCase): def test_nonallowed(self): self.login(READ_ACCESS_USER) @@ -130,11 +142,13 @@ class TestGetUserPrivateCount(ApiTestCase): assert json['privateCount'] == 6 assert json['reposAllowed'] > 0 + class TestConvertToOrganization(ApiTestCase): def test_sameadminuser(self): self.login(READ_ACCESS_USER) json = self.postJsonResponse('api.convert_user_to_organization', - data={'adminUser': READ_ACCESS_USER, 'adminPassword': 'password'}, + data={'adminUser': READ_ACCESS_USER, + 'adminPassword': 'password'}, expected_code=400) self.assertEqual('The admin user is not valid', json['message']) @@ -142,18 +156,22 @@ class TestConvertToOrganization(ApiTestCase): def test_invalidadminuser(self): self.login(READ_ACCESS_USER) json = self.postJsonResponse('api.convert_user_to_organization', - data={'adminUser': 'unknownuser', 'adminPassword': 'password'}, + data={'adminUser': 'unknownuser', + 'adminPassword': 'password'}, expected_code=400) - self.assertEqual('The admin user credentials are not valid', json['message']) + self.assertEqual('The admin user credentials are not valid', + json['message']) def test_invalidadminpassword(self): self.login(READ_ACCESS_USER) json = self.postJsonResponse('api.convert_user_to_organization', - data={'adminUser': ADMIN_ACCESS_USER, 'adminPassword': 'invalidpass'}, + data={'adminUser': ADMIN_ACCESS_USER, + 'adminPassword': 'invalidpass'}, expected_code=400) - self.assertEqual('The admin user credentials are not valid', json['message']) + self.assertEqual('The admin user credentials are not valid', + json['message']) def test_convert(self): self.login(READ_ACCESS_USER) @@ -170,7 +188,8 @@ class TestConvertToOrganization(ApiTestCase): # Verify the admin user is the org's admin. self.login(ADMIN_ACCESS_USER) - json = self.getJsonResponse('api.get_organization', params=dict(orgname=READ_ACCESS_USER)) + json = self.getJsonResponse('api.get_organization', + params=dict(orgname=READ_ACCESS_USER)) self.assertEquals(READ_ACCESS_USER, json['name']) self.assertEquals(True, json['is_admin']) @@ -179,16 +198,19 @@ class TestConvertToOrganization(ApiTestCase): class TestChangeUserDetails(ApiTestCase): def test_changepassword(self): self.login(READ_ACCESS_USER) - self.putJsonResponse('api.change_user_details', data=dict(password='newpasswordiscool')) + self.putJsonResponse('api.change_user_details', + data=dict(password='newpasswordiscool')) self.login(READ_ACCESS_USER, password='newpasswordiscool') def test_changeinvoiceemail(self): self.login(READ_ACCESS_USER) - json = self.putJsonResponse('api.change_user_details', data=dict(invoice_email=True)) + json = self.putJsonResponse('api.change_user_details', + data=dict(invoice_email=True)) self.assertEquals(True, json['invoice_email']) - json = self.putJsonResponse('api.change_user_details', data=dict(invoice_email=False)) + json = self.putJsonResponse('api.change_user_details', + data=dict(invoice_email=False)) self.assertEquals(False, json['invoice_email']) @@ -227,7 +249,8 @@ class TestGetMatchingEntities(ApiTestCase): self.login(NO_ACCESS_USER) json = self.getJsonResponse('api.get_matching_entities', - params=dict(prefix='o', namespace=ORGANIZATION, includeTeams=True)) + params=dict(prefix='o', namespace=ORGANIZATION, + includeTeams=True)) names = set([r['name'] for r in json['results']]) assert 'outsideorg' in names @@ -237,7 +260,8 @@ class TestGetMatchingEntities(ApiTestCase): self.login(ADMIN_ACCESS_USER) json = self.getJsonResponse('api.get_matching_entities', - params=dict(prefix='o', namespace=ORGANIZATION, includeTeams=True)) + params=dict(prefix='o', namespace=ORGANIZATION, + includeTeams=True)) names = set([r['name'] for r in json['results']]) assert 'outsideorg' in names @@ -248,24 +272,29 @@ class TestCreateOrganization(ApiTestCase): def test_existinguser(self): self.login(ADMIN_ACCESS_USER) - json = self.postJsonResponse('api.create_organization', data=dict(name=ADMIN_ACCESS_USER), + json = self.postJsonResponse('api.create_organization', + data=dict(name=ADMIN_ACCESS_USER), expected_code=400) - self.assertEquals('A user or organization with this name already exists', json['message']) + self.assertEquals('A user or organization with this name already exists', + json['message']) def test_existingorg(self): self.login(ADMIN_ACCESS_USER) - json = self.postJsonResponse('api.create_organization', data=dict(name=ORGANIZATION), + json = self.postJsonResponse('api.create_organization', + data=dict(name=ORGANIZATION), expected_code=400) - self.assertEquals('A user or organization with this name already exists', json['message']) + self.assertEquals('A user or organization with this name already exists', + json['message']) def test_createorg(self): self.login(ADMIN_ACCESS_USER) data = self.postResponse('api.create_organization', - data=dict(name='neworg', email='test@example.com'), + data=dict(name='neworg', + email='test@example.com'), expected_code=201) self.assertEquals('Created', data) @@ -275,7 +304,8 @@ class TestCreateOrganization(ApiTestCase): assert organization is not None # Verify the admin user is the org's admin. - json = self.getJsonResponse('api.get_organization', params=dict(orgname='neworg')) + json = self.getJsonResponse('api.get_organization', + params=dict(orgname='neworg')) self.assertEquals('neworg', json['name']) self.assertEquals(True, json['is_admin']) @@ -293,14 +323,16 @@ class TestGetOrganization(ApiTestCase): def test_getorganization(self): self.login(READ_ACCESS_USER) - json = self.getJsonResponse('api.get_organization', params=dict(orgname=ORGANIZATION)) + json = self.getJsonResponse('api.get_organization', + params=dict(orgname=ORGANIZATION)) self.assertEquals(ORGANIZATION, json['name']) self.assertEquals(False, json['is_admin']) def test_getorganization_asadmin(self): self.login(ADMIN_ACCESS_USER) - json = self.getJsonResponse('api.get_organization', params=dict(orgname=ORGANIZATION)) + json = self.getJsonResponse('api.get_organization', + params=dict(orgname=ORGANIZATION)) self.assertEquals(ORGANIZATION, json['name']) self.assertEquals(True, json['is_admin']) @@ -370,7 +402,9 @@ class TestCreateOrganizationPrototypes(ApiTestCase): json = self.postJsonResponse('api.create_organization_prototype_permission', params=dict(orgname=ORGANIZATION), - data=dict(role='read', delegate={'kind': 'team', 'name': 'readers'})) + data=dict(role='read', + delegate={'kind': 'team', + 'name': 'readers'})) self.assertEquals('read', json['role']) pid = json['id'] @@ -419,13 +453,12 @@ class TestUpdateOrganizationPrototypes(ApiTestCase): # Update a prototype. json = self.putJsonResponse('api.delete_organization_prototype_permission', - params=dict(orgname=ORGANIZATION, prototypeid=pid), - data=dict(role='admin')) + params=dict(orgname=ORGANIZATION, + prototypeid=pid), data=dict(role='admin')) self.assertEquals('admin', json['role']) - class TestGetOrganiaztionMembers(ApiTestCase): def test_getmembers(self): self.login(ADMIN_ACCESS_USER) @@ -441,7 +474,8 @@ class TestGetOrganiaztionMembers(ApiTestCase): self.login(ADMIN_ACCESS_USER) json = self.getJsonResponse('api.get_organization_member', - params=dict(orgname=ORGANIZATION, membername=ADMIN_ACCESS_USER)) + params=dict(orgname=ORGANIZATION, + membername=ADMIN_ACCESS_USER)) self.assertEquals(ADMIN_ACCESS_USER, json['member']['name']) self.assertEquals('user', json['member']['kind']) @@ -464,7 +498,8 @@ class TestGetOrganizationPrivateAllowed(ApiTestCase): self.login(ADMIN_ACCESS_USER) data = self.postResponse('api.create_organization', - data=dict(name='neworg', email='test@example.com'), + data=dict(name='neworg', + email='test@example.com'), expected_code=201) json = self.getJsonResponse('api.get_organization_private_allowed', @@ -478,8 +513,10 @@ class TestUpdateOrganizationTeam(ApiTestCase): self.login(ADMIN_ACCESS_USER) data = self.postJsonResponse('api.update_organization_team', - params=dict(orgname=ORGANIZATION, teamname='readers'), - data=dict(description = 'My cool team', role = 'creator')) + params=dict(orgname=ORGANIZATION, + teamname='readers'), + data=dict(description='My cool team', + role='creator')) self.assertEquals('My cool team', data['description']) self.assertEquals('creator', data['role']) @@ -496,15 +533,18 @@ class TestUpdateOrganizationTeam(ApiTestCase): self.login(ADMIN_ACCESS_USER) data = self.putJsonResponse('api.update_organization_team', - params=dict(orgname=ORGANIZATION, teamname='newteam'), - data=dict(description = 'My cool team', role = 'member'), + params=dict(orgname=ORGANIZATION, + teamname='newteam'), + data=dict(description='My cool team', + role='member'), expected_code=201) self.assertEquals('My cool team', data['description']) self.assertEquals('member', data['role']) # Verify the team was created. - json = self.getJsonResponse('api.get_organization', params=dict(orgname=ORGANIZATION)) + json = self.getJsonResponse('api.get_organization', + params=dict(orgname=ORGANIZATION)) assert 'newteam' in json['teams'] @@ -516,7 +556,8 @@ class TestDeleteOrganizationTeam(ApiTestCase): params=dict(orgname=ORGANIZATION, teamname='readers')) # Make sure the team was deleted - json = self.getJsonResponse('api.get_organization', params=dict(orgname=ORGANIZATION)) + json = self.getJsonResponse('api.get_organization', + params=dict(orgname=ORGANIZATION)) assert not 'readers' in json['teams'] def test_attemptdeleteowners(self): @@ -539,7 +580,8 @@ class TestGetOrganizationTeamMembers(ApiTestCase): self.login(ADMIN_ACCESS_USER) json = self.getJsonResponse('api.get_organization_team_members', - params=dict(orgname=ORGANIZATION, teamname='readers')) + params=dict(orgname=ORGANIZATION, + teamname='readers')) assert READ_ACCESS_USER in json['members'] @@ -549,12 +591,14 @@ class TestUpdateOrganizationTeamMember(ApiTestCase): self.login(ADMIN_ACCESS_USER) self.postJsonResponse('api.update_organization_team_member', - params=dict(orgname=ORGANIZATION, teamname='readers', membername=NO_ACCESS_USER)) + params=dict(orgname=ORGANIZATION, teamname='readers', + membername=NO_ACCESS_USER)) # Verify the user was added to the team. json = self.getJsonResponse('api.get_organization_team_members', - params=dict(orgname=ORGANIZATION, teamname='readers')) + params=dict(orgname=ORGANIZATION, + teamname='readers')) assert NO_ACCESS_USER in json['members'] @@ -564,12 +608,14 @@ class TestDeleteOrganizationTeamMember(ApiTestCase): self.login(ADMIN_ACCESS_USER) self.deleteResponse('api.delete_organization_team_member', - params=dict(orgname=ORGANIZATION, teamname='readers', membername=READ_ACCESS_USER)) + params=dict(orgname=ORGANIZATION, teamname='readers', + membername=READ_ACCESS_USER)) # Verify the user was removed from the team. json = self.getJsonResponse('api.get_organization_team_members', - params=dict(orgname=ORGANIZATION, teamname='readers')) + params=dict(orgname=ORGANIZATION, + teamname='readers')) assert not READ_ACCESS_USER in json['members'] @@ -579,7 +625,8 @@ class TestCreateRepo(ApiTestCase): self.login(ADMIN_ACCESS_USER) json = self.postJsonResponse('api.create_repo', - data=dict(repository='simple', visibility='public'), + data=dict(repository='simple', + visibility='public'), expected_code=400) self.assertEquals('Repository already exists', json['message']) @@ -589,7 +636,9 @@ class TestCreateRepo(ApiTestCase): self.login(ADMIN_ACCESS_USER) json = self.postJsonResponse('api.create_repo', - data=dict(repository='newrepo', visibility='public', description='')) + data=dict(repository='newrepo', + visibility='public', + description='')) self.assertEquals(ADMIN_ACCESS_USER, json['namespace']) @@ -645,7 +694,9 @@ class TestListRepos(ApiTestCase): def test_listrepos_filter(self): self.login(READ_ACCESS_USER) - json = self.getJsonResponse('api.list_repos', params=dict(namespace=ORGANIZATION, public=False)) + json = self.getJsonResponse('api.list_repos', + params=dict(namespace=ORGANIZATION, + public=False)) for repo in json['repositories']: self.assertEquals(ORGANIZATION, repo['namespace']) @@ -658,63 +709,68 @@ class TestListRepos(ApiTestCase): class TestUpdateRepo(ApiTestCase): - def test_updatedescription(self): + SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple' + def test_updatedescription(self): self.login(ADMIN_ACCESS_USER) self.putJsonResponse('api.update_repo', - params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + params=dict(repository=self.SIMPLE_REPO), data=dict(description='Some cool repo')) # Verify the repo description was updated. json = self.getJsonResponse('api.get_repo', - params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + params=dict(repository=self.SIMPLE_REPO)) self.assertEquals('Some cool repo', json['description']) class TestChangeRepoVisibility(ApiTestCase): - def test_changevisibility(self): + SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple' + def test_changevisibility(self): self.login(ADMIN_ACCESS_USER) # Make public. self.postJsonResponse('api.change_repo_visibility', - params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + params=dict(repository=self.SIMPLE_REPO), data=dict(visibility='public')) # Verify the visibility. json = self.getJsonResponse('api.get_repo', - params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + params=dict(repository=self.SIMPLE_REPO)) self.assertEquals(True, json['is_public']) # Make private. self.postJsonResponse('api.change_repo_visibility', - params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + params=dict(repository=self.SIMPLE_REPO), data=dict(visibility='private')) # Verify the visibility. json = self.getJsonResponse('api.get_repo', - params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + params=dict(repository=self.SIMPLE_REPO)) self.assertEquals(False, json['is_public']) class TestDeleteRepository(ApiTestCase): - def test_deleterepo(self): + SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple' + def test_deleterepo(self): self.login(ADMIN_ACCESS_USER) self.deleteResponse('api.delete_repository', - params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + params=dict(repository=self.SIMPLE_REPO)) # Verify the repo was deleted. self.getResponse('api.get_repo', - params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + params=dict(repository=self.SIMPLE_REPO), expected_code=404) + class TestGetRepository(ApiTestCase): + PUBLIC_REPO = PUBLIC_USER + '/publicrepo' def test_getrepo_public_asguest(self): json = self.getJsonResponse('api.get_repo', - params=dict(repository=PUBLIC_USER + '/publicrepo')) + params=dict(repository=self.PUBLIC_REPO)) self.assertEquals(PUBLIC_USER, json['namespace']) self.assertEquals('publicrepo', json['name']) @@ -732,7 +788,7 @@ class TestGetRepository(ApiTestCase): self.login(PUBLIC_USER) json = self.getJsonResponse('api.get_repo', - params=dict(repository=PUBLIC_USER + '/publicrepo')) + params=dict(repository=self.PUBLIC_REPO)) self.assertEquals(False, json['is_organization']) self.assertEquals(True, json['can_write']) @@ -887,6 +943,7 @@ class TestListAndGetImage(ApiTestCase): self.assertEquals(image['id'], ijson['id']) + class TestGetImageChanges(ApiTestCase): def test_getimagechanges(self): self.login(ADMIN_ACCESS_USER) @@ -1067,6 +1124,7 @@ class TestRepoPermissions(ApiTestCase): self.assertEquals(1, len(permissions)) assert not 'owners' in permissions + class TestApiTokens(ApiTestCase): def listTokens(self): return self.getJsonResponse('api.list_repo_tokens', From c8f0780aafec00c63570732cff740deec0aedf81 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 6 Feb 2014 14:49:53 -0500 Subject: [PATCH 13/17] Fix a subtle bug with create_or_update_tag. --- data/model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/model.py b/data/model.py index 66cb37821..dc478b682 100644 --- a/data/model.py +++ b/data/model.py @@ -1059,7 +1059,8 @@ def create_or_update_tag(namespace_name, repository_name, tag_name, (namespace_name, repository_name)) try: - image = Image.get(Image.docker_image_id == tag_docker_image_id) + image = Image.get(Image.docker_image_id == tag_docker_image_id, + Image.repository == repo) except Image.DoesNotExist: raise DataModelException('Invalid image with id: %s' % tag_docker_image_id) From c3cb1dfa8788a920921fbd2ace2ce8aa2e59292a Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 6 Feb 2014 15:34:19 -0500 Subject: [PATCH 14/17] Add a tool to fix the images that were wrong before c8f0780 fixed tags. --- tools/audittagimages.py | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tools/audittagimages.py diff --git a/tools/audittagimages.py b/tools/audittagimages.py new file mode 100644 index 000000000..e521a78c3 --- /dev/null +++ b/tools/audittagimages.py @@ -0,0 +1,48 @@ +from data.database import Image, RepositoryTag, Repository + +from app import app + + +store = app.config['STORAGE'] + + +tag_query = (RepositoryTag + .select(RepositoryTag, Image, Repository) + .join(Repository) + .switch(RepositoryTag) + .join(Image)) + +for tag in tag_query: + if tag.image.repository.id != tag.repository.id: + print('Repository tag pointing to external image: %s/%s:%s' % + (tag.repository.namespace, tag.repository.name, tag.name)) + + proper_image_layer_path = store.image_layer_path(tag.repository.namespace, + tag.repository.name, + tag.image.docker_image_id) + + has_storage = False + if store.exists(proper_image_layer_path): + print('Storage already in place: %s' % proper_image_layer_path) + has_storage = True + else: + print('Storage missing: %s' % proper_image_layer_path) + + has_db_entry = False + new_image = None + try: + new_image = Image.get(Image.docker_image_id == tag.image.docker_image_id, + Image.repository == tag.repository) + has_db_entry = True + print('DB image in place: %s invalid image id: %s' % (new_image.id, + tag.image.id)) + except Image.DoesNotExist: + print('DB image missing: %s' % tag.image.docker_image_id) + + if has_storage and has_db_entry: + print('Switching tag to proper image %s/%s/%s -> %s' % + (tag.repository.namespace, tag.repository.name, tag.name, + new_image.id)) + tag.image = new_image + tag.save() + print('Done') From 7d593b10782e8cde3759e18234286b5a2894dc1d Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 6 Feb 2014 17:33:50 -0500 Subject: [PATCH 15/17] Switch the delete tag response code to match the registry spect. --- endpoints/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints/tags.py b/endpoints/tags.py index cbfefda9f..a4ed782ab 100644 --- a/endpoints/tags.py +++ b/endpoints/tags.py @@ -75,6 +75,6 @@ def delete_tag(namespace, repository, tag): model.delete_tag(namespace, repository, tag) model.garbage_collect_repository(namespace, repository) - return make_response('Deleted', 204) + return make_response('Deleted', 200) abort(403) From bd47b9adf4e6e9cf0aadbebb804ebfeb90caefa7 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 6 Feb 2014 19:20:19 -0500 Subject: [PATCH 16/17] Add an about page. Change the terms in the footer. Add colors to the icons for the contact options. Update the sitemap. Remove empty controllers. --- endpoints/web.py | 6 +++ static/css/quay.css | 32 ++++++++++++++- static/js/app.js | 11 +++-- static/js/controllers.js | 12 ------ static/partials/about.html | 78 ++++++++++++++++++++++++++++++++++++ static/partials/contact.html | 8 ++-- static/sitemap.xml | 10 ++++- templates/base.html | 7 ++-- 8 files changed, 137 insertions(+), 27 deletions(-) create mode 100644 static/partials/about.html diff --git a/endpoints/web.py b/endpoints/web.py index 9550266c9..ad1c0571c 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -88,6 +88,12 @@ def contact(): return index('') +@web.route('/about/') +@no_cache +def about(): + return index('') + + @web.route('/new/') @no_cache def new(): diff --git a/static/css/quay.css b/static/css/quay.css index 7ee169fdf..eac3a8c64 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -2723,4 +2723,34 @@ p.editable:hover i { .contact-options { margin-top: 60px; -} \ No newline at end of file +} + +.contact-options .option-twitter .fa-circle { + color: #00b0ed; +} + +.contact-options .option-phone .fa-circle { + color: #1dd924; +} + +.contact-options .option-irc .fa-circle { + color: #e52f00; +} + +.contact-options .option-email .fa-circle { + color: #1b72f1; +} + +.about-us .row { + margin-bottom: 30px; +} + +.about-basic-icon { + display: inline-block; + margin-right: 10px; + color: #555; +} + +.about-basic-text { + display: inline-block; +} diff --git a/static/js/app.js b/static/js/app.js index 31f75158c..eb075afc1 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -783,15 +783,14 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}). when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html', reloadOnSearch: false, controller: UserAdminCtrl}). - when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html', - controller: GuideCtrl}). - when('/contact/', {title: 'Contact Us', description:'Different ways for you to get a hold of us when you need us most.', templateUrl: '/static/partials/contact.html', - controller: ContactCtrl}). + when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html'}). + when('/contact/', {title: 'Contact Us', description:'Different ways for you to get a hold of us when you need us most.', templateUrl: '/static/partials/contact.html'}). + when('/about/', {title: 'About Us', description:'Information about the Quay.io team and the company.', templateUrl: '/static/partials/about.html'}). when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io', 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', controller: SecurityCtrl}). - when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}). + templateUrl: '/static/partials/security.html'}). + when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html'}). 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', diff --git a/static/js/controllers.js b/static/js/controllers.js index fc79963d1..e468faba8 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -15,9 +15,6 @@ $.fn.clipboardCopy = function() { }); }; -function SigninCtrl($scope) { -}; - function PlansCtrl($scope, $location, UserService, PlanService) { // Load the list of plans. PlanService.getPlans(function(plans) { @@ -42,15 +39,6 @@ function PlansCtrl($scope, $location, UserService, PlanService) { }; } -function GuideCtrl($scope) { -} - -function SecurityCtrl($scope) { -} - -function ContactCtrl($scope) { -} - function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { $scope.namespace = null; $scope.page = 1; diff --git a/static/partials/about.html b/static/partials/about.html new file mode 100644 index 000000000..f59865cae --- /dev/null +++ b/static/partials/about.html @@ -0,0 +1,78 @@ +
+

+ About Us +

+ +
+
+

The Basics

+
+
+
+
+ Founded
+ 2012 +
+
+
+
+
+ Location
+ New York City, NY +
+
+
+
+
+ Worker Bees
+ 2 +
+
+
+ +
+
+

Our Story

+

Quay.io was originally created out of necessesity when we wanted to use Docker containers with DevTable IDE. We were using Docker containers to host and isolate server processes invoked on behalf of our users and often running their code. We started by building the Docker image dynamically whenever we spun up a new host node. The image was monolithic. It was too large, took too long to build, and was hard to manage conflicts. It was everything that Docker wasn't supposed to be. When we decided to split it up into pre-built images and host them somewhere, we noticed that there wasn't a good place to host images securely. Determined to scratch our own itch, we built Quay.io, and officially launched it as an aside in our presentation to the Docker New York City Meetup on October 2nd, 2013.

+

Since that time, our users have demanded that Quay.io become our main focus. Our customers rely on us to make sure they can store and distribute their Docker images, and we understand that solemn responsibility. Our customers have been fantastic with giving us great feedback and suggestions. We are working as hard as we can to deliver on the promise and execute our vision of what a top notch Docker registry should be. We thank you for taking this journey with us.

+
+
+ +
+
+

The Team

+ Our team is composed of two software engineers turned entrepreneurs: +
+
+
+
+

Jacob Moshenko
+ Co-Founder

+
+ +
+ +
+
+

Jacob graduated from The University of Michigan with a Bachelors in Computer Engineering. From there he allowed his love of flight and mountains to lure him to Seattle where he took a job with Boeing Commercial Airplanes working on the world's most accurate flight simulator. When he realized how much he also loved web development, he moved to Amazon to work on the e-commerce back-end. Finally, desiring to move to New York City, he moved to Google, where he worked on several products related to Google APIs.

+
+
+
+
+

Joseph Schorr
+ Co-Founder

+
+ +
+ +
+
+

Joseph graduated from University of Pennsylvania with a Bachelors and Masters in Computer Science. After a record setting (probably) five internships with Google, he took a full time position there to continue his work on exciting products such as Google Spreadsheets, the Google Closure Compiler, and Google APIs. Joseph was one of the original duo responsible for inventing the language and framework on which DevTable is built.

+
+
+
+
+

With a combined 10 years experience building tools for software engineers, our founding team knows what it takes to make software engineers happy doing their work. Combined with our love for the web, we are ready to make a difference in the way people think about software development in the cloud.

+
+
+
\ No newline at end of file diff --git a/static/partials/contact.html b/static/partials/contact.html index fe7a7ea33..bdc7dd159 100644 --- a/static/partials/contact.html +++ b/static/partials/contact.html @@ -5,7 +5,7 @@
-
+
@@ -14,7 +14,7 @@

support@quay.io

-
+
@@ -23,7 +23,7 @@

Freenode: #quayio

-
+
@@ -32,7 +32,7 @@

888-930-3475

-
+
diff --git a/static/sitemap.xml b/static/sitemap.xml index db9440376..f947dd3ce 100644 --- a/static/sitemap.xml +++ b/static/sitemap.xml @@ -11,12 +11,20 @@ https://quay.io/organizations/ - monthly + weekly https://quay.io/repository/ always + + https://quay.io/contact/ + monthly + + + https://quay.io/about/ + monthly + https://quay.io/tos monthly diff --git a/templates/base.html b/templates/base.html index 731e19d18..566db647c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -116,10 +116,11 @@ var isProd = document.location.hostname === 'quay.io';
From b9a5e355eba80f736c070dd06edefe9da9b53704 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Thu, 6 Feb 2014 19:59:10 -0500 Subject: [PATCH 17/17] Improve the error message for when a new org name doesn't pass the regex. --- data/model.py | 5 ++++- static/partials/new-organization.html | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/data/model.py b/data/model.py index dc478b682..c44568dc2 100644 --- a/data/model.py +++ b/data/model.py @@ -111,7 +111,10 @@ def create_organization(name, email, creating_user): return new_org except InvalidUsernameException: - raise InvalidOrganizationException('Invalid organization name: %s' % name) + msg = ('Invalid organization name: %s Organization names must consist ' + + 'solely of lower case letters, numbers, and underscores. ' + + '[a-z0-9_]') % name + raise InvalidOrganizationException(msg) def create_robot(robot_shortname, parent): diff --git a/static/partials/new-organization.html b/static/partials/new-organization.html index 79d29e3f9..05abfe185 100644 --- a/static/partials/new-organization.html +++ b/static/partials/new-organization.html @@ -54,7 +54,7 @@ + data-placement="right" data-container="body"> This will also be the namespace for your repositories