Skip to content

Unit and integration tests#18

Open
jeffmaki wants to merge 17 commits into
mainfrom
jeff-tests
Open

Unit and integration tests#18
jeffmaki wants to merge 17 commits into
mainfrom
jeff-tests

Conversation

@jeffmaki

@jeffmaki jeffmaki commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Tests generated via AI via "@test: XXYYZZ" annotations on top of method defs.

This PR adds broad unit and integration test coverage across the backend, along with supporting test infrastructure and documentation.

Highlights:

  • Adds new test utilities for fake DB sessions, HTTP proxy mocking, and deterministic object factories.
  • Introduces extensive unit tests for config loading, JSON schema validation, JWT validation, security/user-info behavior, repository logic, and schema serialization/validation.
  • Adds integration tests covering health checks, OSM proxy behavior, teams, users, and workspaces end-to-end.
  • Updates several core modules to fail fast with clear 503 errors when shared HTTP clients are uninitialized, and adds small validation/assertion hardening in workspace/team routes and schema constructors.
  • Adds @test coverage annotations and repository guidance in CLAUDE.md, plus expanded test-running instructions in README.md and tests/README.md.
  • Adjusts CI to consolidate jobs, enable caching, run Pyright, and simplify test execution.
  • Adds Pyright/isort/pytest configuration updates to support the new test suite and suppress framework-related false positives.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e5a98e3-5125-4905-99fb-1db2bbfb3cbc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR updates CI and project guidance, adds fail-fast HTTP-client guards and validation checks in core and workspace/team/user APIs, and introduces shared test fixtures plus new unit and integration coverage.

Changes

Repository validation and test coverage

Layer / File(s) Summary
Repo guidance and CI
.github/workflows/ci.yml, CLAUDE.md, README.md, pyproject.toml
CI now runs cached uv setup, uses the matrix Python version, and adds pyright before pytest; repository guidance documents test commands, role conventions, Pyright/Alembic notes, and # @test: conventions.
Core client guards
api/core/*.py, api/main.py
Core schema and token helpers add test-outline comments, explicit HTTP-client initialization checks, cached schema fetches, and fail-fast 503 handling for uninitialized shared clients and proxy paths.
Workspace, team, and user API checks
api/src/workspaces/*, api/src/teams/*, api/src/users/*
Workspace, team, and user routes/schemas add Pyright suppressions, # @test: annotations, persisted-id assertions, and explicit validation for missing HTTP/client data and JSON quest payloads.
Shared test harness
tests/README.md, tests/conftest.py, tests/support/*, tests/test_main.py, tests/integration/test_health.py
Shared test documentation, fake sessions, streaming HTTP transport, ASGI fixtures, and the health smoke test are added, and the old direct TestClient health test is removed.
Core unit tests
tests/unit/test_config.py, tests/unit/test_json_schema.py, tests/unit/test_jwt.py, tests/unit/test_security.py, tests/unit/test_user_info.py
Unit tests cover settings parsing, JSON schema validation and caching, JWT validation, security token caching, and UserInfo role helpers.
Domain unit tests
tests/unit/test_teams_schemas.py, tests/unit/test_users_schemas.py, tests/unit/test_workspace_repository.py, tests/unit/test_workspaces_schemas.py
Unit tests verify team, user, and workspace schema metadata, repository behavior, response serialization, and workspace settings validation.
Integration route tests
tests/integration/test_proxy.py, tests/integration/test_teams.py, tests/integration/test_users.py, tests/integration/test_workspaces.py
Integration tests cover OSM proxy behavior plus workspace, team, and user endpoint responses across success, permission, validation, cache, and error cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • cyrossignol

Poem

A rabbit hopped through CI night,
With cached uv purring warm and bright.
I sniffed the guards and tests in rows,
Then twitched my nose at type-check glows.
Hop, thunk! The schemas stayed just right. 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title matches the change category, but it is generic and does not convey the specific focus of the test additions. Use a more specific title such as adding unit and integration coverage for workspaces, teams, users, and proxy behavior.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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.

@jeffmaki jeffmaki changed the title Jeff tests Unit and integration tests Jun 24, 2026
@jeffmaki jeffmaki marked this pull request as ready for review June 24, 2026 18:35
@jeffmaki jeffmaki marked this pull request as draft June 24, 2026 18:36

@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: 9

🧹 Nitpick comments (7)
.github/workflows/ci.yml (2)

10-46: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add an explicit least-privilege permissions block.

The workflow has no permissions: declaration, so the job inherits the repository default GITHUB_TOKEN scopes, which can be broader than this CI job needs (it only checks out and runs tooling). Scoping to read-only reduces blast radius if a step is compromised.

🔒 Proposed least-privilege permissions
 jobs:
   ci:
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
     strategy:
🤖 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 @.github/workflows/ci.yml around lines 10 - 46, Add an explicit
least-privilege permissions block to the ci job/workflow so the default
GITHUB_TOKEN does not inherit broader repository scopes. Update the workflow
around the existing ci job and its checkout/tooling steps to declare read-only
access only, since this job only needs to read the repository and run checks;
use the workflow/job permissions setting alongside actions/checkout, setup-uv,
and the test/lint steps.

Source: Linters/SAST tools


25-26: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Optional: pin actions/setup-python to a commit SHA.

actions/setup-python@v5 (like the other tag-pinned actions here) relies on a mutable tag; a blanket supply-chain policy flagged this. Pinning to a full commit SHA hardens against tag-retargeting. Apply consistently to checkout, setup-uv, and setup-python if you adopt this.

🤖 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 @.github/workflows/ci.yml around lines 25 - 26, The CI workflow currently
uses tag-pinned GitHub Actions, and the review asks to harden supply-chain
security by pinning them to immutable commit SHAs. Update the action references
in the workflow so the existing uses of checkout, setup-uv, and setup-python are
switched from mutable version tags to full commit SHA pins, keeping the same
setup behavior while making the identifiers for those steps more secure and
consistent.

Source: Linters/SAST tools

README.md (1)

22-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language to the fenced code blocks.

markdownlint (MD040) flags the two fenced blocks (Lines 22 and 32) for missing language specifiers. Tag them as shell for proper rendering/linting.

📝 Proposed fix
-```
+```shell
 uv run pytest                 # full suite with coverage (configured in pyproject.toml)
-```
+```shell
 uvx pyright --pythonpath .venv/bin/python api tests
🤖 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 `@README.md` around lines 22 - 35, The fenced code blocks in the README are
missing language specifiers and are triggering markdownlint MD040. Update the
two fenced blocks around the pytest commands and the type-check/format commands
to use a shell language tag, matching the existing command examples, so the
blocks render and lint correctly.

Source: Linters/SAST tools

api/core/security.py (1)

173-175: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Empty # @test: placeholder.

This annotation has no test description, unlike the other authoritative @test outlines. Either fill it in to describe validate_token's intended coverage (cache hit/miss, token rotation eviction, 401 paths) or remove it.

Want me to draft the @test: outline lines for validate_token?

🤖 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 `@api/core/security.py` around lines 173 - 175, The `# `@test`:` annotation under
`validate_token` is empty and should either be completed or removed. Update the
placeholder to describe the intended test coverage for
`validate_token`—including cache hit/miss behavior, token rotation eviction, and
401 response paths—or delete the annotation if it is not meant to be used,
keeping it consistent with the other `@test` outlines in this module.
api/src/workspaces/routes.py (1)

159-159: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

assert for a runtime invariant is stripped under python -O.

This assert doubles as Pyright narrowing for workspace.id: Optional[int], but if the app ever runs with -O, the check vanishes and a None id would silently flow into assign_member_role(...) and the response. Prefer an explicit guard that raises regardless of optimization level. Same pattern exists in api/src/workspaces/schemas.py:238.

♻️ Suggested explicit guard
-        assert workspace.id is not None  # freshly persisted workspace has an id
+        if workspace.id is None:  # freshly persisted workspace must have an id
+            raise RuntimeError("Persisted workspace is missing an id")
🤖 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 `@api/src/workspaces/routes.py` at line 159, The runtime invariant in
workspaces routing is currently enforced with an assert, which disappears under
optimized Python runs. Replace the assert in the workspace member assignment
flow with an explicit guard that raises a real exception if workspace.id is
None, so the check still runs before assign_member_role(...) and response
construction; apply the same pattern anywhere else this is used, including the
similar Optional[int] narrowing case in the schemas code.
api/src/workspaces/schemas.py (1)

84-84: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor grammar typo in @test annotation (repeated).

"any values from Python and properly serialized" should read "any values from Python are properly serialized". Since these comments drive AI test generation, the typo also appears at Lines 112 and 261.

🤖 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 `@api/src/workspaces/schemas.py` at line 84, The repeated `@test` annotation text
has a grammar typo in the schema comments, where “any values from Python and
properly serialized” should be “any values from Python are properly serialized.”
Update the affected `@test` comment(s) in the schemas definitions, including the
repeated occurrences referenced by this same wording, so the test-generation
annotation reads correctly and consistently.
tests/unit/test_teams_schemas.py (1)

21-27: 📐 Maintainability & Code Quality | 🔵 Trivial

Hoist the shared schema helpers _table and _fk_targets are duplicated in tests/unit/test_users_schemas.py, tests/unit/test_workspaces_schemas.py, and this file; move them into tests/support to keep a single definition.

🤖 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/unit/test_teams_schemas.py` around lines 21 - 27, The shared schema
helper functions `_table` and `_fk_targets` are duplicated across multiple
schema test files, so move their single implementation into a reusable helper
under `tests/support` and update `test_teams_schemas`, `test_users_schemas`, and
`test_workspaces_schemas` to import and use it. Keep the helper names or
equivalent unique symbols consistent so the schema tests still reference the
same `__table__` and foreign key target logic from one central place.

Sources: Coding guidelines, Linters/SAST tools

🤖 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 `@api/core/jwt.py`:
- Around line 8-12: The `@test` outline in JWT-related comments is describing JSON
schema validation and network failure handling, which does not match the actual
responsibilities of the JWT logic in core JWT code. Update the `@test` comments in
the JWT class/module to describe JWT-specific behavior that matches the symbols
in this diff and the surrounding JWT implementation, so the authoritative test
contract reflects what this module באמת does. Keep the comments aligned with the
actual JWT parsing/validation/authentication flow rather than generic JSON
payload validation.

In `@api/src/teams/schemas.py`:
- Around line 9-12: The authoritative `@test` annotations in the schema comments
contain a typo that makes the test contract unclear. Update the duplicated `@test`
comment in the schema definition near the relevant class comments so it reads
that values from Python are properly serialized to the database, and keep the
wording consistent wherever that annotation appears. Use the existing schema
comment block in schemas.py as the source of truth and fix the phrasing without
changing the intended test coverage.

In `@api/src/workspaces/repository.py`:
- Around line 151-159: The `quest.url` fetch in `repository.py` is still an SSRF
sink because `QuestSettingsPatch` only validates presence, not safety. Before
`client.get(...)` in the `quest.url` branch, add URL validation/allowlisting to
reject unsafe schemes, non-allowed hosts, and private or internal IP ranges, and
only proceed when the URL passes those checks. Keep the logic localized around
the existing `get_http_client()` and `client.get` flow so the restriction is
enforced right where the server-side request is made.

In `@api/src/workspaces/routes.py`:
- Around line 355-361: Remove the redundant JSON definition guard in the
long_quest_data handling path: `validate_quest_settings()` already enforces that
`QuestDefinitionTypeName.JSON` includes a definition, so the `HTTPException` 400
branch in this route is unreachable. Update the `long_quest_data.type ==
QuestDefinitionTypeName.JSON` block in `routes.py` to rely on the existing
validation and keep only the schema validation call via
`validate_quest_definition_schema`.

In `@tests/README.md`:
- Line 79: The fenced code block in the README opens without a language tag,
triggering markdownlint MD040. Update the fenced block at the referenced spot to
include an appropriate language identifier, using the markdown content around
that block to choose the best fit. Locate the unlabeled fence in the README and
ensure the opening fence is explicitly tagged so the block remains valid
Markdown.

In `@tests/support/factories.py`:
- Around line 65-67: Rename the factory parameters that shadow Python builtins
to explicit names to satisfy Ruff A002; update the workspace factory signature
in create_workspace and the other affected factory helpers that currently use id
and type. Use names like workspace_id, workspace_type, user_id, and team_id, and
make sure all internal references and any call sites in
tests/support/factories.py are updated consistently.

In `@tests/support/fakes.py`:
- Around line 139-140: FakeSession.exec currently consumes queued responses for
session-setup statements, unlike FakeSession.execute, which breaks the shared
call-order semantics. Update FakeSession.exec in FakeSession so it also detects
and ignores SET-style session-setup statements before calling _next(), matching
the documented FakeSession behavior used by integration tests. Keep the response
queue aligned so only real query statements advance the queued result order.

In `@tests/unit/test_config.py`:
- Around line 12-23: The defaults test in test_defaults_loaded_when_env_unset is
still reading ambient process environment variables because _env_file=None only
skips .env loading. Update the test setup to clear or isolate the relevant
Settings inputs before instantiating Settings, so assertions like PROJECT_NAME,
DEBUG, SENTRY_DSN, CORS_ORIGINS, and the database URL fields always reflect
declared defaults. Apply the same guard to test_value_types_and_formats if it
depends on the same Settings behavior, using the Settings class as the main
entry point to locate the fix.

In `@tests/unit/test_users_schemas.py`:
- Around line 20-22: The helper _table currently uses getattr(model,
"__table__"), which is intentional for Pyright but triggers Ruff B009; update
that getattr call to include a targeted noqa for B009 only. Keep the behavior in
_table unchanged so SQLAlchemy runtime access still works while satisfying both
Ruff and Pyright.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 10-46: Add an explicit least-privilege permissions block to the ci
job/workflow so the default GITHUB_TOKEN does not inherit broader repository
scopes. Update the workflow around the existing ci job and its checkout/tooling
steps to declare read-only access only, since this job only needs to read the
repository and run checks; use the workflow/job permissions setting alongside
actions/checkout, setup-uv, and the test/lint steps.
- Around line 25-26: The CI workflow currently uses tag-pinned GitHub Actions,
and the review asks to harden supply-chain security by pinning them to immutable
commit SHAs. Update the action references in the workflow so the existing uses
of checkout, setup-uv, and setup-python are switched from mutable version tags
to full commit SHA pins, keeping the same setup behavior while making the
identifiers for those steps more secure and consistent.

In `@api/core/security.py`:
- Around line 173-175: The `# `@test`:` annotation under `validate_token` is empty
and should either be completed or removed. Update the placeholder to describe
the intended test coverage for `validate_token`—including cache hit/miss
behavior, token rotation eviction, and 401 response paths—or delete the
annotation if it is not meant to be used, keeping it consistent with the other
`@test` outlines in this module.

In `@api/src/workspaces/routes.py`:
- Line 159: The runtime invariant in workspaces routing is currently enforced
with an assert, which disappears under optimized Python runs. Replace the assert
in the workspace member assignment flow with an explicit guard that raises a
real exception if workspace.id is None, so the check still runs before
assign_member_role(...) and response construction; apply the same pattern
anywhere else this is used, including the similar Optional[int] narrowing case
in the schemas code.

In `@api/src/workspaces/schemas.py`:
- Line 84: The repeated `@test` annotation text has a grammar typo in the schema
comments, where “any values from Python and properly serialized” should be “any
values from Python are properly serialized.” Update the affected `@test`
comment(s) in the schemas definitions, including the repeated occurrences
referenced by this same wording, so the test-generation annotation reads
correctly and consistently.

In `@README.md`:
- Around line 22-35: The fenced code blocks in the README are missing language
specifiers and are triggering markdownlint MD040. Update the two fenced blocks
around the pytest commands and the type-check/format commands to use a shell
language tag, matching the existing command examples, so the blocks render and
lint correctly.

In `@tests/unit/test_teams_schemas.py`:
- Around line 21-27: The shared schema helper functions `_table` and
`_fk_targets` are duplicated across multiple schema test files, so move their
single implementation into a reusable helper under `tests/support` and update
`test_teams_schemas`, `test_users_schemas`, and `test_workspaces_schemas` to
import and use it. Keep the helper names or equivalent unique symbols consistent
so the schema tests still reference the same `__table__` and foreign key target
logic from one central place.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f384fde8-9c47-4d89-ad73-8844a96b465b

📥 Commits

Reviewing files that changed from the base of the PR and between e8c0902 and a00362b.

📒 Files selected for processing (41)
  • .github/workflows/ci.yml
  • CLAUDE.md
  • README.md
  • api/core/config.py
  • api/core/json_schema.py
  • api/core/jwt.py
  • api/core/security.py
  • api/main.py
  • api/src/teams/repository.py
  • api/src/teams/routes.py
  • api/src/teams/schemas.py
  • api/src/users/repository.py
  • api/src/users/routes.py
  • api/src/users/schemas.py
  • api/src/workspaces/repository.py
  • api/src/workspaces/routes.py
  • api/src/workspaces/schemas.py
  • pyproject.toml
  • tests/README.md
  • tests/conftest.py
  • tests/integration/__init__.py
  • tests/integration/test_health.py
  • tests/integration/test_proxy.py
  • tests/integration/test_teams.py
  • tests/integration/test_users.py
  • tests/integration/test_workspaces.py
  • tests/support/__init__.py
  • tests/support/factories.py
  • tests/support/fakes.py
  • tests/support/http.py
  • tests/test_main.py
  • tests/unit/__init__.py
  • tests/unit/test_config.py
  • tests/unit/test_json_schema.py
  • tests/unit/test_jwt.py
  • tests/unit/test_security.py
  • tests/unit/test_teams_schemas.py
  • tests/unit/test_user_info.py
  • tests/unit/test_users_schemas.py
  • tests/unit/test_workspace_repository.py
  • tests/unit/test_workspaces_schemas.py
💤 Files with no reviewable changes (1)
  • tests/test_main.py

Comment thread api/core/jwt.py
Comment thread api/src/teams/schemas.py
Comment on lines 151 to 159
if quest.url:
client = get_http_client()
if client is None:
return None
try:
response = await get_http_client().get(quest.url, timeout=10)
response = await client.get(quest.url, timeout=10)
return response.text
except Exception:
return None

@coderabbitai coderabbitai Bot Jun 24, 2026

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 '\b(url)\b' api/src/workspaces/schemas.py
ast-grep run --pattern $'`@field_validator`($$$)' --lang python api/src/workspaces/schemas.py

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 2039


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the touched code and any URL validation/fetch helpers around it.
sed -n '130,180p' api/src/workspaces/repository.py
printf '\n--- schemas ---\n'
sed -n '1,240p' api/src/workspaces/schemas.py
printf '\n--- search for validation/fetch helpers ---\n'
rg -n "url|http_client|get_http_client|model_validator|field_validator|private|loopback|169\\.254|ssrf" api/src/workspaces -S

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 11558


Restrict quest.url before fetching

quest.url is still user-influenced data fetched server-side without any scheme/host/private-range checks. QuestSettingsPatch only enforces presence or absence by type, so this remains an SSRF sink; add URL allowlisting/restrictions before client.get(...).

🧰 Tools
🪛 Ruff (0.15.18)

[warning] 158-158: Do not catch blind exception: Exception

(BLE001)

🤖 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 `@api/src/workspaces/repository.py` around lines 151 - 159, The `quest.url`
fetch in `repository.py` is still an SSRF sink because `QuestSettingsPatch` only
validates presence, not safety. Before `client.get(...)` in the `quest.url`
branch, add URL validation/allowlisting to reject unsafe schemes, non-allowed
hosts, and private or internal IP ranges, and only proceed when the URL passes
those checks. Keep the logic localized around the existing `get_http_client()`
and `client.get` flow so the restriction is enforced right where the server-side
request is made.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a fair point, thanks Rabbit. @cyrossignol thoughts?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@jeffmaki Thanks — agreed, I think this is worth a quick maintainer call.

My concern is specifically the server-side fetch path in WorkspaceRepository.resolve_quest_def(...): even with presence validation, a user-controlled quest.url can still target internal/private destinations unless we add URL restrictions at fetch time.

If @cyrossignol prefers to keep this PR tightly scoped to tests, I’m also fine with tracking it as a small follow-up hardening change rather than blocking the whole PR.


🧠 Learnings used
Learnt from: cyrossignol
Repo: TaskarCenterAtUW/workspaces-backend PR: 1
File: api/src/workspaces/repository.py:238-244
Timestamp: 2026-02-19T03:37:38.382Z
Learning: In api/src/workspaces/repository.py, get_current_user() intentionally uses scalar_one() instead of scalar_one_or_none() so that a missing authenticated user surfaces as a 500 error (systemic fault) rather than being treated as NotFound. This should be maintained as a guideline: treat absence of an authenticated user in the DB as a systemic error, not a normal not-found case. Document this rationale and ensure tests cover that exceptions bubble up appropriately, preserving the intended error signaling.

Comment on lines 355 to 361
if long_quest_data.type == QuestDefinitionTypeName.JSON:
if long_quest_data.definition is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="'definition' is required for JSON quest type.",
)
await validate_quest_definition_schema(long_quest_data.definition)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C5 'class QuestSettingsPatch' api/src/workspaces/schemas.py
ast-grep run --pattern $'`@model_validator`($$$)' --lang python api/src/workspaces/schemas.py

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 639


🏁 Script executed:

#!/bin/bash
sed -n '151,190p' api/src/workspaces/schemas.py
printf '\n---\n'
sed -n '340,370p' api/src/workspaces/routes.py

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 3018


🏁 Script executed:

#!/bin/bash
sed -n '151,190p' api/src/workspaces/schemas.py
printf '\n---\n'
sed -n '340,370p' api/src/workspaces/routes.py

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 3018


🏁 Script executed:

#!/bin/bash
sed -n '151,190p' api/src/workspaces/schemas.py
printf '\n---\n'
sed -n '340,370p' api/src/workspaces/routes.py

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 3018


Remove the redundant JSON definition check. QuestSettingsPatch.validate_quest_settings() already rejects type=JSON without definition with a 422, so this 400 branch in api/src/workspaces/routes.py is unreachable.

🤖 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 `@api/src/workspaces/routes.py` around lines 355 - 361, Remove the redundant
JSON definition guard in the long_quest_data handling path:
`validate_quest_settings()` already enforces that `QuestDefinitionTypeName.JSON`
includes a definition, so the `HTTPException` 400 branch in this route is
unreachable. Update the `long_quest_data.type == QuestDefinitionTypeName.JSON`
block in `routes.py` to rely on the existing validation and keep only the schema
validation call via `validate_quest_definition_schema`.

Comment thread tests/README.md

## Layout

```

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

Add a language tag to the fenced code block.

Line 79 opens a fenced block without a language, which triggers markdownlint MD040.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 79-79: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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` at line 79, The fenced code block in the README opens
without a language tag, triggering markdownlint MD040. Update the fenced block
at the referenced spot to include an appropriate language identifier, using the
markdown content around that block to choose the best fit. Locate the unlabeled
fence in the README and ensure the opening fence is explicitly tagged so the
block remains valid Markdown.

Source: Linters/SAST tools

Comment on lines +65 to +67
id: int | None = 1,
title: str = "Test Workspace",
type: WorkspaceType = WorkspaceType.OSW,

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

Avoid shadowing Python builtins in factory parameters.

Lines 65/67/87/97 use id and type, which are builtin names and flagged by Ruff (A002). Rename to explicit names like workspace_id, workspace_type, user_id, team_id.

Also applies to: 87-87, 97-97

🧰 Tools
🪛 Ruff (0.15.18)

[error] 65-65: Function argument id is shadowing a Python builtin

(A002)


[error] 67-67: Function argument type is shadowing a Python builtin

(A002)

🤖 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/support/factories.py` around lines 65 - 67, Rename the factory
parameters that shadow Python builtins to explicit names to satisfy Ruff A002;
update the workspace factory signature in create_workspace and the other
affected factory helpers that currently use id and type. Use names like
workspace_id, workspace_type, user_id, and team_id, and make sure all internal
references and any call sites in tests/support/factories.py are updated
consistently.

Source: Linters/SAST tools

Comment thread tests/support/fakes.py
Comment on lines +139 to +140
async def exec(self, statement, *args, **kwargs):
return self._raise_if_exc(self._next())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

FakeSession.exec should also ignore session-setup statements.

Line 139 currently consumes queued responses for SET ... statements, while execute skips them. This breaks the queued call-order contract and can misalign subsequent results in integration tests.

As per coding guidelines, tests/support/**/*.py fakes must support the integration-test data-fetcher boundary with documented FakeSession behavior and query-order semantics.

Suggested fix
 async def exec(self, statement, *args, **kwargs):
-    return self._raise_if_exc(self._next())
+    if self._is_session_setup(statement):
+        return FakeResult(rows=[])
+    return self._raise_if_exc(self._next())
📝 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
async def exec(self, statement, *args, **kwargs):
return self._raise_if_exc(self._next())
async def exec(self, statement, *args, **kwargs):
if self._is_session_setup(statement):
return FakeResult(rows=[])
return self._raise_if_exc(self._next())
🤖 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/support/fakes.py` around lines 139 - 140, FakeSession.exec currently
consumes queued responses for session-setup statements, unlike
FakeSession.execute, which breaks the shared call-order semantics. Update
FakeSession.exec in FakeSession so it also detects and ignores SET-style
session-setup statements before calling _next(), matching the documented
FakeSession behavior used by integration tests. Keep the response queue aligned
so only real query statements advance the queued result order.

Source: Coding guidelines

Comment thread tests/unit/test_config.py
Comment on lines +12 to +23
def test_defaults_loaded_when_env_unset():
# _env_file=None ignores any local .env so we observe the declared defaults.
s = Settings(_env_file=None) # type: ignore[call-arg] # pydantic-settings init kwarg

assert s.PROJECT_NAME == "Workspaces API"
assert s.CORS_ORIGINS == []
assert s.DEBUG is False
assert s.SENTRY_DSN == ""
assert s.WS_OSM_HOST == "http://osm-web"
assert s.TDEI_OIDC_REALM == "tdei"
assert s.TASK_DATABASE_URL.startswith("postgresql+asyncpg://")
assert s.OSM_DATABASE_URL.startswith("postgresql+asyncpg://")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Defaults test is vulnerable to ambient environment variables.

_env_file=None only disables .env file loading; it does not stop pydantic-settings from reading process environment variables. If the test environment exports any of these names (e.g. PROJECT_NAME, DEBUG, SENTRY_DSN, CORS_ORIGINS), this test (and test_value_types_and_formats) will assert against the override instead of the declared default and flake. Clear the relevant vars first.

🛡️ Suggested guard
-def test_defaults_loaded_when_env_unset():
+def test_defaults_loaded_when_env_unset(monkeypatch):
     # _env_file=None ignores any local .env so we observe the declared defaults.
+    for var in ("PROJECT_NAME", "CORS_ORIGINS", "DEBUG", "SENTRY_DSN",
+                "WS_OSM_HOST", "TDEI_OIDC_REALM", "TASK_DATABASE_URL", "OSM_DATABASE_URL"):
+        monkeypatch.delenv(var, raising=False)
     s = Settings(_env_file=None)  # type: ignore[call-arg]  # pydantic-settings init kwarg
📝 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_defaults_loaded_when_env_unset():
# _env_file=None ignores any local .env so we observe the declared defaults.
s = Settings(_env_file=None) # type: ignore[call-arg] # pydantic-settings init kwarg
assert s.PROJECT_NAME == "Workspaces API"
assert s.CORS_ORIGINS == []
assert s.DEBUG is False
assert s.SENTRY_DSN == ""
assert s.WS_OSM_HOST == "http://osm-web"
assert s.TDEI_OIDC_REALM == "tdei"
assert s.TASK_DATABASE_URL.startswith("postgresql+asyncpg://")
assert s.OSM_DATABASE_URL.startswith("postgresql+asyncpg://")
def test_defaults_loaded_when_env_unset(monkeypatch):
# _env_file=None ignores any local .env so we observe the declared defaults.
for var in ("PROJECT_NAME", "CORS_ORIGINS", "DEBUG", "SENTRY_DSN",
"WS_OSM_HOST", "TDEI_OIDC_REALM", "TASK_DATABASE_URL", "OSM_DATABASE_URL"):
monkeypatch.delenv(var, raising=False)
s = Settings(_env_file=None) # type: ignore[call-arg] # pydantic-settings init kwarg
assert s.PROJECT_NAME == "Workspaces API"
assert s.CORS_ORIGINS == []
assert s.DEBUG is False
assert s.SENTRY_DSN == ""
assert s.WS_OSM_HOST == "http://osm-web"
assert s.TDEI_OIDC_REALM == "tdei"
assert s.TASK_DATABASE_URL.startswith("postgresql+asyncpg://")
assert s.OSM_DATABASE_URL.startswith("postgresql+asyncpg://")
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 19-19: Do not make http calls without encryption
Context: "http://osm-web"
Note: [CWE-319] Cleartext Transmission of Sensitive Information.

(requests-http)

🤖 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/unit/test_config.py` around lines 12 - 23, The defaults test in
test_defaults_loaded_when_env_unset is still reading ambient process environment
variables because _env_file=None only skips .env loading. Update the test setup
to clear or isolate the relevant Settings inputs before instantiating Settings,
so assertions like PROJECT_NAME, DEBUG, SENTRY_DSN, CORS_ORIGINS, and the
database URL fields always reflect declared defaults. Apply the same guard to
test_value_types_and_formats if it depends on the same Settings behavior, using
the Settings class as the main entry point to locate the fix.

Comment on lines +20 to +22
def _table(model):
# __table__ is added by SQLAlchemy at runtime; getattr keeps type-checkers happy.
return getattr(model, "__table__")

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

Ruff B009 will flag this getattr call; add a noqa to satisfy both Ruff and Pyright.

The getattr(model, "__table__") is intentional to keep Pyright quiet, but Ruff reports B009 (constant attribute), which can fail the lint step in CI. Suppress only this rule rather than switching to plain attribute access (which would reintroduce the Pyright complaint).

🔧 Proposed fix
 def _table(model):
     # __table__ is added by SQLAlchemy at runtime; getattr keeps type-checkers happy.
-    return getattr(model, "__table__")
+    return getattr(model, "__table__")  # noqa: B009 -- avoids Pyright __table__ error
📝 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 _table(model):
# __table__ is added by SQLAlchemy at runtime; getattr keeps type-checkers happy.
return getattr(model, "__table__")
def _table(model):
# __table__ is added by SQLAlchemy at runtime; getattr keeps type-checkers happy.
return getattr(model, "__table__") # noqa: B009 -- avoids Pyright __table__ error
🧰 Tools
🪛 Ruff (0.15.18)

[warning] 22-22: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)

🤖 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/unit/test_users_schemas.py` around lines 20 - 22, The helper _table
currently uses getattr(model, "__table__"), which is intentional for Pyright but
triggers Ruff B009; update that getattr call to include a targeted noqa for B009
only. Keep the behavior in _table unchanged so SQLAlchemy runtime access still
works while satisfying both Ruff and Pyright.

Source: Linters/SAST tools

@jeffmaki jeffmaki requested a review from cyrossignol June 26, 2026 14:29
@jeffmaki jeffmaki marked this pull request as ready for review June 26, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant