Skip to content

Feat: User Invitation flow#739

Open
Ayush8923 wants to merge 50 commits intomainfrom
feat/invitation-flow
Open

Feat: User Invitation flow#739
Ayush8923 wants to merge 50 commits intomainfrom
feat/invitation-flow

Conversation

@Ayush8923
Copy link
Copy Markdown
Collaborator

@Ayush8923 Ayush8923 commented Apr 8, 2026

Issue: ProjectTech4DevAI/kaapi-frontend#112

Summary:

  • Implement functionality to send a user invitation email when an admin adds a user to their project.
  • Create a dedicated API endpoint to verify whether the token is valid.

Notes:

  • Need to add/update the .env with this values:
SMTP_HOST=
SMTP_PORT=
SMTP_TLS=True
SMTP_USER=
SMTP_PASSWORD=
EMAILS_FROM_EMAIL=
EMAILS_FROM_NAME=Kaapi
FRONTEND_HOST=

Summary by CodeRabbit

  • New Features

    • Email-based user invitation flow: send invite emails, generate/verify invite tokens, and auto-activate invited users on first acceptance.
    • Configurable email settings and frontend host for invitation links; customizable sender name.
  • Documentation

    • Added guide for invitation verification endpoint and expected responses.
  • Tests

    • New tests covering invite token generation/verification, invite verification endpoint, and invite-email sending.

vprashrex and others added 30 commits March 27, 2026 16:40
- Add GET /projects/organization/{org_id} endpoint with org validation
- Add has_more pagination to organizations list endpoint
- Add Swagger docs for list_by_org
…organizations; update read_projects_by_organization response type
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Adds email invitation support: SMTP config and env vars, invite token generation/verification, an email template and generator, sending invites when adding project users, a verification endpoint that activates invited users and issues tokens, and accompanying tests and docs.

Changes

Cohort / File(s) Summary
Env & Config
\.env.example, backend/app/core/config.py
Added SMTP/email and frontend settings: SMTP_HOST, SMTP_PORT, SMTP_TLS, SMTP_USER, SMTP_PASSWORD, EMAILS_FROM_EMAIL, EMAILS_FROM_NAME, FRONTEND_HOST, and INVITE_TOKEN_EXPIRE_HOURS; new emails_enabled property.
Auth service
backend/app/services/auth.py
Added generate_invite_token(email, organization_id, project_id) and verify_invite_token(token) to create/validate JWT invite tokens.
API routes
backend/app/api/routes/auth.py, backend/app/api/routes/user_project.py
New GET /auth/invite/verify endpoint that verifies invite tokens, activates invited users, and issues auth tokens; add_project_users now generates invite tokens and sends invite emails per recipient (with error logging).
Email templates & utils
backend/app/email-templates/build/invite_user.html, backend/app/utils.py
Added invite HTML template and generate_invite_email(...) to render invite content and build magic-link URLs.
Docs
backend/app/api/docs/auth/invite_verify.md
Added documentation describing the invitation verification endpoint, expected behavior, and error responses.
Tests
backend/app/tests/api/test_auth.py, backend/app/tests/api/test_user_project.py
Added tests for invite token generation/verification, GET /auth/invite/verify behavior (400/404/200 cases, activation), and a test ensuring add-project-users sends invite emails; minor adjustments to an existing Google-auth test.
sequenceDiagram
    participant Admin as Admin (Client)
    participant API as API Server
    participant AuthSvc as Auth Service
    participant EmailGen as Email Generator
    participant SMTP as SMTP/Email Sender
    participant DB as Database
    participant User as Invited User

    Admin->>API: POST /projects/{id}/users (add emails)
    API->>DB: save user-project associations
    API->>AuthSvc: generate_invite_token(email, org_id, project_id)
    AuthSvc-->>API: invite_token (JWT)
    API->>EmailGen: generate_invite_email(..., invite_token)
    EmailGen-->>API: EmailData (html, subject)
    API->>SMTP: send email to recipient
    SMTP-->>API: send result (ok / error)

    User->>API: GET /auth/invite/verify?token={invite_token}
    API->>AuthSvc: verify_invite_token(token)
    AuthSvc-->>API: {email, organization_id, project_id}
    API->>DB: lookup user by email
    DB-->>API: user (maybe inactive)
    API->>DB: activate user if inactive
    API->>AuthSvc: build_token_response(user_id, org_id, project_id)
    AuthSvc-->>API: access_token + refresh_token
    API-->>User: set HTTP-only cookies, return token payload
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A hop, a token, a link on its way,

Invites in the inbox to brighten the day.
From config to template, the magic unfolds,
Users awaken as the JWT molds.
Hop-hop — emails sent, new journeys begin!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: User Invitation flow' directly and concisely summarizes the primary feature added—a complete user invitation workflow including email delivery and token verification.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/invitation-flow

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.

@Ayush8923 Ayush8923 self-assigned this Apr 8, 2026
@Ayush8923 Ayush8923 changed the title Feat/invitation flow Feat: User Invitation flow Apr 8, 2026
Base automatically changed from feat/add-user-project to feat/google-integration-auth-flow April 9, 2026 11:03
Base automatically changed from feat/google-integration-auth-flow to main April 10, 2026 05:36
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/tests/api/test_auth.py (1)

89-108: ⚠️ Potential issue | 🟡 Minor

Test name contradicts both the docstring and actual behavior.

The test is named test_google_auth_inactive_user_rejected but:

  1. The docstring on line 92 states it tests that "inactive user is activated on first Google login"
  2. The test expects HTTP 200 (success), not a rejection
  3. Per backend/app/api/routes/auth.py lines 78-84, inactive users ARE activated on first Google login

The original name test_google_auth_activates_inactive_user was accurate. Consider reverting the rename or updating the test to match the new name if the intended behavior has changed.

Proposed fix: Revert to accurate test name
-    def test_google_auth_inactive_user_rejected(
+    def test_google_auth_activates_inactive_user(
         self, mock_settings, mock_verify, db: Session, client: TestClient
     ):
         """Test that inactive user is activated on first Google login."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/tests/api/test_auth.py` around lines 89 - 108, The test name is
misleading—rename the test function back to
test_google_auth_activates_inactive_user and ensure its docstring and assertions
match that behavior: update the function name (currently
test_google_auth_inactive_user_rejected) in backend/app/tests/api/test_auth.py
to test_google_auth_activates_inactive_user and keep the existing docstring and
the assert expecting HTTP 200 so it accurately reflects that inactive users are
activated on first Google login (referencing the Google auth flow used in the
test and mock_verify/_mock_idinfo).
🧹 Nitpick comments (4)
backend/app/api/routes/user_project.py (1)

99-124: Synchronous email sending may slow response time.

Sending emails synchronously in the request loop could significantly delay the API response when adding multiple users, especially if the SMTP server is slow or unreachable. Consider offloading email delivery to a background task (e.g., Celery) for better user experience.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/routes/user_project.py` around lines 99 - 124, The loop
currently sends emails synchronously using generate_invite_token,
generate_invite_email and send_email which can block the request; change this to
enqueue an asynchronous background job instead (e.g., push a task to
Celery/RQ/your background worker) that receives the email params (email,
project_name, organization_name, invite_token) and performs
generate_invite_email/send_email there, while the API handler immediately
returns after scheduling the tasks and logs task IDs via logger; ensure
exceptions in the worker are handled and retried rather than blocking the
request flow.
backend/app/email-templates/build/invite_user.html (1)

15-17: Hardcoded app name inconsistent with template variable usage.

Line 16 hardcodes "Kaapi Konsole" while line 27 uses {{ app_name }} from the template context. For consistency and maintainability, use the template variable throughout.

Proposed fix
               <h1 style="margin: 0 0 4px; font-size: 22px; font-weight: 600; color: `#18181b`;">
-                Kaapi Konsole
+                {{ app_name }}
               </h1>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/email-templates/build/invite_user.html` around lines 15 - 17,
Replace the hardcoded "Kaapi Konsole" text inside the h1 element with the
template variable {{ app_name }} so the template uses the same context value
used elsewhere (line that currently uses {{ app_name }}); keep the existing h1
styling/attributes unchanged and ensure the variable is rendered/escaped
consistently with the other occurrences of {{ app_name }} in the template.
backend/app/services/auth.py (1)

190-192: Inconsistent timestamp format between exp and nbf claims.

exp is encoded as a Unix timestamp (float) via .timestamp(), while nbf is passed as a datetime object. While PyJWT accepts both, this inconsistency can be confusing. Consider using the same format for both.

Proposed fix for consistency
     to_encode = {
         "exp": expires.timestamp(),
-        "nbf": now,
+        "nbf": now.timestamp(),
         "sub": email,
         "org_id": organization_id,
         "project_id": project_id,
         "type": "invite",
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/auth.py` around lines 190 - 192, The JWT claims in
to_encode use inconsistent formats: "exp" uses expires.timestamp() while "nbf"
uses the datetime now object; normalize both to the same type (e.g., numeric
Unix timestamps) to avoid confusion. Locate the to_encode construction in
auth.py (the to_encode dict, the "exp" and "nbf" keys, and the expires and now
variables) and change "nbf" to use now.timestamp() (or convert both to datetime
if you prefer consistency), ensuring the token encoding call uses the updated
values.
backend/app/utils.py (1)

204-204: valid_days could display as 0 for short token expiry.

If INVITE_TOKEN_EXPIRE_HOURS is less than 24, integer division results in valid_days = 0. Consider using max(1, ...) or displaying hours when the expiry is less than a day.

Proposed fix
-            "valid_days": settings.INVITE_TOKEN_EXPIRE_HOURS // 24,
+            "valid_days": max(1, settings.INVITE_TOKEN_EXPIRE_HOURS // 24),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/utils.py` at line 204, The current calculation for "valid_days"
in utils.py uses integer division on settings.INVITE_TOKEN_EXPIRE_HOURS which
yields 0 when the expiry is under 24 hours; update the logic that sets
"valid_days" (the dict key "valid_days") to avoid 0 by converting hours to days
using a safe calculation (e.g., compute days = ceil(INVITE_TOKEN_EXPIRE_HOURS /
24) or use max(1, ...)); alternatively, if you want to preserve precision,
return a human-friendly representation that shows hours when
INVITE_TOKEN_EXPIRE_HOURS < 24 (e.g., use hours string), but ensure the code
around the "valid_days" key (where settings.INVITE_TOKEN_EXPIRE_HOURS is
referenced) is updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/api/routes/auth.py`:
- Around line 204-245: The current verify_invitation handler (verify_invitation)
performs side effects (activates user, commits, issues tokens via
build_token_response) while being a GET; change behavior so GET is read-only:
verify the token using verify_invite_token and return a non-destructive status
(e.g., token valid/expired and minimal invite metadata) without activating the
user or issuing tokens and do not perform DB commits. Add a new POST endpoint
(e.g., accept_invitation) that accepts the invite token in the request body (not
URL), calls verify_invite_token, activates the user (set user.is_active,
session.add/commit/refresh), calls build_token_response to issue tokens/cookies,
and returns the auth response; ensure verify_invitation no longer calls
session.commit, session.refresh, or build_token_response and that secrets are
not passed in URLs.
- Around line 219-240: The token is minted directly from invite_data which can
be stale; before calling build_token_response in the verify-invite flow,
re-resolve current access from the DB (using the same logic as select_project on
lines ~142-156) — e.g., query the user's current organization/project membership
with the session (or call select_project(user.id,
invite_data["organization_id"], invite_data["project_id"], session)) and if the
project/organization access no longer exists or the mapping changed, raise an
HTTPException (403 or 404) rejecting the stale invite; only call
build_token_response when the DB-confirmed project access is present.

In `@backend/app/api/routes/user_project.py`:
- Around line 118-123: Log messages in add_project_users currently print raw
emails; update the logger calls (logger.info and logger.error in the
add_project_users block) to pass the email through mask_string before
interpolation so sensitive addresses are masked. Ensure you call
mask_string(entry.email) in both the success and exception log lines and keep
the existing format that prefixes the function name in square brackets; also add
an import for mask_string if it's not already imported.

In `@backend/app/utils.py`:
- Around line 187-207: The generate_invite_email function declares email_to but
doesn't pass it to the template; update generate_invite_email to include the
email address in the render_email_template context (add "email": email_to or
same key used by generate_reset_password_email/generate_new_account_email) so
the invite template can render the recipient, or if the template intentionally
doesn't need it, remove the unused email_to parameter from the function
signature and all call sites; locate the function by name
(generate_invite_email) and adjust either the context dict or the signature
accordingly.

---

Outside diff comments:
In `@backend/app/tests/api/test_auth.py`:
- Around line 89-108: The test name is misleading—rename the test function back
to test_google_auth_activates_inactive_user and ensure its docstring and
assertions match that behavior: update the function name (currently
test_google_auth_inactive_user_rejected) in backend/app/tests/api/test_auth.py
to test_google_auth_activates_inactive_user and keep the existing docstring and
the assert expecting HTTP 200 so it accurately reflects that inactive users are
activated on first Google login (referencing the Google auth flow used in the
test and mock_verify/_mock_idinfo).

---

Nitpick comments:
In `@backend/app/api/routes/user_project.py`:
- Around line 99-124: The loop currently sends emails synchronously using
generate_invite_token, generate_invite_email and send_email which can block the
request; change this to enqueue an asynchronous background job instead (e.g.,
push a task to Celery/RQ/your background worker) that receives the email params
(email, project_name, organization_name, invite_token) and performs
generate_invite_email/send_email there, while the API handler immediately
returns after scheduling the tasks and logs task IDs via logger; ensure
exceptions in the worker are handled and retried rather than blocking the
request flow.

In `@backend/app/email-templates/build/invite_user.html`:
- Around line 15-17: Replace the hardcoded "Kaapi Konsole" text inside the h1
element with the template variable {{ app_name }} so the template uses the same
context value used elsewhere (line that currently uses {{ app_name }}); keep the
existing h1 styling/attributes unchanged and ensure the variable is
rendered/escaped consistently with the other occurrences of {{ app_name }} in
the template.

In `@backend/app/services/auth.py`:
- Around line 190-192: The JWT claims in to_encode use inconsistent formats:
"exp" uses expires.timestamp() while "nbf" uses the datetime now object;
normalize both to the same type (e.g., numeric Unix timestamps) to avoid
confusion. Locate the to_encode construction in auth.py (the to_encode dict, the
"exp" and "nbf" keys, and the expires and now variables) and change "nbf" to use
now.timestamp() (or convert both to datetime if you prefer consistency),
ensuring the token encoding call uses the updated values.

In `@backend/app/utils.py`:
- Line 204: The current calculation for "valid_days" in utils.py uses integer
division on settings.INVITE_TOKEN_EXPIRE_HOURS which yields 0 when the expiry is
under 24 hours; update the logic that sets "valid_days" (the dict key
"valid_days") to avoid 0 by converting hours to days using a safe calculation
(e.g., compute days = ceil(INVITE_TOKEN_EXPIRE_HOURS / 24) or use max(1, ...));
alternatively, if you want to preserve precision, return a human-friendly
representation that shows hours when INVITE_TOKEN_EXPIRE_HOURS < 24 (e.g., use
hours string), but ensure the code around the "valid_days" key (where
settings.INVITE_TOKEN_EXPIRE_HOURS is referenced) is updated accordingly.
🪄 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: b8bd898d-60f0-440e-ad07-982c03efc909

📥 Commits

Reviewing files that changed from the base of the PR and between c24effe and 254fadc.

📒 Files selected for processing (9)
  • .env.example
  • backend/app/api/docs/auth/invite_verify.md
  • backend/app/api/routes/auth.py
  • backend/app/api/routes/user_project.py
  • backend/app/core/config.py
  • backend/app/email-templates/build/invite_user.html
  • backend/app/services/auth.py
  • backend/app/tests/api/test_auth.py
  • backend/app/utils.py

Comment on lines +219 to +240
user = get_user_by_email(session=session, email=invite_data["email"])
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User account not found. Please contact support.",
)

# Activate user if not already active
if not user.is_active:
user.is_active = True
session.add(user)
session.commit()
session.refresh(user)
logger.info(
f"[verify_invitation] User activated via invite | user_id: {user.id}"
)

response = build_token_response(
user_id=user.id,
organization_id=invite_data["organization_id"],
project_id=invite_data["project_id"],
)
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

Re-check current project access before issuing the scoped JWT.

Line 236 builds the session directly from invite_data["organization_id"] / ["project_id"]. If that invite is later revoked or the user-project mapping changes after the email is sent, this endpoint still mints a token for the stale project. Please resolve current access from the DB first, like select_project already does on Lines 142-156, and reject stale invites.

Suggested guard
     user = get_user_by_email(session=session, email=invite_data["email"])
     if not user:
         raise HTTPException(
             status_code=status.HTTP_404_NOT_FOUND,
             detail="User account not found. Please contact support.",
         )

+    available_projects = get_user_accessible_projects(session=session, user_id=user.id)
+    matching = [
+        p
+        for p in available_projects
+        if p["organization_id"] == invite_data["organization_id"]
+        and p["project_id"] == invite_data["project_id"]
+    ]
+    if not matching:
+        raise HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN,
+            detail="Invitation is no longer valid",
+        )
+
     # Activate user if not already active
     if not user.is_active:
         user.is_active = True
         session.add(user)
         session.commit()
         session.refresh(user)
         logger.info(
             f"[verify_invitation] User activated via invite | user_id: {user.id}"
         )

+    proj = matching[0]
     response = build_token_response(
         user_id=user.id,
-        organization_id=invite_data["organization_id"],
-        project_id=invite_data["project_id"],
+        organization_id=proj["organization_id"],
+        project_id=proj["project_id"],
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/routes/auth.py` around lines 219 - 240, The token is minted
directly from invite_data which can be stale; before calling
build_token_response in the verify-invite flow, re-resolve current access from
the DB (using the same logic as select_project on lines ~142-156) — e.g., query
the user's current organization/project membership with the session (or call
select_project(user.id, invite_data["organization_id"],
invite_data["project_id"], session)) and if the project/organization access no
longer exists or the mapping changed, raise an HTTPException (403 or 404)
rejecting the stale invite; only call build_token_response when the DB-confirmed
project access is present.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 97.50000% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
backend/app/api/routes/user_project.py 87.50% 2 Missing ⚠️
backend/app/services/auth.py 93.33% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

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: 2

🧹 Nitpick comments (1)
backend/app/tests/api/test_user_project.py (1)

136-136: Assert send_email call arguments, not only call count.

assert_called_once() can pass even if the email was sent to the wrong recipient. Assert key kwargs to make this test resilient.

Proposed diff
-from unittest.mock import patch
+from unittest.mock import ANY, patch
@@
-        mock_send_email.assert_called_once()
+        mock_send_email.assert_called_once_with(
+            email_to=email,
+            subject=ANY,
+            html_content=ANY,
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/tests/api/test_user_project.py` at line 136, Test currently only
checks mock_send_email.assert_called_once(), which misses verifying
recipients/contents; update the assertion in test_user_project (where
mock_send_email is used) to assert the actual call arguments: either replace
assert_called_once() with assert_called_once_with(...) supplying the expected
positional/keyword params (e.g., recipient/to, subject, template, context) or
retrieve mock_send_email.call_args and assert call_args.kwargs['to'] (and other
key kwargs) equal the expected values so the test verifies the correct recipient
and email payload.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/tests/api/test_auth.py`:
- Around line 305-374: All six test methods in TestInviteVerify and
TestTokenGeneration are missing explicit return type annotations; add "-> None"
to the signatures of test_verify_invalid_token, test_verify_user_not_found,
test_verify_activates_inactive_user, test_verify_success_active_user,
test_generate_and_verify_invite_token, and test_verify_invite_token_invalid so
each method signature declares a None return type to satisfy the repository
typing standard.

In `@backend/app/tests/api/test_user_project.py`:
- Around line 108-115: Update the test signature for
test_add_user_sends_invite_email to add proper type hints: annotate the
mock_settings and mock_send_email parameters as MagicMock and add an explicit
return type -> None; locate the function definition named
test_add_user_sends_invite_email and change its signature to include
mock_settings: MagicMock, mock_send_email: MagicMock, and the trailing -> None
while leaving other parameters unchanged so type checkers accept the test.

---

Nitpick comments:
In `@backend/app/tests/api/test_user_project.py`:
- Line 136: Test currently only checks mock_send_email.assert_called_once(),
which misses verifying recipients/contents; update the assertion in
test_user_project (where mock_send_email is used) to assert the actual call
arguments: either replace assert_called_once() with assert_called_once_with(...)
supplying the expected positional/keyword params (e.g., recipient/to, subject,
template, context) or retrieve mock_send_email.call_args and assert
call_args.kwargs['to'] (and other key kwargs) equal the expected values so the
test verifies the correct recipient and email payload.
🪄 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: ac513b9f-03b5-4368-8dff-2871fd057a9c

📥 Commits

Reviewing files that changed from the base of the PR and between 254fadc and e3db12c.

📒 Files selected for processing (2)
  • backend/app/tests/api/test_auth.py
  • backend/app/tests/api/test_user_project.py

Comment on lines +305 to +374
class TestInviteVerify:
"""Test suite for GET /auth/invite/verify endpoint."""

def test_verify_invalid_token(self, client: TestClient):
"""Test returns 400 for invalid invite token."""
resp = client.get(f"{INVITE_VERIFY_URL}?token=invalid.token")
assert resp.status_code == 400

def test_verify_user_not_found(self, client: TestClient):
"""Test returns 404 when invited user doesn't exist."""
token = generate_invite_token(
email="ghost@example.com", organization_id=1, project_id=1
)
resp = client.get(f"{INVITE_VERIFY_URL}?token={token}")
assert resp.status_code == 404

def test_verify_activates_inactive_user(
self, db: Session, client: TestClient, user_api_key: TestAuthContext
):
"""Test invite verification activates inactive user."""
user = create_random_user(db)
user.is_active = False
db.add(user)
db.commit()
db.refresh(user)

token = generate_invite_token(
email=user.email,
organization_id=user_api_key.organization.id,
project_id=user_api_key.project.id,
)
resp = client.get(f"{INVITE_VERIFY_URL}?token={token}")
assert resp.status_code == 200

db.refresh(user)
assert user.is_active is True
assert "access_token" in resp.json()["data"]

def test_verify_success_active_user(
self, db: Session, client: TestClient, user_api_key: TestAuthContext
):
"""Test invite verification works for already active user."""
user = create_random_user(db)
token = generate_invite_token(
email=user.email,
organization_id=user_api_key.organization.id,
project_id=user_api_key.project.id,
)
resp = client.get(f"{INVITE_VERIFY_URL}?token={token}")
assert resp.status_code == 200
assert "access_token" in resp.json()["data"]


class TestTokenGeneration:
"""Test suite for services/auth.py token generation functions."""

def test_generate_and_verify_invite_token(self):
"""Test invite token roundtrip."""
token = generate_invite_token(
email="test@example.com", organization_id=1, project_id=2
)
result = verify_invite_token(token)
assert result is not None
assert result["email"] == "test@example.com"
assert result["organization_id"] == 1
assert result["project_id"] == 2

def test_verify_invite_token_invalid(self):
"""Test invite verify returns None for garbage tokens."""
assert verify_invite_token("garbage") is None
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify defs without return type annotations in this file.
rg -nP '^\s*def\s+\w+\([^)]*\):\s*$' backend/app/tests/api/test_auth.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 922


🏁 Script executed:

#!/bin/bash
# Extract the exact content from lines 305-374 to see complete method definitions
sed -n '305,374p' backend/app/tests/api/test_auth.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 2747


Add return type annotations (-> None) to all test methods in this block.

The 6 test methods lack explicit -> None return type annotations, which violates the repository's mandatory typing standard for all Python files. The methods are:

  • test_verify_invalid_token
  • test_verify_user_not_found
  • test_verify_activates_inactive_user
  • test_verify_success_active_user
  • test_generate_and_verify_invite_token
  • test_verify_invite_token_invalid
Proposed diff
 class TestInviteVerify:
     """Test suite for GET /auth/invite/verify endpoint."""

     def test_verify_invalid_token(self, client: TestClient):
+    def test_verify_invalid_token(self, client: TestClient) -> None:
         """Test returns 400 for invalid invite token."""

     def test_verify_user_not_found(self, client: TestClient):
+    def test_verify_user_not_found(self, client: TestClient) -> None:
         """Test returns 404 when invited user doesn't exist."""

     def test_verify_activates_inactive_user(
         self, db: Session, client: TestClient, user_api_key: TestAuthContext
     ):
+    ) -> None:
         """Test invite verification activates inactive user."""

     def test_verify_success_active_user(
         self, db: Session, client: TestClient, user_api_key: TestAuthContext
     ):
+    ) -> None:
         """Test invite verification works for already active user."""

 class TestTokenGeneration:
     """Test suite for services/auth.py token generation functions."""

     def test_generate_and_verify_invite_token(self):
+    def test_generate_and_verify_invite_token(self) -> None:
         """Test invite token roundtrip."""

     def test_verify_invite_token_invalid(self):
+    def test_verify_invite_token_invalid(self) -> None:
         """Test invite verify returns None for garbage tokens."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/tests/api/test_auth.py` around lines 305 - 374, All six test
methods in TestInviteVerify and TestTokenGeneration are missing explicit return
type annotations; add "-> None" to the signatures of test_verify_invalid_token,
test_verify_user_not_found, test_verify_activates_inactive_user,
test_verify_success_active_user, test_generate_and_verify_invite_token, and
test_verify_invite_token_invalid so each method signature declares a None return
type to satisfy the repository typing standard.

Comment on lines +108 to +115
def test_add_user_sends_invite_email(
self,
mock_settings,
mock_send_email,
db: Session,
client: TestClient,
superuser_token_headers: dict[str, str],
):
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:

#!/bin/bash
# Verify defs without return type annotations in this file.
rg -nP '^\s*def\s+\w+\([^)]*\):\s*$' backend/app/tests/api/test_user_project.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


🏁 Script executed:

# Check the actual code at lines 108-115 and surrounding context
head -120 backend/app/tests/api/test_user_project.py | tail -30

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1096


🏁 Script executed:

# Check the imports at the top of the file
head -30 backend/app/tests/api/test_user_project.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1004


🏁 Script executed:

# Search for the test method to see full signature
rg -A 10 'def test_add_user_sends_invite_email' backend/app/tests/api/test_user_project.py

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 440


Add type annotations to the test method signature.

The mock_settings and mock_send_email parameters are untyped, and the method lacks an explicit return type. Add type hints using MagicMock and -> None to comply with the coding guidelines.

Proposed changes
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
@@
     def test_add_user_sends_invite_email(
         self,
-        mock_settings,
-        mock_send_email,
+        mock_settings: MagicMock,
+        mock_send_email: MagicMock,
         db: Session,
         client: TestClient,
         superuser_token_headers: dict[str, str],
-    ):
+    ) -> None:
📝 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
def test_add_user_sends_invite_email(
self,
mock_settings,
mock_send_email,
db: Session,
client: TestClient,
superuser_token_headers: dict[str, str],
):
def test_add_user_sends_invite_email(
self,
mock_settings: MagicMock,
mock_send_email: MagicMock,
db: Session,
client: TestClient,
superuser_token_headers: dict[str, str],
) -> None:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/tests/api/test_user_project.py` around lines 108 - 115, Update
the test signature for test_add_user_sends_invite_email to add proper type
hints: annotate the mock_settings and mock_send_email parameters as MagicMock
and add an explicit return type -> None; locate the function definition named
test_add_user_sends_invite_email and change its signature to include
mock_settings: MagicMock, mock_send_email: MagicMock, and the trailing -> None
while leaving other parameters unchanged so type checkers accept the test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants