Get home page and login working #14

Merged
Jaculabilis merged 20 commits from tvb/server-auth into develop 2021-06-29 03:23:59 +00:00
4 changed files with 135 additions and 69 deletions
Showing only changes of commit 6c8f341a4e - Show all commits

View File

@ -44,16 +44,26 @@ class DbContext:
# Create an engine and enable foreign key constraints in sqlite # Create an engine and enable foreign key constraints in sqlite
self.engine = create_engine(self.db_uri, echo=echo) self.engine = create_engine(self.db_uri, echo=echo)
@event.listens_for(self.engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record): def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor() cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON") cursor.execute("PRAGMA foreign_keys=ON")
cursor.close() cursor.close()
event.listens_for(self.engine, "connect")(set_sqlite_pragma)
# Create a thread-safe session factory # Create a thread-safe session factory
self.session = scoped_session( sm = sessionmaker(bind=self.engine)
sessionmaker(bind=self.engine), scopefunc=get_ident
) def add_lifecycle_hook(sm, from_state, to_state):
def object_lifecycle_hook(_, obj):
print(f"object moved from {from_state} to {to_state}: {obj}")
event.listens_for(sm, f"{from_state}_to_{to_state}")(object_lifecycle_hook)
if echo:
add_lifecycle_hook(sm, "persistent", "detached")
self.session = scoped_session(sm, scopefunc=get_ident)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
"""Provides shortcut access to session.execute.""" """Provides shortcut access to session.execute."""

View File

@ -4,7 +4,10 @@ pytest test fixtures
import os import os
import pytest import pytest
import tempfile import tempfile
from typing import Optional
from bs4 import BeautifulSoup
from flask.testing import FlaskClient
from sqlalchemy.orm.session import close_all_sessions from sqlalchemy.orm.session import close_all_sessions
import amanuensis.backend.character as charq import amanuensis.backend.character as charq
@ -12,7 +15,7 @@ import amanuensis.backend.lexicon as lexiq
import amanuensis.backend.membership as memq import amanuensis.backend.membership as memq
import amanuensis.backend.user as userq import amanuensis.backend.user as userq
from amanuensis.config import AmanuensisConfig from amanuensis.config import AmanuensisConfig
from amanuensis.db import DbContext from amanuensis.db import DbContext, User, Lexicon, Membership, Character
from amanuensis.server import get_app from amanuensis.server import get_app
@ -33,12 +36,53 @@ def db(request) -> DbContext:
return db return db
@pytest.fixture class UserClient:
def make_user(db: DbContext): """Class encapsulating user web operations."""
"""Provides a factory function for creating users, with valid default values."""
def user_factory(state={"nonce": 1}, **kwargs): def __init__(self, db: DbContext, user_id: int):
default_kwargs = { self.db = db
self.user_id = user_id
def login(self, client: FlaskClient):
"""Log the user in."""
user: Optional[User] = userq.from_id(self.db, self.user_id)
assert user is not None
# Set the user's password so we know what it is later
password = os.urandom(8).hex()
userq.password_set(self.db, user.username, password)
# Log in
response = client.get("/auth/login/")
assert response.status_code == 200
soup = BeautifulSoup(response.data, features="html.parser")
csrf_token = soup.find(id="csrf_token")
assert csrf_token is not None
response = client.post(
"/auth/login/",
data={
"username": user.username,
"password": password,
"csrf_token": csrf_token["value"],
},
)
assert 300 <= response.status_code <= 399
def logout(self, client: FlaskClient):
"""Log the user out."""
response = client.get("/auth/logout/")
assert 300 <= response.status_code <= 399
class ObjectFactory:
"""Factory class."""
def __init__(self, db):
self.db = db
def user(self, state={"nonce": 1}, **kwargs) -> User:
"""Factory function for creating users, with valid default values."""
default_kwargs: dict = {
"username": f'test_user_{state["nonce"]}', "username": f'test_user_{state["nonce"]}',
"password": "password", "password": "password",
"display_name": None, "display_name": None,
@ -46,87 +90,54 @@ def make_user(db: DbContext):
"is_site_admin": False, "is_site_admin": False,
} }
state["nonce"] += 1 state["nonce"] += 1
updated_kwargs = {**default_kwargs, **kwargs} updated_kwargs: dict = {**default_kwargs, **kwargs}
return userq.create(db, **updated_kwargs) return userq.create(self.db, **updated_kwargs)
return user_factory def lexicon(self, state={"nonce": 1}, **kwargs) -> Lexicon:
"""Factory function for creating lexicons, with valid default values."""
default_kwargs: dict = {
@pytest.fixture
def make_lexicon(db: DbContext):
"""Provides a factory function for creating lexicons, with valid default values."""
def lexicon_factory(state={"nonce": 1}, **kwargs):
default_kwargs = {
"name": f'Test_{state["nonce"]}', "name": f'Test_{state["nonce"]}',
"title": None, "title": None,
"prompt": f'Test Lexicon game {state["nonce"]}', "prompt": f'Test Lexicon game {state["nonce"]}',
} }
state["nonce"] += 1 state["nonce"] += 1
updated_kwargs = {**default_kwargs, **kwargs} updated_kwargs: dict = {**default_kwargs, **kwargs}
lex = lexiq.create(db, **updated_kwargs) lex = lexiq.create(self.db, **updated_kwargs)
lex.joinable = True lex.joinable = True
db.session.commit() self.db.session.commit()
return lex return lex
return lexicon_factory def membership(self, **kwargs) -> Membership:
"""Factory function for creating memberships, with valid default values."""
default_kwargs: dict = {
@pytest.fixture
def make_membership(db: DbContext):
"""Provides a factory function for creating memberships, with valid default values."""
def membership_factory(**kwargs):
default_kwargs = {
"is_editor": False, "is_editor": False,
} }
updated_kwargs = {**default_kwargs, **kwargs} updated_kwargs: dict = {**default_kwargs, **kwargs}
return memq.create(db, **updated_kwargs) return memq.create(self.db, **updated_kwargs)
return membership_factory def character(self, state={"nonce": 1}, **kwargs) -> Character:
"""Factory function for creating characters, with valid default values."""
default_kwargs: dict = {
@pytest.fixture
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"]}', "name": f'Character {state["nonce"]}',
"signature": None, "signature": None,
} }
state["nonce"] += 1 state["nonce"] += 1
updated_kwargs = {**default_kwargs, **kwargs} updated_kwargs: dict = {**default_kwargs, **kwargs}
return charq.create(db, **updated_kwargs) return charq.create(self.db, **updated_kwargs)
return character_factory def client(self, user_id: int) -> UserClient:
"""Factory function for user test clients."""
return UserClient(self.db, user_id)
class TestFactory:
def __init__(self, db, **factories):
self.db = db
self.factories = factories
def __getattr__(self, name):
return self.factories[name]
@pytest.fixture @pytest.fixture
def make( def make(db: DbContext) -> ObjectFactory:
db: DbContext, make_user, make_lexicon, make_membership, make_character """Fixture that provides a factory class."""
) -> TestFactory: return ObjectFactory(db)
"""Fixture that groups all factory fixtures together."""
return TestFactory(
db,
user=make_user,
lexicon=make_lexicon,
membership=make_membership,
character=make_character,
)
@pytest.fixture @pytest.fixture
def lexicon_with_editor(make): def lexicon_with_editor(make: ObjectFactory):
"""Shortcut setup for a lexicon game with an editor.""" """Shortcut setup for a lexicon game with an editor."""
editor = make.user() editor = make.user()
assert editor assert editor

View File

@ -11,7 +11,7 @@ def test_auth_circuit(app: Flask, make):
"""Test the user login/logout path.""" """Test the user login/logout path."""
username: str = f"user_{os.urandom(8).hex()}" username: str = f"user_{os.urandom(8).hex()}"
ub: bytes = username.encode("utf8") ub: bytes = username.encode("utf8")
user: User = make.user(username=username, password=username) assert make.user(username=username, password=username)
with app.test_client() as client: with app.test_client() as client:
# User should not be logged in # User should not be logged in

45
tests/test_home.py Normal file
View File

@ -0,0 +1,45 @@
import os
from urllib.parse import urlsplit
from flask import Flask
from amanuensis.db import DbContext, User, Lexicon
from .conftest import ObjectFactory, UserClient
def test_game_visibility(db: DbContext, app: Flask, make: ObjectFactory):
"""Test lexicon visibility settings."""
user: User = make.user()
auth: UserClient = make.client(user.id)
public_joined: Lexicon = make.lexicon()
public_joined.public = True
make.membership(user_id=auth.user_id, lexicon_id=public_joined.id)
public_joined_title = public_joined.full_title
private_joined: Lexicon = make.lexicon()
private_joined.public = False
make.membership(user_id=auth.user_id, lexicon_id=private_joined.id)
private_joined_title = private_joined.full_title
public_open: Lexicon = make.lexicon()
public_open.public = True
db.session.commit()
public_open_title = public_open.full_title
private_open: Lexicon = make.lexicon()
private_open.public = False
db.session.commit()
private_open_title = private_open.full_title
with app.test_client() as client:
auth.login(client)
# Check that lexicons appear if they should
response = client.get("/home/")
assert response.status_code == 200
assert public_joined_title.encode("utf8") in response.data
assert private_joined_title.encode("utf8") in response.data
assert public_open_title.encode("utf8") in response.data
assert private_open_title.encode("utf8") not in response.data