Merged in issue-30-site-management (pull request #24)
Issue 30 site management * Initial steps in testing setup of site. * Build out initial tests for accessing a site. * Update view routes for retrieving the sites. * Change linkage of site components to the site. Create ability to create a test site. * Add links and self link to the site response. * Break site components in blocks and tokens. * Fix fetching of an existing site. * Add additional tests to test site access against different types of users. * Add support for site deletion. * Add support for site creation. * Build out support for site modification. * Fix issue with testing of modified sites. * Improve readability of test data and tests. Approved-by: Dorian Pula <dorian.pula@amber-penguin-software.ca>
This commit is contained in:
parent
4d689b2f3a
commit
499724732f
2
Makefile
2
Makefile
|
@ -14,7 +14,7 @@ test: build test-api test-ui
|
||||||
# Runs API tests
|
# Runs API tests
|
||||||
test-api: stop build
|
test-api: stop build
|
||||||
docker-compose run --no-deps rookeries inv test.python_style
|
docker-compose run --no-deps rookeries inv test.python_style
|
||||||
docker-compose up -d
|
docker-compose up -d db rookeries
|
||||||
docker-compose run rookeries inv db.wait
|
docker-compose run rookeries inv db.wait
|
||||||
docker-compose run rookeries inv db.init
|
docker-compose run rookeries inv db.init
|
||||||
docker-compose run rookeries inv db.upgrade
|
docker-compose run rookeries inv db.upgrade
|
||||||
|
|
|
@ -16,7 +16,6 @@ services:
|
||||||
testing:
|
testing:
|
||||||
image: selenium/standalone-firefox-debug
|
image: selenium/standalone-firefox-debug
|
||||||
ports:
|
ports:
|
||||||
- "4444:4444"
|
|
||||||
- "5900:5900"
|
- "5900:5900"
|
||||||
|
|
||||||
db:
|
db:
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""
|
||||||
|
Models for blocks.
|
||||||
|
|
||||||
|
:copyright: Copyright 2013-2017, Dorian Pula <dorian.pula@amber-penguin-software.ca>
|
||||||
|
:license: AGPL v3+
|
||||||
|
"""
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
|
from rookeries.database import db
|
||||||
|
|
||||||
|
|
||||||
|
class SupportedBlockTypes(enum.Enum):
|
||||||
|
MENU = 'menu'
|
||||||
|
MARKDOWN = 'markdown'
|
||||||
|
|
||||||
|
|
||||||
|
class Block(db.Model):
|
||||||
|
__tablename__ = 'block'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String)
|
||||||
|
site_id = db.Column(db.Integer, db.ForeignKey('site.id'))
|
||||||
|
site = db.relationship('Site')
|
||||||
|
|
||||||
|
type_ = db.Column(db.Enum(SupportedBlockTypes))
|
||||||
|
function_ = db.Column(db.String) # TODO: Figure out how to declare function of a block... menu, header, etc.
|
||||||
|
|
||||||
|
# TODO: Add in to_json of implementing type...
|
||||||
|
|
||||||
|
|
||||||
|
class MenuBlock(db.Model):
|
||||||
|
__tablename__ = 'menu_blocks'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
orderinal = db.Column(db.Integer)
|
||||||
|
title = db.Column(db.String)
|
||||||
|
|
||||||
|
page_id = db.Column(db.Integer, db.ForeignKey('page.id'))
|
||||||
|
page = db.relationship('Page')
|
||||||
|
|
||||||
|
def __init__(self, title, site, page, orderinal=0):
|
||||||
|
self.title = title
|
||||||
|
self.site = site
|
||||||
|
self.page = page
|
||||||
|
self.orderinal = orderinal
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'<Menu {self.orderinal} - {self.title}>'
|
||||||
|
|
||||||
|
def to_json(self) -> dict:
|
||||||
|
return {
|
||||||
|
'title': self.title,
|
||||||
|
'orderinal': self.orderinal,
|
||||||
|
'page': self.page.slug,
|
||||||
|
# TODO: Improve this into something more regular.
|
||||||
|
'url': '/' + self.page.slug,
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
"""
|
||||||
|
Views for managing blocks of content.
|
||||||
|
|
||||||
|
:copyright: Copyright 2013-2017, Dorian Pula <dorian.pula@amber-penguin-software.ca>
|
||||||
|
:license: AGPL v3+
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import flask
|
||||||
|
|
||||||
|
from rookeries.main import rookeries_app
|
||||||
|
from rookeries.blocks import models
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# TODO: Add in functionality to create and update menus.
|
||||||
|
|
||||||
|
|
||||||
|
def add_self_link(block_json):
|
||||||
|
block_json['urls']['self'] = flask.url_for('rookeries_app.get_site', site_name=block_json['name'], _external=True)
|
||||||
|
return block_json
|
||||||
|
|
||||||
|
|
||||||
|
@rookeries_app.route('/api/sites/<site_name>/menu', methods=['GET'])
|
||||||
|
def menu_config(site_name):
|
||||||
|
# TODO: Migrate to a component/block instead of being part of the site directly.
|
||||||
|
site = models.Block.query.filter_by(name=site_name).first_or_404()
|
||||||
|
# TODO: Then figure out either menu setup... or how to generate blocks of content in a generic fashion.
|
||||||
|
nav_menu = {'menu': [menu_item.to_json() for menu_item in site.menu]}
|
||||||
|
return flask.jsonify(nav_menu)
|
|
@ -18,6 +18,8 @@ class DefaultConfig(object):
|
||||||
|
|
||||||
# Database connection string.
|
# Database connection string.
|
||||||
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://admin:password@localhost:5432/rookeries'
|
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://admin:password@localhost:5432/rookeries'
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
SQLALCHEMY_ECHO = False
|
||||||
|
|
||||||
# Security config
|
# Security config
|
||||||
# Key used by Flask for session secrets.
|
# Key used by Flask for session secrets.
|
||||||
|
|
|
@ -99,7 +99,7 @@ def error_message_from_exception(error: Exception):
|
||||||
error_message = {
|
error_message = {
|
||||||
'error': {
|
'error': {
|
||||||
'status_code': http.HTTPStatus.INTERNAL_SERVER_ERROR,
|
'status_code': http.HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
'message': f'{error}',
|
'message': f'{type(error)} with {error.args}: {error}',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return flask.jsonify(error_message), http.HTTPStatus.INTERNAL_SERVER_ERROR
|
return flask.jsonify(error_message), http.HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
|
|
@ -16,11 +16,13 @@ rookeries_app = flask.Blueprint('rookeries_app', __name__)
|
||||||
def register_views():
|
def register_views():
|
||||||
"""Import the various views in the app."""
|
"""Import the various views in the app."""
|
||||||
rookeries_views = [
|
rookeries_views = [
|
||||||
'rookeries.views',
|
'rookeries.blocks.views',
|
||||||
'rookeries.pages.views',
|
|
||||||
'rookeries.security',
|
|
||||||
'rookeries.sites.views',
|
'rookeries.sites.views',
|
||||||
|
'rookeries.security',
|
||||||
|
'rookeries.pages.views',
|
||||||
|
'rookeries.tokens.views',
|
||||||
'rookeries.users.views',
|
'rookeries.users.views',
|
||||||
|
'rookeries.views',
|
||||||
]
|
]
|
||||||
for views_modules in rookeries_views:
|
for views_modules in rookeries_views:
|
||||||
importlib.import_module(views_modules)
|
importlib.import_module(views_modules)
|
||||||
|
|
|
@ -5,9 +5,7 @@ Models for sites.
|
||||||
:license: AGPL v3+
|
:license: AGPL v3+
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
from sqlalchemy_utils.types import url as sql_url
|
||||||
|
|
||||||
from sqlalchemy_utils.types import url as sql_url, uuid as sql_uuid
|
|
||||||
|
|
||||||
from rookeries.database import db
|
from rookeries.database import db
|
||||||
|
|
||||||
|
@ -16,59 +14,32 @@ class Site(db.Model):
|
||||||
__tablename__ = 'site'
|
__tablename__ = 'site'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
api_key = db.Column(sql_uuid.UUIDType, unique=True, nullable=False)
|
name = db.Column(db.String(64), unique=True, nullable=False)
|
||||||
name = db.Column(db.String(64))
|
base_url = db.Column(sql_url.URLType)
|
||||||
url = db.Column(sql_url.URLType)
|
|
||||||
|
|
||||||
# TODO: Add in tag line, copyright notice, favicon and logo image, other attributes in here.
|
# TODO: Add in relations for blocks and a landing page as well...
|
||||||
config = db.Column(db.JSON)
|
# blocks = db.relationship('Block', back_populates='site')
|
||||||
menu = db.relationship('SiteMenu', order_by='SiteMenu.orderinal', back_populates='site')
|
# landing_page = db.Column('Page', back_populates='site')
|
||||||
|
|
||||||
def __init__(self, name, url, config=None, api_key=None):
|
def __init__(self, name, url):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = url
|
self.base_url = url
|
||||||
self.config = config or {}
|
|
||||||
self.api_key = api_key or uuid.uuid4()
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'<Site {self.name} - {self.url}>'
|
return f'<Site {self.name} - {self.url}>'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_json(json_request):
|
||||||
|
site_name = json_request['name']
|
||||||
|
site_base_url = json_request['base_url']
|
||||||
|
return Site(name=site_name, url=site_base_url)
|
||||||
|
|
||||||
def to_json(self) -> dict:
|
def to_json(self) -> dict:
|
||||||
|
# TODO: Add in links to the site...
|
||||||
|
# site_urls = {block.function_: block.id for block in self.blocks}
|
||||||
|
# site_urls['landing_page'] = f'/api/sites/{self.name}/pages/landing_page'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'api_key': self.api_key,
|
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'url': self.url,
|
'base_url': self.base_url,
|
||||||
'config': self.config,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SiteMenu(db.Model):
|
|
||||||
__tablename__ = 'site_menu'
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
orderinal = db.Column(db.Integer)
|
|
||||||
title = db.Column(db.String)
|
|
||||||
|
|
||||||
site_id = db.Column(db.Integer, db.ForeignKey('site.id'))
|
|
||||||
site = db.relationship('Site', back_populates='menu')
|
|
||||||
|
|
||||||
page_id = db.Column(db.Integer, db.ForeignKey('page.id'))
|
|
||||||
page = db.relationship('Page')
|
|
||||||
|
|
||||||
def __init__(self, title, site, page, orderinal=0):
|
|
||||||
self.title = title
|
|
||||||
self.site = site
|
|
||||||
self.page = page
|
|
||||||
self.orderinal = orderinal
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f'<SiteMenu {self.orderinal} - {self.title}>'
|
|
||||||
|
|
||||||
def to_json(self) -> dict:
|
|
||||||
return {
|
|
||||||
'title': self.title,
|
|
||||||
'orderinal': self.orderinal,
|
|
||||||
'page': self.page.slug,
|
|
||||||
# TODO: Improve this into something more regular.
|
|
||||||
'url': '/' + self.page.slug,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""
|
||||||
|
JSON schema for sites.
|
||||||
|
|
||||||
|
:copyright: Copyright 2013-2017, Dorian Pula <dorian.pula@amber-penguin-software.ca>
|
||||||
|
:license: AGPL v3+
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
SITE_CREATION_MODIFICATION_SCHEMA = {
|
||||||
|
'type': 'object',
|
||||||
|
'required': ['name', 'base_url'],
|
||||||
|
'properties': {
|
||||||
|
'name': {
|
||||||
|
'type': 'string',
|
||||||
|
'minLength': 1,
|
||||||
|
'maxLength': 64,
|
||||||
|
},
|
||||||
|
'base_url': {
|
||||||
|
'type': 'string',
|
||||||
|
'minLength': 8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,33 +5,107 @@ Views for managing sites.
|
||||||
:license: AGPL v3+
|
:license: AGPL v3+
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import http
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
from rookeries import errors
|
||||||
|
from rookeries.database import db
|
||||||
from rookeries.main import rookeries_app
|
from rookeries.main import rookeries_app
|
||||||
from rookeries.sites import models
|
from rookeries.sites import models, schema
|
||||||
|
from rookeries.users import models as user_models
|
||||||
|
from rookeries.vendors import flask_jwt
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
ROOKERIES_API_KEY_HEADER = 'ROOKERIES_API_KEY'
|
|
||||||
|
|
||||||
# TODO: Add in functionality to create and updates sites and menus.
|
# TODO: Add in functionality to create and update sites.
|
||||||
|
# TODO: Add in ability to query on collections of sites.
|
||||||
|
|
||||||
|
|
||||||
@rookeries_app.route('/api/site', methods=['GET'])
|
def add_self_link(site_json):
|
||||||
def app_config():
|
# TODO: Consider creating a better way to resolve IDs in links to resources, in a generic manner.
|
||||||
|
site_json['urls'] = {
|
||||||
# Get API Key from header
|
'self': flask.url_for('rookeries_app.get_site', site_name=site_json['name'], _external=True),
|
||||||
api_key = flask.request.headers.get(ROOKERIES_API_KEY_HEADER) or flask.current_app.config['DEFAULT_SITE']
|
}
|
||||||
site = models.Site.query.filter_by(api_key=api_key).first_or_404()
|
return site_json
|
||||||
return flask.jsonify(site.to_json())
|
|
||||||
|
|
||||||
|
|
||||||
@rookeries_app.route('/api/site/menu', methods=['GET'])
|
@rookeries_app.route('/api/sites/', methods=['POST'])
|
||||||
def menu_config():
|
@flask_jwt.jwt_required()
|
||||||
|
def create_site():
|
||||||
|
# Check if request is JSON, and respects the JSON schema
|
||||||
|
if not flask.request.is_json:
|
||||||
|
flask.abort(http.HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
# Get API Key from header
|
incoming_request = flask.request.get_json()
|
||||||
api_key = flask.request.headers.get(ROOKERIES_API_KEY_HEADER) or flask.current_app.config['DEFAULT_SITE']
|
jsonschema.validate(incoming_request, schema.SITE_CREATION_MODIFICATION_SCHEMA)
|
||||||
site = models.Site.query.filter_by(api_key=api_key).first_or_404()
|
|
||||||
nav_menu = {'menu': [menu_item.to_json() for menu_item in site.menu]}
|
# Check if user allowed to create a site.
|
||||||
return flask.jsonify(nav_menu)
|
current_user = flask_jwt.current_identity
|
||||||
|
requesting_user_role = user_models.UserRole[current_user['role']]
|
||||||
|
|
||||||
|
if requesting_user_role != user_models.UserRole.admin:
|
||||||
|
flask.abort(http.HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
|
# Creates a site from the json if no site exists with the given sitename.
|
||||||
|
if models.Site.query.filter_by(name=incoming_request['name']).first():
|
||||||
|
raise errors.ConflictingResourceError('Existing resource already found. PUT to the resource to update it.')
|
||||||
|
|
||||||
|
site = models.Site.from_json(incoming_request)
|
||||||
|
db.session.add(site)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
site_response = add_self_link(site.to_json())
|
||||||
|
return flask.jsonify(site_response), http.HTTPStatus.CREATED
|
||||||
|
|
||||||
|
|
||||||
|
@rookeries_app.route('/api/sites/<site_name>', methods=['PUT'])
|
||||||
|
@flask_jwt.jwt_required()
|
||||||
|
def update_site(site_name):
|
||||||
|
# Check if request is JSON, and respects the JSON schema
|
||||||
|
if not flask.request.is_json:
|
||||||
|
flask.abort(http.HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
incoming_request = flask.request.get_json()
|
||||||
|
jsonschema.validate(incoming_request, schema.SITE_CREATION_MODIFICATION_SCHEMA)
|
||||||
|
|
||||||
|
# Check if user allowed to modify a site.
|
||||||
|
current_user = flask_jwt.current_identity
|
||||||
|
requesting_user_role = user_models.UserRole[current_user['role']]
|
||||||
|
|
||||||
|
if requesting_user_role != user_models.UserRole.admin:
|
||||||
|
flask.abort(http.HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
|
# Modifies a site from the JSON.
|
||||||
|
existing_site = models.Site.query.filter_by(name=site_name).first_or_404()
|
||||||
|
updated_site = models.Site.from_json(incoming_request)
|
||||||
|
existing_site.base_url = updated_site.base_url
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
site_response = add_self_link(existing_site.to_json())
|
||||||
|
return flask.jsonify(site_response), http.HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
|
@rookeries_app.route('/api/sites/<site_name>', methods=['GET'])
|
||||||
|
def get_site(site_name):
|
||||||
|
site = models.Site.query.filter_by(name=site_name).first_or_404()
|
||||||
|
site_response = add_self_link(site.to_json())
|
||||||
|
return flask.jsonify(site_response)
|
||||||
|
|
||||||
|
|
||||||
|
@rookeries_app.route('/api/sites/<site_name>', methods=['DELETE'])
|
||||||
|
@flask_jwt.jwt_required()
|
||||||
|
def delete_site(site_name):
|
||||||
|
site = models.Site.query.filter_by(name=site_name).first_or_404()
|
||||||
|
|
||||||
|
current_user = flask_jwt.current_identity
|
||||||
|
requesting_user_role = user_models.UserRole[current_user['role']]
|
||||||
|
|
||||||
|
if requesting_user_role != user_models.UserRole.admin:
|
||||||
|
flask.abort(http.HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
|
db.session.delete(site)
|
||||||
|
db.session.commit()
|
||||||
|
return '', http.HTTPStatus.NO_CONTENT
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""
|
||||||
|
Models for tokens.
|
||||||
|
|
||||||
|
:copyright: Copyright 2013-2017, Dorian Pula <dorian.pula@amber-penguin-software.ca>
|
||||||
|
:license: AGPL v3+
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rookeries.database import db
|
||||||
|
|
||||||
|
# TODO: Migrate tag line, copyright notice, favicon and logo image, other attributes into tokens linked to the site.
|
||||||
|
|
||||||
|
|
||||||
|
class Token(db.Model):
|
||||||
|
__tablename__ = 'token'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String)
|
||||||
|
value = db.Column(db.String)
|
||||||
|
site_id = db.Column(db.Integer, db.ForeignKey('site.id'))
|
||||||
|
site = db.relationship('Site')
|
||||||
|
|
||||||
|
# TODO: Add in concept of a language/internationalization?
|
||||||
|
|
||||||
|
def to_json(self) -> dict:
|
||||||
|
return {
|
||||||
|
'site_id': self.site.name,
|
||||||
|
'name': self.name,
|
||||||
|
'value': self.base_url,
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
"""
|
||||||
|
Views for managing tokens.
|
||||||
|
|
||||||
|
:copyright: Copyright 2013-2017, Dorian Pula <dorian.pula@amber-penguin-software.ca>
|
||||||
|
:license: AGPL v3+
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import flask
|
||||||
|
|
||||||
|
from rookeries.main import rookeries_app
|
||||||
|
from rookeries.tokens import models
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# TODO: Add in functionality to create and update tokens.
|
||||||
|
|
||||||
|
|
||||||
|
def add_self_link(block_json):
|
||||||
|
site_name = block_json.pop('site_id')
|
||||||
|
self_link = flask.url_for(
|
||||||
|
'rookeries_app.get_token', site_name=site_name, token_name=block_json['name'], _external=True)
|
||||||
|
block_json['urls']['self'] = self_link
|
||||||
|
return block_json
|
||||||
|
|
||||||
|
|
||||||
|
@rookeries_app.route('/api/sites/<site_name>/tokens/<token_name>', methods=['GET'])
|
||||||
|
def get_token(site_name, token_name):
|
||||||
|
token = models.Token.query.filter_by(site=site_name, name=token_name).first_or_404()
|
||||||
|
return flask.jsonify(token.to_json())
|
|
@ -49,7 +49,7 @@ def create_migration(ctx, message):
|
||||||
docker_container_id = inv.run('cat /etc/hostname', hide='out').stdout.strip()
|
docker_container_id = inv.run('cat /etc/hostname', hide='out').stdout.strip()
|
||||||
|
|
||||||
print('\nRetrieve the generated migration files by running: \n')
|
print('\nRetrieve the generated migration files by running: \n')
|
||||||
path_to_migration = 'api/rookeries/migrations'
|
path_to_migration = 'rookeries/migrations'
|
||||||
|
|
||||||
for migration in migration_files:
|
for migration in migration_files:
|
||||||
print(f'docker cp {docker_container_id}:{migration.path} {path_to_migration}')
|
print(f'docker cp {docker_container_id}:{migration.path} {path_to_migration}')
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
Feature: Site Access
|
Feature: Site Access
|
||||||
The site GET endpoint allows user to access a site, its layout and landing page.
|
The site GET endpoint allows user to access a site, its layout and landing page.
|
||||||
|
|
||||||
|
# TODO: Migrate to concept of a site "owner" vs "user"
|
||||||
|
# TODO: Add in checks to find menu and landing pages links for a site.
|
||||||
|
|
||||||
Scenario: Any user can get an existing site
|
Scenario: Any user can get an existing site
|
||||||
Given I am an unauthenticated user
|
Given I am a visitor
|
||||||
When I get an existing site
|
And I get a published site
|
||||||
Then I get a valid site
|
Then I get a valid site
|
||||||
And I get a valid site menu
|
# And I get links to a menu
|
||||||
|
# And I get links to a landing page
|
||||||
|
|
||||||
# TODO: Create more test cases.
|
Scenario: Any user can not get a site that does not exist
|
||||||
|
Given I am a visitor
|
||||||
|
And I get a site that does not exist
|
||||||
|
Then I get a site can not be found message
|
||||||
|
|
||||||
|
Scenario: An admin can get an existing site
|
||||||
|
Given I am an admin user
|
||||||
|
And I get a published site
|
||||||
|
Then I get a valid site
|
||||||
|
|
||||||
|
Scenario: An editor can get an existing site
|
||||||
|
Given I am an editor user
|
||||||
|
And I get a published site
|
||||||
|
Then I get a valid site
|
||||||
|
|
|
@ -1,14 +1,28 @@
|
||||||
Feature: Site Creation
|
Feature: Site Creation
|
||||||
The site endpoints allows an app admin user to create, modify and delete sites.
|
The site endpoints allows a site admin to create sites.
|
||||||
|
|
||||||
|
Scenario: User cannot create a site using a non-json response
|
||||||
|
Given I am an admin user
|
||||||
|
And I try to create a site with a non-json request
|
||||||
|
Then I get a bad request response
|
||||||
|
|
||||||
|
Scenario: User cannot create a user using an empty response
|
||||||
|
Given I am an admin user
|
||||||
|
And I try to create a site with an empty request
|
||||||
|
Then I get a bad request response
|
||||||
|
|
||||||
|
Scenario: User cannot create a user twice using the same site name
|
||||||
|
Given I am an admin user
|
||||||
|
And I try to create a site
|
||||||
|
And I try to create a site with the same site name
|
||||||
|
Then I get a conflict response about an existing site
|
||||||
|
|
||||||
Scenario: Admin user can create a new site
|
Scenario: Admin user can create a new site
|
||||||
Given I am an admin user
|
Given I am an admin user
|
||||||
When I create a site
|
And I try to create a site
|
||||||
Then I get a valid site
|
Then I get a valid new site
|
||||||
And I get a valid site menu
|
|
||||||
|
|
||||||
Scenario: Editor user can not create a new site
|
Scenario: Editor user can not create a new site
|
||||||
Given I am an editor user
|
Given I am an editor user
|
||||||
When I create a site
|
And I try to create a site
|
||||||
Then I get an unauthorized response
|
Then I get an unauthorized response
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
Feature: Site Deletion
|
Feature: Site Deletion
|
||||||
The site endpoints allows an app admin user to create, modify and delete sites.
|
The site endpoints allows a site admin to delete sites.
|
||||||
|
|
||||||
|
|
||||||
Scenario: Admin user can delete a site
|
Scenario: Admin user can delete a site
|
||||||
Given I am an admin user
|
Given I am an admin user
|
||||||
When I get an existing site
|
And I get a published site
|
||||||
And I delete the site
|
And I try to delete the site
|
||||||
Then I get an error that the site is missing
|
Then I get a response indicating the deletion was successful
|
||||||
And I get an error that the site menu is missing
|
|
||||||
|
Scenario: Admin user can not delete a site that does not exist
|
||||||
|
Given I am an admin user
|
||||||
|
And I get a site that does not exist
|
||||||
|
Then I get a site can not be found message
|
||||||
|
|
||||||
|
Scenario: Admin user can not delete a user twice
|
||||||
|
Given I am an admin user
|
||||||
|
And I get a published site
|
||||||
|
And I try to delete the site
|
||||||
|
And I try to delete the deleted site again
|
||||||
|
Then I get a site can not be found message
|
||||||
|
|
||||||
Scenario: Editor user can not delete a site
|
Scenario: Editor user can not delete a site
|
||||||
Given I am an editor user
|
Given I am an editor user
|
||||||
When I get an existing site
|
And I get a published site
|
||||||
And I delete the site
|
And I try to delete the site
|
||||||
Then I get an unauthorized response
|
Then I get an unauthorized response
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
Feature: Site Modification
|
Feature: Site Modification
|
||||||
The site endpoints allows an app admin user to create, modify and delete sites.
|
The site endpoints allows an site admin user to modify sites.
|
||||||
|
|
||||||
|
Scenario: User cannot modify a site using a non-json request
|
||||||
|
Given I am an admin user
|
||||||
|
And I get a published site
|
||||||
|
And I try to modify the site with a non-json request
|
||||||
|
Then I get a bad request response
|
||||||
|
|
||||||
|
Scenario: User cannot modify a site using an empty request
|
||||||
|
Given I am an admin user
|
||||||
|
And I get a published site
|
||||||
|
And I try to modify the site with an empty request
|
||||||
|
Then I get a bad request response
|
||||||
|
|
||||||
|
Scenario: User cannot modify a non-existent site
|
||||||
|
Given I am an admin user
|
||||||
|
And I try to modify a site that does not exist
|
||||||
|
Then I get a site can not be found message
|
||||||
|
|
||||||
Scenario: Admin user can modify a site
|
Scenario: Admin user can modify a site
|
||||||
Given I am an admin user
|
Given I am an admin user
|
||||||
When I get an existing site
|
And I get a published site
|
||||||
And I modify the site
|
And I try to modify the site
|
||||||
Then my updates are reflected in the site
|
Then my updates are reflected in the site
|
||||||
And I get a valid site menu
|
|
||||||
|
|
||||||
Scenario: Admin user can modify a site's menu
|
|
||||||
Given I am an admin user
|
|
||||||
When I get an existing site menu
|
|
||||||
And I modify the site menu
|
|
||||||
Then my updates are reflected in the site menu
|
|
||||||
|
|
||||||
Scenario: Editor user can not modify a site
|
Scenario: Editor user can not modify a site
|
||||||
Given I am an editor user
|
Given I am an editor user
|
||||||
When I get an existing site
|
And I get a published site
|
||||||
And I modify the site
|
And I try to modify the site
|
||||||
Then I get an unauthorized response
|
|
||||||
|
|
||||||
Scenario: Editor user can not modify a site's menu
|
|
||||||
Given I am an editor user
|
|
||||||
When I get an existing site menu
|
|
||||||
And I modify the site menu
|
|
||||||
Then I get an unauthorized response
|
Then I get an unauthorized response
|
||||||
|
|
|
@ -5,6 +5,10 @@ Functional tests for the managing sites.
|
||||||
:license: AGPL v3+
|
:license: AGPL v3+
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import http
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pytest_bdd as bdd
|
import pytest_bdd as bdd
|
||||||
import requests
|
import requests
|
||||||
from pytest_bdd import parsers
|
from pytest_bdd import parsers
|
||||||
|
@ -12,22 +16,288 @@ from pytest_bdd import parsers
|
||||||
from tests import utils
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
# bdd.scenarios('site_creation.feature')
|
bdd.scenarios('site_creation.feature')
|
||||||
# bdd.scenarios('site_access.feature')
|
bdd.scenarios('site_access.feature')
|
||||||
# bdd.scenarios('site_modification.feature')
|
bdd.scenarios('site_modification.feature')
|
||||||
# bdd.scenarios('site_deletion.feature')
|
bdd.scenarios('site_deletion.feature')
|
||||||
|
|
||||||
|
ResponseSiteBundle = collections.namedtuple('ResponseSiteBundle', ['response', 'site'])
|
||||||
|
|
||||||
|
|
||||||
@bdd.given(parsers.parse('I am an {user_role} user'))
|
@bdd.given(parsers.parse('I am an {user_role} user'))
|
||||||
@bdd.given(parsers.parse('I am a {user_role} user'))
|
@bdd.given(parsers.parse('I am a {user_role} user'))
|
||||||
def requester(user_role, api_base_uri, db_engine):
|
def auth_token_headers(user_role, api_base_uri, db_engine):
|
||||||
|
|
||||||
user = utils.generate_test_user(role=user_role)
|
user = utils.generate_test_user(role=user_role)
|
||||||
utils.save_test_user_in_db(db_engine, user)
|
utils.save_test_user_in_db(db_engine, user)
|
||||||
return requests.post(
|
token = requests.post(
|
||||||
url=f'{api_base_uri}/auth',
|
url=f'{api_base_uri}/auth',
|
||||||
json={
|
json={
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
'password': user.password,
|
'password': user.password,
|
||||||
}
|
}
|
||||||
).json()['access_token']
|
).json()['access_token']
|
||||||
|
|
||||||
|
return {'Authorization': f'JWT {token}'}
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I am a visitor'), target_fixture='auth_token_headers')
|
||||||
|
def no_auth_token_headers():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I try to create a site'), target_fixture='site_response')
|
||||||
|
def create_site_response(auth_token_headers, api_base_uri):
|
||||||
|
|
||||||
|
test_site = utils.generate_test_site()
|
||||||
|
site_creation_request = {
|
||||||
|
'name': test_site.site_name,
|
||||||
|
'base_url': test_site.base_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
url=f'{api_base_uri}/api/sites/',
|
||||||
|
json=site_creation_request,
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
return ResponseSiteBundle(response, test_site)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I try to create a site with the same site name'), target_fixture='site_response')
|
||||||
|
def recreate_existing_site_response(site_response, auth_token_headers, api_base_uri):
|
||||||
|
|
||||||
|
test_site = site_response.site
|
||||||
|
site_creation_request = {
|
||||||
|
'name': test_site.site_name,
|
||||||
|
'base_url': test_site.base_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
url=f'{api_base_uri}/api/sites/',
|
||||||
|
json=site_creation_request,
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
return ResponseSiteBundle(response, test_site)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I try to create a site with a non-json request'), target_fixture='site_response')
|
||||||
|
def invalid_non_json_create_site_response(auth_token_headers, api_base_uri):
|
||||||
|
return requests.post(
|
||||||
|
url=f'{api_base_uri}/api/sites/',
|
||||||
|
data='',
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I try to create a site with an empty request'), target_fixture='site_response')
|
||||||
|
def invalid_empty_create_site_response(auth_token_headers, api_base_uri):
|
||||||
|
return requests.post(
|
||||||
|
url=f'{api_base_uri}/api/sites/',
|
||||||
|
json={},
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I try to modify the site with a non-json request'), target_fixture='site_response')
|
||||||
|
def invalid_non_json_modify_site_response(site_response, auth_token_headers, api_base_uri):
|
||||||
|
test_site = site_response.site
|
||||||
|
return requests.put(
|
||||||
|
url=f'{api_base_uri}/api/sites/{test_site.site_name}',
|
||||||
|
data='',
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I try to modify the site with an empty request'), target_fixture='site_response')
|
||||||
|
def invalid_empty_modify_site_response(site_response, auth_token_headers, api_base_uri):
|
||||||
|
test_site = site_response.site
|
||||||
|
return requests.put(
|
||||||
|
url=f'{api_base_uri}/api/sites/{test_site.site_name}',
|
||||||
|
json={},
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I get a {state} site'), target_fixture='site_response')
|
||||||
|
def get_site_request(state, auth_token_headers, api_base_uri, db_engine):
|
||||||
|
|
||||||
|
# TODO: Generate a test site by its state
|
||||||
|
test_site = utils.generate_test_site()
|
||||||
|
utils.save_test_site_in_db(db_engine, test_site)
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
url=f'{api_base_uri}/api/sites/{test_site.site_name}',
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
return ResponseSiteBundle(site=test_site, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I get a site that does not exist'), target_fixture='site_response')
|
||||||
|
def get_non_existent_site_request(auth_token_headers, api_base_uri):
|
||||||
|
return requests.get(
|
||||||
|
url=f'{api_base_uri}/api/sites/this-does-not-exist',
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(parsers.parse('I get a valid site'))
|
||||||
|
def assert_site_response(site_response, api_base_uri):
|
||||||
|
|
||||||
|
test_site = site_response.site
|
||||||
|
actual_response = utils.extract_response(site_response)
|
||||||
|
assert actual_response.status_code == http.HTTPStatus.OK
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
'name': test_site.site_name,
|
||||||
|
'base_url': test_site.base_url,
|
||||||
|
'urls': mock.ANY,
|
||||||
|
}
|
||||||
|
assert expected_json == actual_response.json()
|
||||||
|
assert f'{api_base_uri}/api/sites/{test_site.site_name}' == actual_response.json()['urls']['self']
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(parsers.parse('I get a valid new site'))
|
||||||
|
def assert_new_site_response(site_response, api_base_uri):
|
||||||
|
|
||||||
|
test_site = site_response.site
|
||||||
|
actual_response = utils.extract_response(site_response)
|
||||||
|
assert actual_response.status_code == http.HTTPStatus.CREATED
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
'name': test_site.site_name,
|
||||||
|
'base_url': test_site.base_url,
|
||||||
|
'urls': mock.ANY,
|
||||||
|
}
|
||||||
|
assert expected_json == actual_response.json()
|
||||||
|
assert f'{api_base_uri}/api/sites/{test_site.site_name}' == actual_response.json()['urls']['self']
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I try to delete the site'), target_fixture='site_response')
|
||||||
|
def delete_site_response(site_response, auth_token_headers, api_base_uri):
|
||||||
|
test_site = site_response.site
|
||||||
|
response = requests.delete(
|
||||||
|
url=f'{api_base_uri}/api/sites/{test_site.site_name}',
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
return ResponseSiteBundle(response, test_site)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I try to delete the deleted site again'), target_fixture='site_response')
|
||||||
|
def delete_deleted_site_response(site_response, auth_token_headers, api_base_uri):
|
||||||
|
return delete_site_response(site_response, auth_token_headers, api_base_uri)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I try to modify the site'), target_fixture='site_response')
|
||||||
|
def modify_site_response(site_response, auth_token_headers, api_base_uri):
|
||||||
|
test_site = site_response.site
|
||||||
|
modified_base_url = f'{test_site.base_url}/new_'
|
||||||
|
modified_site = utils.generate_test_site(test_site.site_name, modified_base_url)
|
||||||
|
site_modification_request = {
|
||||||
|
'name': test_site.site_name,
|
||||||
|
'base_url': modified_base_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.put(
|
||||||
|
url=f'{api_base_uri}/api/sites/{test_site.site_name}',
|
||||||
|
json=site_modification_request,
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
return ResponseSiteBundle(response, modified_site)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.given(parsers.parse('I try to modify a site that does not exist'), target_fixture='site_response')
|
||||||
|
def modify_non_existent_site_request(auth_token_headers, api_base_uri):
|
||||||
|
test_site = utils.generate_test_site()
|
||||||
|
site_modification_request = {
|
||||||
|
'name': 'this-does-not-exist',
|
||||||
|
'base_url': test_site.base_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
return requests.put(
|
||||||
|
url=f'{api_base_uri}/api/sites/this-does-not-exist',
|
||||||
|
json=site_modification_request,
|
||||||
|
headers=auth_token_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(parsers.parse('my updates are reflected in the site'))
|
||||||
|
def assert_updated_site_response(site_response, api_base_uri):
|
||||||
|
|
||||||
|
test_site = site_response.site
|
||||||
|
actual_response = utils.extract_response(site_response)
|
||||||
|
assert actual_response.status_code == http.HTTPStatus.OK
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
'name': test_site.site_name,
|
||||||
|
'base_url': test_site.base_url,
|
||||||
|
'urls': mock.ANY,
|
||||||
|
}
|
||||||
|
assert expected_json == actual_response.json()
|
||||||
|
assert f'{api_base_uri}/api/sites/{test_site.site_name}' == actual_response.json()['urls']['self']
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(parsers.parse('I get a site can not be found message'))
|
||||||
|
def assert_resource_not_found_response(site_response):
|
||||||
|
actual_response = utils.extract_response(site_response)
|
||||||
|
expected_response_json = {
|
||||||
|
'error': {
|
||||||
|
'status_code': http.HTTPStatus.NOT_FOUND.value,
|
||||||
|
'message': 'Resource not found.',
|
||||||
|
'resource': actual_response.request.url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert actual_response.status_code == http.HTTPStatus.NOT_FOUND
|
||||||
|
assert actual_response.json() == expected_response_json
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(parsers.parse('I get an unauthorized response'))
|
||||||
|
def assert_unauthorized_response(site_response):
|
||||||
|
actual_response = utils.extract_response(site_response)
|
||||||
|
expected_response_json = {
|
||||||
|
'error': {
|
||||||
|
'status_code': http.HTTPStatus.UNAUTHORIZED.value,
|
||||||
|
'message': 'Not authorized to access this resource.',
|
||||||
|
'resource': actual_response.request.url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert actual_response.status_code == http.HTTPStatus.UNAUTHORIZED
|
||||||
|
assert actual_response.json() == expected_response_json
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(parsers.parse('I get a conflict response about an existing site'))
|
||||||
|
def assert_conflicting_resource_found_response(site_response):
|
||||||
|
actual_response = utils.extract_response(site_response)
|
||||||
|
expected_response_json = {
|
||||||
|
'error': {
|
||||||
|
'status_code': http.HTTPStatus.CONFLICT.value,
|
||||||
|
'message': 'Existing resource already found. PUT to the resource to update it.',
|
||||||
|
'resource': actual_response.request.url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert actual_response.status_code == http.HTTPStatus.CONFLICT
|
||||||
|
assert actual_response.json() == expected_response_json
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(parsers.parse('I get a bad request response'))
|
||||||
|
def assert_bad_request_response(site_response):
|
||||||
|
actual_response = utils.extract_response(site_response)
|
||||||
|
expected_response_json = {
|
||||||
|
'error': {
|
||||||
|
'status_code': http.HTTPStatus.BAD_REQUEST.value,
|
||||||
|
'message': mock.ANY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert actual_response.json() == expected_response_json
|
||||||
|
assert actual_response.status_code == http.HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(parsers.parse('I get a response indicating the deletion was successful'))
|
||||||
|
def assert_resource_deleted_response(site_response):
|
||||||
|
actual_response = utils.extract_response(site_response)
|
||||||
|
assert actual_response.status_code == http.HTTPStatus.NO_CONTENT
|
||||||
|
assert not actual_response.content
|
||||||
|
assert len(actual_response.content) == 0
|
||||||
|
|
|
@ -30,12 +30,6 @@ TokenUserBundle = collections.namedtuple('TokenUserBundle', ['user', 'token'])
|
||||||
ResponseUserBundle = collections.namedtuple('ResponseUserBundle', ['response', 'user'])
|
ResponseUserBundle = collections.namedtuple('ResponseUserBundle', ['response', 'user'])
|
||||||
|
|
||||||
|
|
||||||
def extract_response(response):
|
|
||||||
if isinstance(response, requests.Response):
|
|
||||||
return response
|
|
||||||
return response.response
|
|
||||||
|
|
||||||
|
|
||||||
@bdd.given(parsers.parse('I am an {user_role} user'))
|
@bdd.given(parsers.parse('I am an {user_role} user'))
|
||||||
@bdd.given(parsers.parse('I am a {user_role} user'))
|
@bdd.given(parsers.parse('I am a {user_role} user'))
|
||||||
def requester(user_role, api_base_uri, db_engine):
|
def requester(user_role, api_base_uri, db_engine):
|
||||||
|
@ -309,7 +303,7 @@ def assert_create_user_response(user_role, user_response, api_base_uri):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
assert actual_response.status_code == http.HTTPStatus.CREATED
|
assert actual_response.status_code == http.HTTPStatus.CREATED
|
||||||
assert actual_response.json() == expected_user_creation_response
|
assert actual_response.json() == expected_user_creation_response
|
||||||
|
|
||||||
|
@ -330,7 +324,7 @@ def assert_my_user_profile(requester, user_response, api_base_uri):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
assert actual_response.status_code == http.HTTPStatus.OK
|
assert actual_response.status_code == http.HTTPStatus.OK
|
||||||
assert actual_response.json() == expected_user_creation_response
|
assert actual_response.json() == expected_user_creation_response
|
||||||
assert MODIFIED_PREFIX not in actual_response.json()['profile']['fullName']
|
assert MODIFIED_PREFIX not in actual_response.json()['profile']['fullName']
|
||||||
|
@ -354,7 +348,7 @@ def assert_user_profile(user_role, user_response, api_base_uri):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
assert actual_response.status_code == http.HTTPStatus.OK
|
assert actual_response.status_code == http.HTTPStatus.OK
|
||||||
assert actual_response.json() == expected_user_creation_response
|
assert actual_response.json() == expected_user_creation_response
|
||||||
|
|
||||||
|
@ -375,7 +369,7 @@ def assert_my_modified_user_profile(requester, user_response, api_base_uri):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
assert actual_response.status_code == http.HTTPStatus.CREATED
|
assert actual_response.status_code == http.HTTPStatus.CREATED
|
||||||
assert actual_response.json() == expected_user_creation_response
|
assert actual_response.json() == expected_user_creation_response
|
||||||
|
|
||||||
|
@ -397,14 +391,14 @@ def assert_modified_user_profile(user_role, user_response, api_base_uri):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
assert actual_response.status_code == http.HTTPStatus.CREATED
|
assert actual_response.status_code == http.HTTPStatus.CREATED
|
||||||
assert actual_response.json() == expected_user_response
|
assert actual_response.json() == expected_user_response
|
||||||
|
|
||||||
|
|
||||||
@bdd.then(parsers.parse('I get an unauthorized response'))
|
@bdd.then(parsers.parse('I get an unauthorized response'))
|
||||||
def assert_unauthorized_response(user_response):
|
def assert_unauthorized_response(user_response):
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
expected_response_json = {
|
expected_response_json = {
|
||||||
'error': {
|
'error': {
|
||||||
'status_code': http.HTTPStatus.UNAUTHORIZED.value,
|
'status_code': http.HTTPStatus.UNAUTHORIZED.value,
|
||||||
|
@ -419,7 +413,7 @@ def assert_unauthorized_response(user_response):
|
||||||
|
|
||||||
@bdd.then(parsers.parse('I get a forbidden response'))
|
@bdd.then(parsers.parse('I get a forbidden response'))
|
||||||
def assert_forbidden_response(user_response):
|
def assert_forbidden_response(user_response):
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
expected_response_json = {
|
expected_response_json = {
|
||||||
'error': {
|
'error': {
|
||||||
'status_code': http.HTTPStatus.FORBIDDEN.value,
|
'status_code': http.HTTPStatus.FORBIDDEN.value,
|
||||||
|
@ -434,7 +428,7 @@ def assert_forbidden_response(user_response):
|
||||||
|
|
||||||
@bdd.then(parsers.parse('I get a user can not be found message'))
|
@bdd.then(parsers.parse('I get a user can not be found message'))
|
||||||
def assert_resource_not_found_response(user_response):
|
def assert_resource_not_found_response(user_response):
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
expected_response_json = {
|
expected_response_json = {
|
||||||
'error': {
|
'error': {
|
||||||
'status_code': http.HTTPStatus.NOT_FOUND.value,
|
'status_code': http.HTTPStatus.NOT_FOUND.value,
|
||||||
|
@ -449,7 +443,7 @@ def assert_resource_not_found_response(user_response):
|
||||||
|
|
||||||
@bdd.then(parsers.parse('I get a conflict response about existing user'))
|
@bdd.then(parsers.parse('I get a conflict response about existing user'))
|
||||||
def assert_conflicting_resource_found_response(user_response):
|
def assert_conflicting_resource_found_response(user_response):
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
expected_response_json = {
|
expected_response_json = {
|
||||||
'error': {
|
'error': {
|
||||||
'status_code': http.HTTPStatus.CONFLICT.value,
|
'status_code': http.HTTPStatus.CONFLICT.value,
|
||||||
|
@ -464,7 +458,7 @@ def assert_conflicting_resource_found_response(user_response):
|
||||||
|
|
||||||
@bdd.then(parsers.parse('I get a bad request response'))
|
@bdd.then(parsers.parse('I get a bad request response'))
|
||||||
def assert_bad_request_response(user_response):
|
def assert_bad_request_response(user_response):
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
expected_response_json = {
|
expected_response_json = {
|
||||||
'error': {
|
'error': {
|
||||||
'status_code': http.HTTPStatus.BAD_REQUEST.value,
|
'status_code': http.HTTPStatus.BAD_REQUEST.value,
|
||||||
|
@ -478,7 +472,7 @@ def assert_bad_request_response(user_response):
|
||||||
|
|
||||||
@bdd.then(parsers.parse('I get a response indicating the deletion was successful'))
|
@bdd.then(parsers.parse('I get a response indicating the deletion was successful'))
|
||||||
def assert_resource_deleted_response(user_response):
|
def assert_resource_deleted_response(user_response):
|
||||||
actual_response = extract_response(user_response)
|
actual_response = utils.extract_response(user_response)
|
||||||
assert actual_response.status_code == http.HTTPStatus.NO_CONTENT
|
assert actual_response.status_code == http.HTTPStatus.NO_CONTENT
|
||||||
assert not actual_response.content
|
assert not actual_response.content
|
||||||
assert len(actual_response.content) == 0
|
assert len(actual_response.content) == 0
|
||||||
|
|
|
@ -8,14 +8,19 @@ Utilities for testing.
|
||||||
import collections
|
import collections
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
import requests
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
from werkzeug import security
|
from werkzeug import security
|
||||||
|
|
||||||
from rookeries.users import models
|
from rookeries.sites import models as site_models
|
||||||
|
from rookeries.users import models as user_models
|
||||||
|
|
||||||
|
|
||||||
|
RANDOM_SUFFIX_CHARACTERS = list(string.ascii_lowercase + string.digits)
|
||||||
|
|
||||||
TestUserInfo = collections.namedtuple('TestUserInfo', ['username', 'password', 'role', 'email'])
|
TestUserInfo = collections.namedtuple('TestUserInfo', ['username', 'password', 'role', 'email'])
|
||||||
|
TestSiteInfo = collections.namedtuple('TestSiteInfo', ['site_name', 'base_url', 'menu', 'landing_page'])
|
||||||
|
|
||||||
|
|
||||||
def generate_test_password():
|
def generate_test_password():
|
||||||
|
@ -26,7 +31,11 @@ def hash_password(password):
|
||||||
return security.generate_password_hash(password)
|
return security.generate_password_hash(password)
|
||||||
|
|
||||||
|
|
||||||
def generate_test_user(user_prefix='', role=models.UserRole.subscriber.name) -> TestUserInfo:
|
def generate_random_suffix():
|
||||||
|
return ''.join(random.choices(RANDOM_SUFFIX_CHARACTERS, k=10))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_test_user(user_prefix='', role=user_models.UserRole.subscriber.name) -> TestUserInfo:
|
||||||
"""
|
"""
|
||||||
Creates a new test user.
|
Creates a new test user.
|
||||||
|
|
||||||
|
@ -35,13 +44,13 @@ def generate_test_user(user_prefix='', role=models.UserRole.subscriber.name) ->
|
||||||
:return: A tuple with information about the test user.
|
:return: A tuple with information about the test user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
username = f'{user_prefix}-{uuid.uuid4()}' if user_prefix else f'test-{role}-{uuid.uuid4()}'
|
username = f'{user_prefix}-{generate_random_suffix()}' if user_prefix else f'test-{role}-{generate_random_suffix()}'
|
||||||
email = f'{username}@test.rookeries.org'
|
email = f'{username}@test.rookeries.org'
|
||||||
password = generate_test_password()
|
password = generate_test_password()
|
||||||
return TestUserInfo(username=username, password=password, role=role, email=email)
|
return TestUserInfo(username=username, password=password, role=role, email=email)
|
||||||
|
|
||||||
|
|
||||||
def save_test_user_in_db(db_engine, test_user):
|
def save_test_user_in_db(db_engine, test_user: TestUserInfo):
|
||||||
"""
|
"""
|
||||||
Saves a given test user into the database.
|
Saves a given test user into the database.
|
||||||
|
|
||||||
|
@ -51,11 +60,11 @@ def save_test_user_in_db(db_engine, test_user):
|
||||||
|
|
||||||
session = orm.sessionmaker(bind=db_engine, autocommit=True)()
|
session = orm.sessionmaker(bind=db_engine, autocommit=True)()
|
||||||
session.begin()
|
session.begin()
|
||||||
user = models.User(test_user.username)
|
user = user_models.User(test_user.username)
|
||||||
user.password = hash_password(test_user.password)
|
user.password = hash_password(test_user.password)
|
||||||
user.role = test_user.role
|
user.role = test_user.role
|
||||||
|
|
||||||
user_profile = models.UserProfile()
|
user_profile = user_models.UserProfile()
|
||||||
user_profile.full_name = f'Test {test_user.role.capitalize()}'
|
user_profile.full_name = f'Test {test_user.role.capitalize()}'
|
||||||
user_profile.email = test_user.email
|
user_profile.email = test_user.email
|
||||||
user.profile = user_profile
|
user.profile = user_profile
|
||||||
|
@ -63,3 +72,42 @@ def save_test_user_in_db(db_engine, test_user):
|
||||||
session.add(user)
|
session.add(user)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_test_site(site_name='', base_url='') -> TestSiteInfo:
|
||||||
|
"""
|
||||||
|
Creates a new test site.
|
||||||
|
|
||||||
|
:param site_name: The name of the test site.
|
||||||
|
:param base_url: The base url for the site.
|
||||||
|
:return: A tuple with information about the test site.
|
||||||
|
"""
|
||||||
|
|
||||||
|
test_site_name = site_name if site_name else f'test-site-{generate_random_suffix()}'
|
||||||
|
site_base_url = f'{test_site_name}.rookeries.org' if not base_url else base_url
|
||||||
|
menu = f'{site_base_url}/blocks/{generate_random_suffix()}'
|
||||||
|
landing_page = f'{site_base_url}/pages/{generate_random_suffix()}'
|
||||||
|
return TestSiteInfo(site_name=test_site_name, base_url=site_base_url, menu=menu, landing_page=landing_page)
|
||||||
|
|
||||||
|
|
||||||
|
def save_test_site_in_db(db_engine, test_site: TestSiteInfo):
|
||||||
|
"""
|
||||||
|
Saves a given test site into the database.
|
||||||
|
|
||||||
|
:param db_engine: The DB engine to spawn a session from.
|
||||||
|
:param test_site: The test site to save.
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = orm.sessionmaker(bind=db_engine, autocommit=True)()
|
||||||
|
session.begin()
|
||||||
|
site = site_models.Site(test_site.site_name, test_site.base_url)
|
||||||
|
|
||||||
|
session.add(site)
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def extract_response(response):
|
||||||
|
if isinstance(response, requests.Response):
|
||||||
|
return response
|
||||||
|
return response.response
|
||||||
|
|
Loading…
Reference in New Issue