Skip to content

feat(tests): improve test coverage#234

Merged
DogukanUrker merged 11 commits into
mainfrom
feat/improve-e2e-tests
Jun 27, 2026
Merged

feat(tests): improve test coverage#234
DogukanUrker merged 11 commits into
mainfrom
feat/improve-e2e-tests

Conversation

@DogukanUrker

@DogukanUrker DogukanUrker commented Feb 6, 2026

Copy link
Copy Markdown
Owner

fixes #222

Summary by CodeRabbit

  • Security / Bug Fixes
    • Post actions (commenting, deleting) now require login; unauthenticated requests are redirected to login.
    • Admin and account pages (admin panels, account settings, password/profile/username changes, dashboard, edit-post, verify) now consistently enforce authentication/authorization before processing.
    • Improved handling for forged/unauthorized POST actions and ownership checks.
  • Tests
    • Expanded Playwright end-to-end coverage for posts/comments, search, admin flows, account settings, and new page objects (including CSRF/authorization scenarios).
  • Documentation
    • Updated end-to-end testing guide with streamlined commands, parallel execution guidance, and current suite structure/fixtures.
  • Localization
    • Translation keys for username-change UI/flash messages were standardized.

@DogukanUrker DogukanUrker self-assigned this Feb 6, 2026
@coderabbitai

coderabbitai Bot commented Feb 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@DogukanUrker, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 36 minutes and 15 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b575183c-0f62-4844-af16-2d1469d0c51c

📥 Commits

Reviewing files that changed from the base of the PR and between a5ccb2c and ac0128f.

📒 Files selected for processing (1)
  • tests/e2e/auth/test_logout.py
📝 Walkthrough

Walkthrough

Adds route guards for login/admin access, rewires protected routes to use them, renames change-username translation keys, and expands Playwright coverage with new helpers, page objects, docs, and domain tests.

Changes

Authentication, translations, and E2E coverage

Layer / File(s) Summary
Route guard utilities and wiring
app/utils/route_guards.py, app/routes/*.py
Adds login_required and admin_required helpers and wires them into protected routes instead of inline session checks.
Route body rewrites
app/routes/account_settings.py, app/routes/admin_panel*.py, app/routes/change_*.py, app/routes/create_post.py, app/routes/dashboard.py, app/routes/edit_post.py, app/routes/post.py, app/routes/verify_user.py
Updates route control flow under the new guards, including POST handling, redirects, ownership checks, and flash behavior.
Username translation key rename
app/templates/change_username.html, app/translations/*.json
Renames change_user_name to change_username in the template and locale files.
E2E helpers, page objects, and docs
tests/README.md, tests/e2e/helpers/*, tests/e2e/pages/*
Adds post/comment DB helpers, page objects for create-post and post views, package exports, and updated E2E documentation.
New Playwright coverage
tests/e2e/account/*, tests/e2e/admin/*, tests/e2e/home/*, tests/e2e/post/*, tests/e2e/search/*
Adds E2E suites for account settings, password/profile changes, admin panels, home/category/search pages, static pages, and post flows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

size/XXL

Poem

🐰 I hopped through guards and tests today,
New routes said “please log in” straight away.
With keys renamed and pages bright,
My carrot stack feels tidy now —
Hooray for paths that land just right!

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description only says 'fixes #222' and omits the required Proposed Changes bullets and other template content. Add the template sections, including a Fixes # line and at least three Proposed Changes bullets summarizing the main code and test updates.
Out of Scope Changes check ⚠️ Warning The PR includes many unrelated account, admin, search, home, and translation changes beyond post create/edit testing. Split unrelated route, translation, and non-post E2E changes into separate PRs so this one stays focused on issue #222.
Docstring Coverage ⚠️ Warning Docstring coverage is 58.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately indicates the PR is about test coverage, even though it is broad.
Linked Issues check ✅ Passed The PR adds post create/edit E2E coverage and related authorization tests, matching issue #222's requested post logic coverage.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 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.

@coderabbitai coderabbitai Bot left a 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.

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 thread app/routes/post.py Outdated
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 thread app/routes/post.py
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.

@coderabbitai coderabbitai Bot left a 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.

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 thread app/routes/change_password.py
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 thread app/routes/create_post.py Outdated
Comment thread app/routes/dashboard.py
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".

Comment thread app/routes/edit_post.py
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.

Comment thread app/routes/verify_user.py Outdated
Comment thread app/utils/route_guards.py
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 thread tests/README.md
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.
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.
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.
@DogukanUrker DogukanUrker force-pushed the feat/improve-e2e-tests branch from bfb53fa to bdd40ba Compare June 27, 2026 12:19

@coderabbitai coderabbitai Bot left a 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/routes/post.py`:
- Around line 45-48: The login guard in post submission logging now interpolates
user-controlled `url_id` directly into `Log.error`, which reintroduces log
injection risk. Re-add and use `sanitize_for_log` in `app/routes/post.py` for
every externally controlled value included in the message, especially `url_id`
and `request.remote_addr`, and keep the check in the `post` route’s `if
"username" not in session:` block so the log entry cannot contain CRLF or forged
content.

In `@tests/e2e/account/test_account_settings.py`:
- Around line 59-87: The current assertion in
test_change_password_redirects_to_login is too permissive because it allows the
buggy no-op outcome as a passing result, so the test cannot catch the documented
change_password issue. Tighten the expectation in
test_change_password_redirects_to_login to require the successful redirect to
the login flow after submission, and use the existing change_password route
behavior plus _login/page interaction to validate that the password change
actually logs the user out. If the route still fails due to the
session.clear()/session["language"] ordering, fix that logic in change_password
so the redirect path becomes deterministic and the test can assert it directly.

In `@tests/README.md`:
- Around line 37-46: The local suite summary has an inconsistent test-file
count: the headline says 14 test files, but the suite table and Project
Structure tree total 13. Update the summary text in tests/README.md so the file
count matches the actual breakdown shown by the suite table, keeping the test
total and suite details aligned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4dcdbf8-a96c-43db-9a74-ab497fa2e794

📥 Commits

Reviewing files that changed from the base of the PR and between a7b286e and bdd40ba.

📒 Files selected for processing (49)
  • app/routes/account_settings.py
  • app/routes/admin_panel.py
  • app/routes/admin_panel_comments.py
  • app/routes/admin_panel_posts.py
  • app/routes/admin_panel_users.py
  • app/routes/change_password.py
  • app/routes/change_profile_picture.py
  • app/routes/change_username.py
  • app/routes/create_post.py
  • app/routes/dashboard.py
  • app/routes/edit_post.py
  • app/routes/post.py
  • app/routes/verify_user.py
  • app/templates/change_username.html
  • app/translations/de.json
  • app/translations/en.json
  • app/translations/es.json
  • app/translations/fr.json
  • app/translations/hi.json
  • app/translations/ja.json
  • app/translations/pl.json
  • app/translations/pt.json
  • app/translations/ru.json
  • app/translations/tr.json
  • app/translations/uk.json
  • app/translations/zh.json
  • app/utils/forms/change_username_form.py
  • app/utils/route_guards.py
  • tests/README.md
  • tests/e2e/account/__init__.py
  • tests/e2e/account/test_account_settings.py
  • tests/e2e/account/test_change_password_flow.py
  • tests/e2e/account/test_dashboard.py
  • tests/e2e/account/test_profile_and_preferences.py
  • tests/e2e/account/test_static_pages.py
  • tests/e2e/admin/__init__.py
  • tests/e2e/admin/test_admin.py
  • tests/e2e/helpers/__init__.py
  • tests/e2e/helpers/database_helpers.py
  • tests/e2e/home/__init__.py
  • tests/e2e/home/test_home.py
  • tests/e2e/pages/__init__.py
  • tests/e2e/pages/create_post_page.py
  • tests/e2e/pages/post_page.py
  • tests/e2e/post/__init__.py
  • tests/e2e/post/test_post.py
  • tests/e2e/search/__init__.py
  • tests/e2e/search/test_category.py
  • tests/e2e/search/test_search.py
✅ Files skipped from review due to trivial changes (7)
  • tests/e2e/search/init.py
  • tests/e2e/admin/init.py
  • tests/e2e/post/init.py
  • tests/e2e/account/init.py
  • app/templates/change_username.html
  • tests/e2e/home/init.py
  • app/translations/pl.json
🚧 Files skipped from review as they are similar to previous changes (24)
  • tests/e2e/account/test_static_pages.py
  • tests/e2e/pages/init.py
  • app/routes/account_settings.py
  • tests/e2e/home/test_home.py
  • app/routes/admin_panel_comments.py
  • app/translations/de.json
  • app/translations/ru.json
  • app/routes/verify_user.py
  • app/translations/hi.json
  • tests/e2e/helpers/database_helpers.py
  • tests/e2e/helpers/init.py
  • app/translations/tr.json
  • app/translations/es.json
  • app/translations/uk.json
  • app/routes/edit_post.py
  • tests/e2e/pages/post_page.py
  • tests/e2e/search/test_search.py
  • app/routes/change_profile_picture.py
  • app/routes/change_password.py
  • app/routes/admin_panel_posts.py
  • app/routes/admin_panel.py
  • app/translations/ja.json
  • app/routes/change_username.py
  • app/routes/create_post.py

Comment thread app/routes/post.py
Comment on lines +45 to +48
if "username" not in session:
Log.error(
f'{request.remote_addr} attempted to submit to post "{url_id}" without login',
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Log injection regression: url_id is logged unsanitized.

This PR removed the sanitize_for_log import (per change details, lines 20-21), and the old per-comment login check that sanitized request.remote_addr/url_id before logging. The new guard now interpolates url_id — which is user-controlled from the URL path — directly into the log message. A crafted url_id containing CRLF can forge log entries (CWE-117).

🛡️ Proposed fix — re-sanitize logged inputs
             if "username" not in session:
                 Log.error(
-                    f'{request.remote_addr} attempted to submit to post "{url_id}" without login',
+                    f'{sanitize_for_log(request.remote_addr)} attempted to submit to post "{sanitize_for_log(url_id)}" without login',
                 )

Re-add the import (around lines 20-21):

+from app.utils.log import sanitize_for_log  # adjust to the actual module path
📝 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 "username" not in session:
Log.error(
f'{request.remote_addr} attempted to submit to post "{url_id}" without login',
)
if "username" not in session:
Log.error(
f'{sanitize_for_log(request.remote_addr)} attempted to submit to post "{sanitize_for_log(url_id)}" without login',
)
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 45-47: Logging request-derived input unsanitized allows log forging (CRLF injection); strip newlines / encode the value before logging.
Context: Log.error(
f'{request.remote_addr} attempted to submit to post "{url_id}" without login',
)
Note: [CWE-117] Improper Output Neutralization for Logs. OWASP A09:2021 Security Logging and Monitoring Failures.

(log-injection-python)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/routes/post.py` around lines 45 - 48, The login guard in post submission
logging now interpolates user-controlled `url_id` directly into `Log.error`,
which reintroduces log injection risk. Re-add and use `sanitize_for_log` in
`app/routes/post.py` for every externally controlled value included in the
message, especially `url_id` and `request.remote_addr`, and keep the check in
the `post` route’s `if "username" not in session:` block so the log entry cannot
contain CRLF or forged content.

Source: Linters/SAST tools

Comment on lines +59 to +87
def test_change_password_redirects_to_login(self, page, flask_server, test_user):
"""Changing password should log out the user and redirect to login.

note: the change_password route has a known bug at line 76-81 where
session.clear() is called before accessing session["language"]. this
test may surface a 500 error. if it does, the route needs fixing first.
"""
_login(page, flask_server, test_user.username, test_user.password)

page.goto(
f"{flask_server['base_url']}/change-password",
wait_until="domcontentloaded",
)

new_password = "NewPassword456!"
page.fill('input[name="old_password"]', test_user.password)
page.fill('input[name="password"]', new_password)
page.fill('input[name="password_confirm"]', new_password)
page.click('button[type="submit"]')

# the route should redirect to /login/ after password change.
# if the session.clear() bug is present, this may result in an error
# page instead - which is also a valid (bug-catching) outcome.
expect(page).to_have_url(
re.compile(
rf"^{re.escape(flask_server['base_url'])}/(login/.*|change-password)$"
),
timeout=10000,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This test passes regardless of the documented change_password bug, weakening coverage.

The URL assertion accepts either /login/... (success) or /change-password (the buggy/no-op outcome described in the docstring), so the test cannot fail on the very bug it references. Since test_change_password_flow.py already exercises the real rotation, consider either tightening this assertion to require the login redirect or fixing the underlying session.clear()-before-session["language"] ordering in the route. Want me to open an issue to track the route bug?

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 82-84: Regex pattern passed to re is built from a non-literal (variable, call, concatenation, or f-string) value. If that value is attacker-controlled it can introduce a malicious pattern with catastrophic backtracking (ReDoS). Use a hardcoded literal pattern, or validate/escape untrusted input with re.escape() and bound the regex complexity before compiling.
Context: re.compile(
rf"^{re.escape(flask_server['base_url'])}/(login/.*|change-password)$"
)
Note: [CWE-1333] Inefficient Regular Expression Complexity.

(redos-non-literal-regex-python)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/account/test_account_settings.py` around lines 59 - 87, The current
assertion in test_change_password_redirects_to_login is too permissive because
it allows the buggy no-op outcome as a passing result, so the test cannot catch
the documented change_password issue. Tighten the expectation in
test_change_password_redirects_to_login to require the successful redirect to
the login flow after submission, and use the existing change_password route
behavior plus _login/page interaction to validate that the password change
actually logs the user out. If the route still fails due to the
session.clear()/session["language"] ordering, fix that logic in change_password
so the redirect path becomes deterministic and the test can assert it directly.

Comment thread tests/README.md
Comment on lines +37 to +46
Current local suite size: **110 tests** across **14 test files**.

| Suite | Files | Tests | Focus |
| ----- | ----- | ----- | ----- |
| `e2e/auth/` | 3 | 62 | Login, signup, logout, session handling |
| `e2e/account/` | 5 | 17 | Account settings, username/profile updates, password change flow, dashboard, static pages, preferences |
| `e2e/post/` | 1 | 14 | Create/edit/delete post, comments, authorization, admin moderation via protected POST flows |
| `e2e/admin/` | 1 | 8 | Admin access control, users (role + delete), comments management |
| `e2e/search/` | 2 | 6 | Search results and category filtering |
| `e2e/home/` | 1 | 3 | Home rendering and sorting routes |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test-file count is inconsistent (14 vs 13).

Line 37 states 14 test files, but the suite table sums to 13 files (3+5+1+1+2+1) and the Project Structure tree also lists 13 test_*.py files. The test count (110) matches the table; only the file count is off.

✏️ Proposed fix
-Current local suite size: **110 tests** across **14 test files**.
+Current local suite size: **110 tests** across **13 test files**.
📝 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
Current local suite size: **110 tests** across **14 test files**.
| Suite | Files | Tests | Focus |
| ----- | ----- | ----- | ----- |
| `e2e/auth/` | 3 | 62 | Login, signup, logout, session handling |
| `e2e/account/` | 5 | 17 | Account settings, username/profile updates, password change flow, dashboard, static pages, preferences |
| `e2e/post/` | 1 | 14 | Create/edit/delete post, comments, authorization, admin moderation via protected POST flows |
| `e2e/admin/` | 1 | 8 | Admin access control, users (role + delete), comments management |
| `e2e/search/` | 2 | 6 | Search results and category filtering |
| `e2e/home/` | 1 | 3 | Home rendering and sorting routes |
Current local suite size: **110 tests** across **13 test files**.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/README.md` around lines 37 - 46, The local suite summary has an
inconsistent test-file count: the headline says 14 test files, but the suite
table and Project Structure tree total 13. Update the summary text in
tests/README.md so the file count matches the actual breakdown shown by the
suite table, keeping the test total and suite details aligned.

…through

When delete_post or delete_comment returned False (unauthorized), the code
fell through to the comment-creation path which crashed on the missing
'comment' form field, producing a 400 BAD REQUEST. Now we always redirect
back to the post page regardless of success/failure.
…egular user login redirect

This helps prevent flaky test failures in CI due to timing issues when the redirect resolves faster or slower than the wait listener expects. This matches the existing pattern in the admin logout test.
@DogukanUrker DogukanUrker merged commit ac3e250 into main Jun 27, 2026
3 checks passed
@DogukanUrker DogukanUrker deleted the feat/improve-e2e-tests branch June 27, 2026 12:50
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