Make a partly-undone import retryable, and refuse a plan it cannot deliver
This commit is contained in:
69
tests/test_core/test_to_dict_floor.py
Normal file
69
tests/test_core/test_to_dict_floor.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""BaseModel.to_dict never publishes a secret-bearing column.
|
||||
|
||||
The default serialiser walks __table__.columns and emits every one, so a model
|
||||
that does not override it publishes whatever it happens to carry - including
|
||||
columns added later by someone not thinking about this method. User is the live
|
||||
example: it has no override, so User.to_dict() returned its bcrypt hash. Nothing
|
||||
called it in a response path, which was luck.
|
||||
|
||||
A blanket allowlist is the stronger rule and would empty the output of every
|
||||
model relying on the default, so this pins the floor instead: named columns are
|
||||
never emitted, and a model may extend the set.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.core.models import User
|
||||
from shopdb.core.models.base import BaseModel
|
||||
|
||||
|
||||
def test_the_default_serialiser_hides_a_password_hash(app):
|
||||
with app.app_context():
|
||||
user = User(username='floor', email='floor@example.invalid',
|
||||
passwordhash='$2b$12$notarealhashbutlookslikeone')
|
||||
data = user.to_dict()
|
||||
assert 'passwordhash' not in data
|
||||
# Still a useful dict - this is a filter, not a mute button.
|
||||
assert data['username'] == 'floor'
|
||||
assert data['email'] == 'floor@example.invalid'
|
||||
|
||||
|
||||
def test_every_hidden_name_is_actually_dropped(app):
|
||||
"""Each entry earns its place: drop one and the column comes back."""
|
||||
with app.app_context():
|
||||
user = User(username='floor2', email='f2@example.invalid',
|
||||
passwordhash='x')
|
||||
assert 'passwordhash' in {c.name for c in User.__table__.columns}
|
||||
assert 'passwordhash' in BaseModel.HIDDEN_COLUMNS
|
||||
assert 'passwordhash' not in user.to_dict()
|
||||
|
||||
|
||||
def test_a_model_can_extend_the_hidden_set(app):
|
||||
"""A model with its own secret adds to the floor rather than replacing
|
||||
to_dict wholesale."""
|
||||
with app.app_context():
|
||||
class Extended(User):
|
||||
__abstract__ = True
|
||||
HIDDEN_COLUMNS = BaseModel.HIDDEN_COLUMNS | {'email'}
|
||||
|
||||
assert 'passwordhash' in Extended.HIDDEN_COLUMNS
|
||||
assert 'email' in Extended.HIDDEN_COLUMNS
|
||||
|
||||
|
||||
@pytest.mark.parametrize('name', sorted(BaseModel.HIDDEN_COLUMNS))
|
||||
def test_no_model_publishes_a_hidden_column_through_the_default(name, app):
|
||||
"""Sweep every mapped model: if it carries a hidden column AND relies on the
|
||||
default to_dict, the column must not come out."""
|
||||
with app.app_context():
|
||||
from shopdb.extensions import db
|
||||
|
||||
for mapper in db.Model.registry.mappers:
|
||||
model = mapper.class_
|
||||
table = getattr(model, '__table__', None)
|
||||
if table is None or name not in table.columns:
|
||||
continue
|
||||
if 'to_dict' in vars(model):
|
||||
continue # overrides the default; its own business
|
||||
assert name in getattr(model, 'HIDDEN_COLUMNS', frozenset()), (
|
||||
'%s carries %s and uses the default to_dict, so it would be '
|
||||
'published' % (model.__name__, name))
|
||||
Reference in New Issue
Block a user