Add authentication through openstackid.org
In Refstack's database store only fullname, email and openid. After sign in refstack backend create session and write it id in cookie. When UI is opened in browser, Angular try to get info from /v1/profile. If data about user received then user is authenticated. Change-Id: Ib2cabc0c6b4de4b2ca1f02cc9e062a6e3550daa0
This commit is contained in:
parent
7e2cc883ce
commit
7b48c99fcb
@ -21,7 +21,6 @@ RUN \
|
|||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
pip install virtualenv tox ipython ipdb httpie && \
|
pip install virtualenv tox ipython ipdb httpie && \
|
||||||
npm install -g bower && \
|
|
||||||
ln -s /usr/bin/nodejs /usr/bin/node
|
ln -s /usr/bin/nodejs /usr/bin/node
|
||||||
|
|
||||||
ADD /docker/scripts/* /usr/bin/
|
ADD /docker/scripts/* /usr/bin/
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
server {
|
server {
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
send_timeout 600;
|
||||||
server_name 127.0.0.1;
|
server_name 127.0.0.1;
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
|
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
[[ ${DEBUG_MODE} ]] && set -x
|
[[ ${DEBUG_MODE} ]] && set -x
|
||||||
api-sync
|
api-sync
|
||||||
cd /home/dev/refstack
|
cd /home/dev/refstack
|
||||||
.venv/bin/pecan serve refstack/api/config.py > /dev/null
|
.venv/bin/pecan serve refstack/api/config.py
|
||||||
|
@ -41,7 +41,8 @@ build_refstack_env () {
|
|||||||
#Install some dev tools
|
#Install some dev tools
|
||||||
.venv/bin/pip install ipython ipdb httpie
|
.venv/bin/pip install ipython ipdb httpie
|
||||||
cd /home/dev/refstack
|
cd /home/dev/refstack
|
||||||
bower install --config.interactive=false
|
npm install
|
||||||
|
# bower install --config.interactive=false
|
||||||
|
|
||||||
build_tmpl /refstack/docker/templates/config.json.tmpl /home/dev/refstack/refstack-ui/app/config.json
|
build_tmpl /refstack/docker/templates/config.json.tmpl /home/dev/refstack/refstack-ui/app/config.json
|
||||||
build_tmpl /refstack/docker/templates/refstack.conf.tmpl /home/dev/refstack.conf
|
build_tmpl /refstack/docker/templates/refstack.conf.tmpl /home/dev/refstack.conf
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
debug = true
|
debug = true
|
||||||
verbose = true
|
verbose = true
|
||||||
|
ui_url = https://${REFSTACK_HOST:-127.0.0.1}
|
||||||
|
|
||||||
[api]
|
[api]
|
||||||
static_root = /home/dev/refstack/refstack-ui/app
|
static_root = /home/dev/refstack/refstack-ui/app
|
||||||
template_path = /home/dev/refstack/refstack-ui/app
|
template_path = /home/dev/refstack/refstack-ui/app
|
||||||
app_dev_mode = true
|
app_dev_mode = true
|
||||||
test_results_url = https://${REFSTACK_HOST:-127.0.0.1}/#/results/%s
|
api_url = https://${REFSTACK_HOST:-127.0.0.1}
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
connection = ${REFSTACK_MYSQL_URL}
|
connection = ${REFSTACK_MYSQL_URL}
|
||||||
|
|
||||||
|
[osid]
|
||||||
|
openstack_openid_endpoint = https://172.17.42.1:8443/accounts/openid2
|
||||||
|
@ -40,14 +40,16 @@
|
|||||||
#log_dir = <None>
|
#log_dir = <None>
|
||||||
|
|
||||||
# Use syslog for logging. Existing syslog format is DEPRECATED during
|
# Use syslog for logging. Existing syslog format is DEPRECATED during
|
||||||
# I, and will change in J to honor RFC5424. (boolean value)
|
# I, and changed in J to honor RFC5424. (boolean value)
|
||||||
#use_syslog = false
|
#use_syslog = false
|
||||||
|
|
||||||
# (Optional) Enables or disables syslog rfc5424 format for logging. If
|
# (Optional) Enables or disables syslog rfc5424 format for logging. If
|
||||||
# enabled, prefixes the MSG part of the syslog message with APP-NAME
|
# enabled, prefixes the MSG part of the syslog message with APP-NAME
|
||||||
# (RFC5424). The format without the APP-NAME is deprecated in I, and
|
# (RFC5424). The format without the APP-NAME is deprecated in K, and
|
||||||
# will be removed in J. (boolean value)
|
# will be removed in M, along with this option. (boolean value)
|
||||||
#use_syslog_rfc_format = false
|
# This option is deprecated for removal.
|
||||||
|
# Its value may be silently ignored in the future.
|
||||||
|
#use_syslog_rfc_format = true
|
||||||
|
|
||||||
# Syslog facility to receive log lines. (string value)
|
# Syslog facility to receive log lines. (string value)
|
||||||
#syslog_log_facility = LOG_USER
|
#syslog_log_facility = LOG_USER
|
||||||
@ -67,7 +69,7 @@
|
|||||||
|
|
||||||
# Prefix each line of exception output with this format. (string
|
# Prefix each line of exception output with this format. (string
|
||||||
# value)
|
# value)
|
||||||
#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s
|
#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s
|
||||||
|
|
||||||
# List of logger=LEVEL pairs. (list value)
|
# List of logger=LEVEL pairs. (list value)
|
||||||
#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN
|
#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN
|
||||||
@ -86,10 +88,17 @@
|
|||||||
# (string value)
|
# (string value)
|
||||||
#instance_uuid_format = "[instance: %(uuid)s] "
|
#instance_uuid_format = "[instance: %(uuid)s] "
|
||||||
|
|
||||||
|
# Enables or disables fatal status of deprecations. (boolean value)
|
||||||
|
#fatal_deprecations = false
|
||||||
|
|
||||||
#
|
#
|
||||||
# From refstack
|
# From refstack
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# Url of user interface for Refstack. Need for redirects after sign in
|
||||||
|
# and sign out. (string value)
|
||||||
|
#ui_url = http://refstack.net
|
||||||
|
|
||||||
# The backend to use for database. (string value)
|
# The backend to use for database. (string value)
|
||||||
#db_backend = sqlalchemy
|
#db_backend = sqlalchemy
|
||||||
|
|
||||||
@ -100,6 +109,9 @@
|
|||||||
# From refstack
|
# From refstack
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# Url of public Refstack API. (string value)
|
||||||
|
#api_url = http://api.refstack.net
|
||||||
|
|
||||||
# The directory where your static files can be found. Pecan comes with
|
# The directory where your static files can be found. Pecan comes with
|
||||||
# middleware that can be used to serve static files (like CSS and
|
# middleware that can be used to serve static files (like CSS and
|
||||||
# Javascript files) during development. %(project_root)s is special
|
# Javascript files) during development. %(project_root)s is special
|
||||||
@ -116,9 +128,9 @@
|
|||||||
# relative the project root. (string value)
|
# relative the project root. (string value)
|
||||||
#template_path = %(project_root)s/templates
|
#template_path = %(project_root)s/templates
|
||||||
|
|
||||||
# List of sites allowed cross-origin resource access. If this is empty,
|
# List of sites allowed cross-site resource access. If this is empty,
|
||||||
# only same-origin requests are allowed.
|
# only same-origin requests are allowed. (list value)
|
||||||
#allowed_cors_origins = http://refstack.net, http://localhost:8080
|
#allowed_cors_origins =
|
||||||
|
|
||||||
# Switch Refstack app into debug mode. Helpful for development. In
|
# Switch Refstack app into debug mode. Helpful for development. In
|
||||||
# debug mode static file will be served by pecan application. Also,
|
# debug mode static file will be served by pecan application. Also,
|
||||||
@ -127,7 +139,17 @@
|
|||||||
#app_dev_mode = false
|
#app_dev_mode = false
|
||||||
|
|
||||||
# Template for test result url. (string value)
|
# Template for test result url. (string value)
|
||||||
#test_results_url = http://refstack.net/output.html?test_id=%s
|
#test_results_url = /#/results/%s
|
||||||
|
|
||||||
|
# The GitHub API URL of the repository and location of the DefCore
|
||||||
|
# capability files. This URL is used to get a listing of all
|
||||||
|
# capability files. (string value)
|
||||||
|
#github_api_capabilities_url = https://api.github.com/repos/openstack/defcore/contents
|
||||||
|
|
||||||
|
# This is the base URL that is used for retrieving specific capability
|
||||||
|
# files. Capability file names will be appended to this URL to get the
|
||||||
|
# contents of that file. (string value)
|
||||||
|
#github_raw_base_url = https://raw.githubusercontent.com/openstack/defcore/master/
|
||||||
|
|
||||||
# Number of results for one page (integer value)
|
# Number of results for one page (integer value)
|
||||||
#results_per_page = 20
|
#results_per_page = 20
|
||||||
@ -135,15 +157,6 @@
|
|||||||
# The format for start_date and end_date parameters (string value)
|
# The format for start_date and end_date parameters (string value)
|
||||||
#input_date_format = %Y-%m-%d %H:%M:%S
|
#input_date_format = %Y-%m-%d %H:%M:%S
|
||||||
|
|
||||||
# The GitHub API URL of the repository and location of the DefCore
|
|
||||||
# capability files. This URL is used to get a listing of all capability
|
|
||||||
# files.
|
|
||||||
#github_api_capabilities_url = https://api.github.com/repos/openstack/defcore/contents
|
|
||||||
|
|
||||||
# The base URL that is used for retrieving specific capability files.
|
|
||||||
# Capability file names will be appended to this URL to get the contents
|
|
||||||
# of that file.
|
|
||||||
#github_raw_base_url = https://raw.githubusercontent.com/openstack/defcore/master/
|
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
|
|
||||||
@ -249,3 +262,51 @@
|
|||||||
# error is raised. Set to -1 to specify an infinite retry count.
|
# error is raised. Set to -1 to specify an infinite retry count.
|
||||||
# (integer value)
|
# (integer value)
|
||||||
#db_max_retries = 20
|
#db_max_retries = 20
|
||||||
|
|
||||||
|
|
||||||
|
[osid]
|
||||||
|
|
||||||
|
#
|
||||||
|
# From refstack
|
||||||
|
#
|
||||||
|
|
||||||
|
# OpenStackID Auth Server URI. (string value)
|
||||||
|
#openstack_openid_endpoint = https://openstackid.org/accounts/openid2
|
||||||
|
|
||||||
|
# Interaction mode. Specifies whether Openstack Id IdP may interact
|
||||||
|
# with the user to determine the outcome of the request. (string
|
||||||
|
# value)
|
||||||
|
#openid_mode = checkid_setup
|
||||||
|
|
||||||
|
# Protocol version. Value identifying the OpenID protocol version
|
||||||
|
# being used. This value should be "http://specs.openid.net/auth/2.0".
|
||||||
|
# (string value)
|
||||||
|
#openid_ns = http://specs.openid.net/auth/2.0
|
||||||
|
|
||||||
|
# Return endpoint in Refstack's API. Value indicating the endpoint
|
||||||
|
# where the user should be returned to after signing in. Openstack Id
|
||||||
|
# Idp only supports HTTPS address types. (string value)
|
||||||
|
#openid_return_to = /v1/auth/signin_return
|
||||||
|
|
||||||
|
# Claimed identifier. This value must be set to
|
||||||
|
# "http://specs.openid.net/auth/2.0/identifier_select". or to user
|
||||||
|
# claimed identity (user local identifier or user owned identity [ex:
|
||||||
|
# custom html hosted on a owned domain set to html discover]). (string
|
||||||
|
# value)
|
||||||
|
#openid_claimed_id = http://specs.openid.net/auth/2.0/identifier_select
|
||||||
|
|
||||||
|
# Alternate identifier. This value must be set to
|
||||||
|
# http://specs.openid.net/auth/2.0/identifier_select. (string value)
|
||||||
|
#openid_identity = http://specs.openid.net/auth/2.0/identifier_select
|
||||||
|
|
||||||
|
# Indicates request for user attribute information. This value must be
|
||||||
|
# set to "http://openid.net/extensions/sreg/1.1". (string value)
|
||||||
|
#openid_ns_sreg = http://openid.net/extensions/sreg/1.1
|
||||||
|
|
||||||
|
# Comma-separated list of field names which, if absent from the
|
||||||
|
# response, will prevent the Consumer from completing the registration
|
||||||
|
# without End User interation. The field names are those that are
|
||||||
|
# specified in the Response Format, with the "openid.sreg." prefix
|
||||||
|
# removed. Valid values include: "country", "email", "firstname",
|
||||||
|
# "language", "lastname" (string value)
|
||||||
|
#openid_sreg_required = email,fullname
|
||||||
|
@ -35,6 +35,29 @@ refstackApp.config([
|
|||||||
url: '/results/:testID',
|
url: '/results/:testID',
|
||||||
templateUrl: '/components/results-report/resultsReport.html',
|
templateUrl: '/components/results-report/resultsReport.html',
|
||||||
controller: 'resultsReportController'
|
controller: 'resultsReportController'
|
||||||
|
}).
|
||||||
|
state('profile', {
|
||||||
|
url: '/profile',
|
||||||
|
templateUrl: '/components/profile/profile.html',
|
||||||
|
controller: 'profileController'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to authenticate user
|
||||||
|
*/
|
||||||
|
|
||||||
|
refstackApp.run(['$http', '$rootScope', 'refstackApiUrl',
|
||||||
|
function($http, $rootScope, refstackApiUrl) {
|
||||||
|
'use strict';
|
||||||
|
var profile_url = refstackApiUrl + '/profile';
|
||||||
|
$http.get(profile_url, {withCredentials: true}).
|
||||||
|
success(function(data) {
|
||||||
|
$rootScope.currentUser = data;
|
||||||
|
}).
|
||||||
|
error(function() {
|
||||||
|
$rootScope.currentUser = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
29
refstack-ui/app/components/auth/authController.js
Normal file
29
refstack-ui/app/components/auth/authController.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
}]);
|
4
refstack-ui/app/components/profile/profile.html
Normal file
4
refstack-ui/app/components/profile/profile.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<h1>Hello, {{user.fullname}}!</h1>
|
||||||
|
{{bar}}
|
||||||
|
<p>openid: {{user.openid}}</p>
|
||||||
|
<p>email: {{user.email}}</p>
|
21
refstack-ui/app/components/profile/profileController.js
Normal file
21
refstack-ui/app/components/profile/profileController.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Refstack User Profile Controller
|
||||||
|
* This controller handles user's profile page, where a user can view
|
||||||
|
* account-specific information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var refstackApp = angular.module('refstackApp');
|
||||||
|
|
||||||
|
refstackApp.controller('profileController',
|
||||||
|
['$scope', '$http', 'refstackApiUrl', '$state',
|
||||||
|
function($scope, $http, refstackApiUrl, $state) {
|
||||||
|
'use strict';
|
||||||
|
var profile_url = refstackApiUrl + '/profile';
|
||||||
|
$http.get(profile_url, {withCredentials: true}).
|
||||||
|
success(function(data) {
|
||||||
|
$scope.user = data;
|
||||||
|
}).
|
||||||
|
error(function() {
|
||||||
|
$state.go('home');
|
||||||
|
});
|
||||||
|
}]);
|
@ -40,6 +40,8 @@
|
|||||||
<script src="components/capabilities/capabilitiesController.js"></script>
|
<script src="components/capabilities/capabilitiesController.js"></script>
|
||||||
<script src="components/results/resultsController.js"></script>
|
<script src="components/results/resultsController.js"></script>
|
||||||
<script src="components/results-report/resultsReportController.js"></script>
|
<script src="components/results-report/resultsReportController.js"></script>
|
||||||
|
<script src="components/profile/profileController.js"></script>
|
||||||
|
<script src="components/auth/authController.js"></script>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<script src="shared/filters.js"></script>
|
<script src="shared/filters.js"></script>
|
||||||
|
@ -20,6 +20,11 @@ Refstack
|
|||||||
<li ng-class="{ active: isActive('/capabilities')}"><a ui-sref="capabilities">DefCore Capabilities</a></li>
|
<li ng-class="{ active: isActive('/capabilities')}"><a ui-sref="capabilities">DefCore Capabilities</a></li>
|
||||||
<li ng-class="{ active: isActive('/results')}"><a ui-sref="results">Community Results</a></li>
|
<li ng-class="{ active: isActive('/results')}"><a ui-sref="results">Community Results</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<ul ng-controller="authController" class="nav navbar-nav navbar-right">
|
||||||
|
<li ng-class="{ active: isActive('/profile')}" ng-if="isAuthenticated()"><a ui-sref="profile">{{currentUser.fullname}}</a></li>
|
||||||
|
<li ng-if="isAuthenticated()"><a href="" ng-click="doSignOut()">Sign Out</a></li>
|
||||||
|
<li ng-if="!isAuthenticated()"><a href="" ng-click="doSignIn()">Sign In / Sign Up</a></li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -2,9 +2,16 @@
|
|||||||
describe('Refstack controllers', function () {
|
describe('Refstack controllers', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var fakeApiUrl = 'http://foo.bar/v1';
|
||||||
|
beforeEach(function () {
|
||||||
|
module(function ($provide) {
|
||||||
|
$provide.constant('refstackApiUrl', fakeApiUrl);
|
||||||
|
});
|
||||||
|
module('refstackApp');
|
||||||
|
});
|
||||||
|
|
||||||
describe('headerController', function () {
|
describe('headerController', function () {
|
||||||
var scope, $location;
|
var scope, $location;
|
||||||
beforeEach(module('refstackApp'));
|
|
||||||
|
|
||||||
beforeEach(inject(function ($rootScope, $controller, _$location_) {
|
beforeEach(inject(function ($rootScope, $controller, _$location_) {
|
||||||
scope = $rootScope.$new();
|
scope = $rootScope.$new();
|
||||||
@ -29,15 +36,39 @@ 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 () {
|
describe('capabilitiesController', function () {
|
||||||
var scope, $httpBackend;
|
var scope, $httpBackend;
|
||||||
var fakeApiUrl = 'http://foo.bar/v1';
|
|
||||||
beforeEach(function () {
|
|
||||||
module('refstackApp');
|
|
||||||
module(function ($provide) {
|
|
||||||
$provide.constant('refstackApiUrl', fakeApiUrl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
|
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
|
||||||
$httpBackend = _$httpBackend_;
|
$httpBackend = _$httpBackend_;
|
||||||
@ -56,6 +87,8 @@ describe('Refstack controllers', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch the selected capabilities version', function () {
|
it('should fetch the selected capabilities version', function () {
|
||||||
|
$httpBackend.expectGET(fakeApiUrl +
|
||||||
|
'/profile').respond(401);
|
||||||
$httpBackend.expectGET(fakeApiUrl +
|
$httpBackend.expectGET(fakeApiUrl +
|
||||||
'/capabilities').respond(['2015.03.json', '2015.04.json']);
|
'/capabilities').respond(['2015.03.json', '2015.04.json']);
|
||||||
// Should call request with latest version.
|
// Should call request with latest version.
|
||||||
@ -115,7 +148,6 @@ describe('Refstack controllers', function () {
|
|||||||
|
|
||||||
describe('resultsController', function () {
|
describe('resultsController', function () {
|
||||||
var scope, $httpBackend;
|
var scope, $httpBackend;
|
||||||
var fakeApiUrl = 'http://foo.bar/v1';
|
|
||||||
var fakeResponse = {
|
var fakeResponse = {
|
||||||
'pagination': {'current_page': 1, 'total_pages': 2},
|
'pagination': {'current_page': 1, 'total_pages': 2},
|
||||||
'results': [{
|
'results': [{
|
||||||
@ -125,13 +157,6 @@ describe('Refstack controllers', function () {
|
|||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
module('refstackApp');
|
|
||||||
module(function ($provide) {
|
|
||||||
$provide.constant('refstackApiUrl', fakeApiUrl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
|
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
|
||||||
$httpBackend = _$httpBackend_;
|
$httpBackend = _$httpBackend_;
|
||||||
scope = $rootScope.$new();
|
scope = $rootScope.$new();
|
||||||
@ -141,6 +166,7 @@ describe('Refstack controllers', function () {
|
|||||||
it('should fetch the first page of results with proper URL args',
|
it('should fetch the first page of results with proper URL args',
|
||||||
function () {
|
function () {
|
||||||
// Initial results should be page 1 of all results.
|
// Initial results should be page 1 of all results.
|
||||||
|
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
|
||||||
$httpBackend.expectGET(fakeApiUrl +
|
$httpBackend.expectGET(fakeApiUrl +
|
||||||
'/results?page=1').respond(fakeResponse);
|
'/results?page=1').respond(fakeResponse);
|
||||||
$httpBackend.flush();
|
$httpBackend.flush();
|
||||||
@ -162,6 +188,7 @@ describe('Refstack controllers', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set an error when results cannot be retrieved', 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,
|
$httpBackend.expectGET(fakeApiUrl + '/results?page=1').respond(404,
|
||||||
{'detail': 'Not Found'});
|
{'detail': 'Not Found'});
|
||||||
$httpBackend.flush();
|
$httpBackend.flush();
|
||||||
@ -174,6 +201,7 @@ describe('Refstack controllers', function () {
|
|||||||
|
|
||||||
it('should have an function to clear filters and update the view',
|
it('should have an function to clear filters and update the view',
|
||||||
function () {
|
function () {
|
||||||
|
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
|
||||||
$httpBackend.expectGET(fakeApiUrl +
|
$httpBackend.expectGET(fakeApiUrl +
|
||||||
'/results?page=1').respond(fakeResponse);
|
'/results?page=1').respond(fakeResponse);
|
||||||
scope.startDate = 'some date';
|
scope.startDate = 'some date';
|
||||||
@ -190,7 +218,6 @@ describe('Refstack controllers', function () {
|
|||||||
|
|
||||||
describe('resultsReportController', function () {
|
describe('resultsReportController', function () {
|
||||||
var scope, $httpBackend, stateparams;
|
var scope, $httpBackend, stateparams;
|
||||||
var fakeApiUrl = 'http://foo.bar/v1';
|
|
||||||
var fakeResultResponse = {'results': ['test_id_1']};
|
var fakeResultResponse = {'results': ['test_id_1']};
|
||||||
var fakeCapabilityResponse = {
|
var fakeCapabilityResponse = {
|
||||||
'platform': {'required': ['compute']},
|
'platform': {'required': ['compute']},
|
||||||
@ -211,13 +238,6 @@ describe('Refstack controllers', function () {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
module('refstackApp');
|
|
||||||
module(function ($provide) {
|
|
||||||
$provide.constant('refstackApiUrl', fakeApiUrl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
|
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
|
||||||
$httpBackend = _$httpBackend_;
|
$httpBackend = _$httpBackend_;
|
||||||
stateparams = {testID: 1234};
|
stateparams = {testID: 1234};
|
||||||
@ -229,6 +249,7 @@ describe('Refstack controllers', function () {
|
|||||||
it('should make all necessary API requests to get results ' +
|
it('should make all necessary API requests to get results ' +
|
||||||
'and capabilities',
|
'and capabilities',
|
||||||
function () {
|
function () {
|
||||||
|
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
|
||||||
$httpBackend.expectGET(fakeApiUrl +
|
$httpBackend.expectGET(fakeApiUrl +
|
||||||
'/results/1234').respond(fakeResultResponse);
|
'/results/1234').respond(fakeResultResponse);
|
||||||
$httpBackend.expectGET(fakeApiUrl +
|
$httpBackend.expectGET(fakeApiUrl +
|
||||||
|
@ -2,9 +2,16 @@
|
|||||||
describe('Refstack filters', function () {
|
describe('Refstack filters', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var fakeApiUrl = 'http://foo.bar/v1';
|
||||||
|
beforeEach(function () {
|
||||||
|
module(function ($provide) {
|
||||||
|
$provide.constant('refstackApiUrl', fakeApiUrl);
|
||||||
|
});
|
||||||
|
module('refstackApp');
|
||||||
|
});
|
||||||
|
|
||||||
describe('Filter: arrayConverter', function () {
|
describe('Filter: arrayConverter', function () {
|
||||||
var $filter;
|
var $filter;
|
||||||
beforeEach(module('refstackApp'));
|
|
||||||
beforeEach(inject(function (_$filter_) {
|
beforeEach(inject(function (_$filter_) {
|
||||||
$filter = _$filter_('arrayConverter');
|
$filter = _$filter_('arrayConverter');
|
||||||
}));
|
}));
|
||||||
@ -19,7 +26,6 @@ describe('Refstack filters', function () {
|
|||||||
|
|
||||||
describe('Filter: capitalize', function() {
|
describe('Filter: capitalize', function() {
|
||||||
var $filter;
|
var $filter;
|
||||||
beforeEach(module('refstackApp'));
|
|
||||||
beforeEach(inject(function(_$filter_) {
|
beforeEach(inject(function(_$filter_) {
|
||||||
$filter = _$filter_('capitalize');
|
$filter = _$filter_('capitalize');
|
||||||
}));
|
}));
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Refstack package."""
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Refstack API package."""
|
@ -19,20 +19,33 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from beaker.middleware import SessionMiddleware
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_log import loggers
|
from oslo_log import loggers
|
||||||
import pecan
|
import pecan
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
|
from refstack.api import utils as api_utils
|
||||||
from refstack.common import validators
|
from refstack.common import validators
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
PROJECT_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
PROJECT_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||||
os.pardir)
|
os.pardir)
|
||||||
|
UI_OPTS = [
|
||||||
|
cfg.StrOpt('ui_url',
|
||||||
|
default='http://refstack.net',
|
||||||
|
help='Url of user interface for Refstack. Need for redirects '
|
||||||
|
'after sign in and sign out.'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
API_OPTS = [
|
API_OPTS = [
|
||||||
|
cfg.StrOpt('api_url',
|
||||||
|
default='http://refstack.net',
|
||||||
|
help='Url of public Refstack API.'
|
||||||
|
),
|
||||||
cfg.StrOpt('static_root',
|
cfg.StrOpt('static_root',
|
||||||
default='%(project_root)s/static',
|
default='%(project_root)s/static',
|
||||||
help='The directory where your static files can '
|
help='The directory where your static files can '
|
||||||
@ -65,7 +78,7 @@ API_OPTS = [
|
|||||||
'contain some details with debug information.'
|
'contain some details with debug information.'
|
||||||
),
|
),
|
||||||
cfg.StrOpt('test_results_url',
|
cfg.StrOpt('test_results_url',
|
||||||
default='http://refstack.net/#/results/%s',
|
default='/#/results/%s',
|
||||||
help='Template for test result url.'
|
help='Template for test result url.'
|
||||||
),
|
),
|
||||||
cfg.StrOpt('github_api_capabilities_url',
|
cfg.StrOpt('github_api_capabilities_url',
|
||||||
@ -89,6 +102,8 @@ CONF = cfg.CONF
|
|||||||
opt_group = cfg.OptGroup(name='api',
|
opt_group = cfg.OptGroup(name='api',
|
||||||
title='Options for the Refstack API')
|
title='Options for the Refstack API')
|
||||||
|
|
||||||
|
CONF.register_opts(UI_OPTS)
|
||||||
|
|
||||||
CONF.register_group(opt_group)
|
CONF.register_group(opt_group)
|
||||||
CONF.register_opts(API_OPTS, opt_group)
|
CONF.register_opts(API_OPTS, opt_group)
|
||||||
|
|
||||||
@ -96,9 +111,8 @@ log.register_options(CONF)
|
|||||||
|
|
||||||
|
|
||||||
class JSONErrorHook(pecan.hooks.PecanHook):
|
class JSONErrorHook(pecan.hooks.PecanHook):
|
||||||
"""
|
|
||||||
A pecan hook that translates webob HTTP errors into a JSON format.
|
"""A pecan hook that translates webob HTTP errors into a JSON format."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Hook init."""
|
"""Hook init."""
|
||||||
@ -106,7 +120,9 @@ class JSONErrorHook(pecan.hooks.PecanHook):
|
|||||||
|
|
||||||
def on_error(self, state, exc):
|
def on_error(self, state, exc):
|
||||||
"""Request error handler."""
|
"""Request error handler."""
|
||||||
if isinstance(exc, webob.exc.HTTPError):
|
if isinstance(exc, webob.exc.HTTPRedirection):
|
||||||
|
return
|
||||||
|
elif isinstance(exc, webob.exc.HTTPError):
|
||||||
status_code = exc.status_int
|
status_code = exc.status_int
|
||||||
body = {'title': exc.title}
|
body = {'title': exc.title}
|
||||||
elif isinstance(exc, validators.ValidationError):
|
elif isinstance(exc, validators.ValidationError):
|
||||||
@ -128,9 +144,8 @@ class JSONErrorHook(pecan.hooks.PecanHook):
|
|||||||
|
|
||||||
|
|
||||||
class CORSHook(pecan.hooks.PecanHook):
|
class CORSHook(pecan.hooks.PecanHook):
|
||||||
"""
|
|
||||||
A pecan hook that handles Cross-Origin Resource Sharing.
|
"""A pecan hook that handles Cross-Origin Resource Sharing."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Init the hook by getting the allowed origins."""
|
"""Init the hook by getting the allowed origins."""
|
||||||
@ -149,6 +164,7 @@ class CORSHook(pecan.hooks.PecanHook):
|
|||||||
'GET, OPTIONS, PUT, POST'
|
'GET, OPTIONS, PUT, POST'
|
||||||
state.response.headers['Access-Control-Allow-Headers'] = \
|
state.response.headers['Access-Control-Allow-Headers'] = \
|
||||||
'origin, authorization, accept, content-type'
|
'origin, authorization, accept, content-type'
|
||||||
|
state.response.headers['Access-Control-Allow-Credentials'] = 'true'
|
||||||
|
|
||||||
|
|
||||||
def setup_app(config):
|
def setup_app(config):
|
||||||
@ -187,8 +203,16 @@ def setup_app(config):
|
|||||||
)]
|
)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
beaker_conf = {
|
||||||
|
'session.key': 'refstack',
|
||||||
|
'session.type': 'memory',
|
||||||
|
'session.timeout': 604800,
|
||||||
|
'session.validate_key': api_utils.get_token(),
|
||||||
|
}
|
||||||
|
app = SessionMiddleware(app, beaker_conf)
|
||||||
|
|
||||||
if CONF.api.app_dev_mode:
|
if CONF.api.app_dev_mode:
|
||||||
LOG.debug('\n\n Refstack is served at %s \n\n',
|
LOG.debug('\n\n <<< Refstack UI is available at %s >>>\n\n',
|
||||||
CONF.api.test_results_url.split('/#/')[0])
|
CONF.ui_url)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
@ -12,12 +12,27 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
"""
|
"""Constants for Refstack API."""
|
||||||
Constants for Refstack API
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Names of input parameters for request
|
# Names of input parameters for request
|
||||||
START_DATE = 'start_date'
|
START_DATE = 'start_date'
|
||||||
END_DATE = 'end_date'
|
END_DATE = 'end_date'
|
||||||
CPID = 'cpid'
|
CPID = 'cpid'
|
||||||
PAGE = 'page'
|
PAGE = 'page'
|
||||||
|
|
||||||
|
# OpenID parameters
|
||||||
|
OPENID_MODE = 'openid.mode'
|
||||||
|
OPENID_NS = 'openid.ns'
|
||||||
|
OPENID_RETURN_TO = 'openid.return_to'
|
||||||
|
OPENID_CLAIMED_ID = 'openid.claimed_id'
|
||||||
|
OPENID_IDENTITY = 'openid.identity'
|
||||||
|
OPENID_REALM = 'openid.realm'
|
||||||
|
OPENID_NS_SREG = 'openid.ns.sreg'
|
||||||
|
OPENID_NS_SREG_REQUIRED = 'openid.sreg.required'
|
||||||
|
OPENID_NS_SREG_EMAIL = 'openid.sreg.email'
|
||||||
|
OPENID_NS_SREG_FULLNAME = 'openid.sreg.fullname'
|
||||||
|
OPENID_ERROR = 'openid.error'
|
||||||
|
|
||||||
|
# User session parameters
|
||||||
|
CSRF_TOKEN = 'csrf_token'
|
||||||
|
USER_OPENID = 'user_openid'
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""API controllers package."""
|
170
refstack/api/controllers/auth.py
Normal file
170
refstack/api/controllers/auth.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Authentication controller."""
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
|
from refstack.api import constants as const
|
||||||
|
from refstack.api import utils as api_utils
|
||||||
|
from refstack import db
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
OPENID_OPTS = [
|
||||||
|
cfg.StrOpt('openstack_openid_endpoint',
|
||||||
|
default='https://openstackid.org/accounts/openid2',
|
||||||
|
help='OpenStackID Auth Server URI.'
|
||||||
|
),
|
||||||
|
cfg.StrOpt('openid_mode',
|
||||||
|
default='checkid_setup',
|
||||||
|
help='Interaction mode. Specifies whether Openstack Id '
|
||||||
|
'IdP may interact with the user to determine the '
|
||||||
|
'outcome of the request.'
|
||||||
|
),
|
||||||
|
cfg.StrOpt('openid_ns',
|
||||||
|
default='http://specs.openid.net/auth/2.0',
|
||||||
|
help='Protocol version. Value identifying the OpenID '
|
||||||
|
'protocol version being used. This value should '
|
||||||
|
'be "http://specs.openid.net/auth/2.0".'
|
||||||
|
),
|
||||||
|
cfg.StrOpt('openid_return_to',
|
||||||
|
default='/v1/auth/signin_return',
|
||||||
|
help='Return endpoint in Refstack\'s API. Value indicating '
|
||||||
|
'the endpoint where the user should be returned to after '
|
||||||
|
'signing in. Openstack Id Idp only supports HTTPS '
|
||||||
|
'address types.'
|
||||||
|
),
|
||||||
|
cfg.StrOpt('openid_claimed_id',
|
||||||
|
default='http://specs.openid.net/auth/2.0/identifier_select',
|
||||||
|
help='Claimed identifier. This value must be set to '
|
||||||
|
'"http://specs.openid.net/auth/2.0/identifier_select". '
|
||||||
|
'or to user claimed identity (user local identifier '
|
||||||
|
'or user owned identity [ex: custom html hosted on a '
|
||||||
|
'owned domain set to html discover]).'
|
||||||
|
),
|
||||||
|
cfg.StrOpt('openid_identity',
|
||||||
|
default='http://specs.openid.net/auth/2.0/identifier_select',
|
||||||
|
help='Alternate identifier. This value must be set to '
|
||||||
|
'http://specs.openid.net/auth/2.0/identifier_select.'
|
||||||
|
),
|
||||||
|
cfg.StrOpt('openid_ns_sreg',
|
||||||
|
default='http://openid.net/extensions/sreg/1.1',
|
||||||
|
help='Indicates request for user attribute information. '
|
||||||
|
'This value must be set to '
|
||||||
|
'"http://openid.net/extensions/sreg/1.1".'
|
||||||
|
),
|
||||||
|
cfg.StrOpt('openid_sreg_required',
|
||||||
|
default='email,fullname',
|
||||||
|
help='Comma-separated list of field names which, '
|
||||||
|
'if absent from the response, will prevent the '
|
||||||
|
'Consumer from completing the registration without '
|
||||||
|
'End User interation. The field names are those that '
|
||||||
|
'are specified in the Response Format, with the '
|
||||||
|
'"openid.sreg." prefix removed. Valid values include: '
|
||||||
|
'"country", "email", "firstname", "language", "lastname"'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
opt_group = cfg.OptGroup(name='osid',
|
||||||
|
title='Options for the Refstack OpenID 2.0 through '
|
||||||
|
'Openstack Authentication Server')
|
||||||
|
CONF.register_group(opt_group)
|
||||||
|
CONF.register_opts(OPENID_OPTS, opt_group)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthController(rest.RestController):
|
||||||
|
|
||||||
|
"""Controller provides user authentication in OpenID 2.0 IdP."""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
"signin": ["GET"],
|
||||||
|
"signin_return": ["GET"],
|
||||||
|
"signout": ["GET"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@pecan.expose()
|
||||||
|
def signin(self):
|
||||||
|
"""Handle signin request."""
|
||||||
|
session = api_utils.get_user_session()
|
||||||
|
if api_utils.is_authenticated():
|
||||||
|
pecan.redirect(CONF.ui_url)
|
||||||
|
else:
|
||||||
|
api_utils.delete_params_from_user_session([const.USER_OPENID])
|
||||||
|
|
||||||
|
csrf_token = api_utils.get_token()
|
||||||
|
session[const.CSRF_TOKEN] = csrf_token
|
||||||
|
session.save()
|
||||||
|
return_endpoint = parse.urljoin(CONF.api.api_url,
|
||||||
|
CONF.osid.openid_return_to)
|
||||||
|
return_to = api_utils.set_query_params(return_endpoint,
|
||||||
|
{const.CSRF_TOKEN: csrf_token})
|
||||||
|
|
||||||
|
params = {
|
||||||
|
const.OPENID_MODE: CONF.osid.openid_mode,
|
||||||
|
const.OPENID_NS: CONF.osid.openid_ns,
|
||||||
|
const.OPENID_RETURN_TO: return_to,
|
||||||
|
const.OPENID_CLAIMED_ID: CONF.osid.openid_claimed_id,
|
||||||
|
const.OPENID_IDENTITY: CONF.osid.openid_identity,
|
||||||
|
const.OPENID_REALM: CONF.api.api_url,
|
||||||
|
const.OPENID_NS_SREG: CONF.osid.openid_ns_sreg,
|
||||||
|
const.OPENID_NS_SREG_REQUIRED: CONF.osid.openid_sreg_required,
|
||||||
|
}
|
||||||
|
url = CONF.osid.openstack_openid_endpoint
|
||||||
|
url = api_utils.set_query_params(url, params)
|
||||||
|
pecan.redirect(location=url)
|
||||||
|
|
||||||
|
@pecan.expose()
|
||||||
|
def signin_return(self):
|
||||||
|
"""Handle returned request from OpenID 2.0 IdP."""
|
||||||
|
session = api_utils.get_user_session()
|
||||||
|
if pecan.request.GET.get(const.OPENID_ERROR):
|
||||||
|
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
|
||||||
|
pecan.abort(401, pecan.request.GET.get(const.OPENID_ERROR))
|
||||||
|
|
||||||
|
if pecan.request.GET.get(const.OPENID_MODE) == 'cancel':
|
||||||
|
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
|
||||||
|
pecan.abort(401, 'Authentication canceled.')
|
||||||
|
|
||||||
|
session_token = session.get(const.CSRF_TOKEN)
|
||||||
|
request_token = pecan.request.GET.get(const.CSRF_TOKEN)
|
||||||
|
if request_token != session_token:
|
||||||
|
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
|
||||||
|
pecan.abort(401, 'Authentication is failed. Try again.')
|
||||||
|
|
||||||
|
api_utils.verify_openid_request(pecan.request)
|
||||||
|
user_info = {
|
||||||
|
'openid': pecan.request.GET.get(const.OPENID_CLAIMED_ID),
|
||||||
|
'email': pecan.request.GET.get(const.OPENID_NS_SREG_EMAIL),
|
||||||
|
'fullname': pecan.request.GET.get(const.OPENID_NS_SREG_FULLNAME)
|
||||||
|
}
|
||||||
|
user = db.user_update_or_create(user_info)
|
||||||
|
|
||||||
|
api_utils.delete_params_from_user_session([const.CSRF_TOKEN])
|
||||||
|
session[const.USER_OPENID] = user.openid
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
pecan.redirect(CONF.ui_url)
|
||||||
|
|
||||||
|
@pecan.expose()
|
||||||
|
def signout(self):
|
||||||
|
"""Handle signout request."""
|
||||||
|
if api_utils.is_authenticated():
|
||||||
|
api_utils.delete_params_from_user_session([const.USER_OPENID])
|
||||||
|
pecan.redirect(CONF.ui_url)
|
@ -32,4 +32,9 @@ class RootController(object):
|
|||||||
if CONF.api.app_dev_mode:
|
if CONF.api.app_dev_mode:
|
||||||
@expose(generic=True, template='index.html')
|
@expose(generic=True, template='index.html')
|
||||||
def index(self):
|
def index(self):
|
||||||
|
"""Return index.html in development mode.
|
||||||
|
|
||||||
|
It allows to run both API and UI with pecan serve.
|
||||||
|
Template path should point into UI app folder
|
||||||
|
"""
|
||||||
return dict()
|
return dict()
|
||||||
|
39
refstack/api/controllers/user.py
Normal file
39
refstack/api/controllers/user.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""User profile controller."""
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
from pecan.secure import secure
|
||||||
|
|
||||||
|
from refstack.api import constants as const
|
||||||
|
from refstack.api import utils as api_utils
|
||||||
|
from refstack import db
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileController(rest.RestController):
|
||||||
|
|
||||||
|
"""Controller provides user information in OpenID 2.0 IdP."""
|
||||||
|
|
||||||
|
@secure(api_utils.is_authenticated)
|
||||||
|
@pecan.expose('json')
|
||||||
|
def get(self):
|
||||||
|
"""Handle get request on user info."""
|
||||||
|
session = api_utils.get_user_session()
|
||||||
|
user = db.user_get(session.get(const.USER_OPENID))
|
||||||
|
return {
|
||||||
|
"openid": user.openid,
|
||||||
|
"email": user.email,
|
||||||
|
"fullname": user.fullname
|
||||||
|
}
|
@ -24,10 +24,13 @@ from pecan import rest
|
|||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
import requests_cache
|
import requests_cache
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
from refstack import db
|
from refstack import db
|
||||||
from refstack.api import constants as const
|
from refstack.api import constants as const
|
||||||
from refstack.api import utils as api_utils
|
from refstack.api import utils as api_utils
|
||||||
|
from refstack.api.controllers import auth
|
||||||
|
from refstack.api.controllers import user
|
||||||
from refstack.common import validators
|
from refstack.common import validators
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
@ -55,7 +58,8 @@ requests_cache.install_cache(cache_name='github_cache',
|
|||||||
|
|
||||||
class BaseRestControllerWithValidation(rest.RestController):
|
class BaseRestControllerWithValidation(rest.RestController):
|
||||||
|
|
||||||
"""
|
"""Rest controller with validation.
|
||||||
|
|
||||||
Controller provides validation for POSTed data
|
Controller provides validation for POSTed data
|
||||||
exposed endpoints:
|
exposed endpoints:
|
||||||
POST base_url/
|
POST base_url/
|
||||||
@ -66,22 +70,24 @@ class BaseRestControllerWithValidation(rest.RestController):
|
|||||||
__validator__ = None
|
__validator__ = None
|
||||||
|
|
||||||
def __init__(self): # pragma: no cover
|
def __init__(self): # pragma: no cover
|
||||||
|
"""Init."""
|
||||||
if self.__validator__:
|
if self.__validator__:
|
||||||
self.validator = self.__validator__()
|
self.validator = self.__validator__()
|
||||||
else:
|
else:
|
||||||
raise ValueError("__validator__ is not defined")
|
raise ValueError("__validator__ is not defined")
|
||||||
|
|
||||||
def get_item(self, item_id): # pragma: no cover
|
def get_item(self, item_id): # pragma: no cover
|
||||||
"""Handler for getting item"""
|
"""Handler for getting item."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def store_item(self, item_in_json): # pragma: no cover
|
def store_item(self, item_in_json): # pragma: no cover
|
||||||
"""Handler for storing item. Should return new item id"""
|
"""Handler for storing item. Should return new item id."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@pecan.expose('json')
|
@pecan.expose('json')
|
||||||
def get_one(self, arg):
|
def get_one(self, arg):
|
||||||
"""Return test results in JSON format.
|
"""Return test results in JSON format.
|
||||||
|
|
||||||
:param arg: item ID in uuid4 format or action
|
:param arg: item ID in uuid4 format or action
|
||||||
"""
|
"""
|
||||||
if self.validator.assert_id(arg):
|
if self.validator.assert_id(arg):
|
||||||
@ -110,7 +116,7 @@ class ResultsController(BaseRestControllerWithValidation):
|
|||||||
__validator__ = validators.TestResultValidator
|
__validator__ = validators.TestResultValidator
|
||||||
|
|
||||||
def get_item(self, item_id):
|
def get_item(self, item_id):
|
||||||
"""Handler for getting item"""
|
"""Handler for getting item."""
|
||||||
test_info = db.get_test(item_id)
|
test_info = db.get_test(item_id)
|
||||||
if not test_info:
|
if not test_info:
|
||||||
pecan.abort(404)
|
pecan.abort(404)
|
||||||
@ -122,7 +128,7 @@ class ResultsController(BaseRestControllerWithValidation):
|
|||||||
"results": test_name_list}
|
"results": test_name_list}
|
||||||
|
|
||||||
def store_item(self, item_in_json):
|
def store_item(self, item_in_json):
|
||||||
"""Handler for storing item. Should return new item id"""
|
"""Handler for storing item. Should return new item id."""
|
||||||
item = item_in_json.copy()
|
item = item_in_json.copy()
|
||||||
if pecan.request.headers.get('X-Public-Key'):
|
if pecan.request.headers.get('X-Public-Key'):
|
||||||
if 'metadata' not in item:
|
if 'metadata' not in item:
|
||||||
@ -132,21 +138,21 @@ class ResultsController(BaseRestControllerWithValidation):
|
|||||||
test_id = db.store_results(item)
|
test_id = db.store_results(item)
|
||||||
LOG.debug(item)
|
LOG.debug(item)
|
||||||
return {'test_id': test_id,
|
return {'test_id': test_id,
|
||||||
'url': CONF.api.test_results_url % test_id}
|
'url': parse.urljoin(CONF.ui_url,
|
||||||
|
CONF.api.test_results_url) % test_id}
|
||||||
|
|
||||||
@pecan.expose('json')
|
@pecan.expose('json')
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""Get information of all uploaded test results.
|
||||||
|
|
||||||
Get information of all uploaded test results in descending
|
Get information of all uploaded test results in descending
|
||||||
chronological order.
|
chronological order. Make it possible to specify some
|
||||||
Make it possible to specify some input parameters
|
input parameters for filtering.
|
||||||
for filtering.
|
|
||||||
For example:
|
For example:
|
||||||
/v1/results?page=<page number>&cpid=1234.
|
/v1/results?page=<page number>&cpid=1234.
|
||||||
By default, page is set to page number 1,
|
By default, page is set to page number 1,
|
||||||
if the page parameter is not specified.
|
if the page parameter is not specified.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
expected_input_params = [
|
expected_input_params = [
|
||||||
const.START_DATE,
|
const.START_DATE,
|
||||||
const.END_DATE,
|
const.END_DATE,
|
||||||
@ -192,8 +198,11 @@ class ResultsController(BaseRestControllerWithValidation):
|
|||||||
|
|
||||||
class CapabilitiesController(rest.RestController):
|
class CapabilitiesController(rest.RestController):
|
||||||
|
|
||||||
"""/v1/capabilities handler. This acts as a proxy for retrieving
|
"""/v1/capabilities handler.
|
||||||
capability files from the openstack/defcore Github repository."""
|
|
||||||
|
This acts as a proxy for retrieving capability files
|
||||||
|
from the openstack/defcore Github repository.
|
||||||
|
"""
|
||||||
|
|
||||||
@pecan.expose('json')
|
@pecan.expose('json')
|
||||||
def get(self):
|
def get(self):
|
||||||
@ -248,3 +257,5 @@ class V1Controller(object):
|
|||||||
|
|
||||||
results = ResultsController()
|
results = ResultsController()
|
||||||
capabilities = CapabilitiesController()
|
capabilities = CapabilitiesController()
|
||||||
|
auth = auth.AuthController()
|
||||||
|
profile = user.ProfileController()
|
||||||
|
@ -15,12 +15,18 @@
|
|||||||
|
|
||||||
"""Refstack API's utils."""
|
"""Refstack API's utils."""
|
||||||
import copy
|
import copy
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
import string
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
import pecan
|
import pecan
|
||||||
|
import six
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
|
from refstack import db
|
||||||
from refstack.api import constants as const
|
from refstack.api import constants as const
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
@ -28,6 +34,9 @@ CONF = cfg.CONF
|
|||||||
|
|
||||||
|
|
||||||
class ParseInputsError(Exception):
|
class ParseInputsError(Exception):
|
||||||
|
|
||||||
|
"""Raise if input params are invalid."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -55,7 +64,6 @@ def parse_input_params(expected_input_params):
|
|||||||
|
|
||||||
:param expecred_params: (array) Expected input
|
:param expecred_params: (array) Expected input
|
||||||
params specified in constants.
|
params specified in constants.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raw_filters = _get_input_params_from_request(expected_input_params)
|
raw_filters = _get_input_params_from_request(expected_input_params)
|
||||||
filters = copy.deepcopy(raw_filters)
|
filters = copy.deepcopy(raw_filters)
|
||||||
@ -83,6 +91,7 @@ def parse_input_params(expected_input_params):
|
|||||||
|
|
||||||
def _calculate_pages_number(per_page, records_count):
|
def _calculate_pages_number(per_page, records_count):
|
||||||
"""Return pages number.
|
"""Return pages number.
|
||||||
|
|
||||||
:param per_page: (int) results number fot one page.
|
:param per_page: (int) results number fot one page.
|
||||||
:param records_count: (int) total records count.
|
:param records_count: (int) total records count.
|
||||||
"""
|
"""
|
||||||
@ -93,7 +102,8 @@ def _calculate_pages_number(per_page, records_count):
|
|||||||
|
|
||||||
|
|
||||||
def get_page_number(records_count):
|
def get_page_number(records_count):
|
||||||
"""Get page number from request
|
"""Get page number from request.
|
||||||
|
|
||||||
:param records_count: (int) total records count.
|
:param records_count: (int) total records count.
|
||||||
"""
|
"""
|
||||||
page_number = pecan.request.GET.get(const.PAGE)
|
page_number = pecan.request.GET.get(const.PAGE)
|
||||||
@ -121,3 +131,80 @@ def get_page_number(records_count):
|
|||||||
'is greater than the total number of pages.')
|
'is greater than the total number of pages.')
|
||||||
|
|
||||||
return (page_number, total_pages)
|
return (page_number, total_pages)
|
||||||
|
|
||||||
|
|
||||||
|
def set_query_params(url, params):
|
||||||
|
"""Set params in given query."""
|
||||||
|
url_parts = parse.urlparse(url)
|
||||||
|
url = parse.urlunparse((
|
||||||
|
url_parts.scheme,
|
||||||
|
url_parts.netloc,
|
||||||
|
url_parts.path,
|
||||||
|
url_parts.params,
|
||||||
|
parse.urlencode(params),
|
||||||
|
url_parts.fragment))
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def get_token(length=30):
|
||||||
|
"""Get random token."""
|
||||||
|
return ''.join(random.choice(string.ascii_lowercase)
|
||||||
|
for i in range(length))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_params_from_user_session(params):
|
||||||
|
"""Delete params from user session."""
|
||||||
|
session = get_user_session()
|
||||||
|
for param in params:
|
||||||
|
if session.get(param):
|
||||||
|
del session[param]
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_session():
|
||||||
|
"""Return user session."""
|
||||||
|
return pecan.request.environ['beaker.session']
|
||||||
|
|
||||||
|
|
||||||
|
def is_authenticated():
|
||||||
|
"""Return True if user is authenticated."""
|
||||||
|
session = get_user_session()
|
||||||
|
if session.get(const.USER_OPENID):
|
||||||
|
try:
|
||||||
|
if db.user_get(session.get(const.USER_OPENID)):
|
||||||
|
return True
|
||||||
|
except db.UserNotFound:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def verify_openid_request(request):
|
||||||
|
"""Verify OpenID returned request in OpenID."""
|
||||||
|
verify_params = dict(request.params.copy())
|
||||||
|
verify_params["openid.mode"] = "check_authentication"
|
||||||
|
|
||||||
|
verify_response = requests.post(
|
||||||
|
CONF.osid.openstack_openid_endpoint, data=verify_params,
|
||||||
|
verify=not CONF.api.app_dev_mode
|
||||||
|
)
|
||||||
|
verify_data_tokens = verify_response.content.split()
|
||||||
|
verify_dict = dict((token.split(":")[0], token.split(":")[1])
|
||||||
|
for token in verify_data_tokens)
|
||||||
|
|
||||||
|
if (verify_response.status_code / 100 != 2
|
||||||
|
or verify_dict['is_valid'] != 'true'):
|
||||||
|
pecan.abort(401, 'Authentication is failed. Try again.')
|
||||||
|
|
||||||
|
# Is the data we've received within our required parameters?
|
||||||
|
required_parameters = {
|
||||||
|
const.OPENID_NS_SREG_EMAIL: 'Please permit access to '
|
||||||
|
'your email address.',
|
||||||
|
const.OPENID_NS_SREG_FULLNAME: 'Please permit access to '
|
||||||
|
'your name.',
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, error in six.iteritems(required_parameters):
|
||||||
|
if name not in verify_params or not verify_params[name]:
|
||||||
|
pecan.abort(401, 'Authentication is failed. %s' % error)
|
||||||
|
|
||||||
|
return True
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Refstack common package."""
|
@ -13,8 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
""" Validators module
|
"""Validators module."""
|
||||||
"""
|
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import uuid
|
import uuid
|
||||||
@ -30,7 +29,10 @@ ext_format_checker = jsonschema.FormatChecker()
|
|||||||
|
|
||||||
class ValidationError(Exception):
|
class ValidationError(Exception):
|
||||||
|
|
||||||
|
"""Raise if request doesn't pass trough validation process."""
|
||||||
|
|
||||||
def __init__(self, title, exc=None):
|
def __init__(self, title, exc=None):
|
||||||
|
"""Init."""
|
||||||
super(ValidationError, self).__init__(title)
|
super(ValidationError, self).__init__(title)
|
||||||
self.exc = exc
|
self.exc = exc
|
||||||
self.title = title
|
self.title = title
|
||||||
@ -40,9 +42,11 @@ class ValidationError(Exception):
|
|||||||
if self.exc else self.title
|
if self.exc else self.title
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
"""Repr method."""
|
||||||
return self.details
|
return self.details
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
"""Str method."""
|
||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
|
|
||||||
@ -59,20 +63,20 @@ def is_uuid(inst):
|
|||||||
format='uuid_hex',
|
format='uuid_hex',
|
||||||
raises=(TypeError, ValueError))
|
raises=(TypeError, ValueError))
|
||||||
def checker_uuid(inst):
|
def checker_uuid(inst):
|
||||||
"""Checker 'uuid_hex' format for jsonschema validator"""
|
"""Checker 'uuid_hex' format for jsonschema validator."""
|
||||||
return is_uuid(inst)
|
return is_uuid(inst)
|
||||||
|
|
||||||
|
|
||||||
class Validator(object):
|
class Validator(object):
|
||||||
|
|
||||||
"""Base class for validators"""
|
"""Base class for validators."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""Init."""
|
||||||
self.schema = {} # pragma: no cover
|
self.schema = {} # pragma: no cover
|
||||||
|
|
||||||
def validate(self, request):
|
def validate(self, request):
|
||||||
"""
|
"""Validate request."""
|
||||||
:param json_data: data for validation
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
body = json.loads(request.body)
|
body = json.loads(request.body)
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
@ -89,7 +93,7 @@ class TestResultValidator(Validator):
|
|||||||
"""Validator for incoming test results."""
|
"""Validator for incoming test results."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""Init."""
|
||||||
self.schema = {
|
self.schema = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
@ -122,6 +126,7 @@ class TestResultValidator(Validator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate(self, request):
|
def validate(self, request):
|
||||||
|
"""Validate uploaded test results."""
|
||||||
super(TestResultValidator, self).validate(request)
|
super(TestResultValidator, self).validate(request)
|
||||||
if request.headers.get('X-Signature') or \
|
if request.headers.get('X-Signature') or \
|
||||||
request.headers.get('X-Public-Key'):
|
request.headers.get('X-Public-Key'):
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
"""
|
"""DB abstraction for Refstack."""
|
||||||
DB abstraction for Refstack
|
|
||||||
"""
|
|
||||||
|
|
||||||
from refstack.db.api import * # noqa
|
from refstack.db.api import * # noqa
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
Functions in this module are imported into the refstack.db namespace.
|
Functions in this module are imported into the refstack.db namespace.
|
||||||
Call these functions from refstack.db namespace, not the refstack.db.api
|
Call these functions from refstack.db namespace, not the refstack.db.api
|
||||||
namespace.
|
namespace.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_db import api as db_api
|
from oslo_db import api as db_api
|
||||||
@ -37,8 +36,9 @@ _BACKEND_MAPPING = {'sqlalchemy': 'refstack.db.sqlalchemy.api'}
|
|||||||
IMPL = db_api.DBAPI.from_config(cfg.CONF, backend_mapping=_BACKEND_MAPPING,
|
IMPL = db_api.DBAPI.from_config(cfg.CONF, backend_mapping=_BACKEND_MAPPING,
|
||||||
lazy=True)
|
lazy=True)
|
||||||
|
|
||||||
|
UserNotFound = IMPL.UserNotFound
|
||||||
|
|
||||||
|
|
||||||
###################
|
|
||||||
def store_results(results):
|
def store_results(results):
|
||||||
"""Storing results into database.
|
"""Storing results into database.
|
||||||
|
|
||||||
@ -79,3 +79,19 @@ def get_test_records_count(filters):
|
|||||||
:param filters: (Dict) Filters that will be applied for records.
|
:param filters: (Dict) Filters that will be applied for records.
|
||||||
"""
|
"""
|
||||||
return IMPL.get_test_records_count(filters)
|
return IMPL.get_test_records_count(filters)
|
||||||
|
|
||||||
|
|
||||||
|
def user_get(user_openid):
|
||||||
|
"""Get user info.
|
||||||
|
|
||||||
|
:param user_openid: User openid
|
||||||
|
"""
|
||||||
|
return IMPL.user_get(user_openid)
|
||||||
|
|
||||||
|
|
||||||
|
def user_update_or_create(user_info):
|
||||||
|
"""Create user DB record if it exists, otherwise record will be updated.
|
||||||
|
|
||||||
|
:param user_info: User record
|
||||||
|
"""
|
||||||
|
return IMPL.user_update_or_create(user_info)
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Migrations."""
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Alembic backend for migrations."""
|
@ -14,6 +14,8 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
"""Alembic environment script."""
|
||||||
|
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
@ -23,12 +25,11 @@ from refstack.db.sqlalchemy import models as db_models
|
|||||||
|
|
||||||
|
|
||||||
def run_migrations_online():
|
def run_migrations_online():
|
||||||
|
|
||||||
"""Run migrations in 'online' mode.
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
In this scenario we need to create an Engine
|
In this scenario we need to create an Engine
|
||||||
and associate a connection with the context."""
|
and associate a connection with the context.
|
||||||
|
"""
|
||||||
engine = db_api.get_engine()
|
engine = db_api.get_engine()
|
||||||
connection = engine.connect()
|
connection = engine.connect()
|
||||||
target_metadata = db_models.RefStackBase.metadata
|
target_metadata = db_models.RefStackBase.metadata
|
||||||
|
@ -12,9 +12,7 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
"""
|
"""Implementation of Alembic commands."""
|
||||||
Implementation of Alembic commands.
|
|
||||||
"""
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import alembic
|
import alembic
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
"""Create user table.
|
||||||
|
|
||||||
|
Revision ID: 2f178b0bf762
|
||||||
|
Revises: 42278d6179b9
|
||||||
|
Create Date: 2015-05-12 12:15:43.810938
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2f178b0bf762'
|
||||||
|
down_revision = '42278d6179b9'
|
||||||
|
MYSQL_CHARSET = 'utf8'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
"""Upgrade DB."""
|
||||||
|
op.create_table(
|
||||||
|
'user',
|
||||||
|
sa.Column('updated_at', sa.DateTime()),
|
||||||
|
sa.Column('deleted_at', sa.DateTime()),
|
||||||
|
sa.Column('deleted', sa.Integer, default=0),
|
||||||
|
sa.Column('_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('openid', sa.String(length=128),
|
||||||
|
nullable=False, unique=True),
|
||||||
|
sa.Column('email', sa.String(length=128)),
|
||||||
|
sa.Column('fullname', sa.String(length=128)),
|
||||||
|
sa.PrimaryKeyConstraint('_id'),
|
||||||
|
mysql_charset=MYSQL_CHARSET
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
"""Downgrade DB."""
|
||||||
|
op.drop_table('user')
|
@ -1,4 +1,4 @@
|
|||||||
"""Init
|
"""Init.
|
||||||
|
|
||||||
Revision ID: 42278d6179b9
|
Revision ID: 42278d6179b9
|
||||||
Revises: None
|
Revises: None
|
||||||
@ -16,6 +16,7 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
|
"""Upgrade DB."""
|
||||||
op.create_table(
|
op.create_table(
|
||||||
'test',
|
'test',
|
||||||
sa.Column('updated_at', sa.DateTime()),
|
sa.Column('updated_at', sa.DateTime()),
|
||||||
@ -67,6 +68,7 @@ def upgrade():
|
|||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
|
"""Downgrade DB."""
|
||||||
op.drop_table('results')
|
op.drop_table('results')
|
||||||
op.drop_table('meta')
|
op.drop_table('meta')
|
||||||
op.drop_table('test')
|
op.drop_table('test')
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""SQLAlchemy backend."""
|
@ -12,9 +12,7 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
"""
|
"""Implementation of SQLAlchemy backend."""
|
||||||
Implementation of SQLAlchemy backend.
|
|
||||||
"""
|
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@ -35,6 +33,7 @@ db_options.set_defaults(cfg.CONF)
|
|||||||
|
|
||||||
|
|
||||||
def _create_facade_lazily():
|
def _create_facade_lazily():
|
||||||
|
"""Create DB facade lazily."""
|
||||||
global _FACADE
|
global _FACADE
|
||||||
if _FACADE is None:
|
if _FACADE is None:
|
||||||
_FACADE = db_session.EngineFacade.from_config(CONF)
|
_FACADE = db_session.EngineFacade.from_config(CONF)
|
||||||
@ -42,25 +41,24 @@ def _create_facade_lazily():
|
|||||||
|
|
||||||
|
|
||||||
def get_engine():
|
def get_engine():
|
||||||
|
"""Get DB engine."""
|
||||||
facade = _create_facade_lazily()
|
facade = _create_facade_lazily()
|
||||||
return facade.get_engine()
|
return facade.get_engine()
|
||||||
|
|
||||||
|
|
||||||
def get_session(**kwargs):
|
def get_session(**kwargs):
|
||||||
|
"""Get DB session."""
|
||||||
facade = _create_facade_lazily()
|
facade = _create_facade_lazily()
|
||||||
return facade.get_session(**kwargs)
|
return facade.get_session(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_backend():
|
def get_backend():
|
||||||
"""The backend is this module itself."""
|
"""The backend is this module itself."""
|
||||||
|
|
||||||
return sys.modules[__name__]
|
return sys.modules[__name__]
|
||||||
|
|
||||||
|
|
||||||
###################
|
|
||||||
|
|
||||||
|
|
||||||
def store_results(results):
|
def store_results(results):
|
||||||
|
"""Store test results."""
|
||||||
test = models.Test()
|
test = models.Test()
|
||||||
test_id = str(uuid.uuid4())
|
test_id = str(uuid.uuid4())
|
||||||
test.id = test_id
|
test.id = test_id
|
||||||
@ -83,6 +81,7 @@ def store_results(results):
|
|||||||
|
|
||||||
|
|
||||||
def get_test(test_id):
|
def get_test(test_id):
|
||||||
|
"""Get test info."""
|
||||||
session = get_session()
|
session = get_session()
|
||||||
test_info = session.query(models.Test).\
|
test_info = session.query(models.Test).\
|
||||||
filter_by(id=test_id).\
|
filter_by(id=test_id).\
|
||||||
@ -91,6 +90,7 @@ def get_test(test_id):
|
|||||||
|
|
||||||
|
|
||||||
def get_test_results(test_id):
|
def get_test_results(test_id):
|
||||||
|
"""Get test results."""
|
||||||
session = get_session()
|
session = get_session()
|
||||||
results = session.query(models.TestResults.name).\
|
results = session.query(models.TestResults.name).\
|
||||||
filter_by(test_id=test_id).\
|
filter_by(test_id=test_id).\
|
||||||
@ -99,6 +99,7 @@ def get_test_results(test_id):
|
|||||||
|
|
||||||
|
|
||||||
def _apply_filters_for_query(query, filters):
|
def _apply_filters_for_query(query, filters):
|
||||||
|
"""Apply filters for DB query."""
|
||||||
start_date = filters.get(api_const.START_DATE)
|
start_date = filters.get(api_const.START_DATE)
|
||||||
if start_date:
|
if start_date:
|
||||||
query = query.filter(models.Test.created_at >= start_date)
|
query = query.filter(models.Test.created_at >= start_date)
|
||||||
@ -115,6 +116,7 @@ def _apply_filters_for_query(query, filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_test_records(page, per_page, filters):
|
def get_test_records(page, per_page, filters):
|
||||||
|
"""Get page with list of test records."""
|
||||||
session = get_session()
|
session = get_session()
|
||||||
query = session.query(models.Test.id,
|
query = session.query(models.Test.id,
|
||||||
models.Test.created_at,
|
models.Test.created_at,
|
||||||
@ -128,8 +130,39 @@ def get_test_records(page, per_page, filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_test_records_count(filters):
|
def get_test_records_count(filters):
|
||||||
|
"""Get total test records count."""
|
||||||
session = get_session()
|
session = get_session()
|
||||||
query = session.query(models.Test.id)
|
query = session.query(models.Test.id)
|
||||||
records_count = _apply_filters_for_query(query, filters).count()
|
records_count = _apply_filters_for_query(query, filters).count()
|
||||||
|
|
||||||
return records_count
|
return records_count
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotFound(Exception):
|
||||||
|
|
||||||
|
"""Raise if user not found."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def user_get(user_openid):
|
||||||
|
"""Get user info by openid."""
|
||||||
|
session = get_session()
|
||||||
|
user = session.query(models.User).filter_by(openid=user_openid).first()
|
||||||
|
if user is None:
|
||||||
|
raise UserNotFound('User with OpenID %s not found' % user_openid)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def user_update_or_create(user_info):
|
||||||
|
"""Create user DB record if it exists, otherwise record will be updated."""
|
||||||
|
try:
|
||||||
|
user = user_get(user_info['openid'])
|
||||||
|
except UserNotFound:
|
||||||
|
user = models.User()
|
||||||
|
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
user.update(user_info)
|
||||||
|
user.save(session=session)
|
||||||
|
return user
|
||||||
|
@ -13,9 +13,7 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
"""
|
"""SQLAlchemy models for Refstack data."""
|
||||||
SQLAlchemy models for Refstack data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_db.sqlalchemy import models
|
from oslo_db.sqlalchemy import models
|
||||||
@ -82,3 +80,15 @@ class TestMeta(BASE, RefStackBase):
|
|||||||
index=True, nullable=False, unique=False)
|
index=True, nullable=False, unique=False)
|
||||||
meta_key = sa.Column(sa.String(64), index=True, nullable=False)
|
meta_key = sa.Column(sa.String(64), index=True, nullable=False)
|
||||||
value = sa.Column(sa.Text())
|
value = sa.Column(sa.Text())
|
||||||
|
|
||||||
|
|
||||||
|
class User(BASE, RefStackBase):
|
||||||
|
|
||||||
|
"""User information."""
|
||||||
|
|
||||||
|
__tablename__ = 'user'
|
||||||
|
_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
|
||||||
|
openid = sa.Column(sa.String(128), nullable=False, unique=True,
|
||||||
|
index=True)
|
||||||
|
email = sa.Column(sa.String(128))
|
||||||
|
fullname = sa.Column(sa.String(128))
|
||||||
|
@ -22,14 +22,17 @@ LOG = log.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class PluggableBackend(object):
|
class PluggableBackend(object):
|
||||||
|
|
||||||
"""A pluggable backend loaded lazily based on some value."""
|
"""A pluggable backend loaded lazily based on some value."""
|
||||||
|
|
||||||
def __init__(self, pivot, **backends):
|
def __init__(self, pivot, **backends):
|
||||||
|
"""Init."""
|
||||||
self.__backends = backends
|
self.__backends = backends
|
||||||
self.__pivot = pivot
|
self.__pivot = pivot
|
||||||
self.__backend = None
|
self.__backend = None
|
||||||
|
|
||||||
def __get_backend(self):
|
def __get_backend(self):
|
||||||
|
"""Get backend."""
|
||||||
if not self.__backend:
|
if not self.__backend:
|
||||||
backend_name = CONF[self.__pivot]
|
backend_name = CONF[self.__pivot]
|
||||||
if backend_name not in self.__backends: # pragma: no cover
|
if backend_name not in self.__backends: # pragma: no cover
|
||||||
@ -48,5 +51,6 @@ class PluggableBackend(object):
|
|||||||
return self.__backend
|
return self.__backend
|
||||||
|
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
|
"""Proxy interface to backend."""
|
||||||
backend = self.__get_backend()
|
backend = self.__get_backend()
|
||||||
return getattr(backend, key)
|
return getattr(backend, key)
|
||||||
|
@ -12,10 +12,10 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
"""
|
"""Function list_opts intended for oslo-config-generator.
|
||||||
Function list_opts intended for oslo-config-generator. It tool used for
|
|
||||||
generate config file with help info and default values for options defined
|
this tool used for generate config file with help info and default values
|
||||||
anywhere in application.
|
for options defined anywhere in application.
|
||||||
All new options must be imported here and must be returned from
|
All new options must be imported here and must be returned from
|
||||||
list_opts function as list that contain tuple.
|
list_opts function as list that contain tuple.
|
||||||
Use itertools.chain if config section contain more than one imported module
|
Use itertools.chain if config section contain more than one imported module
|
||||||
@ -35,13 +35,20 @@ import itertools
|
|||||||
|
|
||||||
import refstack.api.app
|
import refstack.api.app
|
||||||
import refstack.api.controllers.v1
|
import refstack.api.controllers.v1
|
||||||
|
import refstack.api.controllers.auth
|
||||||
import refstack.db.api
|
import refstack.db.api
|
||||||
|
|
||||||
|
|
||||||
def list_opts():
|
def list_opts():
|
||||||
|
"""List oslo config options.
|
||||||
|
|
||||||
|
Keep a list in alphabetical order
|
||||||
|
"""
|
||||||
return [
|
return [
|
||||||
# Keep a list in alphabetical order
|
#
|
||||||
('DEFAULT', refstack.db.api.db_opts),
|
('DEFAULT', itertools.chain(refstack.api.app.UI_OPTS,
|
||||||
|
refstack.db.api.db_opts)),
|
||||||
('api', itertools.chain(refstack.api.app.API_OPTS,
|
('api', itertools.chain(refstack.api.app.API_OPTS,
|
||||||
refstack.api.controllers.v1.CTRLS_OPTS)),
|
refstack.api.controllers.v1.CTRLS_OPTS)),
|
||||||
|
('osid', refstack.api.controllers.auth.OPENID_OPTS),
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Refstack tests."""
|
@ -74,7 +74,7 @@ class FunctionalTest(base.BaseTestCase):
|
|||||||
self.app.reset()
|
self.app.reset()
|
||||||
|
|
||||||
def drop_all_tables_and_constraints(self):
|
def drop_all_tables_and_constraints(self):
|
||||||
"""Drop tables and cyclical constraints between tables"""
|
"""Drop tables and cyclical constraints between tables."""
|
||||||
engine = create_engine(self.connection)
|
engine = create_engine(self.connection)
|
||||||
conn = engine.connect()
|
conn = engine.connect()
|
||||||
trans = conn.begin()
|
trans = conn.begin()
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Refstack unittests."""
|
@ -23,10 +23,14 @@ import mock
|
|||||||
from oslo_config import fixture as config_fixture
|
from oslo_config import fixture as config_fixture
|
||||||
from oslotest import base
|
from oslotest import base
|
||||||
import requests
|
import requests
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
import webob.exc
|
||||||
|
|
||||||
from refstack.api import constants as const
|
from refstack.api import constants as const
|
||||||
from refstack.api import utils as api_utils
|
from refstack.api import utils as api_utils
|
||||||
|
from refstack.api.controllers import auth
|
||||||
from refstack.api.controllers import v1
|
from refstack.api.controllers import v1
|
||||||
|
from refstack.api.controllers import user
|
||||||
|
|
||||||
|
|
||||||
def safe_json_dump(content):
|
def safe_json_dump(content):
|
||||||
@ -62,10 +66,12 @@ class ResultsControllerTestCase(base.BaseTestCase):
|
|||||||
self.controller = v1.ResultsController()
|
self.controller = v1.ResultsController()
|
||||||
self.config_fixture = config_fixture.Config()
|
self.config_fixture = config_fixture.Config()
|
||||||
self.CONF = self.useFixture(self.config_fixture).conf
|
self.CONF = self.useFixture(self.config_fixture).conf
|
||||||
self.test_results_url = 'host?%s'
|
self.test_results_url = '/#/results/%s'
|
||||||
|
self.ui_url = 'host.org'
|
||||||
self.CONF.set_override('test_results_url',
|
self.CONF.set_override('test_results_url',
|
||||||
self.test_results_url,
|
self.test_results_url,
|
||||||
'api')
|
'api')
|
||||||
|
self.CONF.set_override('ui_url', self.ui_url)
|
||||||
|
|
||||||
@mock.patch('refstack.db.get_test')
|
@mock.patch('refstack.db.get_test')
|
||||||
@mock.patch('refstack.db.get_test_results')
|
@mock.patch('refstack.db.get_test_results')
|
||||||
@ -101,9 +107,12 @@ class ResultsControllerTestCase(base.BaseTestCase):
|
|||||||
mock_request.headers = {}
|
mock_request.headers = {}
|
||||||
mock_store_results.return_value = 'fake_test_id'
|
mock_store_results.return_value = 'fake_test_id'
|
||||||
result = self.controller.post()
|
result = self.controller.post()
|
||||||
self.assertEqual(result,
|
self.assertEqual(
|
||||||
|
result,
|
||||||
{'test_id': 'fake_test_id',
|
{'test_id': 'fake_test_id',
|
||||||
'url': self.test_results_url % 'fake_test_id'})
|
'url': parse.urljoin(self.ui_url,
|
||||||
|
self.test_results_url) % 'fake_test_id'}
|
||||||
|
)
|
||||||
self.assertEqual(mock_response.status, 201)
|
self.assertEqual(mock_response.status, 201)
|
||||||
mock_store_results.assert_called_once_with({'answer': 42})
|
mock_store_results.assert_called_once_with({'answer': 42})
|
||||||
|
|
||||||
@ -366,3 +375,152 @@ class BaseRestControllerWithValidationTestCase(base.BaseTestCase):
|
|||||||
self.validator.assert_id = mock.Mock(return_value=False)
|
self.validator.assert_id = mock.Mock(return_value=False)
|
||||||
self.controller.get_one('fake_arg')
|
self.controller.get_one('fake_arg')
|
||||||
mock_abort.assert_called_once_with(404)
|
mock_abort.assert_called_once_with(404)
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileControllerTestCase(base.BaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ProfileControllerTestCase, self).setUp()
|
||||||
|
self.controller = user.ProfileController()
|
||||||
|
|
||||||
|
@mock.patch('refstack.db.user_get',
|
||||||
|
return_value=mock.Mock(openid='foo@bar.org',
|
||||||
|
email='foo@bar.org',
|
||||||
|
fullname='Dobby'))
|
||||||
|
@mock.patch('refstack.api.utils.get_user_session',
|
||||||
|
return_value={const.USER_OPENID: 'foo@bar.org'})
|
||||||
|
@mock.patch('refstack.api.utils.is_authenticated', return_value=True)
|
||||||
|
def test_get(self, mock_is_authenticated, mock_get_user_session,
|
||||||
|
mock_user_get):
|
||||||
|
actual_result = self.controller.get()
|
||||||
|
self.assertEqual({'openid': 'foo@bar.org',
|
||||||
|
'email': 'foo@bar.org',
|
||||||
|
'fullname': 'Dobby'}, actual_result)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthControllerTestCase(base.BaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(AuthControllerTestCase, self).setUp()
|
||||||
|
self.controller = auth.AuthController()
|
||||||
|
self.config_fixture = config_fixture.Config()
|
||||||
|
self.CONF = self.useFixture(self.config_fixture).conf
|
||||||
|
self.CONF.set_override('app_dev_mode', True, 'api')
|
||||||
|
self.CONF.set_override('ui_url', '127.0.0.1')
|
||||||
|
|
||||||
|
@mock.patch('refstack.api.utils.get_user_session')
|
||||||
|
@mock.patch('refstack.api.utils.is_authenticated', return_value=True)
|
||||||
|
@mock.patch('pecan.redirect', side_effect=webob.exc.HTTPRedirection)
|
||||||
|
def test_signed_signin(self, mock_redirect, mock_is_authenticated,
|
||||||
|
mock_get_user_session):
|
||||||
|
mock_session = mock.MagicMock(**{const.USER_OPENID: 'foo@bar.org'})
|
||||||
|
mock_get_user_session.return_value = mock_session
|
||||||
|
self.assertRaises(webob.exc.HTTPRedirection, self.controller.signin)
|
||||||
|
mock_redirect.assert_called_with('127.0.0.1')
|
||||||
|
|
||||||
|
@mock.patch('refstack.api.utils.get_user_session')
|
||||||
|
@mock.patch('refstack.api.utils.is_authenticated', return_value=False)
|
||||||
|
@mock.patch('pecan.redirect', side_effect=webob.exc.HTTPRedirection)
|
||||||
|
def test_unsigned_signin(self, mock_redirect, mock_is_authenticated,
|
||||||
|
mock_get_user_session):
|
||||||
|
mock_session = mock.MagicMock(**{const.USER_OPENID: 'foo@bar.org'})
|
||||||
|
mock_get_user_session.return_value = mock_session
|
||||||
|
self.assertRaises(webob.exc.HTTPRedirection, self.controller.signin)
|
||||||
|
self.assertIn(self.CONF.osid.openstack_openid_endpoint,
|
||||||
|
mock_redirect.call_args[1]['location'])
|
||||||
|
|
||||||
|
@mock.patch('socket.gethostbyname', return_value='1.1.1.1')
|
||||||
|
@mock.patch('pecan.request')
|
||||||
|
@mock.patch('refstack.api.utils.get_user_session')
|
||||||
|
@mock.patch('pecan.abort', side_effect=webob.exc.HTTPError)
|
||||||
|
def test_signin_return_failed(self, mock_abort, mock_get_user_session,
|
||||||
|
mock_request, mock_socket):
|
||||||
|
mock_session = mock.MagicMock(**{const.USER_OPENID: 'foo@bar.org',
|
||||||
|
const.CSRF_TOKEN: '42'})
|
||||||
|
mock_get_user_session.return_value = mock_session
|
||||||
|
mock_request.remote_addr = '1.1.1.2'
|
||||||
|
|
||||||
|
mock_request.GET = {
|
||||||
|
const.OPENID_ERROR: 'foo is not bar!!!'
|
||||||
|
}
|
||||||
|
mock_request.environ['beaker.session'] = {
|
||||||
|
const.CSRF_TOKEN: 42
|
||||||
|
}
|
||||||
|
self.assertRaises(webob.exc.HTTPError, self.controller.signin_return)
|
||||||
|
mock_abort.assert_called_once_with(
|
||||||
|
401, mock_request.GET[const.OPENID_ERROR])
|
||||||
|
self.assertNotIn(const.CSRF_TOKEN,
|
||||||
|
mock_request.environ['beaker.session'])
|
||||||
|
|
||||||
|
mock_abort.reset_mock()
|
||||||
|
mock_request.environ['beaker.session'] = {
|
||||||
|
const.CSRF_TOKEN: 42
|
||||||
|
}
|
||||||
|
mock_request.GET = {
|
||||||
|
const.OPENID_MODE: 'cancel'
|
||||||
|
}
|
||||||
|
self.assertRaises(webob.exc.HTTPError, self.controller.signin_return)
|
||||||
|
mock_abort.assert_called_once_with(
|
||||||
|
401, 'Authentication canceled.')
|
||||||
|
self.assertNotIn(const.CSRF_TOKEN,
|
||||||
|
mock_request.environ['beaker.session'])
|
||||||
|
|
||||||
|
mock_abort.reset_mock()
|
||||||
|
mock_request.environ['beaker.session'] = {
|
||||||
|
const.CSRF_TOKEN: 42
|
||||||
|
}
|
||||||
|
mock_request.GET = {}
|
||||||
|
self.assertRaises(webob.exc.HTTPError, self.controller.signin_return)
|
||||||
|
mock_abort.assert_called_once_with(
|
||||||
|
401, 'Authentication is failed. Try again.')
|
||||||
|
self.assertNotIn(const.CSRF_TOKEN,
|
||||||
|
mock_request.environ['beaker.session'])
|
||||||
|
|
||||||
|
mock_abort.reset_mock()
|
||||||
|
mock_request.environ['beaker.session'] = {
|
||||||
|
const.CSRF_TOKEN: 42
|
||||||
|
}
|
||||||
|
mock_request.GET = {const.CSRF_TOKEN: '24'}
|
||||||
|
mock_request.remote_addr = '1.1.1.1'
|
||||||
|
self.assertRaises(webob.exc.HTTPError, self.controller.signin_return)
|
||||||
|
mock_abort.assert_called_once_with(
|
||||||
|
401, 'Authentication is failed. Try again.')
|
||||||
|
self.assertNotIn(const.CSRF_TOKEN,
|
||||||
|
mock_request.environ['beaker.session'])
|
||||||
|
|
||||||
|
@mock.patch('refstack.api.utils.verify_openid_request', return_value=True)
|
||||||
|
@mock.patch('refstack.db.user_update_or_create')
|
||||||
|
@mock.patch('pecan.request')
|
||||||
|
@mock.patch('refstack.api.utils.get_user_session')
|
||||||
|
@mock.patch('pecan.redirect', side_effect=webob.exc.HTTPRedirection)
|
||||||
|
def test_signin_return_success(self, mock_redirect, mock_get_user_session,
|
||||||
|
mock_request, mock_user, mock_verify):
|
||||||
|
mock_session = mock.MagicMock(**{const.USER_OPENID: 'foo@bar.org',
|
||||||
|
const.CSRF_TOKEN: 42})
|
||||||
|
mock_session.get = mock.Mock(return_value=42)
|
||||||
|
mock_get_user_session.return_value = mock_session
|
||||||
|
|
||||||
|
mock_request.GET = {
|
||||||
|
const.OPENID_CLAIMED_ID: 'foo@bar.org',
|
||||||
|
const.OPENID_NS_SREG_EMAIL: 'foo@bar.org',
|
||||||
|
const.OPENID_NS_SREG_FULLNAME: 'foo',
|
||||||
|
const.CSRF_TOKEN: 42
|
||||||
|
}
|
||||||
|
mock_request.environ['beaker.session'] = {
|
||||||
|
const.CSRF_TOKEN: 42
|
||||||
|
}
|
||||||
|
self.assertRaises(webob.exc.HTTPRedirection,
|
||||||
|
self.controller.signin_return)
|
||||||
|
|
||||||
|
@mock.patch('pecan.request')
|
||||||
|
@mock.patch('refstack.api.utils.is_authenticated', return_value=True)
|
||||||
|
@mock.patch('pecan.redirect', side_effect=webob.exc.HTTPRedirection)
|
||||||
|
def test_signout(self, mock_redirect, mock_is_authenticated,
|
||||||
|
mock_request):
|
||||||
|
mock_request.environ['beaker.session'] = {
|
||||||
|
const.CSRF_TOKEN: 42
|
||||||
|
}
|
||||||
|
self.assertRaises(webob.exc.HTTPRedirection, self.controller.signout)
|
||||||
|
mock_redirect.assert_called_with('127.0.0.1')
|
||||||
|
self.assertNotIn(const.CSRF_TOKEN,
|
||||||
|
mock_request.environ['beaker.session'])
|
||||||
|
@ -19,9 +19,11 @@ import mock
|
|||||||
from oslo_config import fixture as config_fixture
|
from oslo_config import fixture as config_fixture
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
from oslotest import base
|
from oslotest import base
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
from refstack.api import constants as const
|
from refstack.api import constants as const
|
||||||
from refstack.api import utils as api_utils
|
from refstack.api import utils as api_utils
|
||||||
|
from refstack import db
|
||||||
|
|
||||||
|
|
||||||
class APIUtilsTestCase(base.BaseTestCase):
|
class APIUtilsTestCase(base.BaseTestCase):
|
||||||
@ -255,3 +257,75 @@ class APIUtilsTestCase(base.BaseTestCase):
|
|||||||
|
|
||||||
self.assertEqual(page_number, 2)
|
self.assertEqual(page_number, 2)
|
||||||
self.assertEqual(total_pages, total_records / per_page)
|
self.assertEqual(total_pages, total_records / per_page)
|
||||||
|
|
||||||
|
def test_set_query_params(self):
|
||||||
|
url = 'http://e.io/path#fragment'
|
||||||
|
new_url = api_utils.set_query_params(url, {'foo': 'bar', '?': 42})
|
||||||
|
self.assertEqual(parse.parse_qs(parse.urlparse(new_url)[4]),
|
||||||
|
{'foo': ['bar'], '?': ['42']})
|
||||||
|
|
||||||
|
def test_get_token(self):
|
||||||
|
token = api_utils.get_token(42)
|
||||||
|
self.assertRegexpMatches(token, "[a-z]{42}")
|
||||||
|
|
||||||
|
@mock.patch.object(api_utils, 'get_user_session')
|
||||||
|
def test_delete_params_from_user_session(self, mock_get_user_session):
|
||||||
|
mock_session = mock.MagicMock(**{'foo': 'bar', 'answer': 42})
|
||||||
|
mock_get_user_session.return_value = mock_session
|
||||||
|
api_utils.delete_params_from_user_session(('foo', 'answer'))
|
||||||
|
self.assertNotIn('foo', mock_session.__dir__)
|
||||||
|
self.assertNotIn('answer', mock_session.__dir__)
|
||||||
|
mock_session.save.called_once_with()
|
||||||
|
|
||||||
|
@mock.patch('pecan.request')
|
||||||
|
def test_get_user_session(self, mock_request):
|
||||||
|
mock_request.environ = {'beaker.session': 42}
|
||||||
|
session = api_utils.get_user_session()
|
||||||
|
self.assertEqual(42, session)
|
||||||
|
|
||||||
|
@mock.patch.object(api_utils, 'get_user_session')
|
||||||
|
@mock.patch.object(api_utils, 'db')
|
||||||
|
def test_is_authenticated(self, mock_db, mock_get_user_session):
|
||||||
|
mock_session = mock.MagicMock(**{const.USER_OPENID: 'foo@bar.com'})
|
||||||
|
mock_get_user_session.return_value = mock_session
|
||||||
|
mock_get_user = mock_db.user_get
|
||||||
|
mock_get_user.return_value = 'Dobby'
|
||||||
|
self.assertEqual(True, api_utils.is_authenticated())
|
||||||
|
mock_db.user_get.called_once_with(mock_session)
|
||||||
|
mock_db.UserNotFound = db.UserNotFound
|
||||||
|
mock_get_user.side_effect = mock_db.UserNotFound
|
||||||
|
self.assertEqual(False, api_utils.is_authenticated())
|
||||||
|
|
||||||
|
@mock.patch('requests.post')
|
||||||
|
@mock.patch('pecan.abort')
|
||||||
|
def test_verify_openid_request(self, mock_abort, mock_post):
|
||||||
|
mock_response = mock.Mock()
|
||||||
|
mock_response.content = ('is_valid:true\n'
|
||||||
|
'ns:http://specs.openid.net/auth/2.0\n')
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
mock_request = mock.Mock()
|
||||||
|
mock_request.params = {
|
||||||
|
const.OPENID_NS_SREG_EMAIL: 'foo@bar.org',
|
||||||
|
const.OPENID_NS_SREG_FULLNAME: 'foo'
|
||||||
|
}
|
||||||
|
self.assertEqual(True, api_utils.verify_openid_request(mock_request))
|
||||||
|
|
||||||
|
mock_response.content = ('is_valid:false\n'
|
||||||
|
'ns:http://specs.openid.net/auth/2.0\n')
|
||||||
|
api_utils.verify_openid_request(mock_request)
|
||||||
|
mock_abort.assert_called_once_with(
|
||||||
|
401, 'Authentication is failed. Try again.'
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_abort.reset_mock()
|
||||||
|
mock_response.content = ('is_valid:true\n'
|
||||||
|
'ns:http://specs.openid.net/auth/2.0\n')
|
||||||
|
mock_request.params = {
|
||||||
|
const.OPENID_NS_SREG_EMAIL: 'foo@bar.org',
|
||||||
|
}
|
||||||
|
api_utils.verify_openid_request(mock_request)
|
||||||
|
mock_abort.assert_called_once_with(
|
||||||
|
401, 'Authentication is failed. '
|
||||||
|
'Please permit access to your name.'
|
||||||
|
)
|
||||||
|
@ -90,6 +90,15 @@ class JSONErrorHookTestCase(base.BaseTestCase):
|
|||||||
'detail': str(exc)}
|
'detail': str(exc)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@mock.patch.object(webob, 'Response')
|
||||||
|
def test_on_http_redirection(self, response):
|
||||||
|
self.CONF.set_override('app_dev_mode', False, 'api')
|
||||||
|
|
||||||
|
exc = mock.Mock(spec=webob.exc.HTTPRedirection)
|
||||||
|
hook = app.JSONErrorHook()
|
||||||
|
result = hook.on_error(mock.Mock(), exc)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
@mock.patch.object(webob, 'Response')
|
@mock.patch.object(webob, 'Response')
|
||||||
def test_on_error_with_other_exceptions(self, response):
|
def test_on_error_with_other_exceptions(self, response):
|
||||||
self.CONF.set_override('app_dev_mode', False, 'api')
|
self.CONF.set_override('app_dev_mode', False, 'api')
|
||||||
@ -180,7 +189,9 @@ class SetupAppTestCase(base.BaseTestCase):
|
|||||||
@mock.patch.object(app, 'CORSHook')
|
@mock.patch.object(app, 'CORSHook')
|
||||||
@mock.patch('os.path.join')
|
@mock.patch('os.path.join')
|
||||||
@mock.patch('pecan.make_app')
|
@mock.patch('pecan.make_app')
|
||||||
def test_setup_app(self, make_app, os_join,
|
@mock.patch('refstack.api.app.SessionMiddleware')
|
||||||
|
@mock.patch('refstack.api.utils.get_token', return_value='42')
|
||||||
|
def test_setup_app(self, get_token, session_middleware, make_app, os_join,
|
||||||
json_error_hook, cors_hook, pecan_hooks):
|
json_error_hook, cors_hook, pecan_hooks):
|
||||||
|
|
||||||
self.CONF.set_override('app_dev_mode',
|
self.CONF.set_override('app_dev_mode',
|
||||||
@ -201,10 +212,11 @@ class SetupAppTestCase(base.BaseTestCase):
|
|||||||
pecan_config = mock.Mock()
|
pecan_config = mock.Mock()
|
||||||
pecan_config.app = {'root': 'fake_pecan_config'}
|
pecan_config.app = {'root': 'fake_pecan_config'}
|
||||||
make_app.return_value = 'fake_app'
|
make_app.return_value = 'fake_app'
|
||||||
|
session_middleware.return_value = 'fake_app_with_middleware'
|
||||||
|
|
||||||
result = app.setup_app(pecan_config)
|
result = app.setup_app(pecan_config)
|
||||||
|
|
||||||
self.assertEqual(result, 'fake_app')
|
self.assertEqual(result, 'fake_app_with_middleware')
|
||||||
|
|
||||||
app_conf = dict(pecan_config.app)
|
app_conf = dict(pecan_config.app)
|
||||||
make_app.assert_called_once_with(
|
make_app.assert_called_once_with(
|
||||||
@ -214,3 +226,10 @@ class SetupAppTestCase(base.BaseTestCase):
|
|||||||
template_path='fake_template_path',
|
template_path='fake_template_path',
|
||||||
hooks=['cors_hook', 'json_error_hook', 'request_viewer_hook']
|
hooks=['cors_hook', 'json_error_hook', 'request_viewer_hook']
|
||||||
)
|
)
|
||||||
|
session_middleware.assert_called_once_with(
|
||||||
|
'fake_app',
|
||||||
|
{'session.key': 'refstack',
|
||||||
|
'session.type': 'memory',
|
||||||
|
'session.timeout': 604800,
|
||||||
|
'session.validate_key': get_token.return_value}
|
||||||
|
)
|
||||||
|
@ -55,6 +55,18 @@ class DBAPITestCase(base.BaseTestCase):
|
|||||||
db.get_test_records_count(filters)
|
db.get_test_records_count(filters)
|
||||||
mock_db.assert_called_once_with(filters)
|
mock_db.assert_called_once_with(filters)
|
||||||
|
|
||||||
|
@mock.patch.object(api, 'user_get')
|
||||||
|
def test_user_get(self, mock_db):
|
||||||
|
user_openid = 'user@example.com'
|
||||||
|
db.user_get(user_openid)
|
||||||
|
mock_db.assert_called_once_with(user_openid)
|
||||||
|
|
||||||
|
@mock.patch.object(api, 'user_update_or_create')
|
||||||
|
def test_user_update_or_create(self, mock_db):
|
||||||
|
user_info = 'user@example.com'
|
||||||
|
db.user_update_or_create(user_info)
|
||||||
|
mock_db.assert_called_once_with(user_info)
|
||||||
|
|
||||||
|
|
||||||
class DBHelpersTestCase(base.BaseTestCase):
|
class DBHelpersTestCase(base.BaseTestCase):
|
||||||
"""Test case for database backend helpers."""
|
"""Test case for database backend helpers."""
|
||||||
@ -261,3 +273,48 @@ class DBBackendTestCase(base.BaseTestCase):
|
|||||||
session.query.assert_called_once_with(mock_model.id)
|
session.query.assert_called_once_with(mock_model.id)
|
||||||
mock_apply.assert_called_once_with(query, filters)
|
mock_apply.assert_called_once_with(query, filters)
|
||||||
apply_result.count.assert_called_once_with()
|
apply_result.count.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch.object(api, 'get_session',
|
||||||
|
return_value=mock.Mock(name='session'),)
|
||||||
|
@mock.patch('refstack.db.sqlalchemy.models.User')
|
||||||
|
def test_user_get(self, mock_model, mock_get_session):
|
||||||
|
user_openid = 'user@example.com'
|
||||||
|
session = mock_get_session.return_value
|
||||||
|
query = session.query.return_value
|
||||||
|
filtered = query.filter_by.return_value
|
||||||
|
user = filtered.first.return_value
|
||||||
|
|
||||||
|
result = api.user_get(user_openid)
|
||||||
|
self.assertEqual(result, user)
|
||||||
|
|
||||||
|
session.query.assert_called_once_with(mock_model)
|
||||||
|
query.filter_by.assert_called_once_with(openid=user_openid)
|
||||||
|
filtered.first.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch.object(api, 'get_session',
|
||||||
|
return_value=mock.Mock(name='session'),)
|
||||||
|
@mock.patch('refstack.db.sqlalchemy.models.User')
|
||||||
|
def test_user_get_none(self, mock_model, mock_get_session):
|
||||||
|
user_openid = 'user@example.com'
|
||||||
|
session = mock_get_session.return_value
|
||||||
|
query = session.query.return_value
|
||||||
|
filtered = query.filter_by.return_value
|
||||||
|
filtered.first.return_value = None
|
||||||
|
self.assertRaises(api.UserNotFound, api.user_get, user_openid)
|
||||||
|
|
||||||
|
@mock.patch.object(api, 'get_session')
|
||||||
|
@mock.patch('refstack.db.sqlalchemy.models.User')
|
||||||
|
@mock.patch.object(api, 'user_get', side_effect=api.UserNotFound)
|
||||||
|
def test_user_update_or_create(self, mock_get_user, mock_model,
|
||||||
|
mock_get_session):
|
||||||
|
user_info = {'openid': 'user@example.com'}
|
||||||
|
session = mock_get_session.return_value
|
||||||
|
user = mock_model.return_value
|
||||||
|
result = api.user_update_or_create(user_info)
|
||||||
|
self.assertEqual(result, user)
|
||||||
|
|
||||||
|
mock_model.assert_called_once_with()
|
||||||
|
mock_get_session.assert_called_once_with()
|
||||||
|
user.save.assert_called_once_with(session=session)
|
||||||
|
user.update.assert_called_once_with(user_info)
|
||||||
|
session.begin.assert_called_once_with()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
SQLAlchemy>=0.8.3
|
SQLAlchemy>=0.8.3
|
||||||
alembic==0.5.0
|
alembic==0.5.0
|
||||||
|
beaker==1.6.5.post1
|
||||||
#gunicorn 19.1.1 has a bug with threading module
|
#gunicorn 19.1.1 has a bug with threading module
|
||||||
gunicorn==18
|
gunicorn==18
|
||||||
oslo.config>=1.6.0 # Apache-2.0
|
oslo.config>=1.6.0 # Apache-2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user