diff --git a/app/models.py b/app/models.py index 1f691bf..647686d 100644 --- a/app/models.py +++ b/app/models.py @@ -1,48 +1,101 @@ -from sqlalchemy import TIMESTAMP, CheckConstraint, Column, ForeignKey, Integer, Text, func +from sqlalchemy import ( + TIMESTAMP, + CheckConstraint, + Column, + ForeignKey, + Integer, + Text, + func, +) from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() + class Account(Base): __tablename__ = "accounts" __table_args__ = ( - CheckConstraint("account_role IN ('admin', 'author', 'user')", name="accounts_role_check"), + CheckConstraint( + sqltext="account_role IN ('admin', 'author', 'user')", name="accounts_role_check" + ), ) - account_id = Column("account_id", Integer, primary_key=True, autoincrement=True) - account_username = Column("account_username", Text, unique=True, nullable=False) - account_password = Column("account_password", Text, nullable=False) - account_email = Column("account_email", Text) - account_role = Column("account_role", Text, nullable=False) - account_created_at = Column("account_created_at", TIMESTAMP, server_default=func.now(), nullable=False) + account_id = Column(name="account_id", type_=Integer, primary_key=True, autoincrement=True) + account_username = Column(name="account_username", type_=Text, unique=True, nullable=False) + account_password = Column(name="account_password", type_=Text, nullable=False) + account_email = Column(name="account_email", type_=Text) + account_role = Column(name="account_role", type_=Text, nullable=False) + account_created_at = Column( + name="account_created_at", type_=TIMESTAMP, server_default=func.now() + ) - articles = relationship("Article", back_populates="article_author", cascade="all, delete-orphan") - comments = relationship("Comment", back_populates="comment_author", cascade="all, delete-orphan") + articles = relationship( + argument="Article", back_populates="article_author", cascade="all, delete-orphan" + ) + comments = relationship( + argument="Comment", back_populates="comment_author", cascade="all, delete-orphan" + ) class Article(Base): __tablename__ = "articles" - article_id = Column("article_id", Integer, primary_key=True, autoincrement=True) - article_author_id = Column("article_author_id", Integer, ForeignKey("accounts.account_id", ondelete="CASCADE"), nullable=False) - article_title = Column("article_title", Text, nullable=False) - article_content = Column("article_content", Text, nullable=False) - article_published_at = Column("article_published_at", TIMESTAMP, server_default=func.now(), nullable=False) + article_id = Column(name="article_id", type_=Integer, primary_key=True, autoincrement=True) + article_author_id = Column( + ForeignKey("accounts.account_id", ondelete="CASCADE"), + name="article_author_id", + type_=Integer, + nullable=False, + ) + article_title = Column(name="article_title", type_=Text, nullable=False) + article_content = Column(name="article_content", type_=Text, nullable=False) + article_published_at = Column( + name="article_published_at", type_=TIMESTAMP, server_default=func.now() + ) - article_author = relationship("Account", back_populates="articles") - article_comments = relationship("Comment", back_populates="comment_article", cascade="all, delete-orphan") + article_author = relationship(argument="Account", back_populates="articles") + article_comments = relationship( + argument="Comment", back_populates="comment_article", cascade="all, delete-orphan" + ) class Comment(Base): __tablename__ = "comments" - comment_id = Column("comment_id", Integer, primary_key=True, autoincrement=True) - comment_article_id = Column("comment_article_id", Integer, ForeignKey("articles.article_id", ondelete="CASCADE"), nullable=False) - comment_written_account_id = Column("comment_written_account_id", Integer, ForeignKey("accounts.account_id", ondelete="CASCADE"), nullable=False) - comment_reply_to = Column("comment_reply_to", Integer, ForeignKey("comments.comment_id"), nullable=True) - comment_content = Column("comment_content", Text, nullable=False) - comment_posted_at = Column("comment_posted_at", TIMESTAMP, server_default=func.now(), nullable=False) + comment_id = Column(name="comment_id", type_=Integer, primary_key=True, autoincrement=True) + comment_article_id = Column( + ForeignKey("articles.article_id", ondelete="CASCADE"), + name="comment_article_id", + type_=Integer, + nullable=False, + ) + comment_written_account_id = Column( + ForeignKey("accounts.account_id", ondelete="CASCADE"), + name="comment_written_account_id", + type_=Integer, + nullable=False, + ) + comment_reply_to = Column( + ForeignKey("comments.comment_id"), + name="comment_reply_to", + type_=Integer, + nullable=True, + ) + comment_content = Column(name="comment_content", type_=Text, nullable=False) + comment_posted_at = Column( + name="comment_posted_at", type_=TIMESTAMP, server_default=func.now() + ) - comment_article = relationship("Article", back_populates="article_comments") - comment_author = relationship("Account", back_populates="comments") - replies = relationship("Comment", backref="parent", remote_side=[comment_id]) + comment_article = relationship(argument="Article", back_populates="article_comments") + comment_author = relationship(argument="Account", back_populates="comments") + reply_to_comment = relationship( + argument="Comment", + remote_side=[comment_id], + back_populates="comment_replies", + uselist=False, + ) + comment_replies = relationship( + argument="Comment", + back_populates="reply_to_comment", + cascade="all, delete-orphan", + ) diff --git a/tests/conftest.py b/tests/conftest.py index d621886..6060013 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,20 +5,24 @@ from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker -from app.models import Base +from app.models import Account, Article, Base, Comment file_env = dotenv_values(".env.test") +# CI (GitHub Actions) provides TEST_DATABASE_URL via environment variables. +# Locally, we fall back to .env.test for a dedicated test database. +# This keeps the test configuration consistent across environments without changing the code. +database_url = file_env.get("TEST_DATABASE_URL") or os.getenv("TEST_DATABASE_URL") +engine = create_engine(database_url) +SessionLocal = sessionmaker() -# Database selection logic: -# 1. Local environment: use TEST_DATABASE_URL from .env.test -# 2. Optional override: use TEST_DATABASE_URL from os.environ if provided -database_url = ( - file_env.get("TEST_DATABASE_URL") - or os.getenv("TEST_DATABASE_URL") -) +def account_model(): + return Account -engine = create_engine(database_url) -SessionLocal = sessionmaker(bind=engine) +def article_model(): + return Article + +def comment_model(): + return Comment def truncate_all_tables(connection): tables = Base.metadata.sorted_tables @@ -30,7 +34,6 @@ def db_session(): with engine.begin() as connection: truncate_all_tables(connection) session = SessionLocal(bind=connection) - try: yield session finally: diff --git a/tests/test_models.py b/tests/test_models.py index c4a02ab..234b993 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,41 +1,191 @@ -from app.models import Account, Article, Comment +import pytest +import sqlalchemy + +from tests.conftest import account_model, article_model, comment_model + + +def make_account( + account_username="Xxx__D4RK_V4D0R__xxX", + account_password="987654321abcdefg@", + account_email="C4T@exemple.com", + account_role="user", +): + Account = account_model() + return Account( + account_username=account_username, + account_password=account_password, + account_email=account_email, + account_role=account_role, + ) + +def make_article( + article_author_id, + article_title="Luke, I'm your father !", + article_content=( + 'On the platform, Darth Vader stepped forward and spoke the truth: ' + '"Luke, I am your father." Shocked, Luke backed away, unable to accept it.' + ), +): + Article = article_model() + return Article( + article_author_id=article_author_id, + article_title=article_title, + article_content=article_content, + ) + +def make_comment( + comment_article_id, + comment_written_account_id, + comment_content="Bravo !", +): + Comment = comment_model() + return Comment( + comment_article_id=comment_article_id, + comment_written_account_id=comment_written_account_id, + comment_content=comment_content, + ) def test_create_account(db_session): - account = Account(account_username="pytest_user_account", account_password="123456789", account_email="test_account@example.com", account_role="user") + account = make_account() db_session.add(account) db_session.commit() - result = db_session.query(Account).filter_by(account_username="pytest_user_account").first() - assert result is not None - assert result.account_password is not None + Account = account_model() + result = ( + db_session.query(Account) + .filter_by(account_username=account.account_username) + .first() + ) + assert result.account_username == "Xxx__D4RK_V4D0R__xxX" + assert result.account_password == "987654321abcdefg@" + assert result.account_email == "C4T@exemple.com" assert result.account_role == "user" def test_create_article(db_session): - author = Account(account_username="pytest_author_article", account_password="123456789", account_email="author_article@test.com", account_role="author") + author = make_account(account_role="author") db_session.add(author) db_session.commit() - article = Article(article_author_id=author.account_id, article_title="Titre article", article_content="Contenu article") + article = make_article(article_author_id=author.account_id) db_session.add(article) db_session.commit() - result = db_session.query(Article).filter_by(article_title="Titre article").first() - assert result is not None - assert result.article_author.account_username == "pytest_author_article" - assert result.article_author.account_password is not None + Article = article_model() + result = ( + db_session.query(Article) + .filter_by(article_title=article.article_title) + .first() + ) + expected_article_content = ( + 'On the platform, Darth Vader stepped forward and spoke the truth: ' + '"Luke, I am your father." Shocked, Luke backed away, unable to accept it.' + ) + assert result.article_author_id == 1 + assert result.article_title == "Luke, I'm your father !" + assert result.article_content == expected_article_content + assert result.article_author.account_role == "author" def test_create_comment(db_session): - author = Account(account_username="pytest_author_comment", account_password="123456789", account_email="author_comment@test.com", account_role="author") - user = Account(account_username="pytest_user_comment", account_password="123456789", account_email="user_comment@test.com", account_role="user") + author = make_account(account_role="author") + user = make_account( + account_username="Bob", + account_password="2789@_124BBt", + account_email="bob@funny.com" + ) db_session.add_all([author, user]) db_session.commit() - article = Article(article_author_id=author.account_id, article_title="Titre comment", article_content="Contenu comment") + article = make_article(article_author_id=author.account_id) db_session.add(article) db_session.commit() - comment = Comment(comment_article_id=article.article_id, comment_written_account_id=user.account_id, comment_content="Bravo !") + comment = make_comment(comment_article_id=article.article_id, comment_written_account_id=user.account_id) db_session.add(comment) db_session.commit() - result = db_session.query(Comment).filter_by(comment_content="Bravo !").first() - assert result is not None - assert result.comment_author.account_username == "pytest_user_comment" - assert result.comment_author.account_password is not None - assert result.comment_article.article_title == "Titre comment" - assert result.comment_article.article_author.account_password is not None + Comment = comment_model() + result = ( + db_session.query(Comment) + .filter_by(comment_content=comment.comment_content) + .first() + ) + assert result.comment_article_id == 1 + assert result.comment_written_account_id == 2 + assert result.comment_author.account_username == "Bob" + assert result.comment_author.account_password == "2789@_124BBt" + assert result.comment_author.account_email == "bob@funny.com" + assert result.comment_content == "Bravo !" + +def test_create_comment_reply_to(db_session): + author = make_account(account_role="author") + user = make_account( + account_username="Bob", + account_password="2789@_124BBt", + account_email="bob@funny.com" + ) + db_session.add_all([author, user]) + db_session.commit() + article = make_article(article_author_id=author.account_id) + db_session.add(article) + db_session.commit() + parent_comment = make_comment( + comment_article_id=article.article_id, + comment_written_account_id=user.account_id, + comment_content="Bravo !" + ) + db_session.add(parent_comment) + db_session.commit() + reply_comment = make_comment( + comment_article_id=article.article_id, + comment_written_account_id=user.account_id, + comment_content="Thank you !" + ) + reply_comment.comment_reply_to = parent_comment.comment_id + db_session.add(reply_comment) + db_session.commit() + Comment = comment_model() + result = ( + db_session.query(Comment) + .filter_by(comment_content=reply_comment.comment_content) + .first() + ) + assert result.comment_reply_to == 1 + assert result.reply_to_comment.comment_id == 1 + assert result.comment_content == "Thank you !" + +def test_account_username_unique(db_session): + first = make_account() + db_session.add(first) + db_session.commit() + second = make_account() + db_session.add(second) + with pytest.raises(sqlalchemy.exc.IntegrityError): + db_session.commit() + +def test_account_missing_username(db_session): + account = make_account(account_username=None) + db_session.add(account) + with pytest.raises(sqlalchemy.exc.IntegrityError): + db_session.commit() + +def test_account_role_invalid(db_session): + account = make_account(account_role="superadmin") + db_session.add(account) + with pytest.raises(sqlalchemy.exc.IntegrityError): + db_session.commit() + +def test_article_missing_title(db_session): + author = make_account(account_role="author") + db_session.add(author) + db_session.commit() + Article = article_model() + article = Article(article_author_id=author.account_id, article_title=None,) + db_session.add(article) + with pytest.raises(sqlalchemy.exc.IntegrityError): + db_session.commit() + +def test_article_missing_content(db_session): + author = make_account(account_role="author") + db_session.add(author) + db_session.commit() + Article = article_model() + article = Article(article_author_id=author.account_id, article_content=None) + db_session.add(article) + with pytest.raises(sqlalchemy.exc.IntegrityError): + db_session.commit() +