diff --git a/amanuensis/backend/article.py b/amanuensis/backend/article.py new file mode 100644 index 0000000..932f70b --- /dev/null +++ b/amanuensis/backend/article.py @@ -0,0 +1,62 @@ +""" +Article query interface +""" + +from sqlalchemy import select + +from amanuensis.db import * +from amanuensis.errors import ArgumentError + + +def create( + db: DbContext, + lexicon_id: int, + user_id: int, + character_id: int) -> Article: + """ + Create a new article in a lexicon. + """ + # Verify argument types are correct + if not isinstance(lexicon_id, int): + raise ArgumentError('lexicon_id') + if not isinstance(user_id, int): + raise ArgumentError('user_id') + if character_id is not None and not isinstance(character_id, int): + raise ArgumentError('character_id') + + # Check that the user is a member of this lexicon + mem: Membership = db( + select(Membership) + .where(Membership.user_id == user_id) + .where(Membership.lexicon_id == lexicon_id) + ).scalar_one_or_none() + if not mem: + raise ArgumentError('User is not a member of lexicon') + + # If the character id is provided, check that the user owns the character + # and the character belongs to the lexicon + if character_id is not None: + character: Character = db( + select(Character) + .where(Character.id == character_id) + ).scalar_one_or_none() + if not character: + raise ArgumentError('Character does not exist') + if character.user.id != user_id: + raise ArgumentError('Character is owned by the wrong player') + if character.lexicon.id != lexicon_id: + raise ArgumentError('Character belongs to the wrong lexicon') + signature = character.signature + else: + signature = '~Ersatz Scrivener' + + new_article = Article( + lexicon_id=lexicon_id, + user_id=user_id, + character_id=character_id, + title='Article title', + body=f'\n\n{signature}', + ) + db.session.add(new_article) + db.session.commit() + return new_article diff --git a/tests/conftest.py b/tests/conftest.py index c80c7f8..dc6662a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ pytest test fixtures import pytest from amanuensis.db import DbContext +import amanuensis.backend.character as charq import amanuensis.backend.lexicon as lexiq import amanuensis.backend.membership as memq import amanuensis.backend.user as userq @@ -62,12 +63,52 @@ def make_membership(db: DbContext): @pytest.fixture -def lexicon_with_editor(make_user, make_lexicon, make_membership): +def make_character(db: DbContext): + """Provides a factory function for creating characters, with valid default values.""" + def character_factory(state={'nonce': 1}, **kwargs): + default_kwargs = { + 'name': f'Character {state["nonce"]}', + 'signature': None, + } + state['nonce'] += 1 + updated_kwargs = {**default_kwargs, **kwargs} + return charq.create(db, **updated_kwargs) + return character_factory + + +class TestFactory: + def __init__(self, db, **factories): + self.db = db + self.factories = factories + + def __getattr__(self, name): + return self.factories[name] + + +@pytest.fixture +def make( + db: DbContext, + make_user, + make_lexicon, + make_membership, + make_character) -> TestFactory: + """Fixture that groups all factory fixtures together.""" + return TestFactory( + db, + user=make_user, + lexicon=make_lexicon, + membership=make_membership, + character=make_character, + ) + + +@pytest.fixture +def lexicon_with_editor(make): """Shortcut setup for a lexicon game with an editor.""" - editor = make_user() + editor = make.user() assert editor - lexicon = make_lexicon() + lexicon = make.lexicon() assert lexicon - membership = make_membership(user_id=editor.id, lexicon_id=lexicon.id, is_editor=True) + membership = make.membership(user_id=editor.id, lexicon_id=lexicon.id, is_editor=True) assert membership return (lexicon, editor) diff --git a/tests/test_article.py b/tests/test_article.py new file mode 100644 index 0000000..8b7b6cf --- /dev/null +++ b/tests/test_article.py @@ -0,0 +1,53 @@ +import pytest + +from amanuensis.db import DbContext +import amanuensis.backend.article as artiq + +from amanuensis.errors import ArgumentError + + +def test_create_article(db: DbContext, make): + """Test new article creation""" + # Create two users in a shared lexicon + user1 = make.user() + user2 = make.user() + lexicon1 = make.lexicon() + make.membership(user_id=user1.id, lexicon_id=lexicon1.id) + make.membership(user_id=user2.id, lexicon_id=lexicon1.id) + char1_1 = make.character(lexicon_id=lexicon1.id, user_id=user1.id) + char1_2 = make.character(lexicon_id=lexicon1.id, user_id=user2.id) + + # Create a lexicon that only one user is in + lexicon2 = make.lexicon() + make.membership(user_id=user2.id, lexicon_id=lexicon2.id) + char2_2 = make.character(lexicon_id=lexicon2.id, user_id=user2.id) + + # User cannot create article for another user's character + with pytest.raises(ArgumentError): + artiq.create(db, lexicon1.id, user1.id, char1_2.id) + with pytest.raises(ArgumentError): + artiq.create(db, lexicon1.id, user2.id, char1_1.id) + + # User cannot create article for their character in the wrong lexicon + with pytest.raises(ArgumentError): + artiq.create(db, lexicon1.id, user2.id, char2_2.id) + with pytest.raises(ArgumentError): + artiq.create(db, lexicon2.id, user2.id, char1_2.id) + + # User cannot create article in a lexicon they aren't in + with pytest.raises(ArgumentError): + artiq.create(db, lexicon2.id, user1.id, char1_1.id) + + # User cannot create anonymous articles in a lexicon they aren't in + with pytest.raises(ArgumentError): + artiq.create(db, lexicon2.id, user1.id, character_id=None) + + # Users can create character-owned articles + assert artiq.create(db, lexicon1.id, user1.id, char1_1.id) + assert artiq.create(db, lexicon1.id, user2.id, char1_2.id) + assert artiq.create(db, lexicon2.id, user2.id, char2_2.id) + + # Users can create anonymous articles + assert artiq.create(db, lexicon1.id, user1.id, character_id=None) + assert artiq.create(db, lexicon1.id, user2.id, character_id=None) + assert artiq.create(db, lexicon2.id, user2.id, character_id=None) diff --git a/tests/test_character.py b/tests/test_character.py index 26134ed..0a1e2e5 100644 --- a/tests/test_character.py +++ b/tests/test_character.py @@ -5,7 +5,7 @@ import amanuensis.backend.character as charq from amanuensis.errors import ArgumentError -def test_create_character(db: DbContext, lexicon_with_editor, make_user): +def test_create_character(db: DbContext, lexicon_with_editor, make): """Test creating a character.""" lexicon, user = lexicon_with_editor kwargs = { @@ -33,7 +33,7 @@ def test_create_character(db: DbContext, lexicon_with_editor, make_user): assert char.signature is not None # User must be in lexicon - new_user = make_user() + new_user = make.user() with pytest.raises(ArgumentError): charq.create(**{**kwargs, 'user_id': new_user.id}) diff --git a/tests/test_membership.py b/tests/test_membership.py index 3e76c2a..d478394 100644 --- a/tests/test_membership.py +++ b/tests/test_membership.py @@ -7,12 +7,12 @@ from amanuensis.errors import ArgumentError import amanuensis.backend.membership as memq -def test_create_membership(db: DbContext, make_user, make_lexicon): +def test_create_membership(db: DbContext, make): """Test joining a game.""" # Set up a user and a lexicon - new_user = make_user() + new_user = make.user() assert new_user.id, 'Failed to create user' - new_lexicon = make_lexicon() + new_lexicon = make.lexicon() assert new_lexicon.id, 'Failed to create lexicon' # Add the user to the lexicon as an editor