From 806447e2211cd059c3102aca21dda7d9a217a96f Mon Sep 17 00:00:00 2001 From: david Date: Wed, 25 Feb 2026 21:32:57 +0300 Subject: [PATCH] fix tests for kanban logic --- .github/workflows/backend.yml | 14 ++++- backend/.flake8 | 10 ++++ backend/app/__init__.py | 23 +++------ backend/app/celery/tasks/__init__.py | 13 +++-- backend/app/config.py | 23 ++++++--- backend/app/models/board.py | 30 ++++++++--- backend/app/models/card.py | 33 +++++++++--- backend/app/models/card_label.py | 20 ++++++-- backend/app/models/check_item.py | 13 +++-- backend/app/models/checklist.py | 20 ++++++-- backend/app/models/comment.py | 13 +++-- backend/app/models/label.py | 13 +++-- backend/app/models/list_model.py | 13 +++-- backend/app/models/user.py | 4 +- backend/app/routes/api.py | 8 +-- backend/app/routes/kanban.py | 28 +++++----- backend/tests/conftest.py | 68 ++++++++++++++++++------- backend/tests/routes/__init__.py | 2 +- backend/tests/routes/test_boards.py | 14 ++--- backend/tests/routes/test_cards.py | 8 +-- backend/tests/routes/test_checklists.py | 44 +++++++++++----- backend/tests/routes/test_comments.py | 30 +++++++---- backend/tests/routes/test_labels.py | 20 +++++--- backend/tests/routes/test_lists.py | 16 ++++-- backend/tests/test_models.py | 14 ++--- backend/tests/test_routes.py | 12 +++-- 26 files changed, 346 insertions(+), 160 deletions(-) create mode 100644 backend/.flake8 diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index b475078..144ca43 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -57,12 +57,24 @@ jobs: cd backend flake8 app tests --count --max-complexity=10 --max-line-length=127 --statistics --show-source + # - name: Run migrations + # env: + # TEST_DATABASE_URL: postgresql://test:test@postgres:5432/test_db + # DATABASE_URL: postgresql://test:test@postgres:5432/test_db + # SECRET_KEY: test-secret-key + # JWT_SECRET_KEY: test-jwt-secret + # FLASK_ENV: test + # run: | + # cd backend + # flask db upgrade + - name: Run tests env: + TEST_DATABASE_URL: postgresql://test:test@postgres:5432/test_db DATABASE_URL: postgresql://test:test@postgres:5432/test_db SECRET_KEY: test-secret-key JWT_SECRET_KEY: test-jwt-secret - FLASK_ENV: testing + FLASK_ENV: test run: | cd backend pytest --cov=app --cov-report=xml --cov-report=term diff --git a/backend/.flake8 b/backend/.flake8 new file mode 100644 index 0000000..48d9e6b --- /dev/null +++ b/backend/.flake8 @@ -0,0 +1,10 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203, W503 +exclude = + .git, + __pycache__, + .venv, + venv, + migrations, + alembic \ No newline at end of file diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 667020f..4c4f203 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -29,10 +29,7 @@ def create_app(config_name=None): app.config.from_object(config_by_name[config_name]) print("----------------------------------------------------------") - print( - f"------------------ENVIRONMENT: {config_name}-------------------------------------" - ) - # print(F'------------------CONFIG: {app.config}-------------------------------------') + print(f"------------------ENVIRONMENT: {config_name}-----------------------") print(json.dumps(dict(app.config), indent=2, default=str)) print("----------------------------------------------------------") # Initialize extensions with app @@ -49,19 +46,6 @@ def create_app(config_name=None): init_celery(app) # Import models (required for migrations) - from app.models import ( - board, - card, - card_label, - check_item, - checklist, - comment, - label, - list_model, - order, - product, - user, - ) # noqa: F401 # Register blueprints from app.routes import api_bp, health_bp @@ -87,4 +71,9 @@ def create_app(config_name=None): print(f"422 Error: {error}") return jsonify({"error": "Validation error"}), 422 + @app.teardown_appcontext + def shutdown_session(exception=None): + """Remove session at end of request to return connection to pool""" + db.session.remove() + return app diff --git a/backend/app/celery/tasks/__init__.py b/backend/app/celery/tasks/__init__.py index f58ad37..56da22b 100644 --- a/backend/app/celery/tasks/__init__.py +++ b/backend/app/celery/tasks/__init__.py @@ -5,11 +5,10 @@ Tasks are organized by domain/functionality. # Import all task modules here to ensure they're registered with Celery from . import example_tasks # noqa: F401 - # Re-export tasks for easier imports -from .example_tasks import ( # noqa: F401 - divide_numbers, - print_hello, - send_daily_report, - update_product_statistics, -) +# fmt: off +# noqa: F401 +from .example_tasks import (divide_numbers, print_hello, # noqa: F401 + send_daily_report, update_product_statistics) + +# fmt: on diff --git a/backend/app/config.py b/backend/app/config.py index be03ff3..d0d26bb 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -30,13 +30,13 @@ class Config: "worker_max_tasks_per_child": 100, "broker_connection_retry_on_startup": True, } - + SQLALCHEMY_ENGINE_OPTIONS = { - 'pool_size': 5, # Reduce from default - 'max_overflow': 2, # Reduce overflow - 'pool_timeout': 30, - 'pool_recycle': 1800, # Recycle connections after 30 minutes - 'pool_pre_ping': True, # Verify connections before using + "pool_size": 20, # Reduce from default + "max_overflow": 10, # Reduce overflow + "pool_timeout": 30, + "pool_recycle": 1800, # Recycle connections after 30 minutes + "pool_pre_ping": True, # Verify connections before using } @@ -51,9 +51,18 @@ class TestingConfig(Config): """Testing configuration""" TESTING = True - SQLALCHEMY_DATABASE_URI = "postgresql://kanban:devpassword@localhost:5451/kanban_test" + SQLALCHEMY_DATABASE_URI = os.environ["TEST_DATABASE_URL"] WTF_CSRF_ENABLED = False + # Conservative connection pool settings for testing + SQLALCHEMY_ENGINE_OPTIONS = { + "pool_size": 20, # Only one connection in the pool + "max_overflow": 10, # No overflow connections allowed + "pool_timeout": 30, + "pool_recycle": 3600, # Recycle after 1 hour + "pool_pre_ping": True, # Verify connections before using + } + class ProductionConfig(Config): """Production configuration""" diff --git a/backend/app/models/board.py b/backend/app/models/board.py index 92f3c12..a640f2d 100644 --- a/backend/app/models/board.py +++ b/backend/app/models/board.py @@ -1,4 +1,5 @@ from datetime import UTC, datetime + from sqlalchemy.dialects.postgresql import JSONB from app import db @@ -18,7 +19,12 @@ class Board(db.Model): short_url = db.Column(db.String(500)) # Foreign keys - user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) + user_id = db.Column( + db.Integer, + db.ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) # Timestamps date_last_activity = db.Column(db.DateTime) @@ -36,9 +42,15 @@ class Board(db.Model): limits = db.Column(JSONB) # various limits # Relationships - lists = db.relationship("List", backref="board", cascade="all, delete-orphan", lazy="dynamic") - cards = db.relationship("Card", backref="board", cascade="all, delete-orphan", lazy="dynamic") - labels = db.relationship("Label", backref="board", cascade="all, delete-orphan", lazy="dynamic") + lists = db.relationship( + "List", backref="board", cascade="all, delete-orphan", lazy="dynamic" + ) + cards = db.relationship( + "Card", backref="board", cascade="all, delete-orphan", lazy="dynamic" + ) + labels = db.relationship( + "Label", backref="board", cascade="all, delete-orphan", lazy="dynamic" + ) def to_dict(self): """Convert board to dictionary""" @@ -51,8 +63,12 @@ class Board(db.Model): "short_link": self.short_link, "short_url": self.short_url, "user_id": self.user_id, - "date_last_activity": self.date_last_activity.isoformat() if self.date_last_activity else None, - "date_last_view": self.date_last_view.isoformat() if self.date_last_view else None, + "date_last_activity": self.date_last_activity.isoformat() + if self.date_last_activity + else None, + "date_last_view": self.date_last_view.isoformat() + if self.date_last_view + else None, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, "prefs": self.prefs, @@ -61,4 +77,4 @@ class Board(db.Model): } def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/backend/app/models/card.py b/backend/app/models/card.py index b4d5568..0cd2f9a 100644 --- a/backend/app/models/card.py +++ b/backend/app/models/card.py @@ -1,4 +1,5 @@ from datetime import UTC, datetime + from sqlalchemy.dialects.postgresql import JSONB from app import db @@ -19,8 +20,18 @@ class Card(db.Model): id_short = db.Column(db.Integer) # short ID for URLs # Foreign keys - board_id = db.Column(db.Integer, db.ForeignKey("boards.id", ondelete="CASCADE"), nullable=False, index=True) - list_id = db.Column(db.Integer, db.ForeignKey("lists.id", ondelete="CASCADE"), nullable=False, index=True) + board_id = db.Column( + db.Integer, + db.ForeignKey("boards.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + list_id = db.Column( + db.Integer, + db.ForeignKey("lists.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) # Timestamps date_last_activity = db.Column(db.DateTime) @@ -37,9 +48,15 @@ class Card(db.Model): desc_data = db.Column(JSONB) # Relationships - checklists = db.relationship("Checklist", backref="card", cascade="all, delete-orphan", lazy="dynamic") - labels = db.relationship("CardLabel", backref="card", cascade="all, delete-orphan", lazy="dynamic") - comments = db.relationship("Comment", backref="card", cascade="all, delete-orphan", lazy="dynamic") + checklists = db.relationship( + "Checklist", backref="card", cascade="all, delete-orphan", lazy="dynamic" + ) + labels = db.relationship( + "CardLabel", backref="card", cascade="all, delete-orphan", lazy="dynamic" + ) + comments = db.relationship( + "Comment", backref="card", cascade="all, delete-orphan", lazy="dynamic" + ) def to_dict(self): """Convert card to dictionary""" @@ -54,7 +71,9 @@ class Card(db.Model): "id_short": self.id_short, "board_id": self.board_id, "list_id": self.list_id, - "date_last_activity": self.date_last_activity.isoformat() if self.date_last_activity else None, + "date_last_activity": self.date_last_activity.isoformat() + if self.date_last_activity + else None, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, "badges": self.badges, @@ -63,4 +82,4 @@ class Card(db.Model): } def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/backend/app/models/card_label.py b/backend/app/models/card_label.py index f4c49c3..17acde3 100644 --- a/backend/app/models/card_label.py +++ b/backend/app/models/card_label.py @@ -9,14 +9,26 @@ class CardLabel(db.Model): __tablename__ = "card_labels" id = db.Column(db.Integer, primary_key=True) - card_id = db.Column(db.Integer, db.ForeignKey("cards.id", ondelete="CASCADE"), nullable=False, index=True) - label_id = db.Column(db.Integer, db.ForeignKey("labels.id", ondelete="CASCADE"), nullable=False, index=True) + card_id = db.Column( + db.Integer, + db.ForeignKey("cards.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + label_id = db.Column( + db.Integer, + db.ForeignKey("labels.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) # Timestamps created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) # Unique constraint to prevent duplicate associations - __table_args__ = (db.UniqueConstraint("card_id", "label_id", name="_card_label_uc"),) + __table_args__ = ( + db.UniqueConstraint("card_id", "label_id", name="_card_label_uc"), + ) def to_dict(self): """Convert card label to dictionary""" @@ -28,4 +40,4 @@ class CardLabel(db.Model): } def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/backend/app/models/check_item.py b/backend/app/models/check_item.py index dcbe712..bc8c35d 100644 --- a/backend/app/models/check_item.py +++ b/backend/app/models/check_item.py @@ -15,8 +15,15 @@ class CheckItem(db.Model): due = db.Column(db.DateTime) # Foreign keys - checklist_id = db.Column(db.Integer, db.ForeignKey("checklists.id", ondelete="CASCADE"), nullable=False, index=True) - user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True, index=True) + checklist_id = db.Column( + db.Integer, + db.ForeignKey("checklists.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + user_id = db.Column( + db.Integer, db.ForeignKey("users.id"), nullable=True, index=True + ) # Timestamps created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) @@ -41,4 +48,4 @@ class CheckItem(db.Model): } def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/backend/app/models/checklist.py b/backend/app/models/checklist.py index f04197a..90808cf 100644 --- a/backend/app/models/checklist.py +++ b/backend/app/models/checklist.py @@ -13,8 +13,18 @@ class Checklist(db.Model): pos = db.Column(db.Float) # position for sorting # Foreign keys - board_id = db.Column(db.Integer, db.ForeignKey("boards.id", ondelete="CASCADE"), nullable=False, index=True) - card_id = db.Column(db.Integer, db.ForeignKey("cards.id", ondelete="CASCADE"), nullable=False, index=True) + board_id = db.Column( + db.Integer, + db.ForeignKey("boards.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + card_id = db.Column( + db.Integer, + db.ForeignKey("cards.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) # Timestamps created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) @@ -25,7 +35,9 @@ class Checklist(db.Model): ) # Relationships - check_items = db.relationship("CheckItem", backref="checklist", cascade="all, delete-orphan", lazy="dynamic") + check_items = db.relationship( + "CheckItem", backref="checklist", cascade="all, delete-orphan", lazy="dynamic" + ) def to_dict(self): """Convert checklist to dictionary""" @@ -40,4 +52,4 @@ class Checklist(db.Model): } def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/backend/app/models/comment.py b/backend/app/models/comment.py index bd873d1..60f42f4 100644 --- a/backend/app/models/comment.py +++ b/backend/app/models/comment.py @@ -12,8 +12,15 @@ class Comment(db.Model): text = db.Column(db.Text, nullable=False) # Foreign keys - card_id = db.Column(db.Integer, db.ForeignKey("cards.id", ondelete="CASCADE"), nullable=False, index=True) - user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) + card_id = db.Column( + db.Integer, + db.ForeignKey("cards.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + user_id = db.Column( + db.Integer, db.ForeignKey("users.id"), nullable=False, index=True + ) # Timestamps created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) @@ -35,4 +42,4 @@ class Comment(db.Model): } def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/backend/app/models/label.py b/backend/app/models/label.py index 0bdb205..b8482ef 100644 --- a/backend/app/models/label.py +++ b/backend/app/models/label.py @@ -14,7 +14,12 @@ class Label(db.Model): uses = db.Column(db.Integer, default=0) # Foreign keys - board_id = db.Column(db.Integer, db.ForeignKey("boards.id", ondelete="CASCADE"), nullable=False, index=True) + board_id = db.Column( + db.Integer, + db.ForeignKey("boards.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) # Timestamps created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) @@ -25,7 +30,9 @@ class Label(db.Model): ) # Relationships - card_labels = db.relationship("CardLabel", backref="label", cascade="all, delete-orphan", lazy="dynamic") + card_labels = db.relationship( + "CardLabel", backref="label", cascade="all, delete-orphan", lazy="dynamic" + ) def to_dict(self): """Convert label to dictionary""" @@ -40,4 +47,4 @@ class Label(db.Model): } def __repr__(self): - return f"