Add tests to product page

This patch updates the product page to include tests
associated to versions of the product. This allows filtering
results based on a product_id. Tests associated with private
products will have their product information hidden.

Change-Id: Ic5b6b45c9e3d14d9c2cb36a8eba72f2a6e31d2aa
This commit is contained in:
Paul Van Eck 2016-10-02 01:31:46 -07:00
parent 02307f3a1e
commit 5ad18f067f
16 changed files with 444 additions and 46 deletions

View File

@ -1,4 +1,5 @@
<h3>Cloud Product</h3> <h3>Cloud Product</h3>
<div cg-busy="{promise:ctrl.productRequest,message:'Loading'}"></div>
<div ng-show="ctrl.product" class="container-fluid"> <div ng-show="ctrl.product" class="container-fluid">
<div class="row"> <div class="row">
<div class="pull-left"> <div class="pull-left">
@ -14,6 +15,8 @@
<div ng-include src="'components/products/partials/management.html'"></div> <div ng-include src="'components/products/partials/management.html'"></div>
<div class="clearfix"></div> <div class="clearfix"></div>
<div ng-include src="'components/products/partials/versions.html'"></div> <div ng-include src="'components/products/partials/versions.html'"></div>
<hr>
<div ng-include src="'components/products/partials/testsTable.html'"></div>
</div> </div>
</div> </div>
<div ng-show="ctrl.showError" class="alert alert-danger" role="alert"> <div ng-show="ctrl.showError" class="alert alert-danger" role="alert">

View File

@ -1,4 +1,5 @@
<h3>Distro Product</h3> <h3>Distro Product</h3>
<div cg-busy="{promise:ctrl.productRequest,message:'Loading'}"></div>
<div ng-show="ctrl.product" class="container-fluid"> <div ng-show="ctrl.product" class="container-fluid">
<div class="row"> <div class="row">
<div class="pull-left"> <div class="pull-left">
@ -14,6 +15,8 @@
<div ng-include src="'components/products/partials/management.html'"></div> <div ng-include src="'components/products/partials/management.html'"></div>
<div class="clearfix"></div> <div class="clearfix"></div>
<div ng-include src="'components/products/partials/versions.html'"></div> <div ng-include src="'components/products/partials/versions.html'"></div>
<hr>
<div ng-include src="'components/products/partials/testsTable.html'"></div>
</div> </div>
</div> </div>
<div ng-show="ctrl.showError" class="alert alert-danger" role="alert"> <div ng-show="ctrl.showError" class="alert alert-danger" role="alert">

View File

@ -0,0 +1,137 @@
<h4><strong>Test Runs on Product</strong></h4>
<div cg-busy="{promise:ctrl.testsRequest,message:'Loading'}"></div>
<table class="table table-striped table-hover">
<thead>
<tr>
<th></th>
<th>Upload Date</th>
<th>Test Run ID</th>
<th>Product Version</th>
<th>Shared</th>
</tr>
</thead>
<tbody>
<tr ng-repeat-start="(index, result) in ctrl.testsData">
<td>
<a ng-if="!result.expanded"
class="glyphicon glyphicon-plus"
ng-click="result.expanded = true">
</a>
<a ng-if="result.expanded"
class="glyphicon glyphicon-minus"
ng-click="result.expanded = false">
</a>
</td>
<td>{{result.created_at}}</td>
<td><a ui-sref="resultsDetail({testID: result.id})">{{result.id}}</a></td>
<td>{{result.product_version.version}}</td>
<td>
<span ng-show="result.meta.shared" class="glyphicon glyphicon-share"></span>
</td>
</tr>
<tr ng-if="result.expanded" ng-repeat-end>
<td></td>
<td colspan="4">
<strong>Publicly Shared:</strong>
<span ng-if="result.meta.shared == 'true' && !result.sharedEdit">Yes</span>
<span ng-if="!result.meta.shared && !result.sharedEdit">
<em>No</em>
</span>
<select ng-if="result.sharedEdit"
ng-model="result.meta.shared"
class="form-inline">
<option value="true">Yes</option>
<option value="">No</option>
</select>
<a ng-if="!result.sharedEdit"
ng-click="result.sharedEdit = true"
title="Edit"
class="glyphicon glyphicon-pencil"></a>
<a ng-if="result.sharedEdit"
ng-click="ctrl.associateTestMeta(index,'shared',result.meta.shared)"
title="Save"
class="glyphicon glyphicon-floppy-disk"></a>
<br />
<strong>Associated Guideline:</strong>
<span ng-if="!result.meta.guideline && !result.guidelineEdit">
<em>None</em>
</span>
<span ng-if="result.meta.guideline && !result.guidelineEdit">
{{result.meta.guideline.slice(0, -5)}}
</span>
<select ng-if="result.guidelineEdit"
ng-model="result.meta.guideline"
ng-options="o as o.slice(0, -5) for o in ctrl.versionList"
class="form-inline">
<option value="">None</option>
</select>
<a ng-if="!result.guidelineEdit"
ng-click="ctrl.getGuidelineVersionList();result.guidelineEdit = true"
title="Edit"
class="glyphicon glyphicon-pencil"></a>
<a ng-if="result.guidelineEdit"
ng-click="ctrl.associateTestMeta(index, 'guideline', result.meta.guideline)"
title="Save"
class="glyphicon glyphicon-floppy-disk">
</a>
<br />
<strong>Associated Target Program:</strong>
<span ng-if="!result.meta.target && !result.targetEdit">
<em>None</em>
</span>
<span ng-if="result.meta.target && !result.targetEdit">
{{ctrl.targetMappings[result.meta.target]}}</span>
<select ng-if="result.targetEdit"
ng-model="result.meta.target"
class="form-inline">
<option value="">None</option>
<option value="platform">OpenStack Powered Platform</option>
<option value="compute">OpenStack Powered Compute</option>
<option value="object">OpenStack Powered Object Storage</option>
</select>
<a ng-if="!result.targetEdit"
ng-click="result.targetEdit = true"
title="Edit"
class="glyphicon glyphicon-pencil">
</a>
<a ng-if="result.targetEdit"
ng-click="ctrl.associateTestMeta(index, 'target', result.meta.target)"
title="Save"
class="glyphicon glyphicon-floppy-disk">
</a>
<br />
<br />
<small>
<a ng-click="ctrl.unassociateTest(index)"
confirm="Are you sure you want to unassociate this test result with product: {{ctrl.product.name}}? Test result ownership will be given back to the original owner only.">
<span class="glyphicon glyphicon-remove-circle" ></span> Unassociate test result from product
</a>
</small>
</td>
</tr>
</tbody>
</table>
<div class="pages">
<uib-pagination
total-items="ctrl.totalItems"
ng-model="ctrl.currentPage"
items-per-page="ctrl.itemsPerPage"
max-size="ctrl.maxSize"
class="pagination-sm"
boundary-links="true"
rotate="false"
num-pages="ctrl.numPages"
ng-change="ctrl.getProductTests()">
</uib-pagination>
</div>
<div ng-show="ctrl.showTestsError" class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<span class="sr-only">Error:</span>
{{ctrl.testsError}}
</div>

View File

@ -37,8 +37,12 @@
ctrl.getProductVersions = getProductVersions; ctrl.getProductVersions = getProductVersions;
ctrl.deleteProduct = deleteProduct; ctrl.deleteProduct = deleteProduct;
ctrl.deleteProductVersion = deleteProductVersion; ctrl.deleteProductVersion = deleteProductVersion;
ctrl.getProductTests = getProductTests;
ctrl.switchProductPublicity = switchProductPublicity; ctrl.switchProductPublicity = switchProductPublicity;
ctrl.associateTestMeta = associateTestMeta;
ctrl.getGuidelineVersionList = getGuidelineVersionList;
ctrl.addProductVersion = addProductVersion; ctrl.addProductVersion = addProductVersion;
ctrl.unassociateTest = unassociateTest;
ctrl.openVersionModal = openVersionModal; ctrl.openVersionModal = openVersionModal;
/** The product id extracted from the URL route. */ /** The product id extracted from the URL route. */
@ -49,8 +53,21 @@
$state.go('home'); $state.go('home');
} }
/** Mappings of DefCore components to marketing program names. */
ctrl.targetMappings = {
'platform': 'Openstack Powered Platform',
'compute': 'OpenStack Powered Compute',
'object': 'OpenStack Powered Object Storage'
};
// Pagination controls.
ctrl.currentPage = 1;
ctrl.itemsPerPage = 20;
ctrl.maxSize = 5;
ctrl.getProduct(); ctrl.getProduct();
ctrl.getProductVersions(); ctrl.getProductVersions();
ctrl.getProductTests();
/** /**
* This will contact the Refstack API to get a product information. * This will contact the Refstack API to get a product information.
@ -147,6 +164,30 @@
}); });
} }
/**
* Get tests runs associated with the current product.
*/
function getProductTests() {
ctrl.showTestsError = false;
var content_url = refstackApiUrl + '/results' +
'?page=' + ctrl.currentPage + '&product_id='
+ ctrl.id;
ctrl.testsRequest = $http.get(content_url).success(
function(data) {
ctrl.testsData = data.results;
ctrl.totalItems = data.pagination.total_pages *
ctrl.itemsPerPage;
ctrl.currentPage = data.pagination.current_page;
}
).error(function(error) {
ctrl.showTestsError = true;
ctrl.testsError =
'Error retrieving tests from server: ' +
angular.toJson(error);
});
}
/** /**
* This will switch public/private property of the product. * This will switch public/private property of the product.
*/ */
@ -161,6 +202,73 @@
}); });
} }
/**
* This will send an API request in order to associate a metadata
* key-value pair with the given testId
* @param {Number} index - index of the test object in the results list
* @param {String} key - metadata key
* @param {String} value - metadata value
*/
function associateTestMeta(index, key, value) {
var testId = ctrl.testsData[index].id;
var metaUrl = [
refstackApiUrl, '/results/', testId, '/meta/', key
].join('');
var editFlag = key + 'Edit';
if (value) {
ctrl.associateRequest = $http.post(metaUrl, value)
.success(function () {
ctrl.testsData[index][editFlag] = false;
}).error(function (error) {
raiseAlert('danger', error.title, error.detail);
});
}
else {
ctrl.unassociateRequest = $http.delete(metaUrl)
.success(function () {
ctrl.testsData[index][editFlag] = false;
}).error(function (error) {
raiseAlert('danger', error.title, error.detail);
});
}
}
/**
* Retrieve an array of available capability files from the Refstack
* API server, sort this array reverse-alphabetically, and store it in
* a scoped variable.
* Sample API return array: ["2015.03.json", "2015.04.json"]
*/
function getGuidelineVersionList() {
if (ctrl.versionList) {
return;
}
var content_url = refstackApiUrl + '/guidelines';
ctrl.versionsRequest =
$http.get(content_url).success(function (data) {
ctrl.versionList = data.sort().reverse();
}).error(function (error) {
raiseAlert('danger', error.title,
'Unable to retrieve version list');
});
}
/**
* Send a PUT request to the API server to unassociate a product with
* a test result.
*/
function unassociateTest(index) {
var testId = ctrl.testsData[index].id;
var url = refstackApiUrl + '/results/' + testId;
ctrl.associateRequest = $http.put(url, {'product_version_id': null})
.success(function () {
ctrl.testsData.splice(index, 1);
}).error(function (error) {
raiseAlert('danger', error.title, error.detail);
});
}
/** /**
* This will open the modal that will allow a product version * This will open the modal that will allow a product version
* to be managed. * to be managed.

View File

@ -133,7 +133,8 @@
*/ */
function isEditingAllowed() { function isEditingAllowed() {
return Boolean(ctrl.resultsData && return Boolean(ctrl.resultsData &&
ctrl.resultsData.user_role === 'owner'); (ctrl.resultsData.user_role === 'owner' ||
ctrl.resultsData.user_role == 'foundation'));
} }
/** /**
* This tells you whether the current results are shared with the * This tells you whether the current results are shared with the

View File

@ -1107,6 +1107,9 @@ describe('Refstack controllers', function () {
'cpid': null, 'cpid': null,
'version': '1.0', 'version': '1.0',
'product_id': '1234'}]; 'product_id': '1234'}];
var fakeTestsResp = {'pagination': {'current_page': 1,
'total_pages': 1},
'results':[{'id': 'foo-test'}]};
var fakeVendorResp = {'id': 'fake-org-id', var fakeVendorResp = {'id': 'fake-org-id',
'type': 3, 'type': 3,
'can_manage': true, 'can_manage': true,
@ -1134,6 +1137,8 @@ describe('Refstack controllers', function () {
'/products/1234').respond(fakeProdResp); '/products/1234').respond(fakeProdResp);
$httpBackend.when('GET', fakeApiUrl + $httpBackend.when('GET', fakeApiUrl +
'/products/1234/versions').respond(fakeVersionResp); '/products/1234/versions').respond(fakeVersionResp);
$httpBackend.when('GET', fakeApiUrl +
'/results?page=1&product_id=1234').respond(fakeTestsResp);
$httpBackend.when('GET', fakeApiUrl + $httpBackend.when('GET', fakeApiUrl +
'/vendors/fake-org-id').respond(fakeVendorResp); '/vendors/fake-org-id').respond(fakeVendorResp);
})); }));
@ -1189,6 +1194,26 @@ describe('Refstack controllers', function () {
$httpBackend.flush(); $httpBackend.flush();
}); });
it('should have a function to get tests on a product',
function () {
ctrl.getProductTests();
$httpBackend.flush();
expect(ctrl.testsData).toEqual(fakeTestsResp.results);
expect(ctrl.currentPage).toEqual(1);
});
it('should have a function to unassociate a test from a product',
function () {
ctrl.testsData = [{'id': 'foo-test'}];
$httpBackend.expectPUT(
fakeApiUrl + '/results/foo-test',
{product_version_id: null})
.respond(200, {'id': 'foo-test'});
ctrl.unassociateTest(0);
$httpBackend.flush();
expect(ctrl.testsData).toEqual([]);
});
it('should have a function to switch the publicity of a project', it('should have a function to switch the publicity of a project',
function () { function () {
ctrl.product = {'public': true}; ctrl.product = {'public': true};

View File

@ -21,6 +21,8 @@ CPID = 'cpid'
PAGE = 'page' PAGE = 'page'
SIGNED = 'signed' SIGNED = 'signed'
VERIFICATION_STATUS = 'verification_status' VERIFICATION_STATUS = 'verification_status'
PRODUCT_ID = 'product_id'
ALL_PRODUCT_TESTS = 'all_product_tests'
OPENID = 'openid' OPENID = 'openid'
USER_PUBKEYS = 'pubkeys' USER_PUBKEYS = 'pubkeys'

View File

@ -51,7 +51,8 @@ class VersionsController(validation.BaseRestControllerWithValidation):
if not product['public'] and not is_admin: if not product['public'] and not is_admin:
pecan.abort(403, 'Forbidden.') pecan.abort(403, 'Forbidden.')
return db.get_product_versions(id) allowed_keys = ['id', 'product_id', 'version', 'cpid']
return db.get_product_versions(id, allowed_keys=allowed_keys)
@pecan.expose('json') @pecan.expose('json')
def get_one(self, id, version_id): def get_one(self, id, version_id):
@ -62,8 +63,8 @@ class VersionsController(validation.BaseRestControllerWithValidation):
api_utils.check_user_is_vendor_admin(vendor_id)) api_utils.check_user_is_vendor_admin(vendor_id))
if not product['public'] and not is_admin: if not product['public'] and not is_admin:
pecan.abort(403, 'Forbidden.') pecan.abort(403, 'Forbidden.')
allowed_keys = ['id', 'product_id', 'version', 'cpid']
return db.get_product_version(version_id) return db.get_product_version(version_id, allowed_keys=allowed_keys)
@secure(api_utils.is_authenticated) @secure(api_utils.is_authenticated)
@pecan.expose('json') @pecan.expose('json')
@ -171,19 +172,21 @@ class ProductsController(validation.BaseRestControllerWithValidation):
@pecan.expose('json') @pecan.expose('json')
def get_one(self, id): def get_one(self, id):
"""Get information about product.""" """Get information about product."""
product = db.get_product(id) allowed_keys = ['id', 'name', 'description',
'product_ref_id', 'product_type',
'public', 'properties', 'created_at', 'updated_at',
'organization_id', 'created_by_user', 'type']
product = db.get_product(id, allowed_keys=allowed_keys)
vendor_id = product['organization_id'] vendor_id = product['organization_id']
is_admin = (api_utils.check_user_is_foundation_admin() or is_admin = (api_utils.check_user_is_foundation_admin() or
api_utils.check_user_is_vendor_admin(vendor_id)) api_utils.check_user_is_vendor_admin(vendor_id))
if not is_admin and not product['public']: if not is_admin and not product['public']:
pecan.abort(403, 'Forbidden.') pecan.abort(403, 'Forbidden.')
if not is_admin: if not is_admin:
allowed_keys = ['id', 'name', 'description', 'product_ref_id', admin_only_keys = ['created_by_user', 'created_at', 'updated_at',
'type', 'product_type', 'public', 'properties']
'organization_id']
for key in product.keys(): for key in product.keys():
if key not in allowed_keys: if key in admin_only_keys:
product.pop(key) product.pop(key)
product['can_manage'] = is_admin product['can_manage'] = is_admin

View File

@ -112,7 +112,7 @@ class ResultsController(validation.BaseRestControllerWithValidation):
test_info = db.get_test( test_info = db.get_test(
test_id, allowed_keys=['id', 'cpid', 'created_at', test_id, allowed_keys=['id', 'cpid', 'created_at',
'duration_seconds', 'meta', 'duration_seconds', 'meta',
'product_version_id', 'product_version',
'verification_status'] 'verification_status']
) )
else: else:
@ -123,6 +123,12 @@ class ResultsController(validation.BaseRestControllerWithValidation):
'user_role': user_role}) 'user_role': user_role})
if user_role not in (const.ROLE_FOUNDATION, const.ROLE_OWNER): if user_role not in (const.ROLE_FOUNDATION, const.ROLE_OWNER):
# Don't expose product information if product is not public.
if (test_info.get('product_version') and
not test_info['product_version']['product_info']['public']):
test_info['product_version'] = None
test_info['meta'] = { test_info['meta'] = {
k: v for k, v in six.iteritems(test_info['meta']) k: v for k, v in six.iteritems(test_info['meta'])
if k in MetadataController.rw_access_keys if k in MetadataController.rw_access_keys
@ -176,10 +182,22 @@ class ResultsController(validation.BaseRestControllerWithValidation):
const.END_DATE, const.END_DATE,
const.CPID, const.CPID,
const.SIGNED, const.SIGNED,
const.VERIFICATION_STATUS const.VERIFICATION_STATUS,
const.PRODUCT_ID
] ]
filters = api_utils.parse_input_params(expected_input_params) filters = api_utils.parse_input_params(expected_input_params)
if const.PRODUCT_ID in filters:
product = db.get_product(filters[const.PRODUCT_ID])
vendor_id = product['organization_id']
is_admin = (api_utils.check_user_is_foundation_admin() or
api_utils.check_user_is_vendor_admin(vendor_id))
if is_admin:
filters[const.ALL_PRODUCT_TESTS] = True
elif not product['public']:
pecan.abort(403, 'Forbidden.')
records_count = db.get_test_records_count(filters) records_count = db.get_test_records_count(filters)
page_number, total_pages_number = \ page_number, total_pages_number = \
api_utils.get_page_number(records_count) api_utils.get_page_number(records_count)
@ -187,13 +205,18 @@ class ResultsController(validation.BaseRestControllerWithValidation):
try: try:
per_page = CONF.api.results_per_page per_page = CONF.api.results_per_page
results = db.get_test_records(page_number, per_page, filters) results = db.get_test_records(page_number, per_page, filters)
is_foundation = api_utils.check_user_is_foundation_admin()
for result in results: for result in results:
# Only show all metadata if the user is the owner or a member
# of the Foundation group.
if (not api_utils.check_owner(result['id']) and
not api_utils.check_user_is_foundation_admin()):
if not (api_utils.check_owner(result['id']) or is_foundation):
# Don't expose product info if the product is not public.
if (result.get('product_version') and not
result['product_version']['product_info']['public']):
result['product_version'] = None
# Only show all metadata if the user is the owner or a
# member of the Foundation group.
result['meta'] = { result['meta'] = {
k: v for k, v in six.iteritems(result['meta']) k: v for k, v in six.iteritems(result['meta'])
if k in MetadataController.rw_access_keys if k in MetadataController.rw_access_keys
@ -209,8 +232,8 @@ class ResultsController(validation.BaseRestControllerWithValidation):
}} }}
except Exception as ex: except Exception as ex:
LOG.debug('An error occurred during ' LOG.debug('An error occurred during '
'operation with database: %s' % ex) 'operation with database: %s' % str(ex))
pecan.abort(400) pecan.abort(500)
return page return page
@ -229,7 +252,8 @@ class ResultsController(validation.BaseRestControllerWithValidation):
if kw['product_version_id']: if kw['product_version_id']:
# Verify that the user is a member of the product's vendor. # Verify that the user is a member of the product's vendor.
version = db.get_product_version(kw['product_version_id']) version = db.get_product_version(kw['product_version_id'],
allowed_keys=['product_id'])
is_vendor_admin = ( is_vendor_admin = (
api_utils api_utils
.check_user_is_product_admin(version['product_id']) .check_user_is_product_admin(version['product_id'])

View File

@ -210,9 +210,9 @@ def update_product(product_info):
return IMPL.update_product(product_info) return IMPL.update_product(product_info)
def get_product(id): def get_product(id, allowed_keys=None):
"""Get product by id.""" """Get product by id."""
return IMPL.get_product(id) return IMPL.get_product(id, allowed_keys=allowed_keys)
def delete_product(id): def delete_product(id):

View File

@ -243,6 +243,13 @@ def _apply_filters_for_query(query, filters):
query = query.filter(models.Test.verification_status == query = query.filter(models.Test.verification_status ==
verification_status) verification_status)
if api_const.PRODUCT_ID in filters:
query = (query
.join(models.ProductVersion)
.filter(models.ProductVersion.product_id ==
filters[api_const.PRODUCT_ID]))
all_product_tests = filters.get(api_const.ALL_PRODUCT_TESTS)
signed = api_const.SIGNED in filters signed = api_const.SIGNED in filters
# If we only want to get the user's test results. # If we only want to get the user's test results.
if signed: if signed:
@ -251,7 +258,9 @@ def _apply_filters_for_query(query, filters):
.filter(models.TestMeta.meta_key == api_const.USER) .filter(models.TestMeta.meta_key == api_const.USER)
.filter(models.TestMeta.value == filters[api_const.OPENID]) .filter(models.TestMeta.value == filters[api_const.OPENID])
) )
else: elif not all_product_tests:
# Get all non-signed (aka anonymously uploaded) test results
# along with signed but shared test results.
signed_results = (query.session signed_results = (query.session
.query(models.TestMeta.test_id) .query(models.TestMeta.test_id)
.filter_by(meta_key=api_const.USER)) .filter_by(meta_key=api_const.USER))
@ -260,6 +269,7 @@ def _apply_filters_for_query(query, filters):
.filter_by(meta_key=api_const.SHARED_TEST_RUN)) .filter_by(meta_key=api_const.SHARED_TEST_RUN))
query = (query.filter(models.Test.id.notin_(signed_results)) query = (query.filter(models.Test.id.notin_(signed_results))
.union(query.filter(models.Test.id.in_(shared_results)))) .union(query.filter(models.Test.id.in_(shared_results))))
return query return query
@ -505,13 +515,13 @@ def update_product(product_info):
return _to_dict(product) return _to_dict(product)
def get_product(id): def get_product(id, allowed_keys=None):
"""Get product by id.""" """Get product by id."""
session = get_session() session = get_session()
product = session.query(models.Product).filter_by(id=id).first() product = session.query(models.Product).filter_by(id=id).first()
if product is None: if product is None:
raise NotFound('Product with id "%s" not found' % id) raise NotFound('Product with id "%s" not found' % id)
return _to_dict(product) return _to_dict(product, allowed_keys=allowed_keys)
def delete_product(id): def delete_product(id):
@ -653,7 +663,8 @@ def get_product_versions(product_id, allowed_keys=None):
"""Get all versions for a product.""" """Get all versions for a product."""
session = get_session() session = get_session()
version_info = ( version_info = (
session.query(models.ProductVersion).filter_by(product_id=product_id) session.query(models.ProductVersion)
.filter_by(product_id=product_id).all()
) )
return _to_dict(version_info, allowed_keys=allowed_keys) return _to_dict(version_info, allowed_keys=allowed_keys)

View File

@ -63,11 +63,12 @@ class Test(BASE, RefStackBase): # pragma: no cover
sa.ForeignKey('product_version.id'), sa.ForeignKey('product_version.id'),
nullable=True, unique=False) nullable=True, unique=False)
verification_status = sa.Column(sa.Integer, nullable=False, default=0) verification_status = sa.Column(sa.Integer, nullable=False, default=0)
product_version = orm.relationship('ProductVersion', backref='test')
@property @property
def _extra_keys(self): def _extra_keys(self):
"""Relation should be pointed directly.""" """Relation should be pointed directly."""
return ['results', 'meta'] return ['results', 'meta', 'product_version']
@property @property
def metadata_keys(self): def metadata_keys(self):
@ -79,7 +80,7 @@ class Test(BASE, RefStackBase): # pragma: no cover
def default_allowed_keys(self): def default_allowed_keys(self):
"""Default keys.""" """Default keys."""
return ('id', 'created_at', 'duration_seconds', 'meta', return ('id', 'created_at', 'duration_seconds', 'meta',
'product_version_id', 'verification_status') 'verification_status', 'product_version')
class TestResults(BASE, RefStackBase): # pragma: no cover class TestResults(BASE, RefStackBase): # pragma: no cover
@ -245,9 +246,7 @@ class Product(BASE, RefStackBase): # pragma: no cover
@property @property
def default_allowed_keys(self): def default_allowed_keys(self):
"""Default keys.""" """Default keys."""
return ('id', 'name', 'description', 'product_ref_id', 'product_type', return ('id', 'name', 'organization_id', 'public')
'public', 'properties', 'created_at', 'updated_at',
'organization_id', 'created_by_user', 'type')
class ProductVersion(BASE, RefStackBase): class ProductVersion(BASE, RefStackBase):
@ -266,8 +265,14 @@ class ProductVersion(BASE, RefStackBase):
cpid = sa.Column(sa.String(36), nullable=True) cpid = sa.Column(sa.String(36), nullable=True)
created_by_user = sa.Column(sa.String(128), sa.ForeignKey('user.openid'), created_by_user = sa.Column(sa.String(128), sa.ForeignKey('user.openid'),
nullable=False) nullable=False)
product_info = orm.relationship('Product', backref='product_version')
@property
def _extra_keys(self):
"""Relation should be pointed directly."""
return ['product_info']
@property @property
def default_allowed_keys(self): def default_allowed_keys(self):
"""Default keys.""" """Default keys."""
return ('id', 'product_id', 'version', 'cpid') return ('id', 'version', 'cpid', 'product_info')

View File

@ -135,6 +135,17 @@ class TestProductsEndpoint(api.FunctionalTest):
self.get_json, self.get_json,
self.URL + post_response.get('id')) self.URL + post_response.get('id'))
mock_get_user.return_value = 'foo-open-id'
# Make product public.
product_info = {'id': post_response.get('id'), 'public': 1}
db.update_product(product_info)
# Test when getting product info when not owner/foundation.
get_response = self.get_json(self.URL + post_response.get('id'))
self.assertNotIn('created_by_user', get_response)
self.assertNotIn('created_at', get_response)
self.assertNotIn('updated_at', get_response)
@mock.patch('refstack.api.utils.get_user_id', return_value='test-open-id') @mock.patch('refstack.api.utils.get_user_id', return_value='test-open-id')
def test_delete(self, mock_get_user): def test_delete(self, mock_get_user):
"""Test delete request.""" """Test delete request."""
@ -182,7 +193,7 @@ class TestProductsEndpoint(api.FunctionalTest):
class TestProductVersionEndpoint(api.FunctionalTest): class TestProductVersionEndpoint(api.FunctionalTest):
"""Test case for the 'product/<product_id>/version' API endpoint.""" """Test case for the 'products/<product_id>/version' API endpoint."""
def setUp(self): def setUp(self):
super(TestProductVersionEndpoint, self).setUp() super(TestProductVersionEndpoint, self).setUp()
@ -218,7 +229,7 @@ class TestProductVersionEndpoint(api.FunctionalTest):
response = self.get_json(self.URL) response = self.get_json(self.URL)
self.assertEqual(2, len(response)) self.assertEqual(2, len(response))
self.assertEqual(post_response, response[1]) self.assertEqual(post_response['version'], response[1]['version'])
def test_get_one(self): def test_get_one(self):
""""Test get a specific version.""" """"Test get a specific version."""
@ -228,7 +239,7 @@ class TestProductVersionEndpoint(api.FunctionalTest):
version_id = post_response['id'] version_id = post_response['id']
response = self.get_json(self.URL + version_id) response = self.get_json(self.URL + version_id)
self.assertEqual(post_response, response) self.assertEqual(post_response['version'], response['version'])
# Test nonexistent version. # Test nonexistent version.
self.assertRaises(webtest.app.AppError, self.get_json, self.assertRaises(webtest.app.AppError, self.get_json,
@ -238,14 +249,17 @@ class TestProductVersionEndpoint(api.FunctionalTest):
"""Test creating a product version.""" """Test creating a product version."""
version = {'cpid': '123', 'version': '5.0'} version = {'cpid': '123', 'version': '5.0'}
post_response = self.post_json(self.URL, params=json.dumps(version)) post_response = self.post_json(self.URL, params=json.dumps(version))
self.assertEqual(version['cpid'], post_response['cpid'])
self.assertEqual(version['version'], post_response['version']) get_response = self.get_json(self.URL + post_response['id'])
self.assertEqual(self.product_id, post_response['product_id']) self.assertEqual(version['cpid'], get_response['cpid'])
self.assertIn('id', post_response) self.assertEqual(version['version'], get_response['version'])
self.assertEqual(self.product_id, get_response['product_id'])
self.assertIn('id', get_response)
# Test 'version' not in response body. # Test 'version' not in response body.
self.assertRaises(webtest.app.AppError, self.get_json, response = self.post_json(self.URL, expect_errors=True,
self.URL + '/sdsdsds') params=json.dumps({'cpid': '123'}))
self.assertEqual(400, response.status_code)
def test_put(self): def test_put(self):
"""Test updating a product version.""" """Test updating a product version."""

View File

@ -119,13 +119,13 @@ class TestResultsEndpoint(api.FunctionalTest):
self.put_json(url, params=json.dumps(body)) self.put_json(url, params=json.dumps(body))
get_response = self.get_json(url) get_response = self.get_json(url)
self.assertEqual(version_response['id'], self.assertEqual(version_response['id'],
get_response['product_version_id']) get_response['product_version']['id'])
# Test when product_version_id is None. # Test when product_version_id is None.
body = {'product_version_id': None} body = {'product_version_id': None}
self.put_json(url, params=json.dumps(body)) self.put_json(url, params=json.dumps(body))
get_response = self.get_json(url) get_response = self.get_json(url)
self.assertIsNone(get_response['product_version_id']) self.assertIsNone(get_response['product_version'])
# Test when test verification preconditions are not met. # Test when test verification preconditions are not met.
body = {'verification_status': api_const.TEST_VERIFIED} body = {'verification_status': api_const.TEST_VERIFIED}
@ -167,7 +167,7 @@ class TestResultsEndpoint(api.FunctionalTest):
self.put_json(url, params=json.dumps(body)) self.put_json(url, params=json.dumps(body))
get_response = self.get_json(url) get_response = self.get_json(url)
self.assertEqual(version_response['id'], self.assertEqual(version_response['id'],
get_response['product_version_id']) get_response['product_version']['id'])
# Test non-Foundation user can't change verification_status. # Test non-Foundation user can't change verification_status.
body = {'verification_status': 1} body = {'verification_status': 1}
@ -328,6 +328,67 @@ class TestResultsEndpoint(api.FunctionalTest):
filtering_results = self.get_json(url) filtering_results = self.get_json(url)
self.assertEqual([], filtering_results['results']) self.assertEqual([], filtering_results['results'])
@mock.patch('refstack.api.utils.get_user_id')
def test_get_with_product_id(self, mock_get_user):
user_info = {
'openid': 'test-open-id',
'email': 'foo@bar.com',
'fullname': 'Foo Bar'
}
db.user_save(user_info)
mock_get_user.return_value = 'test-open-id'
fake_product = {
'name': 'product name',
'description': 'product description',
'product_type': api_const.CLOUD,
}
product = json.dumps(fake_product)
response = self.post_json('/v1/products/', params=product)
product_id = response['id']
# Create a version.
version_url = '/v1/products/' + product_id + '/versions'
version = {'cpid': '123', 'version': '6.0'}
post_response = self.post_json(version_url, params=json.dumps(version))
version_id = post_response['id']
# Create a test and associate it to the product version and user.
results = json.dumps(FAKE_TESTS_RESULT)
post_response = self.post_json('/v1/results', params=results)
test_id = post_response['test_id']
test_info = {'id': test_id, 'product_version_id': version_id}
db.update_test(test_info)
db.save_test_meta_item(test_id, api_const.USER, 'test-open-id')
url = self.URL + '?page=1&product_id=' + product_id
# Test GET.
response = self.get_json(url)
self.assertEqual(1, len(response['results']))
self.assertEqual(test_id, response['results'][0]['id'])
# Test unauthorized.
mock_get_user.return_value = 'test-foo-id'
response = self.get_json(url, expect_errors=True)
self.assertEqual(403, response.status_code)
# Make product public.
product_info = {'id': product_id, 'public': 1}
db.update_product(product_info)
# Test result is not shared yet, so no tests should return.
response = self.get_json(url)
self.assertFalse(response['results'])
# Share the test run.
db.save_test_meta_item(test_id, api_const.SHARED_TEST_RUN, 1)
response = self.get_json(url)
self.assertEqual(1, len(response['results']))
self.assertEqual(test_id, response['results'][0]['id'])
@mock.patch('refstack.api.utils.check_owner') @mock.patch('refstack.api.utils.check_owner')
def test_delete(self, mock_check_owner): def test_delete(self, mock_check_owner):
results = json.dumps(FAKE_TESTS_RESULT) results = json.dumps(FAKE_TESTS_RESULT)

View File

@ -141,7 +141,7 @@ class ResultsControllerTestCase(BaseControllerTestCase):
mock_get_test.assert_called_once_with( mock_get_test.assert_called_once_with(
'fake_arg', allowed_keys=['id', 'cpid', 'created_at', 'fake_arg', allowed_keys=['id', 'cpid', 'created_at',
'duration_seconds', 'meta', 'duration_seconds', 'meta',
'product_version_id', 'product_version',
'verification_status'] 'verification_status']
) )
@ -251,6 +251,7 @@ class ResultsControllerTestCase(BaseControllerTestCase):
const.CPID, const.CPID,
const.SIGNED, const.SIGNED,
const.VERIFICATION_STATUS, const.VERIFICATION_STATUS,
const.PRODUCT_ID
] ]
page_number = 1 page_number = 1
total_pages_number = 10 total_pages_number = 10

View File

@ -769,7 +769,7 @@ class DBBackendTestCase(base.BaseTestCase):
@mock.patch.object(api, 'get_session', @mock.patch.object(api, 'get_session',
return_value=mock.Mock(name='session'),) return_value=mock.Mock(name='session'),)
@mock.patch('refstack.db.sqlalchemy.models.Product') @mock.patch('refstack.db.sqlalchemy.models.Product')
@mock.patch.object(api, '_to_dict', side_effect=lambda x: x) @mock.patch.object(api, '_to_dict', side_effect=lambda x, allowed_keys: x)
def test_product_get(self, mock_to_dict, mock_model, mock_get_session): def test_product_get(self, mock_to_dict, mock_model, mock_get_session):
_id = 12345 _id = 12345
session = mock_get_session.return_value session = mock_get_session.return_value