Implement category management API
Adds new API calls, responsible for add, browse and delete categories. Implements blueprint enable-category-management Change-Id: I9da0680cfa244ef225be0706a54f492644c0dcba
This commit is contained in:
parent
42c320a085
commit
2c23f73e72
@ -25,7 +25,8 @@ General information
|
|||||||
Murano Service API is a programmatic interface used for interaction with
|
Murano Service API is a programmatic interface used for interaction with
|
||||||
Murano. Other interaction mechanisms like Murano Dashboard or Murano CLI
|
Murano. Other interaction mechanisms like Murano Dashboard or Murano CLI
|
||||||
should use API as underlying protocol for interaction.
|
should use API as underlying protocol for interaction.
|
||||||
* **Allowed HTTPs requests**
|
|
||||||
|
* **Allowed HTTPs requests**
|
||||||
|
|
||||||
* *POST* : To create a resource
|
* *POST* : To create a resource
|
||||||
* *GET* : Get a resource or list of resources
|
* *GET* : Get a resource or list of resources
|
||||||
|
@ -235,6 +235,10 @@ See the full specification `here <http://tools.ietf.org/html/rfc6902>`_.
|
|||||||
|
|
||||||
``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
|
``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||||
|
|
||||||
|
**Content type**
|
||||||
|
|
||||||
|
application/murano-packages-json-patch
|
||||||
|
|
||||||
Allowed operations:
|
Allowed operations:
|
||||||
|
|
||||||
::
|
::
|
||||||
@ -305,7 +309,7 @@ Delete application definition from the catalog
|
|||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
``id`` (required) Hexadecimal `id` (or fully qualified name) of the package to delete
|
* ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package to delete
|
||||||
|
|
||||||
**Response 404**
|
**Response 404**
|
||||||
|
|
||||||
@ -321,7 +325,7 @@ Get application definition package
|
|||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
* id (required) Hexadecimal `id` (or fully qualified name) of the package
|
* ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||||
|
|
||||||
**Response 200 (application/octetstream)**
|
**Response 200 (application/octetstream)**
|
||||||
|
|
||||||
@ -341,7 +345,7 @@ Retrieve UI definition for a application which described in a package with provi
|
|||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
* id (required) Hexadecimal `id` (or fully qualified name) of the package
|
* ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||||
|
|
||||||
**Response 200 (application/octet-stream)**
|
**Response 200 (application/octet-stream)**
|
||||||
|
|
||||||
@ -369,7 +373,7 @@ Retrieve application logo which described in a package with provided id
|
|||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
id (required) Hexadecimal `id` (or fully qualified name) of the package
|
``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||||
|
|
||||||
**Response 200 (application/octet-stream)**
|
**Response 200 (application/octet-stream)**
|
||||||
|
|
||||||
@ -377,26 +381,208 @@ The sequence of bytes representing application logo
|
|||||||
|
|
||||||
**Response 403**
|
**Response 403**
|
||||||
|
|
||||||
Specified package is not public and not owned by user tenant, performing the request
|
Specified package is not public and not owned by user tenant,
|
||||||
|
performing the request
|
||||||
|
|
||||||
**Response 404**
|
**Response 404**
|
||||||
|
|
||||||
Specified package is not public and not owned by user tenant, performing the request
|
Specified package is not public and not owned by user tenant,
|
||||||
|
performing the request
|
||||||
|
|
||||||
Categories
|
Categories
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
Provides category management. Categories are used in the Application Catalog
|
||||||
|
to group application for easy browsing and search.
|
||||||
|
|
||||||
List categories
|
List categories
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
`/v1/catalog/packages/categories [GET]`
|
* `/v1/catalog/packages/categories [GET]`
|
||||||
|
|
||||||
Retrieve list of all available application categories
|
!DEPRECATED (Plan to remove in L release) Retrieve list of all available application categories
|
||||||
|
|
||||||
**Response 200 (application/json)**
|
**Response 200 (application/json)**
|
||||||
|
|
||||||
::
|
A list, containing category names
|
||||||
|
|
||||||
|
*Content-Type*
|
||||||
|
application/json
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
{
|
{
|
||||||
"categories": ["Web service", "Directory", "Database", "Storage"]
|
"categories": ["Web service", "Directory", "Database", "Storage"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
* `/v1/catalog/categories [GET]`
|
||||||
|
|
||||||
|
+----------+----------------------------------+----------------------------------+
|
||||||
|
| Method | URI | Description |
|
||||||
|
+==========+==================================+==================================+
|
||||||
|
| GET | /catalog/categories | Get list of existing categories |
|
||||||
|
+----------+----------------------------------+----------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
Retrieve list of all available application categories
|
||||||
|
|
||||||
|
**Response 200 (application/json)**
|
||||||
|
|
||||||
|
A list, containing detailed information about each category
|
||||||
|
|
||||||
|
*Content-Type*
|
||||||
|
application/json
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{"categories": [
|
||||||
|
{
|
||||||
|
"id": "0420045dce7445fabae7e5e61fff9e2f",
|
||||||
|
"updated": "2014-12-26T13:57:04",
|
||||||
|
"name": "Web",
|
||||||
|
"created": "2014-12-26T13:57:04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3dd486b1e26f40ac8f35416b63f52042",
|
||||||
|
"updated": "2014-12-26T13:57:04",
|
||||||
|
"name": "Databases",
|
||||||
|
"created": "2014-12-26T13:57:04"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Get category details
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
`/catalog/categories/<category_id> [GET]`
|
||||||
|
|
||||||
|
Return detailed information for a provided category
|
||||||
|
|
||||||
|
*Request*
|
||||||
|
|
||||||
|
+----------+-----------------------------------+----------------------------------+
|
||||||
|
| Method | URI | Description |
|
||||||
|
+==========+===================================+==================================+
|
||||||
|
| GET | /catalog/categories/<category_id> | Get category detail |
|
||||||
|
+----------+-----------------------------------+----------------------------------+
|
||||||
|
|
||||||
|
*Parameters*
|
||||||
|
|
||||||
|
* ``category_id`` - required, category ID, required
|
||||||
|
|
||||||
|
*Response*
|
||||||
|
|
||||||
|
*Content-Type*
|
||||||
|
application/json
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "b308f7fa8a2f4a5eb419970c827f4466",
|
||||||
|
"updated": "2015-01-28T17:00:19",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"fully_qualified_name": "io.murano.apps.ZabbixServer",
|
||||||
|
"id": "4dfb566e69e6445fbd4aea5099fe95e9",
|
||||||
|
"name": "Zabbix Server"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Web",
|
||||||
|
"created": "2015-01-28T17:00:19"
|
||||||
|
}
|
||||||
|
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
| Code | Description |
|
||||||
|
+================+===========================================================+
|
||||||
|
| 200 | OK. Category deleted successfully |
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
| 401 | User is not authorized to access this session |
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
| 404 | Not found. Specified category doesn`t exist |
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
|
||||||
|
Add new category
|
||||||
|
----------------
|
||||||
|
|
||||||
|
`/catalog/categories [POST]`
|
||||||
|
|
||||||
|
Add new category to the Application Catalog
|
||||||
|
|
||||||
|
*Parameters*
|
||||||
|
|
||||||
|
+----------------------+------------+--------------------------------------------------------+
|
||||||
|
| Attribute | Type | Description |
|
||||||
|
+======================+============+========================================================+
|
||||||
|
| name | string | Environment name; only alphanumeric characters and '-' |
|
||||||
|
+----------------------+------------+--------------------------------------------------------+
|
||||||
|
|
||||||
|
*Request*
|
||||||
|
|
||||||
|
+----------+----------------------------------+----------------------------------+
|
||||||
|
| Method | URI | Description |
|
||||||
|
+==========+==================================+==================================+
|
||||||
|
| POST | /catalog/categories | Create new category |
|
||||||
|
+----------+----------------------------------+----------------------------------+
|
||||||
|
|
||||||
|
*Content-Type*
|
||||||
|
application/json
|
||||||
|
|
||||||
|
*Example*
|
||||||
|
{"name": "category_name"}
|
||||||
|
|
||||||
|
*Response*
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "ce373a477f211e187a55404a662f968",
|
||||||
|
"name": "category_name",
|
||||||
|
"created": "2013-11-30T03:23:42Z",
|
||||||
|
"updated": "2013-11-30T03:23:44Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
| Code | Description |
|
||||||
|
+================+===========================================================+
|
||||||
|
| 200 | OK. Category created successfully |
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
| 401 | User is not authorized to access this session |
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
| 409 | Conflict. Category with specified name already exist |
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
Delete category
|
||||||
|
---------------
|
||||||
|
|
||||||
|
`/catalog/categories [DELETE]`
|
||||||
|
|
||||||
|
*Request*
|
||||||
|
|
||||||
|
+----------+-----------------------------------+-----------------------------------+
|
||||||
|
| Method | URI | Description |
|
||||||
|
+==========+===================================+===================================+
|
||||||
|
| DELETE | /catalog/categories/<category_id> | Delete category with specified id |
|
||||||
|
+----------+-----------------------------------+-----------------------------------+
|
||||||
|
|
||||||
|
*Parameters:*
|
||||||
|
|
||||||
|
* ``category_id`` - required, category ID, required
|
||||||
|
|
||||||
|
*Response*
|
||||||
|
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
| Code | Description |
|
||||||
|
+================+===========================================================+
|
||||||
|
| 200 | OK. Category deleted successfully |
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
| 401 | User is not authorized to access this session |
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
| 404 | Not found. Specified category doesn`t exist |
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
| 403 | Forbidden. Category with specified name is assigned to |
|
||||||
|
| | the package, presented in the catalog |
|
||||||
|
+----------------+-----------------------------------------------------------+
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
"update_package": "rule:admin_api",
|
"update_package": "rule:admin_api",
|
||||||
"upload_package": "rule:admin_api",
|
"upload_package": "rule:admin_api",
|
||||||
"delete_package": "rule:admin_api"
|
"delete_package": "rule:admin_api",
|
||||||
|
"delete_category": "rule:admin_api",
|
||||||
|
"add_category": "rule:admin_api"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,12 +268,45 @@ class Controller(object):
|
|||||||
|
|
||||||
db_api.package_delete(package_id, req.context)
|
db_api.package_delete(package_id, req.context)
|
||||||
|
|
||||||
|
def get_category(self, req, category_id):
|
||||||
|
policy.check("get_category", req.context)
|
||||||
|
category = db_api.category_get(category_id, packages=True)
|
||||||
|
return category.to_dict()
|
||||||
|
|
||||||
def show_categories(self, req):
|
def show_categories(self, req):
|
||||||
policy.check("show_categories", req.context)
|
policy.check("show_categories", req.context)
|
||||||
|
|
||||||
categories = db_api.categories_list()
|
categories = db_api.categories_list()
|
||||||
return {'categories': [category.name for category in categories]}
|
return {'categories': [category.name for category in categories]}
|
||||||
|
|
||||||
|
def list_categories(self, req):
|
||||||
|
policy.check("list_categories", req.context)
|
||||||
|
categories = db_api.categories_list()
|
||||||
|
return {'categories': [category.to_dict() for category in categories]}
|
||||||
|
|
||||||
|
def add_category(self, req, body=None):
|
||||||
|
policy.check("add_category", req.context)
|
||||||
|
|
||||||
|
if not body.get('name'):
|
||||||
|
raise exc.HTTPBadRequest(
|
||||||
|
explanation='Please, specify a name of the category to create')
|
||||||
|
try:
|
||||||
|
category = db_api.category_add(body['name'])
|
||||||
|
except db_exc.DBDuplicateEntry:
|
||||||
|
msg = _('Category with specified name is already exist')
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exc.HTTPConflict(explanation=msg)
|
||||||
|
return category.to_dict()
|
||||||
|
|
||||||
|
def delete_category(self, req, category_id):
|
||||||
|
target = {'category_id': category_id}
|
||||||
|
policy.check("delete_category", req.context, target)
|
||||||
|
category = db_api.category_get(category_id, packages=True)
|
||||||
|
if category.packages:
|
||||||
|
msg = _("It's impossible to delete categories assigned"
|
||||||
|
" to the package, uploaded to the catalog")
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
|
db_api.category_delete(category_id)
|
||||||
|
|
||||||
|
|
||||||
class PackageSerializer(wsgi.ResponseSerializer):
|
class PackageSerializer(wsgi.ResponseSerializer):
|
||||||
def serialize(self, action_result, accept, action):
|
def serialize(self, action_result, accept, action):
|
||||||
|
@ -235,7 +235,22 @@ class API(wsgi.Router):
|
|||||||
controller=catalog_resource,
|
controller=catalog_resource,
|
||||||
action='download',
|
action='download',
|
||||||
conditions={'method': ['GET']})
|
conditions={'method': ['GET']})
|
||||||
|
mapper.connect('/catalog/categories',
|
||||||
|
controller=catalog_resource,
|
||||||
|
action='list_categories',
|
||||||
|
conditions={'method': ['GET']})
|
||||||
|
mapper.connect('/catalog/categories/{category_id}',
|
||||||
|
controller=catalog_resource,
|
||||||
|
action='get_category',
|
||||||
|
conditions={'method': ['GET']})
|
||||||
|
mapper.connect('/catalog/categories',
|
||||||
|
controller=catalog_resource,
|
||||||
|
action='add_category',
|
||||||
|
conditions={'method': ['POST']})
|
||||||
|
mapper.connect('/catalog/categories/{category_id}',
|
||||||
|
controller=catalog_resource,
|
||||||
|
action='delete_category',
|
||||||
|
conditions={'method': ['DELETE']})
|
||||||
req_stats_resource = request_statistics.create_resource()
|
req_stats_resource = request_statistics.create_resource()
|
||||||
mapper.connect('/stats',
|
mapper.connect('/stats',
|
||||||
controller=req_stats_resource,
|
controller=req_stats_resource,
|
||||||
|
@ -200,6 +200,27 @@ def _do_remove(package, change):
|
|||||||
return package
|
return package
|
||||||
|
|
||||||
|
|
||||||
|
def _get_packages_for_category(session, category_id):
|
||||||
|
"""Return detailed list of packages, belonging to the provided category
|
||||||
|
:param session:
|
||||||
|
:param category_id:
|
||||||
|
:return: list of dictionaries, containing id, name and package frn
|
||||||
|
"""
|
||||||
|
pkg = models.Package
|
||||||
|
packages = (session.query(pkg.id, pkg.name, pkg.fully_qualified_name)
|
||||||
|
.filter(pkg.categories
|
||||||
|
.any(models.Category.id == category_id))
|
||||||
|
.all())
|
||||||
|
|
||||||
|
result_packages = []
|
||||||
|
for package in packages:
|
||||||
|
id, name, fqn = package
|
||||||
|
result_packages.append({'id': id,
|
||||||
|
'name': name,
|
||||||
|
'fully_qualified_name': fqn})
|
||||||
|
return result_packages
|
||||||
|
|
||||||
|
|
||||||
def package_update(pkg_id_or_name, changes, context):
|
def package_update(pkg_id_or_name, changes, context):
|
||||||
"""Update package information
|
"""Update package information
|
||||||
:param changes: parameters to update
|
:param changes: parameters to update
|
||||||
@ -357,6 +378,24 @@ def package_delete(package_id_or_name, context):
|
|||||||
session.delete(package)
|
session.delete(package)
|
||||||
|
|
||||||
|
|
||||||
|
def category_get(category_id, session=None, packages=False):
|
||||||
|
"""Return category details
|
||||||
|
:param category_id: ID of a category, string
|
||||||
|
:returns: detailed information about category, dict
|
||||||
|
"""
|
||||||
|
if not session:
|
||||||
|
session = db_session.get_session()
|
||||||
|
|
||||||
|
category = session.query(models.Category).get(category_id)
|
||||||
|
if not category:
|
||||||
|
msg = _("Category id or '{0}' not found").format(category_id)
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exc.HTTPNotFound(msg)
|
||||||
|
if packages:
|
||||||
|
category.packages = _get_packages_for_category(session, category_id)
|
||||||
|
return category
|
||||||
|
|
||||||
|
|
||||||
def categories_list():
|
def categories_list():
|
||||||
session = db_session.get_session()
|
session = db_session.get_session()
|
||||||
return session.query(models.Category).all()
|
return session.query(models.Category).all()
|
||||||
@ -380,3 +419,16 @@ def category_add(category_name):
|
|||||||
category.save(session)
|
category.save(session)
|
||||||
|
|
||||||
return category
|
return category
|
||||||
|
|
||||||
|
|
||||||
|
def category_delete(category_id):
|
||||||
|
"""Delete a category by ID."""
|
||||||
|
session = db_session.get_session()
|
||||||
|
|
||||||
|
with session.begin():
|
||||||
|
category = session.query(models.Category).get(category_id)
|
||||||
|
if not category:
|
||||||
|
msg = _("Category id '{0}' not found").format(category_id)
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exc.HTTPNotFound(msg)
|
||||||
|
session.delete(category)
|
||||||
|
@ -285,6 +285,10 @@ class Category(Base, TimestampMixin):
|
|||||||
default=uuidutils.generate_uuid)
|
default=uuidutils.generate_uuid)
|
||||||
name = sa.Column(sa.String(80), nullable=False, index=True, unique=True)
|
name = sa.Column(sa.String(80), nullable=False, index=True, unique=True)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
dictionary = super(Category, self).to_dict()
|
||||||
|
return dictionary
|
||||||
|
|
||||||
|
|
||||||
class Tag(Base, TimestampMixin):
|
class Tag(Base, TimestampMixin):
|
||||||
"""Represents tags in the datastore."""
|
"""Represents tags in the datastore."""
|
||||||
|
@ -16,15 +16,19 @@
|
|||||||
import cgi
|
import cgi
|
||||||
import cStringIO
|
import cStringIO
|
||||||
import imghdr
|
import imghdr
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
from oslo.utils import timeutils
|
||||||
|
|
||||||
from murano.api.v1 import catalog
|
from murano.api.v1 import catalog
|
||||||
from murano.common import policy
|
from murano.common import policy
|
||||||
from murano.db.catalog import api as db_catalog_api
|
from murano.db.catalog import api as db_catalog_api
|
||||||
|
from murano.db import models
|
||||||
from murano.packages import load_utils
|
from murano.packages import load_utils
|
||||||
import murano.tests.unit.api.base as test_base
|
import murano.tests.unit.api.base as test_base
|
||||||
|
import murano.tests.unit.utils as test_utils
|
||||||
|
|
||||||
|
|
||||||
class TestCatalogApi(test_base.ControllerTest, test_base.MuranoApiTestCase):
|
class TestCatalogApi(test_base.ControllerTest, test_base.MuranoApiTestCase):
|
||||||
@ -132,3 +136,64 @@ Content-Type: application/json
|
|||||||
content_type='multipart/form-data; ; boundary=BOUNDARY',
|
content_type='multipart/form-data; ; boundary=BOUNDARY',
|
||||||
params={"is_public": "true"})
|
params={"is_public": "true"})
|
||||||
res = req.get_response(self.api)
|
res = req.get_response(self.api)
|
||||||
|
self.assertEqual(403, res.status_code)
|
||||||
|
|
||||||
|
def test_add_category(self):
|
||||||
|
"""Check that category added successfully
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._set_policy_rules({'add_category': '@'})
|
||||||
|
self.expect_policy_check('add_category')
|
||||||
|
|
||||||
|
fake_now = timeutils.utcnow()
|
||||||
|
timeutils.utcnow.override_time = fake_now
|
||||||
|
|
||||||
|
expected = {'name': 'new_category',
|
||||||
|
'created': timeutils.isotime(fake_now)[:-1],
|
||||||
|
'updated': timeutils.isotime(fake_now)[:-1]}
|
||||||
|
|
||||||
|
body = {'name': 'new_category'}
|
||||||
|
req = self._post('/catalog/categories', json.dumps(body))
|
||||||
|
result = req.get_response(self.api)
|
||||||
|
processed_result = json.loads(result.body)
|
||||||
|
self.assertIn('id', processed_result.keys())
|
||||||
|
expected['id'] = processed_result['id']
|
||||||
|
self.assertDictEqual(expected, processed_result)
|
||||||
|
|
||||||
|
def test_delete_category(self):
|
||||||
|
"""Check that category deleted successfully
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._set_policy_rules({'delete_category': '@'})
|
||||||
|
self.expect_policy_check('delete_category',
|
||||||
|
{'category_id': '12345'})
|
||||||
|
|
||||||
|
fake_now = timeutils.utcnow()
|
||||||
|
expected = {'name': 'new_category',
|
||||||
|
'created': fake_now,
|
||||||
|
'updated': fake_now,
|
||||||
|
'id': '12345'}
|
||||||
|
|
||||||
|
e = models.Category(**expected)
|
||||||
|
test_utils.save_models(e)
|
||||||
|
|
||||||
|
req = self._delete('/catalog/categories/12345')
|
||||||
|
processed_result = req.get_response(self.api)
|
||||||
|
self.assertEqual('', processed_result.body)
|
||||||
|
self.assertEqual(200, processed_result.status_code)
|
||||||
|
|
||||||
|
def test_add_category_failed_for_non_admin(self):
|
||||||
|
"""Check that non admin user couldn't add new category
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._set_policy_rules({'add_category': 'role:context_admin'})
|
||||||
|
self.is_admin = False
|
||||||
|
self.expect_policy_check('add_category')
|
||||||
|
|
||||||
|
fake_now = timeutils.utcnow()
|
||||||
|
timeutils.utcnow.override_time = fake_now
|
||||||
|
|
||||||
|
body = {'name': 'new_category'}
|
||||||
|
req = self._post('/catalog/categories', json.dumps(body))
|
||||||
|
result = req.get_response(self.api)
|
||||||
|
self.assertEqual(403, result.status_code)
|
||||||
|
Loading…
Reference in New Issue
Block a user