From 2b89f65ad4fde263c1ad442de4510b3d3dd72f4b Mon Sep 17 00:00:00 2001 From: sslypushenko Date: Wed, 22 Jul 2015 20:22:53 +0300 Subject: [PATCH] Add filter for signed test results and related UI With this patch users can list their own test results. Change-Id: Ie2d944924f6ae966a13d0ca9908810c315ade5ab --- refstack-ui/app/app.js | 46 +++++++++++-- .../app/components/auth/authController.js | 29 -------- .../components/capabilities/capabilities.html | 2 +- .../app/components/profile/profile.html | 15 ++-- .../components/profile/profileController.js | 11 --- .../app/components/results/results.html | 2 +- .../components/results/resultsController.js | 11 ++- refstack-ui/app/index.html | 3 +- .../alerts/alertModal.html | 0 .../alerts/alertModalFactory.js | 2 +- refstack-ui/app/shared/header/header.html | 12 ++-- refstack-ui/tests/unit/AuthSpec.js | 40 +++++++++++ refstack-ui/tests/unit/ControllerSpec.js | 69 ++++--------------- refstack/api/constants.py | 4 ++ refstack/api/controllers/results.py | 9 ++- refstack/api/utils.py | 15 ++++ .../534e20be9964_create_pubkey_table.py | 1 + refstack/db/sqlalchemy/api.py | 13 ++++ refstack/tests/unit/test_api.py | 3 +- refstack/tests/unit/test_db.py | 26 +++++-- test-requirements.txt | 1 - 21 files changed, 178 insertions(+), 136 deletions(-) delete mode 100644 refstack-ui/app/components/auth/authController.js rename refstack-ui/app/{components => shared}/alerts/alertModal.html (100%) rename refstack-ui/app/{components => shared}/alerts/alertModalFactory.js (94%) create mode 100644 refstack-ui/tests/unit/AuthSpec.js diff --git a/refstack-ui/app/app.js b/refstack-ui/app/app.js index eaa608c4..db76ea8c 100644 --- a/refstack-ui/app/app.js +++ b/refstack-ui/app/app.js @@ -26,8 +26,13 @@ refstackApp.config([ templateUrl: '/components/capabilities/capabilities.html', controller: 'capabilitiesController' }). - state('results', { - url: '/results', + state('community_results', { + url: '/community_results', + templateUrl: '/components/results/results.html', + controller: 'resultsController' + }). + state('user_results', { + url: '/user_results', templateUrl: '/components/results/results.html', controller: 'resultsController' }). @@ -45,19 +50,46 @@ refstackApp.config([ ]); /** - * Try to authenticate user + * Injections in $rootscope */ -refstackApp.run(['$http', '$rootScope', 'refstackApiUrl', - function($http, $rootScope, refstackApiUrl) { +refstackApp.run(['$http', '$rootScope', '$window', 'refstackApiUrl', + function($http, $rootScope, $window, refstackApiUrl) { 'use strict'; + + /** + * This function injects sign in function in all scopes + */ + + $rootScope.auth = {}; + + var sign_in_url = refstackApiUrl + '/auth/signin'; + $rootScope.auth.doSignIn = function () { + $window.location.href = sign_in_url; + }; + + /** + * This function injects sign out function in all scopes + */ + var sign_out_url = refstackApiUrl + '/auth/signout'; + $rootScope.auth.doSignOut = function () { + $rootScope.currentUser = null; + $rootScope.isAuthenticated = false; + $window.location.href = sign_out_url; + }; + + /** + * This block tries to authenticate user + */ var profile_url = refstackApiUrl + '/profile'; $http.get(profile_url, {withCredentials: true}). success(function(data) { - $rootScope.currentUser = data; + $rootScope.auth.currentUser = data; + $rootScope.auth.isAuthenticated = true; }). error(function() { - $rootScope.currentUser = null; + $rootScope.auth.currentUser = null; + $rootScope.auth.isAuthenticated = false; }); } ]); diff --git a/refstack-ui/app/components/auth/authController.js b/refstack-ui/app/components/auth/authController.js deleted file mode 100644 index 19ea009f..00000000 --- a/refstack-ui/app/components/auth/authController.js +++ /dev/null @@ -1,29 +0,0 @@ -var refstackApp = angular.module('refstackApp'); - - /** - * Refstack Auth Controller - * This controller handles account authentication for users. - */ - -refstackApp.controller('authController', - ['$scope', '$window', '$rootScope', 'refstackApiUrl', - function($scope, $window, $rootScope, refstackApiUrl){ - 'use strict'; - var sign_in_url = refstackApiUrl + '/auth/signin'; - $scope.doSignIn = function () { - $window.location.href = sign_in_url; - }; - - var sign_out_url = refstackApiUrl + '/auth/signout'; - $scope.doSignOut = function () { - $rootScope.currentUser = null; - $window.location.href = sign_out_url; - }; - - $scope.isAuthenticated = function () { - if ($scope.currentUser) { - return !!$scope.currentUser.openid; - } - return false; - }; - }]); diff --git a/refstack-ui/app/components/capabilities/capabilities.html b/refstack-ui/app/components/capabilities/capabilities.html index 889fcc9f..26b42d6e 100644 --- a/refstack-ui/app/components/capabilities/capabilities.html +++ b/refstack-ui/app/components/capabilities/capabilities.html @@ -58,7 +58,7 @@
-
+
-
- +
+
@@ -33,4 +33,3 @@
{{pubKey.format}}
- diff --git a/refstack-ui/app/components/profile/profileController.js b/refstack-ui/app/components/profile/profileController.js index 11e837cc..c139aee0 100644 --- a/refstack-ui/app/components/profile/profileController.js +++ b/refstack-ui/app/components/profile/profileController.js @@ -19,16 +19,6 @@ refstackApp.controller('profileController', function($scope, $http, refstackApiUrl, $state, PubKeys, $modal, raiseAlert) { 'use strict'; - $scope.updateProfile = function () { - var profile_url = refstackApiUrl + '/profile'; - $http.get(profile_url, {withCredentials: true}). - success(function(data) { - $scope.user = data; - }). - error(function() { - $state.go('home'); - }); - }; $scope.updatePubKeys = function (){ var keys = PubKeys.query(function(){ @@ -77,7 +67,6 @@ refstackApp.controller('profileController', $scope.showRes = function(pubKey){ raiseAlert('success', '', pubKey.key); }; - $scope.updateProfile(); $scope.updatePubKeys(); } ]); diff --git a/refstack-ui/app/components/results/results.html b/refstack-ui/app/components/results/results.html index 13935e91..487bbf3d 100644 --- a/refstack-ui/app/components/results/results.html +++ b/refstack-ui/app/components/results/results.html @@ -1,4 +1,4 @@ -

Community Results

+

{{pageHeader}}

The most recently uploaded community test results are listed here. Currently, these results are anonymous.

diff --git a/refstack-ui/app/components/results/resultsController.js b/refstack-ui/app/components/results/resultsController.js index 126726e0..40a08207 100644 --- a/refstack-ui/app/components/results/resultsController.js +++ b/refstack-ui/app/components/results/resultsController.js @@ -6,8 +6,8 @@ var refstackApp = angular.module('refstackApp'); * a listing of community uploaded results. */ refstackApp.controller('resultsController', - ['$scope', '$http', '$filter', 'refstackApiUrl', - function ($scope, $http, $filter, refstackApiUrl) { + ['$scope', '$http', '$filter', '$state', 'refstackApiUrl', + function ($scope, $http, $filter, $state, refstackApiUrl) { 'use strict'; /** Initial page to be on. */ @@ -33,6 +33,9 @@ refstackApp.controller('resultsController', /** The upload date upper limit to be used in filtering results. */ $scope.endDate = ''; + $scope.isUserResults = $state.current.name === 'user_results'; + $scope.pageHeader = $scope.isUserResults ? + 'Private test results' : 'Community test results'; /** * This will contact the Refstack API to get a listing of test run * results. @@ -51,7 +54,9 @@ refstackApp.controller('resultsController', if (end) { content_url = content_url + '&end_date=' + end + ' 23:59:59'; } - + if ($scope.isUserResults) { + content_url = content_url + '&signed'; + } $scope.resultsRequest = $http.get(content_url).success(function (data) { $scope.data = data; diff --git a/refstack-ui/app/index.html b/refstack-ui/app/index.html index f0ef0a9d..0a27a51e 100644 --- a/refstack-ui/app/index.html +++ b/refstack-ui/app/index.html @@ -38,12 +38,11 @@ + - - diff --git a/refstack-ui/app/components/alerts/alertModal.html b/refstack-ui/app/shared/alerts/alertModal.html similarity index 100% rename from refstack-ui/app/components/alerts/alertModal.html rename to refstack-ui/app/shared/alerts/alertModal.html diff --git a/refstack-ui/app/components/alerts/alertModalFactory.js b/refstack-ui/app/shared/alerts/alertModalFactory.js similarity index 94% rename from refstack-ui/app/components/alerts/alertModalFactory.js rename to refstack-ui/app/shared/alerts/alertModalFactory.js index 8cb76e47..25f5e5e2 100644 --- a/refstack-ui/app/components/alerts/alertModalFactory.js +++ b/refstack-ui/app/shared/alerts/alertModalFactory.js @@ -5,7 +5,7 @@ refstackApp.factory('raiseAlert', 'use strict'; return function(mode, title, text) { $modal.open({ - templateUrl: '/components/alerts/alertModal.html', + templateUrl: '/shared/alerts/alertModal.html', controller: 'raiseAlertModalController', backdrop: true, keyboard: true, diff --git a/refstack-ui/app/shared/header/header.html b/refstack-ui/app/shared/header/header.html index 866aa7fc..94c822ba 100644 --- a/refstack-ui/app/shared/header/header.html +++ b/refstack-ui/app/shared/header/header.html @@ -18,14 +18,14 @@ Refstack
  • Home
  • About
  • DefCore Capabilities
  • -
  • Community Results
  • +
  • Community Results
  • -
    - diff --git a/refstack-ui/tests/unit/AuthSpec.js b/refstack-ui/tests/unit/AuthSpec.js new file mode 100644 index 00000000..c02709be --- /dev/null +++ b/refstack-ui/tests/unit/AuthSpec.js @@ -0,0 +1,40 @@ +describe('Auth', function () { + 'use strict'; + + var fakeApiUrl = 'http://foo.bar/v1'; + var $window; + beforeEach(function () { + $window = {location: { href: jasmine.createSpy()} }; + module(function ($provide) { + $provide.constant('refstackApiUrl', fakeApiUrl); + $provide.value('$window', $window); + }); + module('refstackApp'); + }); + + var $rootScope, $httpBackend; + beforeEach(inject(function (_$httpBackend_, _$rootScope_) { + $httpBackend = _$httpBackend_; + $rootScope = _$rootScope_; + })); + + it('should show signin url for signed user', function () { + $httpBackend.expectGET(fakeApiUrl + + '/profile').respond({'openid': 'foo@bar.com', + 'email': 'foo@bar.com', + 'fullname': 'foo' }); + $httpBackend.flush(); + $rootScope.auth.doSignIn(); + expect($window.location.href).toBe(fakeApiUrl + '/auth/signin'); + expect($rootScope.auth.isAuthenticated).toBe(true); + }); + + it('should show signout url for not signed user', function () { + $httpBackend.expectGET(fakeApiUrl + + '/profile').respond(401); + $httpBackend.flush(); + $rootScope.auth.doSignOut(); + expect($window.location.href).toBe(fakeApiUrl + '/auth/signout'); + expect($rootScope.auth.isAuthenticated).toBe(false); + }); +}); diff --git a/refstack-ui/tests/unit/ControllerSpec.js b/refstack-ui/tests/unit/ControllerSpec.js index 37b929c1..3d0d9ede 100644 --- a/refstack-ui/tests/unit/ControllerSpec.js +++ b/refstack-ui/tests/unit/ControllerSpec.js @@ -3,11 +3,18 @@ describe('Refstack controllers', function () { 'use strict'; var fakeApiUrl = 'http://foo.bar/v1'; + var $httpBackend; beforeEach(function () { module(function ($provide) { $provide.constant('refstackApiUrl', fakeApiUrl); }); module('refstackApp'); + inject(function(_$httpBackend_){ + $httpBackend = _$httpBackend_; + }); + $httpBackend.whenGET(fakeApiUrl + '/profile').respond(401); + $httpBackend.whenGET('/components/home/home.html') + .respond('
    mock template
    '); }); describe('headerController', function () { @@ -36,42 +43,10 @@ describe('Refstack controllers', function () { }); }); - describe('authController', function () { - var scope, $httpBackend, $window; - - beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) { - $httpBackend = _$httpBackend_; - scope = $rootScope.$new(); - $window = {location: { href: jasmine.createSpy()} }; - $controller('authController', {$scope: scope, $window: $window}); - })); - - it('should show signin url for signed user', function () { - $httpBackend.expectGET(fakeApiUrl + - '/profile').respond({'openid': 'foo@bar.com', - 'email': 'foo@bar.com', - 'fullname': 'foo' }); - $httpBackend.flush(); - scope.doSignIn(); - expect($window.location.href).toBe(fakeApiUrl + '/auth/signin'); - expect(scope.isAuthenticated()).toBe(true); - }); - - it('should show signout url for not signed user', function () { - $httpBackend.expectGET(fakeApiUrl + - '/profile').respond(401); - $httpBackend.flush(); - scope.doSignOut(); - expect($window.location.href).toBe(fakeApiUrl + '/auth/signout'); - expect(scope.isAuthenticated()).toBe(false); - }); - }); - describe('capabilitiesController', function () { - var scope, $httpBackend; + var scope; - beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) { - $httpBackend = _$httpBackend_; + beforeEach(inject(function ($rootScope, $controller) { scope = $rootScope.$new(); $controller('capabilitiesController', {$scope: scope}); })); @@ -102,8 +77,6 @@ describe('Refstack controllers', function () { } }; - $httpBackend.expectGET(fakeApiUrl + - '/profile').respond(401); $httpBackend.expectGET(fakeApiUrl + '/capabilities').respond(['2015.03.json', '2015.04.json']); // Should call request with latest version. @@ -170,7 +143,7 @@ describe('Refstack controllers', function () { }); describe('resultsController', function () { - var scope, $httpBackend; + var scope; var fakeResponse = { 'pagination': {'current_page': 1, 'total_pages': 2}, 'results': [{ @@ -180,8 +153,7 @@ describe('Refstack controllers', function () { }] }; - beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) { - $httpBackend = _$httpBackend_; + beforeEach(inject(function ($rootScope, $controller) { scope = $rootScope.$new(); $controller('resultsController', {$scope: scope}); })); @@ -189,9 +161,8 @@ describe('Refstack controllers', function () { it('should fetch the first page of results with proper URL args', function () { // Initial results should be page 1 of all results. - $httpBackend.expectGET(fakeApiUrl + '/profile').respond(401); - $httpBackend.expectGET(fakeApiUrl + - '/results?page=1').respond(fakeResponse); + $httpBackend.expectGET(fakeApiUrl + '/results?page=1') + .respond(fakeResponse); $httpBackend.flush(); expect(scope.data).toEqual(fakeResponse); expect(scope.currentPage).toBe(1); @@ -211,7 +182,6 @@ describe('Refstack controllers', function () { }); it('should set an error when results cannot be retrieved', function () { - $httpBackend.expectGET(fakeApiUrl + '/profile').respond(401); $httpBackend.expectGET(fakeApiUrl + '/results?page=1').respond(404, {'detail': 'Not Found'}); $httpBackend.flush(); @@ -224,23 +194,16 @@ describe('Refstack controllers', function () { it('should have an function to clear filters and update the view', function () { - $httpBackend.expectGET(fakeApiUrl + '/profile').respond(401); - $httpBackend.expectGET(fakeApiUrl + - '/results?page=1').respond(fakeResponse); scope.startDate = 'some date'; scope.endDate = 'some other date'; scope.clearFilters(); expect(scope.startDate).toBe(null); expect(scope.endDate).toBe(null); - $httpBackend.expectGET(fakeApiUrl + - '/results?page=1').respond(fakeResponse); - $httpBackend.flush(); - expect(scope.data).toEqual(fakeResponse); }); }); describe('resultsReportController', function () { - var scope, $httpBackend, stateparams; + var scope, stateparams; var fakeResultResponse = {'results': ['test_id_1']}; var fakeCapabilityResponse = { 'platform': {'required': ['compute']}, @@ -261,8 +224,7 @@ describe('Refstack controllers', function () { } }; - beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) { - $httpBackend = _$httpBackend_; + beforeEach(inject(function ($rootScope, $controller) { stateparams = {testID: 1234}; scope = $rootScope.$new(); $controller('resultsReportController', @@ -272,7 +234,6 @@ describe('Refstack controllers', function () { it('should make all necessary API requests to get results ' + 'and capabilities', function () { - $httpBackend.expectGET(fakeApiUrl + '/profile').respond(401); $httpBackend.expectGET(fakeApiUrl + '/results/1234').respond(fakeResultResponse); $httpBackend.expectGET(fakeApiUrl + diff --git a/refstack/api/constants.py b/refstack/api/constants.py index c4a22117..08522990 100644 --- a/refstack/api/constants.py +++ b/refstack/api/constants.py @@ -19,6 +19,7 @@ START_DATE = 'start_date' END_DATE = 'end_date' CPID = 'cpid' PAGE = 'page' +SIGNED = 'signed' # OpenID parameters OPENID_MODE = 'openid.mode' @@ -36,3 +37,6 @@ OPENID_ERROR = 'openid.error' # User session parameters CSRF_TOKEN = 'csrf_token' USER_OPENID = 'user_openid' + +# Test metadata fields +PUBLIC_KEY = 'public_key' diff --git a/refstack/api/controllers/results.py b/refstack/api/controllers/results.py index e14ea55b..9ec41229 100644 --- a/refstack/api/controllers/results.py +++ b/refstack/api/controllers/results.py @@ -55,7 +55,7 @@ class ResultsController(validation.BaseRestControllerWithValidation): if pecan.request.headers.get('X-Public-Key'): if 'metadata' not in item: item['metadata'] = {} - item['metadata']['public_key'] = \ + item['metadata'][const.PUBLIC_KEY] = \ pecan.request.headers.get('X-Public-Key') test_id = db.store_results(item) LOG.debug(item) @@ -79,6 +79,7 @@ class ResultsController(validation.BaseRestControllerWithValidation): const.START_DATE, const.END_DATE, const.CPID, + const.SIGNED ] try: @@ -88,9 +89,6 @@ class ResultsController(validation.BaseRestControllerWithValidation): api_utils.get_page_number(records_count) except api_utils.ParseInputsError as ex: pecan.abort(400, 'Reason: %s' % ex) - except Exception as ex: - LOG.debug('An error occurred: %s' % ex) - pecan.abort(500) try: per_page = CONF.api.results_per_page @@ -102,7 +100,8 @@ class ResultsController(validation.BaseRestControllerWithValidation): 'test_id': r.id, 'created_at': r.created_at, 'cpid': r.cpid, - 'url': CONF.api.test_results_url % r.id + 'url': parse.urljoin(CONF.ui_url, + CONF.api.test_results_url) % r.id }) page = {'results': results, diff --git a/refstack/api/utils.py b/refstack/api/utils.py index 40fc3b68..72f297a0 100644 --- a/refstack/api/utils.py +++ b/refstack/api/utils.py @@ -86,6 +86,16 @@ def parse_input_params(expected_input_params): 'start': const.START_DATE, 'end': const.END_DATE }) + if const.SIGNED in filters: + if is_authenticated(): + filters['openid'] = get_user_id() + filters['pubkeys'] = [ + ' '.join((pk['format'], pk['key'])) + for pk in db.get_user_pubkeys(filters['openid']) + ] + else: + raise ParseInputsError('To see signed test ' + 'results you need to authenticate') return filters @@ -176,6 +186,11 @@ def get_user(): return db.user_get(get_user_id()) +def get_user_public_keys(): + """Return db record for authenticated user.""" + return db.get_user_pubkeys(get_user_id()) + + def is_authenticated(): """Return True if user is authenticated.""" if get_user_id(): diff --git a/refstack/db/migrations/alembic/versions/534e20be9964_create_pubkey_table.py b/refstack/db/migrations/alembic/versions/534e20be9964_create_pubkey_table.py index 5f8185e9..a6e5a12e 100644 --- a/refstack/db/migrations/alembic/versions/534e20be9964_create_pubkey_table.py +++ b/refstack/db/migrations/alembic/versions/534e20be9964_create_pubkey_table.py @@ -34,6 +34,7 @@ def upgrade(): sa.ForeignKeyConstraint(['openid'], ['user.openid'], ), mysql_charset=MYSQL_CHARSET ) + op.create_index('indx_meta_value', 'meta', ['value'], mysql_length=32) def downgrade(): diff --git a/refstack/db/sqlalchemy/api.py b/refstack/db/sqlalchemy/api.py index ad57842c..fea0b30a 100644 --- a/refstack/db/sqlalchemy/api.py +++ b/refstack/db/sqlalchemy/api.py @@ -117,6 +117,19 @@ def _apply_filters_for_query(query, filters): if cpid: query = query.filter(models.Test.cpid == cpid) + signed = api_const.SIGNED in filters + if signed: + query = (query + .join(models.Test.meta) + .filter(models.TestMeta.meta_key == api_const.PUBLIC_KEY) + .filter(models.TestMeta.value.in_(filters['pubkeys'])) + ) + else: + signed_results = (query.session + .query(models.TestMeta.test_id) + .filter_by(meta_key=api_const.PUBLIC_KEY)) + query = query.filter(models.Test.id.notin_(signed_results)) + return query diff --git a/refstack/tests/unit/test_api.py b/refstack/tests/unit/test_api.py index faa10304..44bde7e8 100644 --- a/refstack/tests/unit/test_api.py +++ b/refstack/tests/unit/test_api.py @@ -134,7 +134,7 @@ class ResultsControllerTestCase(base.BaseTestCase): 'url': self.test_results_url % 'fake_test_id'}) self.assertEqual(mock_response.status, 201) mock_store_results.assert_called_once_with( - {'answer': 42, 'metadata': {'public_key': 'fake-key'}} + {'answer': 42, 'metadata': {const.PUBLIC_KEY: 'fake-key'}} ) @mock.patch('pecan.abort') @@ -216,6 +216,7 @@ class ResultsControllerTestCase(base.BaseTestCase): const.START_DATE, const.END_DATE, const.CPID, + const.SIGNED ] page_number = 1 total_pages_number = 10 diff --git a/refstack/tests/unit/test_db.py b/refstack/tests/unit/test_db.py index 71975887..a629412c 100644 --- a/refstack/tests/unit/test_db.py +++ b/refstack/tests/unit/test_db.py @@ -193,9 +193,12 @@ class DBBackendTestCase(base.BaseTestCase): self.assertEqual(expected_result, actual_result) @mock.patch('refstack.db.sqlalchemy.models.Test') - def test_apply_filters_for_query(self, mock_model): + @mock.patch('refstack.db.sqlalchemy.models.TestMeta') + def test_apply_filters_for_query_unsigned(self, mock_meta, + mock_test): query = mock.Mock() - mock_model.created_at = six.text_type() + mock_test.created_at = six.text_type() + mock_meta.test_id = six.text_type() filters = { api_const.START_DATE: 'fake1', @@ -205,19 +208,30 @@ class DBBackendTestCase(base.BaseTestCase): result = api._apply_filters_for_query(query, filters) - query.filter.assert_called_once_with(mock_model.created_at >= + query.filter.assert_called_once_with(mock_test.created_at >= filters[api_const.START_DATE]) query = query.filter.return_value - query.filter.assert_called_once_with(mock_model.created_at <= + query.filter.assert_called_once_with(mock_test.created_at <= filters[api_const.END_DATE]) query = query.filter.return_value - query.filter.assert_called_once_with(mock_model.cpid == + query.filter.assert_called_once_with(mock_test.cpid == filters[api_const.CPID]) query = query.filter.return_value - self.assertEqual(result, query) + + query.session.query.assert_called_once_with(mock_meta.test_id) + meta_query = query.session.query.return_value + + meta_query.filter_by.\ + assert_called_once_with(meta_key=api_const.PUBLIC_KEY) + unsigned_test_id_query = meta_query.filter_by.return_value + mock_test.id.notin_.assert_called_once_with(unsigned_test_id_query) + query.filter.assert_called_once_with(mock_test.id.notin_.return_value) + + filtered_query = query.filter.return_value + self.assertEqual(result, filtered_query) @mock.patch.object(api, '_apply_filters_for_query') @mock.patch.object(api, 'get_session') diff --git a/test-requirements.txt b/test-requirements.txt index 40387cb2..3b4cf5d3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,6 +8,5 @@ oslotest>=1.2.0 # Apache-2.0 python-subunit>=0.0.18 testrepository>=0.0.18 testtools>=0.9.34 -mysqlclient six>=1.7.0 pep257>=0.5.0 \ No newline at end of file