Skip to content
105 changes: 79 additions & 26 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -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",
)
25 changes: 14 additions & 11 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,7 +34,6 @@ def db_session():
with engine.begin() as connection:
truncate_all_tables(connection)
session = SessionLocal(bind=connection)

try:
yield session
finally:
Expand Down
192 changes: 171 additions & 21 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -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()