Skip to content

feat(tests): improve test coverage#234

Open
DogukanUrker wants to merge 9 commits intomainfrom
feat/improve-e2e-tests
Open

feat(tests): improve test coverage#234
DogukanUrker wants to merge 9 commits intomainfrom
feat/improve-e2e-tests

Conversation

@DogukanUrker
Copy link
Copy Markdown
Owner

@DogukanUrker DogukanUrker commented Feb 6, 2026

fixes #222

Summary by CodeRabbit

  • Bug Fixes

    • Enforced login for post actions; restricted post/comment deletion to authors or admins; improved logging, error handling, and redirects for unauthorized attempts
  • Tests

    • Added a comprehensive end-to-end test suite covering posts, comments, account flows, admin panel, search, and UI page objects
  • Documentation

    • Modernized test-run docs with targeted test commands, fixture guidance, and parallel execution notes

Add a comprehensive set of end-to-end tests and supporting utilities, and harden post/comment handling in the app.

- app/routes/post.py: require login for POST actions, add permission checks for deleting posts/comments, validate comment existence, and log unauthorized attempts; ensure proper redirects.
- tests/: add many E2E test modules (account, admin, home, post, search, pages) covering create/edit/delete posts, comments, admin flows, dashboard, static pages, and auth flows.
- tests/e2e/pages/: add page objects CreatePostPage and PostPage and export them from package init.
- tests/e2e/helpers/database_helpers.py: add helpers to create/query test posts and comments (create_test_post, create_test_comment, get_post_by_title/url_id, get_comment_by_id, etc.) and small utilities used by tests.
- tests/e2e/helpers/__init__.py: export new helper functions.
- tests/README.md: update run instructions, suite overview, fixtures, and CI notes.

These changes provide test coverage and developer tooling for E2E scenarios and fix authorization/validation gaps in post/comment handling.
@DogukanUrker DogukanUrker self-assigned this Feb 6, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 6, 2026

Warning

Rate limit exceeded

@DogukanUrker has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 4 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds route guard decorators enforcing login/admin checks and tightens post/comment delete permissions; introduces app/utils/route_guards.py. Large E2E test expansion: new tests, page objects, and DB helpers across account, admin, home, post, and search domains; updates test docs and translation keys.

Changes

Cohort / File(s) Summary
Route guards & access control
app/utils/route_guards.py, app/routes/*.py (many: post.py, create_post.py, edit_post.py, dashboard.py, change_password.py, change_username.py, change_profile_picture.py, verify_user.py, admin_panel*.py, account_settings.py, etc.)
Adds login_required and admin_required decorators and applies them across many routes; removes repetitive session-based login checks and centralizes auth, adds explicit ownership/admin checks for post/comment deletions in app/routes/post.py.
Post route changes
app/routes/post.py
Enforces login for submitting comments/posts, adds ownership/admin validation for deleting posts and comments, improves comment deletion flow (existence check, logging), and standardizes redirects for unauthorized attempts.
E2E test suite — new tests
tests/e2e/account/*, tests/e2e/admin/*, tests/e2e/home/*, tests/e2e/post/*, tests/e2e/search/*
Adds numerous Playwright E2E test modules (account, admin, home, post, search) covering settings, dashboard, admin flows, post lifecycle, search/category, and static pages; many new test classes and helpers.
E2E page objects & exports
tests/e2e/pages/__init__.py, tests/e2e/pages/create_post_page.py, tests/e2e/pages/post_page.py
New Page Object classes CreatePostPage and PostPage with navigation, form interactions, comment and post lifecycle helpers; updated __all__ exports.
E2E DB helpers & exports
tests/e2e/helpers/database_helpers.py, tests/e2e/helpers/__init__.py
New DB helper functions for creating and querying test posts/comments (create_test_post, create_test_comment, get_post_by_title/url_id, get_post_views, get_comment_by_id/for_post) and re-exported in helpers package.
Test docs
tests/README.md
Rewrites E2E test README: targeted pytest commands, suite coverage notes, fixtures and POM guidance, parallel execution instructions, and updated helpers/fixtures descriptions.
Translations & template key rename
app/templates/change_username.html, app/translations/*.json
Renames translation keys from change_user_namechange_username across templates and multiple locale JSON files; small template key updates.
Misc — package initializers
many tests/e2e/*/__init__.py
Adds package initializer files for new E2E test packages (account, admin, home, post, search).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (Browser/Test)
    participant Route as Flask Route (e.g., /post/delete)
    participant Guard as route_guards.login_required / admin_required
    participant DB as Database / User store
    participant App as Route Handler

    Client->>Route: HTTP POST /post/<id>/delete
    Route->>Guard: invoke decorator
    Guard->>DB: lookup session["username"] → get_user_by_username
    alt user not found / not logged in
        Guard-->>Client: redirect to /login (with flash)
    else user found
        Guard->>App: call wrapped handler (user in session)
        App->>DB: fetch post/comment, verify owner or admin
        alt authorized
            App->>DB: delete post/comment
            App-->>Client: 302/301 redirect (success)
        else unauthorized
            App-->>Client: 301 redirect (unauthorized), log attempt
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through tests and guards tonight,
I tightened gates with gentle might,
Comments checked and posts kept true,
E2E paths now bounce anew —
A carrot, log, and passing light.

🚥 Pre-merge checks | ✅ 2 | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is severely incomplete, containing only 'fixes #222' without any proposed changes, rationale, or details about the substantial test additions and application refactoring. Expand the description to include a summary of new test modules added, key application changes (route guards, login/admin decorators, translation updates), and the rationale for these changes to match the repository's template.
Docstring Coverage ⚠️ Warning Docstring coverage is 60.40% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive While most changes support test infrastructure, the scope includes refactoring of application routes (login_required, admin_required decorators) and translation key renames that, though related to test enablement, extend beyond strictly testing concerns and merit clarification.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(tests): improve test coverage' directly and accurately summarizes the main change: a significant expansion of end-to-end test coverage across multiple test modules and scenarios.
Linked Issues check ✅ Passed The PR implements comprehensive end-to-end tests for post creation, editing, authorization, and comment workflows [#222], exceeding the linked issue's core objective with extensive test coverage for post-related logic validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/improve-e2e-tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@app/routes/post.py`:
- Around line 52-59: The redirect responses in app/routes/post.py currently
force a 301 status (e.g., the redirect(...) , 301 returned after the permission
check and other POST actions), which is incorrect for POST form actions; remove
the explicit ", 301" status overrides on the redirect(...) calls (leave the
default 302 or use 303 for POST→GET if you prefer) for the permission-denied and
post/comment actions referenced by redirect(url_for("post.post", url_id=url_id))
and the other three identical redirects so they no longer return 301.
- Around line 64-84: Guard against missing form fields and validate comment
submission: replace direct index access to request.form["comment_id"] and
request.form["comment"] with safe access (request.form.get(...)) and ensure the
branch only proceeds when the form indicates a comment submission and the
CommentForm (or equivalent) validates. Specifically, in the block handling
"comment_delete_button" use comment_id = request.form.get("comment_id") and bail
out (redirect) if it's None or invalid before calling Comment.query.get; and for
comment creation ensure you check for the comment-submit indicator (or validate
CommentForm.is_submitted()/validate()) before reading
request.form.get("comment"), validate/escape the text, and only then call
create_comment (or the existing create path), keeping permission checks and
delete_comment(comment_id) unchanged.

In `@tests/e2e/account/test_dashboard.py`:
- Around line 33-35: Two tests that require an authenticated session are missing
the `@pytest.mark.auth` marker: add `@pytest.mark.auth` above the test functions
test_dashboard_shows_user_posts_and_comments and test_dashboard_can_delete_post
so they are included in auth-filtered runs; ensure pytest is imported in the
file if not already and place the decorator directly above each def (keeping the
existing _login usage and test parameters intact).

In `@tests/e2e/post/test_post.py`:
- Around line 211-216: Replace the unnecessary BasePage instantiation by calling
the inherited method on PostPage directly: remove creation of BasePage(page,
flask_server["base_url"]) and the variable base_page, then call
post_page.expect_success_flash() instead of base_page.expect_success_flash();
ensure you keep the subsequent page.wait_for_url and
post_page.expect_comment_visible(comment_text) calls unchanged so the flow and
assertions remain intact.
🧹 Nitpick comments (6)
tests/e2e/helpers/database_helpers.py (1)

160-167: Optional: extract a context-manager to reduce connection boilerplate.

Every helper repeats the same conn = get_db_connection(...) / try / finally: conn.close() pattern. A small context manager would remove ~4 lines per function and make the pattern less error-prone if new helpers are added.

Example context manager
from contextlib import contextmanager

`@contextmanager`
def db_connection(db_path: str, row_factory=None):
    conn = get_db_connection(db_path)
    if row_factory:
        conn.row_factory = row_factory
    try:
        yield conn, conn.cursor()
    finally:
        conn.close()

Usage:

def get_post_by_title(db_path: str, title: str) -> dict | None:
    with db_connection(db_path, row_factory=sqlite3.Row) as (conn, cursor):
        cursor.execute("SELECT * FROM posts WHERE title = ?", (title,))
        row = cursor.fetchone()
        return dict(row) if row else None

Also applies to: 266-268, 299-300

tests/e2e/home/test_home.py (1)

13-14: _suffix() is duplicated across multiple test files.

This same helper appears in test_home.py, test_search.py, test_category.py, and test_admin.py. Consider extracting it to tests/e2e/helpers/ (e.g., alongside test_data.py) to avoid repetition.

tests/e2e/pages/post_page.py (1)

35-37: get_views_count will raise an unhandled ValueError on non-numeric text.

If the rendered view count includes formatting like commas ("1,234") or abbreviations ("1.2K"), int(views_text) will throw. For a test page object this is likely fine in practice, but worth noting if the app ever changes its display format.

tests/e2e/post/test_post.py (2)

39-43: _login helper is duplicated across three test files.

This identical helper appears in test_post.py, test_dashboard.py, and test_account_settings.py. Consider extracting it to a shared conftest fixture or a common test helper module (e.g., tests/e2e/helpers/auth_helpers.py) to reduce duplication.


145-162: @pytest.mark.auth on tests that don't require user authentication may be misleading.

test_post_url_redirects_to_canonical_slug and test_post_views_increment_when_post_is_opened (lines 145–184) create posts via DB helpers and access them without logging in. If the auth marker is used to filter authentication-related tests, these may not belong under it. If the post route itself requires authentication, then the tests would fail for a different reason (redirect to login rather than testing routing/views). Consider whether a different marker (or no marker) is more appropriate.

tests/e2e/account/test_static_pages.py (1)

17-18: CSS framework class selectors (.btn-primary, .badge-ghost) may be fragile.

These are DaisyUI/Tailwind utility classes that could change across UI framework versions. Consider using data-testid attributes or more semantic selectors if the project adopts that pattern. Not critical for smoke tests, just a resilience consideration.

Comment on lines +52 to +59
if (
post.author != session["username"]
and session.get("user_role") != "admin"
):
Log.error(
f'User: "{session["username"]}" tried to delete post "{url_id}" without permission',
)
return redirect(url_for("post.post", url_id=url_id)), 301
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

HTTP 301 (Moved Permanently) is semantically incorrect for action responses.

return redirect(url), 301 overrides the redirect status to 301. These are all responses to POST form submissions (delete post, delete comment, add comment), not permanent URL relocations. Browsers and proxies cache 301 redirects, meaning a browser might skip hitting the server entirely on subsequent requests to the same URL, leading to stale or broken behavior.

Use the default 302 (or 303 for POST→GET) instead:

Proposed fix — drop the 301 overrides
-                    return redirect(url_for("post.post", url_id=url_id)), 301
+                    return redirect(url_for("post.post", url_id=url_id))

Apply the same change to all four occurrences (Lines 59, 70, 79, 82, and 108). For example:

-                    return redirect(url_for("post.post", url_id=url_id)), 301
+                    return redirect(url_for("post.post", url_id=url_id))

-                delete_comment(comment_id)
-                return redirect(url_for("post.post", url_id=url_id)), 301
+                delete_comment(comment_id)
+                return redirect(url_for("post.post", url_id=url_id))

-            return redirect(url_for("post.post", url_id=url_id)), 301
+            return redirect(url_for("post.post", url_id=url_id))

Also applies to: 68-70, 79-82, 108-108

🤖 Prompt for AI Agents
In `@app/routes/post.py` around lines 52 - 59, The redirect responses in
app/routes/post.py currently force a 301 status (e.g., the redirect(...) , 301
returned after the permission check and other POST actions), which is incorrect
for POST form actions; remove the explicit ", 301" status overrides on the
redirect(...) calls (leave the default 302 or use 303 for POST→GET if you
prefer) for the permission-denied and post/comment actions referenced by
redirect(url_for("post.post", url_id=url_id)) and the other three identical
redirects so they no longer return 301.

Comment on lines 64 to 84
if "comment_delete_button" in request.form:
delete_comment(request.form["comment_id"])
comment_id = request.form["comment_id"]
comment = Comment.query.get(comment_id)

if not comment:
Log.error(f'Comment: "{comment_id}" not found for post "{url_id}"')
return redirect(url_for("post.post", url_id=url_id)), 301

if (
comment.username != session["username"]
and session.get("user_role") != "admin"
):
Log.error(
f'User: "{session["username"]}" tried to delete comment "{comment_id}" without permission',
)
return redirect(url_for("post.post", url_id=url_id)), 301

delete_comment(comment_id)
return redirect(url_for("post.post", url_id=url_id)), 301

comment_text = escape(request.form["comment"])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing guard for the comment creation fallthrough and missing KeyError handling.

Two concerns in this block:

  1. Line 65: request.form["comment_id"] will raise a KeyError if a malicious or malformed request omits the field. Use request.form.get("comment_id") with a guard, or catch the exception.

  2. Lines 84+: If a POST request reaches this point without any recognized button and without a "comment" key in the form, line 84 (request.form["comment"]) will raise a 500. There's no explicit elif or guard ensuring this is actually a comment submission. A CommentForm is instantiated (line 29) but never validated.

Proposed fix — use safe form access and validate before creating a comment
             if "comment_delete_button" in request.form:
-                comment_id = request.form["comment_id"]
+                comment_id = request.form.get("comment_id")
+                if not comment_id:
+                    return redirect(url_for("post.post", url_id=url_id))
                 comment = Comment.query.get(comment_id)

For the comment creation block, consider validating the form:

-            comment_text = escape(request.form["comment"])
+            if not form.validate():
+                return redirect(url_for("post.post", url_id=url_id))
+            comment_text = escape(form.comment.data)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if "comment_delete_button" in request.form:
delete_comment(request.form["comment_id"])
comment_id = request.form["comment_id"]
comment = Comment.query.get(comment_id)
if not comment:
Log.error(f'Comment: "{comment_id}" not found for post "{url_id}"')
return redirect(url_for("post.post", url_id=url_id)), 301
if (
comment.username != session["username"]
and session.get("user_role") != "admin"
):
Log.error(
f'User: "{session["username"]}" tried to delete comment "{comment_id}" without permission',
)
return redirect(url_for("post.post", url_id=url_id)), 301
delete_comment(comment_id)
return redirect(url_for("post.post", url_id=url_id)), 301
comment_text = escape(request.form["comment"])
if "comment_delete_button" in request.form:
comment_id = request.form.get("comment_id")
if not comment_id:
return redirect(url_for("post.post", url_id=url_id))
comment = Comment.query.get(comment_id)
if not comment:
Log.error(f'Comment: "{comment_id}" not found for post "{url_id}"')
return redirect(url_for("post.post", url_id=url_id)), 301
if (
comment.username != session["username"]
and session.get("user_role") != "admin"
):
Log.error(
f'User: "{session["username"]}" tried to delete comment "{comment_id}" without permission',
)
return redirect(url_for("post.post", url_id=url_id)), 301
delete_comment(comment_id)
return redirect(url_for("post.post", url_id=url_id)), 301
if not form.validate():
return redirect(url_for("post.post", url_id=url_id))
comment_text = escape(form.comment.data)
🤖 Prompt for AI Agents
In `@app/routes/post.py` around lines 64 - 84, Guard against missing form fields
and validate comment submission: replace direct index access to
request.form["comment_id"] and request.form["comment"] with safe access
(request.form.get(...)) and ensure the branch only proceeds when the form
indicates a comment submission and the CommentForm (or equivalent) validates.
Specifically, in the block handling "comment_delete_button" use comment_id =
request.form.get("comment_id") and bail out (redirect) if it's None or invalid
before calling Comment.query.get; and for comment creation ensure you check for
the comment-submit indicator (or validate CommentForm.is_submitted()/validate())
before reading request.form.get("comment"), validate/escape the text, and only
then call create_comment (or the existing create path), keeping permission
checks and delete_comment(comment_id) unchanged.

Comment on lines +33 to +35
def test_dashboard_shows_user_posts_and_comments(
self, page, flask_server, test_user, db_path
):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent @pytest.mark.auth usage — two tests that require login are missing the marker.

test_dashboard_shows_user_posts_and_comments (line 33) and test_dashboard_can_delete_post (line 96) both log in via _login and exercise authenticated flows, but lack the @pytest.mark.auth marker that other auth-dependent tests in this PR consistently use. If the marker is used for filtering or CI gating, these tests would be excluded from auth test runs.

♻️ Suggested fix
+    `@pytest.mark.auth`
     def test_dashboard_shows_user_posts_and_comments(
         self, page, flask_server, test_user, db_path
     ):
+    `@pytest.mark.auth`
     def test_dashboard_can_delete_post(self, page, flask_server, test_user, db_path):

Also applies to: 96-97

🤖 Prompt for AI Agents
In `@tests/e2e/account/test_dashboard.py` around lines 33 - 35, Two tests that
require an authenticated session are missing the `@pytest.mark.auth` marker: add
`@pytest.mark.auth` above the test functions
test_dashboard_shows_user_posts_and_comments and test_dashboard_can_delete_post
so they are included in auth-filtered runs; ensure pytest is imported in the
file if not already and place the decorator directly above each def (keeping the
existing _login usage and test parameters intact).

Comment on lines +211 to +216
post_page.add_comment(comment_text)

base_page = BasePage(page, flask_server["base_url"])
base_page.expect_success_flash()
page.wait_for_url("**/post/**", timeout=5000)
post_page.expect_comment_visible(comment_text)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Unnecessary BasePage instantiation — PostPage inherits expect_success_flash.

PostPage extends BasePage, so post_page.expect_success_flash() works directly. No need to create a separate BasePage instance.

♻️ Suggested fix
         post_page.add_comment(comment_text)
 
-        base_page = BasePage(page, flask_server["base_url"])
-        base_page.expect_success_flash()
+        post_page.expect_success_flash()
         page.wait_for_url("**/post/**", timeout=5000)
         post_page.expect_comment_visible(comment_text)
🤖 Prompt for AI Agents
In `@tests/e2e/post/test_post.py` around lines 211 - 216, Replace the unnecessary
BasePage instantiation by calling the inherited method on PostPage directly:
remove creation of BasePage(page, flask_server["base_url"]) and the variable
base_page, then call post_page.expect_success_flash() instead of
base_page.expect_success_flash(); ensure you keep the subsequent
page.wait_for_url and post_page.expect_comment_visible(comment_text) calls
unchanged so the flow and assertions remain intact.

Capture the user's language before clearing the session to avoid reading from a cleared session in change_password route. Pass that language into the flash_message after session.clear to preserve localization.

Add a new end-to-end test (tests/e2e/account/test_change_password_flow.py) that verifies credential rotation: after changing password the old password is rejected and the new password authenticates. Update tests/README.md to reflect the added test and adjust suite counts and file listings.
Standardize the change-username key/name across the app: rename "change_user_name" to "change_username" in all translation files, update the change_username template to use the new keys, adjust route flash_page and flash_message calls, and fix the form import path (utils.forms.change_username_form) in change_username route. This unifies naming for the change-username feature and fixes related template/import references.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In `@app/routes/admin_panel_users.py`:
- Around line 23-36: The POST handlers call delete_user(...) and
change_user_role(...) but ignore their return values (which may be Flask
redirect responses); update the POST branch in the request.method == "POST"
block to capture the result of delete_user(request.form["username"]) and
change_user_role(request.form["username"]) and, if the result is a
Response/redirect, immediately return it (otherwise continue). Ensure you still
log with Log.info(...) before calling those functions and propagate any redirect
returned by those functions so redirects from delete_user and change_user_role
are not lost.
- Around line 41-54: Remove the password field from the tuple built from
users_objects in the users list comprehension (currently includes u.password) so
templates do not receive password hashes; then update the corresponding index
references in admin_panel_users.html to account for the shifted tuple positions
(change user[4]→user[3], user[5]→user[4], user[6]→user[5], user[7]→user[6]).
Locate the list construction around the users = [ (... ) for u in users_objects
] in admin_panel_users.py and the index uses in admin_panel_users.html and
adjust them accordingly.

In `@app/routes/change_password.py`:
- Around line 56-97: The three independent conditionals after
encryption.verify(old_password, user.password) can trigger multiple flash
messages; change them to an exclusive control flow (use if / elif / elif or
return early) so only one branch runs: check old_password == password first
(flash "same" and return/redirect), else if password != password_confirm (flash
"match" and return/redirect), else (passwords different from old and match each
other) set user.password via encryption.hash(password), call
db.session.commit(), call Log.success(...), clear session, flash "success" using
session.get("language", "en") and then return redirect("/login/redirect=&").
Ensure you update the block containing encryption.verify, flash_message,
user.password, db.session.commit, Log.success, session, and redirect
accordingly.

In `@app/routes/change_username.py`:
- Around line 49-65: Post and Comment rows (Post.query.filter_by(...) and
Comment.query.filter_by(...)) and db.session.commit() run even when the User
lookup fails; move the Post and Comment update calls and the db.session.commit()
inside the same if user: guard that wraps user.username = new_username (or
replace the guard so the entire update block is conditional on user being
truthy), and add handling for the case when
User.query.filter_by(username=old_username).first() returns none (e.g.,
return/abort or skip updates) so Post, Comment and commit only execute when a
corresponding User exists.

In `@app/routes/create_post.py`:
- Around line 42-50: CreatePostForm is instantiated but its validators are
bypassed because code reads values directly from request.form; call
form.validate() (or use form.validate_on_submit()) and, on success, use
form.post_title.data, form.post_tags.data, form.post_abstract.data,
form.post_content.data, form.post_category.data instead of request.form[...] so
the min/max validators on post_abstract and post_content run server-side; if
validate() fails, return/flash form.errors and re-render the form. Keep using
request.files["post_banner"] for the file but only process it after form
validation.

In `@app/routes/dashboard.py`:
- Around line 34-37: The redirect after a POST in app/routes/dashboard.py
currently returns redirect(url_for("dashboard.dashboard", username=username)),
301 which is incorrect; change it to use a non-permanent redirect (either return
redirect(url_for("dashboard.dashboard", username=username)) to get a 302 or
explicitly return redirect(url_for("dashboard.dashboard", username=username),
code=303) to enforce a POST→GET See Other). Update the return in the handler
that issues this redirect (the call site using
redirect(url_for("dashboard.dashboard", username=username))) accordingly.
- Around line 76-80: session.get("language") can be None causing
translation_file to become "./translations/None.json" and raise
FileNotFoundError; update the code around session.get("language") and
translation_file so you use a safe default (e.g., "en") when
session.get("language") returns None or an empty value. Concretely, replace the
raw session.get("language") use with something like language =
session.get("language") or "en" (or session.get("language", "en")), then build
translation_file and proceed to open/load; ensure the change touches the
language variable, translation_file, and the with open(...) load(...) sequence
so it never attempts to open "None.json".

In `@app/routes/edit_post.py`:
- Line 60: The current code accesses request.files["post_banner"] directly which
can raise KeyError if the field is missing; change it to use
request.files.get("post_banner") and guard before calling .read(): retrieve the
file object into a variable (e.g., post_banner_file =
request.files.get("post_banner")), check if post_banner_file is truthy (and
optionally has a filename) and only then call post_banner_file.read() to assign
post_banner; otherwise set post_banner to None or handle the missing-file branch
(validation error/flash/redirect) inside the same function handling the form
submission.

In `@app/routes/verify_user.py`:
- Line 52: The module-level variable verification_code is a concurrency hazard
because it allows concurrent flows to clobber each other's codes; replace this
global by persisting per-user codes (e.g., attach the code to the user's session
or to your user verification DB record) instead of using verification_code.
Update any functions or routes that read/write verification_code (e.g., the
route handler that generates/sends the code and the handler that validates it)
to read from and write to the chosen per-user store (session key or a
Verification table keyed by user_id/email), ensure proper expiration timestamps
and single-use semantics, and remove the global declaration so all reads/writes
are scoped to the user context.

In `@app/utils/route_guards.py`:
- Around line 10-44: The decorator uses session["username"] which conflicts with
the coding guideline's session["userName"] but the rest of the codebase
consistently uses the lowercase "username"; resolve the mismatch by choosing one
canonical key and making code + docs consistent: either (A) change this
decorator to use session.get("userName") and perform a project-wide replace of
session["username"] -> session["userName"] (update any tests and auth code that
read/write the session), or (B) accept the existing lowercase key by updating
the coding guideline and any docs to declare "username" as the canonical session
key; locate the decorator function login_required and the other auth read/write
sites to apply the chosen consistent change.

In `@tests/README.md`:
- Around line 186-190: In the CI section of tests/README.md update the product
name to use correct capitalization: change any occurrence of "github actions" or
"github" to "GitHub Actions" (and "GitHub" where used alone) so the CI heading
and the sentence "E2E tests run in GitHub Actions
(`.github/workflows/e2e-tests.yaml`) on:" use the official product casing.
🧹 Nitpick comments (7)
app/routes/admin_panel_posts.py (1)

15-15: Consider removing "POST" from allowed methods.

This route only queries and renders data — there's no request.method == "POST" handling. If the template doesn't submit forms to this endpoint, the method list can be narrowed to ["GET"].

app/routes/change_profile_picture.py (1)

41-62: Form validation is bypassed — raw request.form access can raise KeyError.

request.form["new_profile_picture_seed"] on line 42 is accessed directly without calling form.validate(). If the field is missing from the POST body, this raises a 400 Bad Request. Consider validating via the form object first:

♻️ Suggested improvement
     if request.method == "POST":
-        new_profile_picture_seed = request.form["new_profile_picture_seed"]
+        if not form.validate():
+            return render_template("change_profile_picture.html", form=form)
+
+        new_profile_picture_seed = form.new_profile_picture_seed.data
app/routes/account_settings.py (1)

18-20: Return value from delete_user is discarded.

delete_user() (in app/utils/delete.py) returns a redirect(...) response, but it's ignored here — the route always returns its own redirect("/"). The side effects (DB deletion, session.clear(), flash) still fire, so this works for the self-deletion case. However, silently discarding the response is fragile if delete_user's contract changes.

Consider using the return value:

♻️ Suggested improvement
     if request.method == "POST":
-        delete_user(user.username)
-        return redirect("/")
+        return delete_user(user.username)
app/routes/verify_user.py (1)

85-87: Redundant if user: check — user is guaranteed to exist.

Line 44–47 already verifies the user exists and returns early if not. The if user: guard on Line 87 is dead code.

♻️ Suggested cleanup
         elif code_sent == "false":
             if request.method == "POST":
-                if user:
-                    context = ssl.create_default_context()
+                context = ssl.create_default_context()

(and dedent the entire block below accordingly)

tests/e2e/account/test_change_password_flow.py (1)

14-18: _login helper is duplicated across multiple E2E test files.

The AI summary notes identical _login helpers in test_account_settings.py and test_profile_and_preferences.py. Consider extracting this into a shared fixture or utility in a conftest/helpers module to reduce duplication as the test suite grows.

app/routes/dashboard.py (1)

44-60: Fragile positional list representation for posts with magic index access.

Posts are stored as plain lists and accessed by magic indices (e.g., post[9] for category on Line 83). This is error-prone—any change to the field order silently breaks the category translation and any template code that consumes these lists. Consider using dictionaries or named tuples instead.

Also applies to: 82-83

tests/e2e/admin/test_admin.py (1)

19-28: _suffix() and _login() are duplicated across test files.

These helpers appear identically in test_profile_and_preferences.py as well. Consider extracting them into a shared test utility (e.g., tests/e2e/helpers/) to reduce duplication.

Comment on lines +23 to +36
if request.method == "POST":
if "user_delete_button" in request.form:
Log.info(
f"Admin: {session['username']} deleted user: {request.form['username']}"
)

Log.info(f"Rendering admin_panel_users.html: params: users={len(users)}")
delete_user(request.form["username"])

return render_template(
"admin_panel_users.html",
users=users,
page=page,
total_pages=total_pages,
)
else:
Log.error(
f"{request.remote_addr} tried to reach user admin panel without being admin"
if "user_role_change_button" in request.form:
Log.info(
f"Admin: {session['username']} changed {request.form['username']}'s role"
)

return redirect("/")
else:
Log.error(
f"{request.remote_addr} tried to reach user admin panel being logged in"
change_user_role(request.form["username"])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Return values from delete_user and change_user_role are silently discarded — redirects won't fire.

Both delete_user() and change_user_role() can return redirect(...) responses (see app/utils/delete.py lines 67–86 and app/utils/change_user_role.py lines 25–27). Since their return values are not propagated here, the code falls through to the query/render block on lines 38–63. This means:

  1. After deleting a user, the flash message fires but the page re-renders (potentially showing the deleted user if the DB commit already happened, or crashing if it didn't).
  2. If an admin demotes themselves, the redirect("/") from change_user_role is lost — the page re-renders, and subsequent requests will fail because the @admin_required guard will reject the now-non-admin user.
🐛 Proposed fix — propagate redirect responses
     if request.method == "POST":
         if "user_delete_button" in request.form:
             Log.info(
                 f"Admin: {session['username']} deleted user: {request.form['username']}"
             )
-            delete_user(request.form["username"])
+            result = delete_user(request.form["username"])
+            if result:
+                return result

-        if "user_role_change_button" in request.form:
+        elif "user_role_change_button" in request.form:
             Log.info(
                 f"Admin: {session['username']} changed {request.form['username']}'s role"
             )
-            change_user_role(request.form["username"])
+            result = change_user_role(request.form["username"])
+            if result:
+                return result
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if request.method == "POST":
if "user_delete_button" in request.form:
Log.info(
f"Admin: {session['username']} deleted user: {request.form['username']}"
)
Log.info(f"Rendering admin_panel_users.html: params: users={len(users)}")
delete_user(request.form["username"])
return render_template(
"admin_panel_users.html",
users=users,
page=page,
total_pages=total_pages,
)
else:
Log.error(
f"{request.remote_addr} tried to reach user admin panel without being admin"
if "user_role_change_button" in request.form:
Log.info(
f"Admin: {session['username']} changed {request.form['username']}'s role"
)
return redirect("/")
else:
Log.error(
f"{request.remote_addr} tried to reach user admin panel being logged in"
change_user_role(request.form["username"])
if request.method == "POST":
if "user_delete_button" in request.form:
Log.info(
f"Admin: {session['username']} deleted user: {request.form['username']}"
)
result = delete_user(request.form["username"])
if result:
return result
elif "user_role_change_button" in request.form:
Log.info(
f"Admin: {session['username']} changed {request.form['username']}'s role"
)
result = change_user_role(request.form["username"])
if result:
return result
🤖 Prompt for AI Agents
In `@app/routes/admin_panel_users.py` around lines 23 - 36, The POST handlers call
delete_user(...) and change_user_role(...) but ignore their return values (which
may be Flask redirect responses); update the POST branch in the request.method
== "POST" block to capture the result of delete_user(request.form["username"])
and change_user_role(request.form["username"]) and, if the result is a
Response/redirect, immediately return it (otherwise continue). Ensure you still
log with Log.info(...) before calling those functions and propagate any redirect
returned by those functions so redirects from delete_user and change_user_role
are not lost.

Comment on lines +41 to +54
users = [
(
u.user_id,
u.username,
u.email,
u.password,
u.profile_picture,
u.role,
u.points,
u.time_stamp,
u.is_verified,
)
for u in users_objects
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "(admin_panel|routes)" | head -20

Repository: DogukanUrker/FlaskBlog

Length of output: 603


🏁 Script executed:

fd 'admin_panel_users' --type f

Repository: DogukanUrker/FlaskBlog

Length of output: 134


🏁 Script executed:

cat -n app/routes/admin_panel_users.py 2>/dev/null | head -60

Repository: DogukanUrker/FlaskBlog

Length of output: 2117


🏁 Script executed:

cat -n app/templates/admin_panel_users.html

Repository: DogukanUrker/FlaskBlog

Length of output: 5791


Remove password from the user tuple passed to template — it's never used and exposes unnecessary sensitive data.

Line 46 includes u.password in the tuple passed to the template, but the template never accesses this field. Even though passwords are hashed, passing password hashes to the template violates the principle of least privilege and risks unnecessary exposure in HTML source, caches, or logs.

🔒 Proposed fix
     users = [
         (
             u.user_id,
             u.username,
             u.email,
-            u.password,
             u.profile_picture,
             u.role,
             u.points,
             u.time_stamp,
             u.is_verified,
         )
         for u in users_objects
     ]

Update the template indices: after removing u.password, the tuple indices shift by 1. In admin_panel_users.html, update:

  • user[4]user[3] (profile_picture)
  • user[5]user[4] (role)
  • user[6]user[5] (points)
  • user[7]user[6] (timestamp)
🤖 Prompt for AI Agents
In `@app/routes/admin_panel_users.py` around lines 41 - 54, Remove the password
field from the tuple built from users_objects in the users list comprehension
(currently includes u.password) so templates do not receive password hashes;
then update the corresponding index references in admin_panel_users.html to
account for the shifted tuple positions (change user[4]→user[3],
user[5]→user[4], user[6]→user[5], user[7]→user[6]). Locate the list construction
around the users = [ (... ) for u in users_objects ] in admin_panel_users.py and
the index uses in admin_panel_users.html and adjust them accordingly.

Comment on lines +56 to +97
if encryption.verify(old_password, user.password):
if old_password == password:
flash_message(
page="change_password",
message="login",
message="same",
category="error",
language=session["language"],
)
return redirect("/login/redirect=change-password")

if encryption.verify(old_password, user.password):
if old_password == password:
flash_message(
page="change_password",
message="same",
category="error",
language=session["language"],
)

if password != password_confirm:
flash_message(
page="change_password",
message="match",
category="error",
language=session["language"],
)

if old_password != password and password == password_confirm:
user.password = encryption.hash(password)
db.session.commit()

Log.success(
f'User: "{session["username"]}" changed his password',
)

session.clear()
flash_message(
page="change_password",
message="success",
category="success",
language=session["language"],
)

return redirect("/login/redirect=&")
else:

if password != password_confirm:
flash_message(
page="change_password",
message="old",
message="match",
category="error",
language=session["language"],
)

return render_template(
"change_password.html",
form=form,
)
else:
Log.error(
f"{request.remote_addr} tried to change his password without logging in"
)
flash_message(
page="change_password",
message="login",
category="error",
language=session["language"],
)

return redirect("/login/redirect=change-password")
if old_password != password and password == password_confirm:
user.password = encryption.hash(password)
db.session.commit()

Log.success(
f'User: "{session["username"]}" changed his password',
)

language = session.get("language", "en")
session.clear()
flash_message(
page="change_password",
message="success",
category="success",
language=language,
)

return redirect("/login/redirect=&")
else:
flash_message(
page="change_password",
message="old",
category="error",
language=session["language"],
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Non-exclusive if blocks allow double flash messages.

Lines 57, 65, and 73 are independent if statements rather than if/elif/elif. When old_password == password and password != password_confirm, both the "same password" and "passwords don't match" flash messages fire in the same request. Use elif chains or early returns to ensure mutual exclusivity.

🐛 Proposed fix
         if encryption.verify(old_password, user.password):
-            if old_password == password:
+            if old_password == password:
                 flash_message(
                     page="change_password",
                     message="same",
                     category="error",
                     language=session["language"],
                 )
-
-            if password != password_confirm:
+            elif password != password_confirm:
                 flash_message(
                     page="change_password",
                     message="match",
                     category="error",
                     language=session["language"],
                 )
-
-            if old_password != password and password == password_confirm:
+            else:
                 user.password = encryption.hash(password)
                 db.session.commit()
🤖 Prompt for AI Agents
In `@app/routes/change_password.py` around lines 56 - 97, The three independent
conditionals after encryption.verify(old_password, user.password) can trigger
multiple flash messages; change them to an exclusive control flow (use if / elif
/ elif or return early) so only one branch runs: check old_password == password
first (flash "same" and return/redirect), else if password != password_confirm
(flash "match" and return/redirect), else (passwords different from old and
match each other) set user.password via encryption.hash(password), call
db.session.commit(), call Log.success(...), clear session, flash "success" using
session.get("language", "en") and then return redirect("/login/redirect=&").
Ensure you update the block containing encryption.verify, flash_message,
user.password, db.session.commit, Log.success, session, and redirect
accordingly.

Comment on lines +49 to +65
if not existing_user:
old_username = session["username"]

# Update username in users table
user = User.query.filter_by(username=old_username).first()
if user:
user.username = new_username

# Update author in posts table
Post.query.filter_by(author=old_username).update({"author": new_username})

# Update username in comments table
Comment.query.filter_by(username=old_username).update(
{"username": new_username}
)

db.session.commit()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Post and Comment updates execute even if the User record isn't found.

The if user: guard on Line 54 only protects the User.username assignment, but Post.query.filter_by(...) and Comment.query.filter_by(...) updates on Lines 58–63 run unconditionally, and the commit on Line 65 persists them. If the user lookup fails (defensive edge case), posts and comments get reassigned to new_username while no User record is updated.

🐛 Proposed fix — guard the entire update block
         if not existing_user:
             old_username = session["username"]
 
             # Update username in users table
             user = User.query.filter_by(username=old_username).first()
-            if user:
-                user.username = new_username
+            if not user:
+                return redirect("/")
 
-            # Update author in posts table
-            Post.query.filter_by(author=old_username).update({"author": new_username})
+            user.username = new_username
 
-            # Update username in comments table
-            Comment.query.filter_by(username=old_username).update(
-                {"username": new_username}
-            )
+            # Update author in posts table
+            Post.query.filter_by(author=old_username).update({"author": new_username})
 
-            db.session.commit()
+            # Update username in comments table
+            Comment.query.filter_by(username=old_username).update(
+                {"username": new_username}
+            )
+
+            db.session.commit()
🤖 Prompt for AI Agents
In `@app/routes/change_username.py` around lines 49 - 65, Post and Comment rows
(Post.query.filter_by(...) and Comment.query.filter_by(...)) and
db.session.commit() run even when the User lookup fails; move the Post and
Comment update calls and the db.session.commit() inside the same if user: guard
that wraps user.username = new_username (or replace the guard so the entire
update block is conditional on user being truthy), and add handling for the case
when User.query.filter_by(username=old_username).first() returns none (e.g.,
return/abort or skip updates) so Post, Comment and commit only execute when a
corresponding User exists.

Comment on lines +42 to +50
form = CreatePostForm(request.form)

if request.method == "POST":
post_title = request.form["post_title"]
post_tags = request.form["post_tags"]
post_abstract = request.form["post_abstract"]
post_content = request.form["post_content"]
post_banner = request.files["post_banner"].read()
post_category = request.form["post_category"]
if request.method == "POST":
post_title = request.form["post_title"]
post_tags = request.form["post_tags"]
post_abstract = request.form["post_abstract"]
post_content = request.form["post_content"]
post_banner = request.files["post_banner"].read()
post_category = request.form["post_category"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

WTForms validation is bypassed — form values are read directly from request.form.

CreatePostForm is instantiated on Line 42 but form.validate() is never called. Fields are pulled directly from request.form, so the min/max length validators on post_abstract (150–200 chars) and post_content (≥ 50 chars) are never enforced server-side — only the empty-string check on Line 52 gates submission.

This is pre-existing, but worth noting since it means the form validators serve only as client-side hints (if rendered), and the server accepts arbitrarily short or long content.

🤖 Prompt for AI Agents
In `@app/routes/create_post.py` around lines 42 - 50, CreatePostForm is
instantiated but its validators are bypassed because code reads values directly
from request.form; call form.validate() (or use form.validate_on_submit()) and,
on success, use form.post_title.data, form.post_tags.data,
form.post_abstract.data, form.post_content.data, form.post_category.data instead
of request.form[...] so the min/max validators on post_abstract and post_content
run server-side; if validate() fails, return/flash form.errors and re-render the
form. Keep using request.files["post_banner"] for the file but only process it
after form validation.

Comment on lines +76 to +80
language = session.get("language")
translation_file = f"./translations/{language}.json"

with open(translation_file, "r", encoding="utf-8") as file:
translations = load(file)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

session.get("language") can be None, leading to a FileNotFoundError.

If the session has no "language" key, language will be None, and the file path becomes ./translations/None.json, which will raise a FileNotFoundError on Line 79. Provide a default fallback.

Proposed fix
-        language = session.get("language")
+        language = session.get("language", "en")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
language = session.get("language")
translation_file = f"./translations/{language}.json"
with open(translation_file, "r", encoding="utf-8") as file:
translations = load(file)
language = session.get("language", "en")
translation_file = f"./translations/{language}.json"
with open(translation_file, "r", encoding="utf-8") as file:
translations = load(file)
🤖 Prompt for AI Agents
In `@app/routes/dashboard.py` around lines 76 - 80, session.get("language") can be
None causing translation_file to become "./translations/None.json" and raise
FileNotFoundError; update the code around session.get("language") and
translation_file so you use a safe default (e.g., "en") when
session.get("language") returns None or an empty value. Concretely, replace the
raw session.get("language") use with something like language =
session.get("language") or "en" (or session.get("language", "en")), then build
translation_file and proceed to open/load; ensure the change touches the
language variable, translation_file, and the with open(...) load(...) sequence
so it never attempts to open "None.json".

post_content = request.form["post_content"]
post_abstract = request.form["post_abstract"]
post_category = request.form["post_category"]
post_banner = request.files["post_banner"].read()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

request.files["post_banner"] can raise KeyError if the file field is absent.

If the form is submitted without a file input named post_banner, this will raise an unhandled KeyError/400. Use .get() with a fallback:

🛡️ Proposed fix
-                post_banner = request.files["post_banner"].read()
+                post_banner_file = request.files.get("post_banner")
+                post_banner = post_banner_file.read() if post_banner_file else b""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
post_banner = request.files["post_banner"].read()
post_banner_file = request.files.get("post_banner")
post_banner = post_banner_file.read() if post_banner_file else b""
🤖 Prompt for AI Agents
In `@app/routes/edit_post.py` at line 60, The current code accesses
request.files["post_banner"] directly which can raise KeyError if the field is
missing; change it to use request.files.get("post_banner") and guard before
calling .read(): retrieve the file object into a variable (e.g.,
post_banner_file = request.files.get("post_banner")), check if post_banner_file
is truthy (and optionally has a filename) and only then call
post_banner_file.read() to assign post_banner; otherwise set post_banner to None
or handle the missing-file branch (validation error/flash/redirect) inside the
same function handling the form submission.

if user.is_verified == "True":
return redirect("/")
elif user.is_verified == "False":
global verification_code
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

global verification_code is a shared-state concurrency hazard.

This module-level global means concurrent verification flows for different users will overwrite each other's codes. If User A requests a code, then User B requests one before A submits, A's code is lost. This is pre-existing, but worth flagging as the guard refactor is a good opportunity to address it — store the code in the user's session or in the database instead.

🤖 Prompt for AI Agents
In `@app/routes/verify_user.py` at line 52, The module-level variable
verification_code is a concurrency hazard because it allows concurrent flows to
clobber each other's codes; replace this global by persisting per-user codes
(e.g., attach the code to the user's session or to your user verification DB
record) instead of using verification_code. Update any functions or routes that
read/write verification_code (e.g., the route handler that generates/sends the
code and the handler that validates it) to read from and write to the chosen
per-user store (session key or a Verification table keyed by user_id/email),
ensure proper expiration timestamps and single-use semantics, and remove the
global declaration so all reads/writes are scoped to the user context.

Comment on lines +10 to +44
def login_required(
route_name: str,
redirect_to="/",
flash_page: str | None = None,
flash_message_key: str = "login",
flash_category: str = "error",
):
"""Ensure the current session has a logged in user."""

def decorator(view_func):
@wraps(view_func)
def wrapped_view(*args, **kwargs):
if "username" in session:
return view_func(*args, **kwargs)

Log.error(
f"{request.remote_addr} tried to reach {route_name} without being logged in"
)

if flash_page:
flash_message(
page=flash_page,
message=flash_message_key,
category=flash_category,
language=session.get("language", "en"),
)

if callable(redirect_to):
return redirect(redirect_to(*args, **kwargs))

return redirect(redirect_to)

return wrapped_view

return decorator
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify which session key is used across the codebase for authentication

echo "=== Occurrences of session[\"userName\"] ==="
rg -n 'session\["userName"\]' --type=py || echo "No matches found"

echo ""
echo "=== Occurrences of session[\"username\"] ==="
rg -n 'session\["username"\]' --type=py || echo "No matches found"

echo ""
echo "=== Occurrences of session.get\(\"userName\"\) ==="
rg -n "session\.get\(\"userName\"" --type=py || echo "No matches found"

echo ""
echo "=== Occurrences of session.get\(\"username\"\) ==="
rg -n "session\.get\(\"username\"" --type=py || echo "No matches found"

echo ""
echo "=== All session key references (for context) ==="
rg -n 'session\[' --type=py | head -40

Repository: DogukanUrker/FlaskBlog

Length of output: 7209


Session key usage violates coding guideline but is systemic across codebase.

The code uses session["username"] (line 54), which conflicts with the coding guideline requiring session["userName"]. However, verification reveals this discrepancy is systemic: the entire codebase (40+ locations across all authentication files) consistently uses the lowercase variant. No instances of the guideline-compliant session["userName"] exist anywhere.

The new code correctly follows the established codebase convention, but this represents a guideline-to-implementation mismatch that requires clarification from maintainers: either update the coding guideline to match actual usage, or commit to a codebase-wide migration.

The decorator design itself is sound: clean separation of concerns, proper @wraps usage for Flask endpoint registration, and sensible defaults (language fallback via session.get("language", "en")).

🤖 Prompt for AI Agents
In `@app/utils/route_guards.py` around lines 10 - 44, The decorator uses
session["username"] which conflicts with the coding guideline's
session["userName"] but the rest of the codebase consistently uses the lowercase
"username"; resolve the mismatch by choosing one canonical key and making code +
docs consistent: either (A) change this decorator to use session.get("userName")
and perform a project-wide replace of session["username"] -> session["userName"]
(update any tests and auth code that read/write the session), or (B) accept the
existing lowercase key by updating the coding guideline and any docs to declare
"username" as the canonical session key; locate the decorator function
login_required and the other auth read/write sites to apply the chosen
consistent change.

Comment on lines +186 to +190
## CI

- Push to `main`
- Pull requests
E2E tests run in GitHub Actions (`.github/workflows/e2e-tests.yaml`) on:

See `.github/workflows/e2e-tests.yml` for configuration.
- Push to `main` (when `app/**`, `tests/**`, or workflow file changes)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Capitalize “GitHub” in the CI section.

Use “GitHub Actions” to match the official product name.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~188-~188: The official name of this software platform is spelled with a capital “H”.
Context: ...## CI E2E tests run in GitHub Actions (.github/workflows/e2e-tests.yaml) on: - Push ...

(GITHUB)

🤖 Prompt for AI Agents
In `@tests/README.md` around lines 186 - 190, In the CI section of tests/README.md
update the product name to use correct capitalization: change any occurrence of
"github actions" or "github" to "GitHub Actions" (and "GitHub" where used alone)
so the CI heading and the sentence "E2E tests run in GitHub Actions
(`.github/workflows/e2e-tests.yaml`) on:" use the official product casing.

Rename app/utils/forms/change_user_name_form.py to app/utils/forms/change_username_form.py for naming consistency. No code changes were made; update any imports or references to the old filename as needed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(tests): create tests for post create/edit logic

1 participant