diff --git a/amanuensis/backend/character.py b/amanuensis/backend/character.py index c9be1fc..49e51a1 100644 --- a/amanuensis/backend/character.py +++ b/amanuensis/backend/character.py @@ -78,4 +78,6 @@ def get_in_lexicon(db: DbContext, lexicon_id: int) -> Sequence[Character]: def try_from_public_id(db: DbContext, public_id: UUID) -> Optional[Character]: """Get a character by its public id.""" - return db(select(Character).where(Character.public_id == public_id)).scalar_one_or_none() + return db( + select(Character).where(Character.public_id == public_id) + ).scalar_one_or_none() diff --git a/amanuensis/db/models.py b/amanuensis/db/models.py index 5f7a600..2e63c4c 100644 --- a/amanuensis/db/models.py +++ b/amanuensis/db/models.py @@ -30,6 +30,7 @@ class Uuid(TypeDecorator): """ impl = CHAR(32) + cache_ok = True def process_bind_param(self, value, dialect): if value is None: diff --git a/amanuensis/server/lexicon/characters/__init__.py b/amanuensis/server/lexicon/characters/__init__.py index 19773ed..c2b5c86 100644 --- a/amanuensis/server/lexicon/characters/__init__.py +++ b/amanuensis/server/lexicon/characters/__init__.py @@ -2,7 +2,7 @@ from typing import Optional import uuid from flask import Blueprint, render_template, url_for, g, flash -from flask_login import current_user +from flask_login import current_user from werkzeug.utils import redirect from amanuensis.backend import charq @@ -15,14 +15,14 @@ from .forms import CharacterCreateForm bp = Blueprint("characters", __name__, url_prefix="/characters", template_folder=".") -@bp.get('/') +@bp.get("/") @lexicon_param @player_required def list(name): - return render_template('characters.jinja', name=name) + return render_template("characters.jinja", name=name) -@bp.route('/edit/', methods=['GET', 'POST']) +@bp.route("/edit/", methods=["GET", "POST"]) @lexicon_param @player_required def edit(name, character_id): @@ -30,11 +30,11 @@ def edit(name, character_id): char_uuid = uuid.UUID(character_id) except: flash("Character not found") - return redirect(url_for('lexicon.characters.list', name=name)) + return redirect(url_for("lexicon.characters.list", name=name)) character: Optional[Character] = charq.try_from_public_id(g.db, char_uuid) if not character: flash("Character not found") - return redirect(url_for('lexicon.characters.list', name=name)) + return redirect(url_for("lexicon.characters.list", name=name)) form = CharacterCreateForm() @@ -42,7 +42,7 @@ def edit(name, character_id): # GET form.name.data = character.name form.signature.data = character.signature - return render_template('characters.edit.jinja', character=character, form=form) + return render_template("characters.edit.jinja", character=character, form=form) else: # POST @@ -51,18 +51,24 @@ def edit(name, character_id): character.name = form.name.data character.signature = form.signature.data g.db.session.commit() - return redirect(url_for('lexicon.characters.list', name=name)) + return redirect(url_for("lexicon.characters.list", name=name)) else: # POST submitted invalid data - return render_template('characters.edit.jinja', character=character, form=form) + return render_template( + "characters.edit.jinja", character=character, form=form + ) -@bp.get('/new/') +@bp.get("/new/") @lexicon_param @player_required def new(name): dummy_name = f"{current_user.username}'s new character" dummy_signature = "~" - charq.create(g.db, g.lexicon.id, current_user.id, dummy_name, dummy_signature) - return redirect(url_for('lexicon.characters.list', name=name)) + char = charq.create( + g.db, g.lexicon.id, current_user.id, dummy_name, dummy_signature + ) + return redirect( + url_for("lexicon.characters.edit", name=name, character_id=char.public_id) + ) diff --git a/tests/test_character.py b/tests/test_character.py new file mode 100644 index 0000000..ccd5b51 --- /dev/null +++ b/tests/test_character.py @@ -0,0 +1,74 @@ +import os +from urllib.parse import urlsplit + +from bs4 import BeautifulSoup +from flask import Flask, url_for + +from amanuensis.backend import memq, charq +from amanuensis.db import DbContext + +from tests.conftest import ObjectFactory + + +def test_character_view(db: DbContext, app: Flask, make: ObjectFactory): + """Test the lexicon character list, create, and edit pages.""" + username: str = f"user_{os.urandom(8).hex()}" + charname: str = f"char_{os.urandom(8).hex()}" + char_sig: str = f"signature_{os.urandom(8).hex()}" + # ub: bytes = username.encode("utf8") + + with app.test_client() as client: + # Create the user and log in + user = make.user(username=username, password=username) + assert user + user_client = make.client(user.id) + assert client + user_client.login(client) + + # Create a lexicon and join + lexicon = make.lexicon() + assert lexicon + mem = memq.create(db, user.id, lexicon.id, is_editor=False) + assert mem + + # The character page exists + list_url = url_for("lexicon.characters.list", name=lexicon.name) + response = client.get(list_url) + assert response.status_code == 200 + assert charname.encode("utf8") not in response.data + assert char_sig.encode("utf8") not in response.data + new_url = url_for("lexicon.characters.new", name=lexicon.name) + assert new_url.encode("utf8") in response.data + + # The character creation endpoint works + response = client.get(new_url) + assert response.status_code == 302 + chars = list(charq.get_in_lexicon(db, lexicon.id)) + assert len(chars) == 1 + assert chars[0].user_id == user.id + created_redirect = response.location + assert str(chars[0].public_id) in created_redirect + + # The character edit page works + response = client.get(created_redirect) + assert chars[0].name.encode("utf8") in response.data + assert chars[0].signature.encode("utf8") in response.data + assert b"csrf_token" in response.data + + # Submitting the edit page works + soup = BeautifulSoup(response.data, features="html.parser") + csrf_token = soup.find(id="csrf_token")["value"] + assert csrf_token + response = client.post( + created_redirect, + data={"name": charname, "signature": char_sig, "csrf_token": csrf_token}, + ) + print(response.data.decode("utf8")) + assert 300 <= response.status_code <= 399 + + # The character is updated + chars = list(charq.get_in_lexicon(db, lexicon.id)) + assert len(chars) == 1 + assert chars[0].user_id == user.id + assert chars[0].name == charname + assert chars[0].signature == char_sig