Skip to content

Refactor data models and improve API robustness#473

Open
RohanExploit wants to merge 4 commits intomainfrom
fix-issues-optimize-data-structure-5203763552477368163
Open

Refactor data models and improve API robustness#473
RohanExploit wants to merge 4 commits intomainfrom
fix-issues-optimize-data-structure-5203763552477368163

Conversation

@RohanExploit
Copy link
Owner

@RohanExploit RohanExploit commented Feb 25, 2026

This PR addresses several issues and optimizations:

  1. Data Structure Optimization: Switched from a custom text-based JSON decorator to SQLAlchemy's native JSON type. This allows PostgreSQL to use JSONB for better performance and query capabilities, while maintaining compatibility with SQLite.
  2. API Consistency: Fixed get_recent_issues to always return the raw data structure (list of dicts) instead of returning a JSONResponse object when cached. This ensures consistent Pydantic validation and serialization by FastAPI.
  3. Resource Cleanup: Enhanced create_issue to guarantee that uploaded files are deleted if the issue is not created (e.g., due to deduplication or database errors), preventing storage leaks.
  4. Code Refactoring: Cleaned up backend/utils.py to remove duplicated image validation logic.
  5. Testing: Added unit tests to verify the fixes and ensure no regressions.

PR created automatically by Jules for task 5203763552477368163 started by @RohanExploit


Summary by cubic

Reverted the model JSON type change for DB compatibility and hardened the issues API; also fixed a missing bcrypt dependency to prevent deployment errors. Aligns with Linear 5203763552477368163 by ensuring consistent API responses, cleaning up unused uploads, and simplifying image processing.

  • Bug Fixes

    • get_recent_issues now returns a list of dicts (not JSONResponse) even when cached.
    • create_issue deletes uploaded images on dedup or DB errors to prevent leaks.
    • Models reverted to JSONEncodedDict to avoid schema mismatch with existing TEXT columns.
    • Added tests for cache return type, uncached query path, and file cleanup.
  • Dependencies

    • Added bcrypt to requirements-render.txt to satisfy direct import and avoid ImportError.

Written for commit 8b4a63d. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Removed orphaned image files from failed issue creation and clarified image-processing error messages.
  • Performance

    • Improved recent-issues retrieval by returning cached results more directly for faster responses.
  • Tests

    • Added comprehensive tests covering caching, uncached retrieval, and image upload cleanup behavior.
  • Chores

    • Added bcrypt to the project's Python requirements.

- Replace custom `JSONEncodedDict` with `sqlalchemy.types.JSON` in `backend/models.py` to enable native JSON storage (e.g., JSONB in Postgres).
- Fix `get_recent_issues` in `backend/routers/issues.py` to return consistent data types (list of dicts) when serving from cache, avoiding `JSONResponse` wrapper issues.
- Add robust file cleanup in `create_issue` in `backend/routers/issues.py` using `try...finally` to ensure uploaded images are deleted if the issue is a duplicate or DB save fails.
- Refactor `process_uploaded_image_sync` in `backend/utils.py` to reuse validation logic and remove code duplication.
- Add `backend/tests/test_optimizations.py` to verify the fixes.
Copilot AI review requested due to automatic review settings February 25, 2026 11:27
@google-labs-jules
Copy link
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@netlify
Copy link

netlify bot commented Feb 25, 2026

Deploy Preview for fixmybharat canceled.

Name Link
🔨 Latest commit cf10103
🔍 Latest deploy log https://app.netlify.com/projects/fixmybharat/deploys/699edc97b299a30008457cdd

@netlify
Copy link

netlify bot commented Feb 25, 2026

Deploy Preview for fixmybharat canceled.

Name Link
🔨 Latest commit 8b4a63d
🔍 Latest deploy log https://app.netlify.com/projects/fixmybharat/deploys/699ee12a23151600087f35e6

@github-actions
Copy link

🙏 Thank you for your contribution, @RohanExploit!

PR Details:

Quality Checklist:
Please ensure your PR meets the following criteria:

  • Code follows the project's style guidelines
  • Self-review of code completed
  • Code is commented where necessary
  • Documentation updated (if applicable)
  • No new warnings generated
  • Tests added/updated (if applicable)
  • All tests passing locally
  • No breaking changes to existing functionality

Review Process:

  1. Automated checks will run on your code
  2. A maintainer will review your changes
  3. Address any requested changes promptly
  4. Once approved, your PR will be merged! 🎉

Note: The maintainers will monitor code quality and ensure the overall project flow isn't broken.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors data models and improves API robustness through several key changes: migrating from custom JSONEncodedDict to SQLAlchemy's native JSON type for better PostgreSQL JSONB support, fixing cache inconsistency in get_recent_issues to ensure proper FastAPI serialization, adding file cleanup on deduplication, and consolidating image validation logic in utils.py.

Changes:

  • Replaced custom JSONEncodedDict type decorator with SQLAlchemy's native JSON type for better database performance
  • Fixed get_recent_issues to return raw data instead of JSONResponse when cached, ensuring consistent Pydantic validation
  • Enhanced create_issue to cleanup uploaded files when issues are deduplicated

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
backend/models.py Migrated from custom JSONEncodedDict to SQLAlchemy's native JSON type for geographic_coverage, action_plan, and visit_images columns
backend/routers/issues.py Fixed cached response type in get_recent_issues and added file cleanup for deduplicated issues
backend/utils.py Refactored process_uploaded_image_sync to use existing _validate_uploaded_file_sync for validation
backend/tests/test_optimizations.py Added tests for get_recent_issues return type consistency and create_issue cleanup behavior

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +163 to 168
img = _validate_uploaded_file_sync(file)

try:
img = Image.open(file.file)
original_format = img.format

# Resize if needed
if img.width > 1024 or img.height > 1024:
ratio = min(1024 / img.width, 1024 / img.height)
new_width = int(img.width * ratio)
new_height = int(img.height * ratio)
img = img.resize((new_width, new_height), Image.Resampling.BILINEAR)

# Strip EXIF
img_no_exif = Image.new(img.mode, img.size)
img_no_exif.paste(img)
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

Missing EXIF orientation handling before stripping metadata. Images with EXIF orientation tags (commonly from mobile phones) will display incorrectly after EXIF stripping. You should apply ImageOps.exif_transpose(img) immediately after calling _validate_uploaded_file_sync and before creating the new image without EXIF. This ensures the image pixels are rotated correctly before removing the orientation metadata.

Copilot uses AI. Check for mistakes.
upvotes = Column(Integer, default=0, index=True)
latitude = Column(Float, nullable=True, index=True)
longitude = Column(Float, nullable=True, index=True)
location = Column(String, nullable=True)
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The action_plan column migration is missing. The migrate_db function adds action_plan as TEXT in line 65, but the model now defines it as JSON type. This mismatch could cause issues with existing databases. You should either add a migration to alter the column type from TEXT to JSON (or JSONB for PostgreSQL), or document that a database recreation is needed for this change.

Suggested change
location = Column(String, nullable=True)
location = Column(String, nullable=True)
# NOTE: The initial schema (via migrate_db) created action_plan as TEXT.
# The model now defines this column as JSON. Existing databases should either:
# 1) run a migration to alter issues.action_plan from TEXT to JSON/JSONB, or
# 2) be recreated to match this JSON column definition.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +104
try:
await create_issue(
request=request,
background_tasks=background_tasks,
description="Test description length check",
category="Road",
language="en",
user_email="test@example.com",
latitude=10.0,
longitude=10.0,
location="Test Loc",
image=image,
db=db
)
except Exception as e:
assert "Failed to save issue to database" in str(e)
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

Test doesn't verify that an exception is actually raised. If create_issue doesn't raise an exception (e.g., if error handling changes), the test will continue to line 107 and potentially pass even though the expected failure didn't occur. Consider adding an explicit failure after line 102 like assert False, "Expected create_issue to raise an exception" or using pytest.raises context manager to ensure an exception is raised.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/utils.py">

<violation number="1" location="backend/utils.py:176">
P2: Defaulting to JPEG when img.format is missing breaks palette-mode images (e.g., GIF/PNG with mode "P") after resize, because PIL can’t save "P" as JPEG. This causes valid uploads to fail. Treat non-JPEG-compatible modes as PNG (or convert to RGB) when img.format is unavailable.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +176 to 179
if img.format:
fmt = img.format
else:
fmt = 'PNG' if img.mode == 'RGBA' else 'JPEG'
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 25, 2026

Choose a reason for hiding this comment

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

P2: Defaulting to JPEG when img.format is missing breaks palette-mode images (e.g., GIF/PNG with mode "P") after resize, because PIL can’t save "P" as JPEG. This causes valid uploads to fail. Treat non-JPEG-compatible modes as PNG (or convert to RGB) when img.format is unavailable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/utils.py, line 176:

<comment>Defaulting to JPEG when img.format is missing breaks palette-mode images (e.g., GIF/PNG with mode "P") after resize, because PIL can’t save "P" as JPEG. This causes valid uploads to fail. Treat non-JPEG-compatible modes as PNG (or convert to RGB) when img.format is unavailable.</comment>

<file context>
@@ -159,54 +159,22 @@ def process_uploaded_image_sync(file: UploadFile) -> tuple[Image.Image, bytes]:
+        # _validate_uploaded_file_sync doesn't return the format explicitly if resized,
+        # but img.format is None if resized.
+        # If not resized, img.format is available.
+        if img.format:
+            fmt = img.format
         else:
</file context>
Suggested change
if img.format:
fmt = img.format
else:
fmt = 'PNG' if img.mode == 'RGBA' else 'JPEG'
if img.format:
fmt = img.format
else:
fmt = 'PNG' if img.mode in ('RGBA', 'P', 'LA') else 'JPEG'
Fix with Cubic

…zations

- Revert `backend/models.py` to use `JSONEncodedDict` instead of `sqlalchemy.types.JSON`. This fixes the schema mismatch with the existing database (which uses `TEXT` columns) causing deployment/startup failures.
- Retain API fix in `backend/routers/issues.py` (returning raw list from cache instead of `JSONResponse`).
- Retain resource cleanup fix in `create_issue` (deleting uploaded files on failure).
- Retain code deduplication in `backend/utils.py`.
- Keep `backend/tests/test_optimizations.py` to verify the API behavior.
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Return path for cached recent-issues responses was simplified; image-processing validation was centralized and saving logic streamlined; new tests exercise cached/uncached recent-issues paths and create_issue cleanup; bcrypt was added to requirements.

Changes

Cohort / File(s) Summary
Router change
backend/routers/issues.py
get_recent_issues now returns cached data directly when present (removed wrapping in JSONResponse) while keeping declared response_model.
Utility refactor
backend/utils.py
process_uploaded_image_sync delegates validation to _validate_uploaded_file_sync, removed redundant checks and simplified image format selection and error messages.
Tests added
backend/tests/test_optimizations.py
New test module with three tests: cached-return type, uncached DB-query path, and create_issue cleanup behavior using extensive mocking of external deps and I/O.
Dependencies
backend/requirements-render.txt
Added bcrypt to the Python requirements list.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐇 I hopped through code with nimble feet,

Cached replies now come back neat,
Images vetted by one smart gate,
Tests hop in to check the state,
A carrot-coded cheer — tidy and sweet!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description provides a clear overview of changes and the summary documents fixes, but the author-provided section lacks the required template structure with explicit sections for Type of Change, Testing Done, and Checklist. Consider using the repository's PR template format: include explicit 'Type of Change' checkboxes, 'Testing Done' section with test status, and complete the 'Checklist' items to align with repository standards.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Refactor data models and improve API robustness' broadly captures the main changes (refactoring and API improvements) but is generic and doesn't highlight the most impactful specifics like API consistency fixes or cache handling.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-issues-optimize-data-structure-5203763552477368163

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

❤️ Share

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

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/models.py">

<violation number="1">
P2: Storing these JSON fields with JSONEncodedDict uses a TEXT column, so PostgreSQL can’t use JSONB operators/indexes and JSON validity isn’t enforced. If you need JSON querying/perf, keep these columns as SQLAlchemy JSON/JSONB types instead of a text-based decorator.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link

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

🧹 Nitpick comments (3)
backend/utils.py (1)

186-190: Prefer logger.exception to preserve the stack trace.

logger.error without exc_info=True silently drops the traceback. The static analyzer also flags this (Ruff TRY400). Since this branch is the only signal of a processing failure, the full trace is valuable.

♻️ Proposed fix
-    except Exception as e:
-        logger.error(f"Image processing failed: {e}")
+    except Exception as e:
+        logger.exception(f"Image processing failed: {e}")
         raise HTTPException(
             status_code=400,
             detail="Failed to process image file."
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/utils.py` around lines 186 - 190, In the except Exception as e block
(the image processing failure handler) replace the logger.error call with
logger.exception (or logger.error(..., exc_info=True)) so the full traceback is
preserved when logging the error, keeping the HTTPException raise(detail="Failed
to process image file.") unchanged; locate the except block that currently reads
"except Exception as e:" and update the logging call accordingly.
backend/tests/test_optimizations.py (2)

5-5: class MockTensor: pass should be split into two lines (Ruff E701).

♻️ Proposed fix
-class MockTensor: pass
+class MockTensor:
+    pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/test_optimizations.py` at line 5, The single-line class
definition "class MockTensor: pass" violates Ruff E701; update the MockTensor
declaration (class MockTensor) to use a normal multi-line class block by placing
the body on the next line and indenting the "pass" (i.e., replace the
single-line form with a two-line class definition for class MockTensor).

33-47: Cache state leaks between test runs — add teardown.

test_get_recent_issues_return_type writes mock_data into recent_issues_cache under key "recent_issues_10_0" with no teardown. A subsequent call to get_recent_issues(limit=10, offset=0) anywhere in the test suite (or on the next run if the cache is module-level and persistent) will receive the stale mock data. test_get_recent_issues_uncached defensively calls recent_issues_cache.clear(), so the ordering currently works, but adding explicit teardown removes the implicit dependency.

♻️ Proposed fix
 def test_get_recent_issues_return_type():
     mock_data = [{"id": 1, "description": "test"}]
     recent_issues_cache.set(mock_data, "recent_issues_10_0")

     db = MagicMock()
     response = get_recent_issues(limit=10, offset=0, db=db)

     assert not isinstance(response, JSONResponse)
     assert response == mock_data
     assert isinstance(response, list)
+
+    # Teardown: remove the test entry to avoid polluting other tests
+    recent_issues_cache.clear()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/test_optimizations.py` around lines 33 - 47,
test_get_recent_issues_return_type leaves recent_issues_cache populated causing
cross-test pollution; after setting recent_issues_cache.set(mock_data,
"recent_issues_10_0") ensure the cache is cleared in teardown by calling
recent_issues_cache.clear() (or use a test fixture/try/finally) so the cache is
emptied after get_recent_issues(limit=10, offset=0) completes; update the
test_get_recent_issues_return_type to clear the cache at the end (or wrap the
call to get_recent_issues in a try/finally that calls
recent_issues_cache.clear()) to remove the implicit dependency on test ordering.
🤖 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/routers/issues.py`:
- Around line 671-673: The cache check uses a truthy test (if cached_data:)
which treats empty lists as misses; change it to an explicit None check by
testing cached_data is not None after calling recent_issues_cache.get(cache_key)
so stored empty result sets (e.g., [] saved later in the function) are honored
and avoid needless DB hits; update the guard around
recent_issues_cache.get(cache_key) and keep the variables cached_data and
cache_key as-is.

In `@backend/tests/test_optimizations.py`:
- Around line 50-110: The test is flaky because create_issue calls
check_upload_limits which mutates the real user_upload_cache and can raise
HTTPException(429) after repeated runs, and the test writes real files without
teardown; mock check_upload_limits (or reset/patch user_upload_cache) so it
returns success and cannot raise, ensure you only inspect
mock_save_image.call_args after confirming mock_save_image was called (e.g.,
check mock_save_image.call_count > 0) to avoid TypeError, and change the
save_processed_image side_effect to write into a temporary directory (or return
a temp path) and remove any created file/dirs in a finally/teardown block so
filesystem artifacts are cleaned up; reference symbols: check_upload_limits,
create_issue, save_processed_image, save_issue_db, user_upload_cache, and
mock_save_image.call_args.

In `@backend/utils.py`:
- Around line 162-163: The code calls _validate_uploaded_file_sync(file) and
immediately dereferences its result (used in Image.new(img.mode, img.size)), but
_validate_uploaded_file_sync has Optional[Image.Image] return type; add a
defensive guard after the call to check if img is None and raise a clear
HTTP/client error (e.g., raise HTTPException(status_code=400, detail="Invalid or
empty uploaded image") or ValueError that your handler maps to a 400) before
using img, so the failure produces an actionable HTTP response instead of an
AttributeError.

---

Nitpick comments:
In `@backend/tests/test_optimizations.py`:
- Line 5: The single-line class definition "class MockTensor: pass" violates
Ruff E701; update the MockTensor declaration (class MockTensor) to use a normal
multi-line class block by placing the body on the next line and indenting the
"pass" (i.e., replace the single-line form with a two-line class definition for
class MockTensor).
- Around line 33-47: test_get_recent_issues_return_type leaves
recent_issues_cache populated causing cross-test pollution; after setting
recent_issues_cache.set(mock_data, "recent_issues_10_0") ensure the cache is
cleared in teardown by calling recent_issues_cache.clear() (or use a test
fixture/try/finally) so the cache is emptied after get_recent_issues(limit=10,
offset=0) completes; update the test_get_recent_issues_return_type to clear the
cache at the end (or wrap the call to get_recent_issues in a try/finally that
calls recent_issues_cache.clear()) to remove the implicit dependency on test
ordering.

In `@backend/utils.py`:
- Around line 186-190: In the except Exception as e block (the image processing
failure handler) replace the logger.error call with logger.exception (or
logger.error(..., exc_info=True)) so the full traceback is preserved when
logging the error, keeping the HTTPException raise(detail="Failed to process
image file.") unchanged; locate the except block that currently reads "except
Exception as e:" and update the logging call accordingly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39d5fbb and c149222.

📒 Files selected for processing (3)
  • backend/routers/issues.py
  • backend/tests/test_optimizations.py
  • backend/utils.py

Comment on lines 671 to +673
cached_data = recent_issues_cache.get(cache_key)
if cached_data:
return JSONResponse(content=cached_data)
return cached_data
Copy link

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
# First, find the cache implementation
fd -t f "cache.py" backend/ 2>/dev/null | head -20

Repository: RohanExploit/VishwaGuru

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Check the lines in question from the issues.py file
cat -n backend/routers/issues.py | sed -n '670,715p'

Repository: RohanExploit/VishwaGuru

Length of output: 1825


🏁 Script executed:

#!/bin/bash
# Search for the cache.get() method definition
rg -n "def get" backend/ -A 5 --type py | grep -A 5 "cache"

Repository: RohanExploit/VishwaGuru

Length of output: 3137


🏁 Script executed:

#!/bin/bash
# Read the complete cache.py to understand get() return behavior
cat -n backend/cache.py

Repository: RohanExploit/VishwaGuru

Length of output: 6516


if cached_data: silently bypasses the cache for empty result sets.

When the DB legitimately returns zero issues, data = [] is stored in the cache at line 711, but if []: evaluates to False, so every subsequent request for that key re-hits the database. The get() method returns None for a cache miss and the actual cached value (including empty lists) on hit, so the guard should use an explicit None check.

🐛 Proposed fix
-    cached_data = recent_issues_cache.get(cache_key)
-    if cached_data:
-        return cached_data
+    cached_data = recent_issues_cache.get(cache_key)
+    if cached_data is not None:
+        return cached_data
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/routers/issues.py` around lines 671 - 673, The cache check uses a
truthy test (if cached_data:) which treats empty lists as misses; change it to
an explicit None check by testing cached_data is not None after calling
recent_issues_cache.get(cache_key) so stored empty result sets (e.g., [] saved
later in the function) are honored and avoid needless DB hits; update the guard
around recent_issues_cache.get(cache_key) and keep the variables cached_data and
cache_key as-is.

Comment on lines +50 to +110
@pytest.mark.asyncio
async def test_create_issue_cleanup():
# Mock dependencies
request = MagicMock()
background_tasks = MagicMock()
db = MagicMock()

# Mock file upload
image = MagicMock()
image.filename = "test.jpg"

# Mock process_uploaded_image to return dummy data
with patch("backend.routers.issues.process_uploaded_image") as mock_process:
mock_process.return_value = (MagicMock(), b"fake_bytes")

# Mock save_processed_image to create a dummy file
with patch("backend.routers.issues.save_processed_image") as mock_save_image:
def side_effect(bytes_data, path):
# Create directory if needed
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(bytes_data)
mock_save_image.side_effect = side_effect

# Mock save_issue_db to raise exception
with patch("backend.routers.issues.save_issue_db") as mock_save_db:
mock_save_db.side_effect = Exception("DB Error")

# Mock spatial utils
with patch("backend.routers.issues.get_bounding_box") as mock_bbox:
mock_bbox.return_value = (0, 0, 0, 0)
with patch("backend.routers.issues.find_nearby_issues") as mock_nearby:
mock_nearby.return_value = []

# Mock rag_service
with patch("backend.routers.issues.rag_service") as mock_rag:
mock_rag.retrieve.return_value = None

# Call create_issue
try:
await create_issue(
request=request,
background_tasks=background_tasks,
description="Test description length check",
category="Road",
language="en",
user_email="test@example.com",
latitude=10.0,
longitude=10.0,
location="Test Loc",
image=image,
db=db
)
except Exception as e:
assert "Failed to save issue to database" in str(e)

# Check if file was cleaned up
args, _ = mock_save_image.call_args
file_path = args[1]

assert not os.path.exists(file_path), f"File {file_path} should have been deleted"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

check_upload_limits is not mocked — the test becomes flaky after 5 runs per hour and produces a misleading failure.

create_issue calls check_upload_limits("test@example.com", UPLOAD_LIMIT_PER_USER) (Line 64 of issues.py), which writes to the real user_upload_cache. After 5 executions within an hour check_upload_limits raises HTTPException(429). At that point:

  1. process_uploaded_image is never awaited, so image_path stays None.
  2. mock_save_image is never called, so mock_save_image.call_args is None.
  3. Line 107 (args, _ = mock_save_image.call_args) raises TypeError, completely masking the intended assertion.

Additionally, the side_effect at lines 67-72 creates the data/uploads/ directory and writes a real file; there is no teardown, leaving filesystem artifacts after each test run.

🐛 Proposed fix — mock `check_upload_limits` and add cleanup
+import shutil

 `@pytest.mark.asyncio`
 async def test_create_issue_cleanup():
     request = MagicMock()
     background_tasks = MagicMock()
     db = MagicMock()

     image = MagicMock()
     image.filename = "test.jpg"

+    upload_dir = "data/uploads"
     with patch("backend.routers.issues.process_uploaded_image") as mock_process:
         mock_process.return_value = (MagicMock(), b"fake_bytes")

         with patch("backend.routers.issues.save_processed_image") as mock_save_image:
             def side_effect(bytes_data, path):
-                os.makedirs(os.path.dirname(path), exist_ok=True)
+                os.makedirs(upload_dir, exist_ok=True)
                 with open(path, "wb") as f:
                     f.write(bytes_data)
             mock_save_image.side_effect = side_effect

             with patch("backend.routers.issues.save_issue_db") as mock_save_db:
                 mock_save_db.side_effect = Exception("DB Error")

                 with patch("backend.routers.issues.get_bounding_box") as mock_bbox:
                     mock_bbox.return_value = (0, 0, 0, 0)
                     with patch("backend.routers.issues.find_nearby_issues") as mock_nearby:
                         mock_nearby.return_value = []

                         with patch("backend.routers.issues.rag_service") as mock_rag:
+                            with patch("backend.routers.issues.check_upload_limits"):
                                 mock_rag.retrieve.return_value = None

-                                try:
-                                    await create_issue(...)
-                                except Exception as e:
-                                    assert "Failed to save issue to database" in str(e)
-
-                                args, _ = mock_save_image.call_args
-                                file_path = args[1]
-                                assert not os.path.exists(file_path), ...
+                                try:
+                                    await create_issue(
+                                        request=request,
+                                        background_tasks=background_tasks,
+                                        description="Test description length check",
+                                        category="Road",
+                                        language="en",
+                                        user_email="test@example.com",
+                                        latitude=10.0,
+                                        longitude=10.0,
+                                        location="Test Loc",
+                                        image=image,
+                                        db=db
+                                    )
+                                except Exception as e:
+                                    assert "Failed to save issue to database" in str(e)
+                                finally:
+                                    # Cleanup test artifacts
+                                    if os.path.exists(upload_dir):
+                                        shutil.rmtree(upload_dir)
+
+                                args, _ = mock_save_image.call_args
+                                file_path = args[1]
+                                assert not os.path.exists(file_path), \
+                                    f"File {file_path} should have been deleted"
🧰 Tools
🪛 Ruff (0.15.2)

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

(BLE001)

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

In `@backend/tests/test_optimizations.py` around lines 50 - 110, The test is flaky
because create_issue calls check_upload_limits which mutates the real
user_upload_cache and can raise HTTPException(429) after repeated runs, and the
test writes real files without teardown; mock check_upload_limits (or
reset/patch user_upload_cache) so it returns success and cannot raise, ensure
you only inspect mock_save_image.call_args after confirming mock_save_image was
called (e.g., check mock_save_image.call_count > 0) to avoid TypeError, and
change the save_processed_image side_effect to write into a temporary directory
(or return a temp path) and remove any created file/dirs in a finally/teardown
block so filesystem artifacts are cleaned up; reference symbols:
check_upload_limits, create_issue, save_processed_image, save_issue_db,
user_upload_cache, and mock_save_image.call_args.

Comment on lines +162 to +163
# Use existing validation logic (which handles size limits and basic validation)
img = _validate_uploaded_file_sync(file)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unguarded dereference if _validate_uploaded_file_sync ever returns None.

The return type of _validate_uploaded_file_sync is Optional[Image.Image]. While the current implementation always returns a valid image or raises, the type contract permits None. Line 167 (Image.new(img.mode, img.size)) would immediately raise AttributeError with no actionable HTTP error for the caller.

🛡️ Proposed defensive guard
     img = _validate_uploaded_file_sync(file)
+    if img is None:
+        raise HTTPException(status_code=400, detail="Failed to process image file.")
📝 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
# Use existing validation logic (which handles size limits and basic validation)
img = _validate_uploaded_file_sync(file)
# Use existing validation logic (which handles size limits and basic validation)
img = _validate_uploaded_file_sync(file)
if img is None:
raise HTTPException(status_code=400, detail="Failed to process image file.")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/utils.py` around lines 162 - 163, The code calls
_validate_uploaded_file_sync(file) and immediately dereferences its result (used
in Image.new(img.mode, img.size)), but _validate_uploaded_file_sync has
Optional[Image.Image] return type; add a defensive guard after the call to check
if img is None and raise a clear HTTP/client error (e.g., raise
HTTPException(status_code=400, detail="Invalid or empty uploaded image") or
ValueError that your handler maps to a 400) before using img, so the failure
produces an actionable HTTP response instead of an AttributeError.

- Reverted `backend/models.py` to use `JSONEncodedDict` (compatible with existing TEXT columns) to resolve deployment startup failures.
- Re-applied API optimization in `backend/routers/issues.py`: `get_recent_issues` now returns raw list from cache instead of `JSONResponse`.
- Re-applied file cleanup fix in `create_issue`: Uploaded images are deleted if deduplication occurs or DB save fails.
- Re-applied refactoring in `backend/utils.py` to deduplicate image validation logic.
- Added `backend/tests/test_optimizations.py` to verify the fixes.
@github-actions
Copy link

🔍 Quality Reminder

Thanks for the updates! Please ensure:
- Your changes don't break existing functionality
- All tests still pass
- Code quality standards are maintained

*The maintainers will verify that the overall project flow remains intact.*

- Add `bcrypt` to `backend/requirements-render.txt` to satisfy direct import in `backend/utils.py`.
- This fixes a potential deployment failure where `import bcrypt` failed despite `passlib[bcrypt]` being present (which sometimes doesn't expose the top-level module depending on installation environment).
Copy link

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

🤖 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/requirements-render.txt`:
- Line 17: Pin the bcrypt dependency to a compatible version and pin
passlib[bcrypt] to avoid runtime breakage: replace the bare "bcrypt" entry with
"bcrypt<=4.0.1" and add or update an explicit "passlib[bcrypt]==1.7.4" entry in
the requirements list so both packages are version-locked and compatible at
install time.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c149222 and 8b4a63d.

📒 Files selected for processing (2)
  • backend/requirements-render.txt
  • backend/routers/issues.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/routers/issues.py

a2wsgi
python-jose[cryptography]
passlib[bcrypt]
bcrypt
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check the requirements-render.txt file and surrounding context
cat -n backend/requirements-render.txt | head -30

Repository: RohanExploit/VishwaGuru

Length of output: 538


🏁 Script executed:

# Check if there are other requirements files with pinning patterns
fd -e txt -e txt -e toml -e cfg requirements | head -10

Repository: RohanExploit/VishwaGuru

Length of output: 150


🌐 Web query:

latest stable bcrypt version PyPI passlib compatibility 2025 2026

💡 Result:

  • Latest stable bcrypt on PyPI (as of Feb 25, 2026): bcrypt 5.0.0 (released Sep 25, 2025). [1]
  • Latest passlib on PyPI: passlib 1.7.4 (released Oct 8, 2020). [2]

Compatibility (Passlib + bcrypt, 2025–2026 reality):

  • bcrypt 4.1.0 was yanked on PyPI explicitly due to “Incompatibility with assumptions made by passlib.” [3]
  • A common break is that Passlib can’t detect bcrypt’s version after changes in bcrypt metadata; a documented workaround is to restrict bcrypt to <= 4.0.1 to keep Passlib working. [4]

Practical guidance: if you must stay on passlib==1.7.4, expect to pin bcrypt (often 4.0.1) rather than using the latest bcrypt (5.0.0). [2][4]

Sources

  1. PyPI: bcrypt release history (shows 5.0.0 on 2025-09-25) [1]
  2. PyPI: passlib upload date for 1.7.4 (2020-10-08) [2]
  3. PyPI: bcrypt 4.1.0 yanked reason mentions passlib incompatibility [3]
  4. Launchpad bug: Passlib cannot detect bcrypt version; workaround pins bcrypt <=4.0.1 [4]

Pin bcrypt to a compatible version with passlib[bcrypt].

Latest bcrypt versions (4.1.0+, 5.0.0) are incompatible with passlib 1.7.4—bcrypt 4.1.0 was yanked due to breaking passlib's version detection. Pin bcrypt<=4.0.1 to ensure runtime compatibility and reproducible deployments. Additionally, pin passlib[bcrypt] to avoid unexpected major version changes.

Proposed change
-passlib[bcrypt]
-bcrypt
+passlib[bcrypt]==1.7.4
+bcrypt==4.0.1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/requirements-render.txt` at line 17, Pin the bcrypt dependency to a
compatible version and pin passlib[bcrypt] to avoid runtime breakage: replace
the bare "bcrypt" entry with "bcrypt<=4.0.1" and add or update an explicit
"passlib[bcrypt]==1.7.4" entry in the requirements list so both packages are
version-locked and compatible at install time.

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.

2 participants