Add Tests
This commit is contained in:
13
Makefile
13
Makefile
@@ -30,4 +30,15 @@ serve :
|
|||||||
shell :
|
shell :
|
||||||
poetry run flask shell
|
poetry run flask shell
|
||||||
|
|
||||||
.PHONY : lint shell serve
|
test :
|
||||||
|
poetry run python -m pytest tests
|
||||||
|
|
||||||
|
test-x :
|
||||||
|
poetry run python -m pytest tests -x
|
||||||
|
|
||||||
|
test-wip :
|
||||||
|
poetry run python -m pytest tests -m wip
|
||||||
|
|
||||||
|
.PHONY : db-init db-migrate db-upgrad db-downgrade \
|
||||||
|
lint shell serve \
|
||||||
|
test test-wip test-x
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"""empty message
|
"""empty message
|
||||||
|
|
||||||
Revision ID: 938218f911e8
|
Revision ID: 04f3fe65d40b
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2022-07-12 06:31:18.835124
|
Create Date: 2022-07-12 07:03:12.713616
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
@@ -10,7 +10,7 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '938218f911e8'
|
revision = '04f3fe65d40b'
|
||||||
down_revision = None
|
down_revision = None
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
@@ -31,8 +31,14 @@ def upgrade():
|
|||||||
sa.Column('restoredAt', sa.DateTime(), nullable=True),
|
sa.Column('restoredAt', sa.DateTime(), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('key')
|
sa.PrimaryKeyConstraint('key')
|
||||||
)
|
)
|
||||||
|
op.create_table('sigl_config',
|
||||||
|
sa.Column('key', sa.String(), nullable=False),
|
||||||
|
sa.Column('value', sa.String(length=128), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('key')
|
||||||
|
)
|
||||||
op.create_table('access_tokens',
|
op.create_table('access_tokens',
|
||||||
sa.Column('token', sa.String(length=64), nullable=False),
|
sa.Column('token', sa.String(length=64), nullable=False),
|
||||||
|
sa.Column('key', sa.String(length=64), nullable=True),
|
||||||
sa.Column('clientIP', sa.String(length=46), nullable=False),
|
sa.Column('clientIP', sa.String(length=46), nullable=False),
|
||||||
sa.Column('userAgent', sa.String(length=255), nullable=False),
|
sa.Column('userAgent', sa.String(length=255), nullable=False),
|
||||||
sa.Column('expired', sa.Boolean(), nullable=False),
|
sa.Column('expired', sa.Boolean(), nullable=False),
|
||||||
@@ -40,6 +46,7 @@ def upgrade():
|
|||||||
sa.Column('issuedAt', sa.DateTime(), nullable=True),
|
sa.Column('issuedAt', sa.DateTime(), nullable=True),
|
||||||
sa.Column('expiresAt', sa.DateTime(), nullable=True),
|
sa.Column('expiresAt', sa.DateTime(), nullable=True),
|
||||||
sa.Column('revokedAt', sa.DateTime(), nullable=True),
|
sa.Column('revokedAt', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['key'], ['access_keys.key'], ),
|
||||||
sa.PrimaryKeyConstraint('token')
|
sa.PrimaryKeyConstraint('token')
|
||||||
)
|
)
|
||||||
op.create_table('lists',
|
op.create_table('lists',
|
||||||
@@ -49,24 +56,21 @@ def upgrade():
|
|||||||
sa.Column('notes', sa.String(), nullable=True),
|
sa.Column('notes', sa.String(), nullable=True),
|
||||||
sa.Column('createdAt', sa.DateTime(), nullable=True),
|
sa.Column('createdAt', sa.DateTime(), nullable=True),
|
||||||
sa.Column('modifiedAt', sa.DateTime(), nullable=True),
|
sa.Column('modifiedAt', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['accessKey'], ['access_keys.key'], ),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
op.create_table('products',
|
op.create_table('products',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('accessKey', sa.String(length=64), nullable=True),
|
sa.Column('key', sa.String(length=64), nullable=True),
|
||||||
sa.Column('name', sa.String(length=128), nullable=False),
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
sa.Column('category', sa.String(length=128), nullable=False),
|
sa.Column('category', sa.String(length=128), nullable=False),
|
||||||
sa.Column('notes', sa.String(), nullable=True),
|
sa.Column('notes', sa.String(), nullable=True),
|
||||||
sa.Column('createdAt', sa.DateTime(), nullable=True),
|
sa.Column('createdAt', sa.DateTime(), nullable=True),
|
||||||
sa.Column('modifiedAt', sa.DateTime(), nullable=True),
|
sa.Column('modifiedAt', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['key'], ['access_keys.key'], ),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_products_category'), 'products', ['category'], unique=False)
|
op.create_index(op.f('ix_products_category'), 'products', ['category'], unique=False)
|
||||||
op.create_table('sigl_config',
|
|
||||||
sa.Column('key', sa.String(), nullable=False),
|
|
||||||
sa.Column('value', sa.String(length=128), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('key')
|
|
||||||
)
|
|
||||||
op.create_table('list_entries',
|
op.create_table('list_entries',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('list_id', sa.Integer(), nullable=False),
|
sa.Column('list_id', sa.Integer(), nullable=False),
|
||||||
@@ -99,10 +103,10 @@ def downgrade():
|
|||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_table('product_locations')
|
op.drop_table('product_locations')
|
||||||
op.drop_table('list_entries')
|
op.drop_table('list_entries')
|
||||||
op.drop_table('sigl_config')
|
|
||||||
op.drop_index(op.f('ix_products_category'), table_name='products')
|
op.drop_index(op.f('ix_products_category'), table_name='products')
|
||||||
op.drop_table('products')
|
op.drop_table('products')
|
||||||
op.drop_table('lists')
|
op.drop_table('lists')
|
||||||
op.drop_table('access_tokens')
|
op.drop_table('access_tokens')
|
||||||
|
op.drop_table('sigl_config')
|
||||||
op.drop_table('access_keys')
|
op.drop_table('access_keys')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
@@ -27,43 +27,48 @@ __all__ = ('init_orm', )
|
|||||||
def init_orm():
|
def init_orm():
|
||||||
"""Initialize the Sigl ORM."""
|
"""Initialize the Sigl ORM."""
|
||||||
|
|
||||||
# Access Keys
|
# # Access Keys
|
||||||
db.mapper(AccessKey, access_keys, properties={
|
# db.mapper(AccessKey, access_keys, properties={
|
||||||
'tokens': db.relationship(
|
# 'tokens': db.relationship(
|
||||||
AccessToken,
|
# AccessToken,
|
||||||
backref='accessKey',
|
# backref='accessKey',
|
||||||
cascade='all, delete-orphan',
|
# cascade='all, delete-orphan',
|
||||||
)
|
# )
|
||||||
})
|
# })
|
||||||
|
|
||||||
# Access Tokens
|
# # Access Tokens
|
||||||
db.mapper(AccessToken, access_tokens)
|
# db.mapper(AccessToken, access_tokens)
|
||||||
|
|
||||||
# List Entries
|
# # List Entries
|
||||||
db.mapper(ListEntry, list_entries)
|
# db.mapper(ListEntry, list_entries)
|
||||||
|
|
||||||
# Products
|
# Products
|
||||||
db.mapper(Product, products, properties={
|
db.mapper(Product, products, properties={
|
||||||
'locations': db.relationship(
|
'locations': db.relationship(
|
||||||
ProductLocation,
|
ProductLocation,
|
||||||
backref='product',
|
back_populates='product',
|
||||||
cascade='all, delete-orphan',
|
|
||||||
),
|
|
||||||
'shoppingLists': db.relationship(
|
|
||||||
ShoppingList,
|
|
||||||
backref='product',
|
|
||||||
cascade='all, delete-orphan',
|
cascade='all, delete-orphan',
|
||||||
),
|
),
|
||||||
|
# 'shoppingLists': db.relationship(
|
||||||
|
# ShoppingList,
|
||||||
|
# backref='product',
|
||||||
|
# cascade='all, delete-orphan',
|
||||||
|
# ),
|
||||||
})
|
})
|
||||||
|
|
||||||
# Product Locations
|
# Product Locations
|
||||||
db.mapper(ProductLocation, product_locations)
|
db.mapper(ProductLocation, product_locations, properties={
|
||||||
|
'product': db.relationship(
|
||||||
# Shopping Lists
|
Product,
|
||||||
db.mapper(ShoppingList, lists, properties={
|
back_populates='locations',
|
||||||
'entries': db.relationship(
|
|
||||||
ListEntry,
|
|
||||||
backref='shoppingList',
|
|
||||||
cascade='all, delete-orphan',
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# # Shopping Lists
|
||||||
|
# db.mapper(ShoppingList, lists, properties={
|
||||||
|
# 'entries': db.relationship(
|
||||||
|
# ListEntry,
|
||||||
|
# backref='shoppingList',
|
||||||
|
# cascade='all, delete-orphan',
|
||||||
|
# )
|
||||||
|
# })
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ access_tokens = db.Table(
|
|||||||
# Primary Key
|
# Primary Key
|
||||||
db.Column('token', db.String(64), primary_key=True),
|
db.Column('token', db.String(64), primary_key=True),
|
||||||
|
|
||||||
# Client Attributes
|
# Token Attributes
|
||||||
|
db.Column('key', db.ForeignKey('access_keys.key'), default=None),
|
||||||
db.Column('clientIP', db.String(46), nullable=False),
|
db.Column('clientIP', db.String(46), nullable=False),
|
||||||
db.Column('userAgent', db.String(255), nullable=False),
|
db.Column('userAgent', db.String(255), nullable=False),
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ lists = db.Table(
|
|||||||
db.Column('id', db.Integer, primary_key=True),
|
db.Column('id', db.Integer, primary_key=True),
|
||||||
|
|
||||||
# Access Key
|
# Access Key
|
||||||
db.Column('accessKey', db.String(64), default=None),
|
db.Column('accessKey', db.ForeignKey('access_keys.key'), default=None),
|
||||||
|
|
||||||
# List Attributes
|
# List Attributes
|
||||||
db.Column('name', db.String(128), nullable=False),
|
db.Column('name', db.String(128), nullable=False),
|
||||||
@@ -108,7 +109,7 @@ products = db.Table(
|
|||||||
db.Column('id', db.Integer, primary_key=True),
|
db.Column('id', db.Integer, primary_key=True),
|
||||||
|
|
||||||
# Access Key
|
# Access Key
|
||||||
db.Column('accessKey', db.String(64), default=None),
|
db.Column('key', db.ForeignKey('access_keys.key'), default=None),
|
||||||
|
|
||||||
# Product Attributes
|
# Product Attributes
|
||||||
db.Column('name', db.String(128), nullable=False),
|
db.Column('name', db.String(128), nullable=False),
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class AccessToken:
|
|||||||
User Agent string are logged for future auditing purposes.
|
User Agent string are logged for future auditing purposes.
|
||||||
"""
|
"""
|
||||||
token: str = None
|
token: str = None
|
||||||
accessKey: str = None
|
accessKey: 'AccessKey' = None
|
||||||
clientIP: str = None
|
clientIP: str = None
|
||||||
userAgent: str = None
|
userAgent: str = None
|
||||||
|
|
||||||
|
|||||||
5
tests/__init__.py
Normal file
5
tests/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Sigl Test Package.
|
||||||
|
|
||||||
|
Simple Grocery List (Sigl) | sigl.app
|
||||||
|
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||||
|
"""
|
||||||
137
tests/conftest.py
Normal file
137
tests/conftest.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"""Sigl Test Fixtures.
|
||||||
|
|
||||||
|
Simple Grocery List (Sigl) | sigl.app
|
||||||
|
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from alembic.command import upgrade
|
||||||
|
from alembic.config import Config
|
||||||
|
import flask
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import sigl
|
||||||
|
from sigl.database import db as _db
|
||||||
|
from sigl.factory import create_app
|
||||||
|
|
||||||
|
DATA_DIR = '.pytest-data'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def app_config(request):
|
||||||
|
"""Application Configuration for the Test Session.
|
||||||
|
|
||||||
|
Loads configuration from config/test.py, patches the database file with
|
||||||
|
a session-global temporary file, and returns the new configuration data
|
||||||
|
to pass to create_app().
|
||||||
|
"""
|
||||||
|
test_dir = os.path.dirname(__file__)
|
||||||
|
test_cfg = os.path.join(test_dir, '../config/test.py')
|
||||||
|
cfg_file = os.environ.get('SIGL_TEST_CONFIG', test_cfg)
|
||||||
|
cfg_data = flask.Config(test_dir)
|
||||||
|
cfg_data.from_pyfile(cfg_file)
|
||||||
|
|
||||||
|
# Patch Test Configuration
|
||||||
|
cfg_data['TESTING'] = True
|
||||||
|
cfg_data['DB_DRIVER'] = 'sqlite'
|
||||||
|
cfg_data['DB_FILE'] = os.path.join(test_dir, DATA_DIR, 'test.db')
|
||||||
|
|
||||||
|
# Ensure Database Path exists
|
||||||
|
db_dir = os.path.dirname(cfg_data['DB_FILE'])
|
||||||
|
if not os.path.isdir(db_dir):
|
||||||
|
os.mkdir(db_dir)
|
||||||
|
if not os.path.isdir(db_dir):
|
||||||
|
raise Exception('Database path "%s" does not exist' % (db_dir))
|
||||||
|
|
||||||
|
db_file = cfg_data['DB_FILE']
|
||||||
|
if os.path.exists(db_file):
|
||||||
|
os.unlink(db_file)
|
||||||
|
|
||||||
|
def teardown():
|
||||||
|
if os.path.exists(db_file):
|
||||||
|
os.unlink(db_file)
|
||||||
|
if os.path.exists(db_dir) and len(os.listdir(db_dir)) == 0:
|
||||||
|
os.rmdir(db_dir)
|
||||||
|
|
||||||
|
request.addfinalizer(teardown)
|
||||||
|
return cfg_data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='module')
|
||||||
|
def app(request, app_config):
|
||||||
|
"""Module-Wide Sigl Flask Application with Database.
|
||||||
|
|
||||||
|
Loads configuration from config/test.py, patches the database file with
|
||||||
|
a session-global temporary file, and initializes the application.
|
||||||
|
"""
|
||||||
|
# Apply Database Migrations
|
||||||
|
_db.clear_mappers()
|
||||||
|
_app = create_app(app_config, __name__)
|
||||||
|
with _app.app_context():
|
||||||
|
alembic_config = Config('migrations/alembic.ini')
|
||||||
|
alembic_config.set_main_option('script_location', 'migrations')
|
||||||
|
upgrade(alembic_config, 'head')
|
||||||
|
_db.clear_mappers()
|
||||||
|
|
||||||
|
# Initialize Application and Context
|
||||||
|
app = create_app(app_config, __name__)
|
||||||
|
ctx = app.app_context()
|
||||||
|
ctx.push()
|
||||||
|
|
||||||
|
# Add Finalizers
|
||||||
|
def teardown():
|
||||||
|
_db.drop_all()
|
||||||
|
_db.clear_mappers()
|
||||||
|
if os.path.exists(app.config['DB_FILE']):
|
||||||
|
os.unlink(app.config['DB_FILE'])
|
||||||
|
ctx.pop()
|
||||||
|
|
||||||
|
request.addfinalizer(teardown)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='module')
|
||||||
|
def app_without_database(request, app_config):
|
||||||
|
"""Module-Wide Sigl Flask Application without Database.
|
||||||
|
|
||||||
|
Loads configuration from config/test.py and initializes the application,
|
||||||
|
but does not create the database.
|
||||||
|
"""
|
||||||
|
_db.clear_mappers()
|
||||||
|
|
||||||
|
# Initialize Application and Context
|
||||||
|
app = create_app(app_config, __name__)
|
||||||
|
ctx = app.app_context()
|
||||||
|
ctx.push()
|
||||||
|
|
||||||
|
# Add Finalizers
|
||||||
|
def teardown():
|
||||||
|
_db.clear_mappers()
|
||||||
|
_db.drop_all()
|
||||||
|
ctx.pop()
|
||||||
|
|
||||||
|
request.addfinalizer(teardown)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def session(request, monkeypatch, app):
|
||||||
|
"""Create a new Database Session for the Request."""
|
||||||
|
with app.app_context():
|
||||||
|
connection = _db.engine.connect()
|
||||||
|
transaction = connection.begin()
|
||||||
|
|
||||||
|
options = dict(bind=connection, binds={})
|
||||||
|
session = _db.create_scoped_session(options=options)
|
||||||
|
|
||||||
|
# Patch sigl.db with the current session
|
||||||
|
monkeypatch.setattr(_db, 'session', session)
|
||||||
|
|
||||||
|
def teardown():
|
||||||
|
transaction.rollback()
|
||||||
|
connection.close()
|
||||||
|
session.remove()
|
||||||
|
|
||||||
|
request.addfinalizer(teardown)
|
||||||
|
return session
|
||||||
180
tests/test_01_db_makeuri.py
Normal file
180
tests/test_01_db_makeuri.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"""Test Database URI Helper.
|
||||||
|
|
||||||
|
Simple Grocery List (Sigl) | sigl.app
|
||||||
|
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sigl.database.util import make_uri
|
||||||
|
from sigl.exc import ConfigError
|
||||||
|
|
||||||
|
BASE_TEST_CONFIG = {
|
||||||
|
'APP_SESSION_KEY': 'test',
|
||||||
|
'APP_TOKEN_KEY': 'test',
|
||||||
|
'APP_TOKEN_SALT': 'test',
|
||||||
|
'DB_DRIVER': 'sqlite',
|
||||||
|
'DB_FILE': '/dev/null',
|
||||||
|
'MAIL_SERVER': 'localhost',
|
||||||
|
'MAIL_SENDER': 'test@localhost',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MockApp(object):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
|
||||||
|
def test_dbconfig_no_driver():
|
||||||
|
'''
|
||||||
|
Application startup should fail if DB_DRIVER is not in the configuration
|
||||||
|
'''
|
||||||
|
test_cfg = dict(BASE_TEST_CONFIG)
|
||||||
|
del test_cfg['DB_DRIVER']
|
||||||
|
|
||||||
|
app = MockApp(test_cfg)
|
||||||
|
with pytest.raises(ConfigError, match='application configuration') as excinfo:
|
||||||
|
make_uri(app)
|
||||||
|
|
||||||
|
# Ensure that the offending key is DB_DRIVER
|
||||||
|
assert excinfo.value.config_key == 'DB_DRIVER'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dbconfig_sqlite_no_file():
|
||||||
|
'''
|
||||||
|
Application Startup should fail if an SQlite database is specified
|
||||||
|
without a DB_FILE key
|
||||||
|
'''
|
||||||
|
test_cfg = dict(BASE_TEST_CONFIG)
|
||||||
|
del test_cfg['DB_FILE']
|
||||||
|
|
||||||
|
app = MockApp(test_cfg)
|
||||||
|
with pytest.raises(ConfigError, match='application configuration') as excinfo:
|
||||||
|
make_uri(app)
|
||||||
|
|
||||||
|
# Ensure that the offending key is DB_FILE
|
||||||
|
assert excinfo.value.config_key == 'DB_FILE'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dbconfig_sqlite_with_file():
|
||||||
|
'''
|
||||||
|
Application Startup should succeed if an SQlite database is specified
|
||||||
|
with a DB_FILE key
|
||||||
|
'''
|
||||||
|
test_cfg = dict(BASE_TEST_CONFIG)
|
||||||
|
app = MockApp(test_cfg)
|
||||||
|
uri = make_uri(app)
|
||||||
|
|
||||||
|
# Ensure that the URI was correctly configured and logged
|
||||||
|
assert uri == 'sqlite:////dev/null'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dbconfig_mysql_no_host():
|
||||||
|
'''
|
||||||
|
Application Startup should fail if a MySQL database is specified
|
||||||
|
without a DB_HOST key
|
||||||
|
'''
|
||||||
|
test_cfg = dict(BASE_TEST_CONFIG)
|
||||||
|
test_cfg['DB_DRIVER'] = 'mysql'
|
||||||
|
app = MockApp(test_cfg)
|
||||||
|
|
||||||
|
with pytest.raises(ConfigError, match='application configuration') as excinfo:
|
||||||
|
make_uri(app)
|
||||||
|
|
||||||
|
# Ensure that the offending key is DB_FILE
|
||||||
|
assert excinfo.value.config_key == 'DB_HOST'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dbconfig_mysql_no_name():
|
||||||
|
'''
|
||||||
|
Application Startup should fail if a MySQL database is specified
|
||||||
|
without a DB_HOST key
|
||||||
|
'''
|
||||||
|
test_cfg = dict(BASE_TEST_CONFIG)
|
||||||
|
test_cfg['DB_DRIVER'] = 'mysql'
|
||||||
|
test_cfg['DB_HOST'] = 'localhost'
|
||||||
|
app = MockApp(test_cfg)
|
||||||
|
|
||||||
|
with pytest.raises(ConfigError, match='application configuration') as excinfo:
|
||||||
|
make_uri(app)
|
||||||
|
|
||||||
|
# Ensure that the offending key is DB_NAME
|
||||||
|
assert excinfo.value.config_key == 'DB_NAME'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dbconfig_mysql_no_username():
|
||||||
|
'''
|
||||||
|
Application Startup should fail if a MySQL database is specified
|
||||||
|
with a DB_USERNAME key but without a DB_PASSWORD key
|
||||||
|
'''
|
||||||
|
test_cfg = dict(BASE_TEST_CONFIG)
|
||||||
|
test_cfg['DB_DRIVER'] = 'mysql'
|
||||||
|
test_cfg['DB_HOST'] = 'localhost'
|
||||||
|
test_cfg['DB_NAME'] = 'test'
|
||||||
|
test_cfg['DB_PASSWORD'] = 'hunter2'
|
||||||
|
app = MockApp(test_cfg)
|
||||||
|
|
||||||
|
with pytest.raises(ConfigError, match='application configuration') as excinfo:
|
||||||
|
make_uri(app)
|
||||||
|
|
||||||
|
# Ensure that the offending key is DB_USERNAME
|
||||||
|
assert excinfo.value.config_key == 'DB_USERNAME'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dbconfig_mysql_without_port():
|
||||||
|
'''
|
||||||
|
Application Startup should succeed when a MySQL database is specified
|
||||||
|
with a DB_PORT key
|
||||||
|
'''
|
||||||
|
test_cfg = dict(BASE_TEST_CONFIG)
|
||||||
|
test_cfg['DB_DRIVER'] = 'mysql'
|
||||||
|
test_cfg['DB_HOST'] = 'localhost'
|
||||||
|
test_cfg['DB_NAME'] = 'test'
|
||||||
|
test_cfg['DB_USERNAME'] = 'root'
|
||||||
|
test_cfg['DB_PASSWORD'] = 'hunter^2'
|
||||||
|
|
||||||
|
app = MockApp(test_cfg)
|
||||||
|
uri = make_uri(app)
|
||||||
|
|
||||||
|
# Ensure that the application startup succeeded
|
||||||
|
assert uri == 'mysql://root:hunter%5E2@localhost/test'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dbconfig_mysql_with_port():
|
||||||
|
'''
|
||||||
|
Application Startup should succeed when a MySQL database is specified
|
||||||
|
with a DB_PORT key
|
||||||
|
'''
|
||||||
|
test_cfg = dict(BASE_TEST_CONFIG)
|
||||||
|
test_cfg['DB_DRIVER'] = 'mysql'
|
||||||
|
test_cfg['DB_HOST'] = 'localhost'
|
||||||
|
test_cfg['DB_PORT'] = 1234
|
||||||
|
test_cfg['DB_NAME'] = 'test'
|
||||||
|
test_cfg['DB_USERNAME'] = 'root'
|
||||||
|
test_cfg['DB_PASSWORD'] = 'hunter^2'
|
||||||
|
|
||||||
|
app = MockApp(test_cfg)
|
||||||
|
uri = make_uri(app)
|
||||||
|
|
||||||
|
# Ensure that the application startup succeeded
|
||||||
|
assert uri == 'mysql://root:hunter%5E2@localhost:1234/test'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dbconfig_mysql_with_username():
|
||||||
|
'''
|
||||||
|
Application Startup should succeed when a MySQL database is specified
|
||||||
|
with a DB_USERNAME key and without a DB_PASSWORD key
|
||||||
|
'''
|
||||||
|
test_cfg = dict(BASE_TEST_CONFIG)
|
||||||
|
test_cfg['DB_DRIVER'] = 'mysql'
|
||||||
|
test_cfg['DB_HOST'] = 'localhost'
|
||||||
|
test_cfg['DB_PORT'] = 1234
|
||||||
|
test_cfg['DB_NAME'] = 'test'
|
||||||
|
test_cfg['DB_USERNAME'] = 'root'
|
||||||
|
|
||||||
|
app = MockApp(test_cfg)
|
||||||
|
uri = make_uri(app)
|
||||||
|
|
||||||
|
# Ensure that the application startup succeeded
|
||||||
|
assert uri == 'mysql://root@localhost:1234/test'
|
||||||
78
tests/test_11_product_model.py
Normal file
78
tests/test_11_product_model.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"""Test the Product and ProductLocation Models.
|
||||||
|
|
||||||
|
Simple Grocery List (Sigl) | sigl.app
|
||||||
|
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from sigl.domain.models import Product, ProductLocation
|
||||||
|
|
||||||
|
# Always use 'app' fixture so ORM gets initialized
|
||||||
|
pytestmark = pytest.mark.usefixtures('app')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_product_model_init(session):
|
||||||
|
"""Test newly created Products have no Locations."""
|
||||||
|
p = Product(name='Eggs', category='Dairy')
|
||||||
|
|
||||||
|
session.add(p)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
assert p.id is not None
|
||||||
|
assert p.name == 'Eggs'
|
||||||
|
assert p.category == 'Dairy'
|
||||||
|
assert not p.locations
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_product_model_can_add_location(session):
|
||||||
|
"""Test that a Location can be added to a Product."""
|
||||||
|
p = Product(name='Eggs', category='Dairy')
|
||||||
|
l = ProductLocation(product=p, store='Pavilions', aisle='Back Wall')
|
||||||
|
|
||||||
|
session.add(p)
|
||||||
|
session.add(l)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
assert l.aisle == 'Back Wall'
|
||||||
|
assert l.bin is None
|
||||||
|
assert l.product == p
|
||||||
|
|
||||||
|
assert l in p.locations
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_product_model_can_add_multiple_stores(session):
|
||||||
|
"""Test that multiple Locations can be added to a Product."""
|
||||||
|
p = Product(name='Eggs', category='Dairy')
|
||||||
|
l1 = ProductLocation(product=p, store='Pavilions', aisle='Back Wall')
|
||||||
|
l2 = ProductLocation(product=p, store='Stater Brothers', aisle='Left Wall')
|
||||||
|
|
||||||
|
session.add(p)
|
||||||
|
session.add(l1)
|
||||||
|
session.add(l2)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
assert l1.product == p
|
||||||
|
assert l1 in p.locations
|
||||||
|
|
||||||
|
assert l2.product == p
|
||||||
|
assert l2 in p.locations
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_product_model_same_store_fails(session):
|
||||||
|
"""Test that two Locations with the same Store cannot be added to a Product."""
|
||||||
|
p = Product(name='Eggs', category='Dairy')
|
||||||
|
l1 = ProductLocation(product=p, store='Pavilions', aisle='Back Wall')
|
||||||
|
l2 = ProductLocation(product=p, store='Pavilions', aisle='Left Wall')
|
||||||
|
|
||||||
|
session.add(p)
|
||||||
|
session.add(l1)
|
||||||
|
session.add(l2)
|
||||||
|
|
||||||
|
with pytest.raises(IntegrityError):
|
||||||
|
session.commit()
|
||||||
11
tests/tox.ini
Normal file
11
tests/tox.ini
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[flake8]
|
||||||
|
ignore = D300,D400,E201,E202,E203,E241,E501,E712
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
addopts = --strict-markers
|
||||||
|
markers =
|
||||||
|
spike: marks test as "spikes" that are intended to develop algorithms but not test behavior (deselected by default, select with '-m spike')
|
||||||
|
unit: marks tests as domain-level unit tests (deselect with '-m "not unit"')
|
||||||
|
wip: marks tests as works in progress and likely to fail (deselect with '-m "not wip"')
|
||||||
|
filterwarnings =
|
||||||
|
ignore::DeprecationWarning:eventlet.*
|
||||||
Reference in New Issue
Block a user