Skip to content

Feature/snapshot subscription graphql#5058

Draft
HarshitVerma109 wants to merge 4 commits into
OWASP:feature/community-snapshotsfrom
HarshitVerma109:feature/snapshot-subscription-graphql
Draft

Feature/snapshot subscription graphql#5058
HarshitVerma109 wants to merge 4 commits into
OWASP:feature/community-snapshotsfrom
HarshitVerma109:feature/snapshot-subscription-graphql

Conversation

@HarshitVerma109

Copy link
Copy Markdown
Collaborator

Proposed change

Resolves #5056

Adds GraphQL mutations and query for managing snapshot digest subscriptions. Users can create, update, and cancel their subscription, and unsubscribe directly from email links using a token-based endpoint that requires no login.

Checklist

  • Required: I followed the contributing workflow
  • Required: I verified that my code works as intended and resolves the issue as described
  • Required: I ran all required checks and tests locally; all warnings addressed and failures resolved
  • I used AI for code, documentation, tests, or communication related to this PR

Signed-off-by: Harsh <harshit1092004@gmail.com>
Signed-off-by: Harsh <harshit1092004@gmail.com>
Signed-off-by: Harsh <harshit1092004@gmail.com>
Signed-off-by: Harsh <harshit1092004@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Added snapshot email subscriptions with weekly/monthly delivery options and content preference controls.
    • Users can view their current subscription and manage it through the app’s API.
    • Added an unsubscribe link flow using a unique token for easy cancellation.
  • Bug Fixes

    • Improved handling for missing, inactive, or invalid subscriptions with clearer responses.
    • Added admin support for managing subscriptions more easily in the dashboard.

Walkthrough

Adds snapshot subscription storage, Django admin registration, GraphQL query and mutation support, schema wiring, and unit tests for the model, admin, query, node, and mutation paths.

Changes

Snapshot subscription feature

Layer / File(s) Summary
Model and migration
backend/apps/owasp/migrations/0075_snapshotsubscription.py, backend/apps/owasp/models/snapshot_subscription.py, backend/apps/owasp/models/__init__.py, backend/tests/unit/apps/owasp/models/snapshot_subscription_test.py
Creates the SnapshotSubscription schema and model, exports it from the app package, and adds model tests for string formatting, content preference mapping, frequency values, and unsubscribe token defaults.
Admin registration
backend/apps/owasp/admin/snapshot_subscription.py, backend/apps/owasp/admin/__init__.py, backend/tests/unit/apps/owasp/admin/snapshot_subscription_test.py
Adds Django admin configuration for SnapshotSubscription, wires the admin module into package import side effects, and verifies the admin field, filter, search, read-only, and fieldset settings.
GraphQL query surface
backend/apps/owasp/api/internal/nodes/snapshot_subscription.py, backend/apps/owasp/api/internal/queries/snapshot_subscription.py, backend/apps/owasp/api/internal/queries/__init__.py, backend/tests/unit/apps/owasp/api/internal/nodes/snapshot_subscription_test.py, backend/tests/unit/apps/owasp/api/internal/queries/snapshot_subscription_test.py
Adds the Relay node and current-user query, wires the query into OwaspQuery, and tests schema presence, authentication checks, and lookup behavior.
GraphQL mutation surface
backend/apps/owasp/api/internal/mutations/__init__.py, backend/apps/owasp/api/internal/mutations/snapshot_subscription.py, backend/settings/graphql.py, backend/tests/unit/apps/owasp/api/internal/mutations/snapshot_subscription_test.py
Adds mutation inputs, result type, create/update/cancel/token-unsubscribe resolvers, mutation root wiring, and tests for success and error paths.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #4945: This PR adds the SnapshotSubscription model/admin plus GraphQL query and mutation support described in the issue.

Suggested reviewers

  • arkid15r
  • kasya
  • cubic-dev-ai
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly points to the snapshot subscription GraphQL feature.
Description check ✅ Passed The description matches the PR and summarizes the snapshot subscription GraphQL work.
Linked Issues check ✅ Passed The changes cover the required query, mutations, token unsubscribe flow, node, and tests.
Out of Scope Changes check ✅ Passed The added admin, model, migration, GraphQL wiring, and tests all support the snapshot subscription feature.
Docstring Coverage ✅ Passed Docstring coverage is 85.37% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@sonarqubecloud

Copy link
Copy Markdown

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.31507% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 98.82%. Comparing base (0479503) to head (22b8e4b).
⚠️ Report is 1 commits behind head on feature/community-snapshots.

Files with missing lines Patch % Lines
...sp/api/internal/mutations/snapshot_subscription.py 98.76% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                      Coverage Diff                      @@
##           feature/community-snapshots    #5058    +/-   ##
=============================================================
  Coverage                        98.82%   98.82%            
=============================================================
  Files                              541      546     +5     
  Lines                            17289    17435   +146     
  Branches                          2496     2504     +8     
=============================================================
+ Hits                             17086    17231   +145     
  Misses                              88       88            
- Partials                           115      116     +1     
Flag Coverage Δ
backend 99.41% <99.31%> (-0.01%) ⬇️
frontend 97.20% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
backend/apps/owasp/admin/snapshot_subscription.py 100.00% <100.00%> (ø)
.../owasp/api/internal/nodes/snapshot_subscription.py 100.00% <100.00%> (ø)
...wasp/api/internal/queries/snapshot_subscription.py 100.00% <100.00%> (ø)
backend/apps/owasp/models/snapshot_subscription.py 100.00% <100.00%> (ø)
...sp/api/internal/mutations/snapshot_subscription.py 98.76% <98.76%> (ø)

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5a15194...22b8e4b. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@backend/apps/owasp/api/internal/mutations/snapshot_subscription.py`:
- Around line 167-173: The unsubscribe-token lookup in SnapshotSubscription
mutation only handles SnapshotSubscription.DoesNotExist and ValueError, but
invalid UUID input raises Django’s ValidationError before the query completes.
Update the exception handling in the mutation that calls
SnapshotSubscription.objects.get(unsubscribe_token=token) to also catch
ValidationError, and make sure the needed import from django.core.exceptions is
added so malformed tokens return the existing invalid-token result instead of an
unhandled error.
- Around line 63-86: The subscription creation logic in SnapshotSubscription
mutation is checking only
SnapshotSubscription.objects.filter(user=user).exists(), which blocks
re-subscription after cancel and still allows a race before create(). Update the
check to distinguish active vs inactive subscriptions (for example, based on
is_active in the SnapshotSubscription model) so canceled users can subscribe
again, and make the create path resilient to concurrent requests by handling the
one-to-one constraint failure around the subscription creation in the mutation
that builds the SnapshotSubscriptionResult.

In `@backend/apps/owasp/models/snapshot_subscription.py`:
- Around line 34-45: The subscription creation flow is blocking re-subscribes
because it treats inactive records as existing subscriptions. Update the
existence check in create_snapshot_subscription to only consider active
SnapshotSubscription rows by filtering on user and is_active=True, while keeping
the cancellation/unsubscribe behavior in cancel_snapshot_subscription and
unsubscribe_by_token unchanged so users can recreate a subscription after
canceling it.

In `@backend/tests/unit/apps/owasp/admin/snapshot_subscription_test.py`:
- Around line 12-45: The test only checks SnapshotSubscriptionAdmin directly and
misses whether apps.owasp.admin actually registers the model with admin.site.
Update the test in snapshot_subscription_test to import the admin package/module
that performs registration and assert SnapshotSubscription is present in
admin.site._registry, or that the registry entry is an instance of
SnapshotSubscriptionAdmin, so a broken import or missing admin.site.register
call is caught.

In
`@backend/tests/unit/apps/owasp/api/internal/mutations/snapshot_subscription_test.py`:
- Around line 187-195: Update test_malformed_token in
snapshot_subscription_test.py to match Django ORM behavior by mocking
django.core.exceptions.ValidationError instead of ValueError for
SnapshotSubscription.objects.get and keep the same unsubscribe_by_token
assertion. Also add a new test_create_after_cancel around
create_snapshot_subscription to verify a user can create a new subscription
after a previously cancelled/inactive one, so the test covers the intended
recreate flow rather than blocking on any existing record.

In
`@backend/tests/unit/apps/owasp/api/internal/queries/snapshot_subscription_test.py`:
- Around line 34-59: The snapshot subscription tests are calling the Strawberry
Django field descriptor directly instead of the underlying resolver, so they
never execute the real my_subscription logic. Update the tests in
SnapshotSubscriptionQuery to fetch the my_subscription field from the class and
invoke its base_resolver.func with self.query and info, so the resolver
implementation is actually exercised for the authenticated, not found, and found
cases.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1a833131-728a-4590-803c-f98c71da2af2

📥 Commits

Reviewing files that changed from the base of the PR and between 5a15194 and 22b8e4b.

⛔ Files ignored due to path filters (1)
  • frontend/src/types/__generated__/graphql.ts is excluded by !**/__generated__/**
📒 Files selected for processing (18)
  • backend/apps/owasp/admin/__init__.py
  • backend/apps/owasp/admin/snapshot_subscription.py
  • backend/apps/owasp/api/internal/mutations/__init__.py
  • backend/apps/owasp/api/internal/mutations/snapshot_subscription.py
  • backend/apps/owasp/api/internal/nodes/snapshot_subscription.py
  • backend/apps/owasp/api/internal/queries/__init__.py
  • backend/apps/owasp/api/internal/queries/snapshot_subscription.py
  • backend/apps/owasp/migrations/0075_snapshotsubscription.py
  • backend/apps/owasp/models/__init__.py
  • backend/apps/owasp/models/snapshot_subscription.py
  • backend/data/nest.dump
  • backend/settings/graphql.py
  • backend/tests/unit/apps/owasp/admin/snapshot_subscription_test.py
  • backend/tests/unit/apps/owasp/api/internal/mutations/__init__.py
  • backend/tests/unit/apps/owasp/api/internal/mutations/snapshot_subscription_test.py
  • backend/tests/unit/apps/owasp/api/internal/nodes/snapshot_subscription_test.py
  • backend/tests/unit/apps/owasp/api/internal/queries/snapshot_subscription_test.py
  • backend/tests/unit/apps/owasp/models/snapshot_subscription_test.py

Comment on lines +63 to +86
if SnapshotSubscription.objects.filter(user=user).exists():
return SnapshotSubscriptionResult(
ok=False,
message="Subscription already exists.",
)

if input_data.frequency not in dict(SnapshotSubscription.Frequency.choices):
return SnapshotSubscriptionResult(
ok=False,
message="Invalid frequency. Must be 'weekly' or 'monthly'.",
)

subscription = SnapshotSubscription.objects.create(
user=user,
frequency=input_data.frequency,
include_chapters=input_data.include_chapters,
include_events=input_data.include_events,
include_issues=input_data.include_issues,
include_posts=input_data.include_posts,
include_projects=input_data.include_projects,
include_pull_requests=input_data.include_pull_requests,
include_releases=input_data.include_releases,
include_users=input_data.include_users,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Canceled users cannot subscribe again, and concurrent creates can still 500.

Because cancel/unsubscribe only flips is_active to False, this exists() check permanently blocks re-subscription for that user. It also leaves a race window before create() against the model’s one-to-one constraint, so two overlapping requests can both pass Line 63 and one will fail with an unhandled integrity error.

Proposed fix
+from django.db import IntegrityError, transaction
+
     `@strawberry.mutation`(permission_classes=[IsAuthenticated])
     def create_snapshot_subscription(
         self,
         info: Info,
         input_data: CreateSnapshotSubscriptionInput,
     ) -> SnapshotSubscriptionResult:
         """Create a new snapshot subscription for the logged-in user."""
         user = info.context.request.user
-
-        if SnapshotSubscription.objects.filter(user=user).exists():
-            return SnapshotSubscriptionResult(
-                ok=False,
-                message="Subscription already exists.",
-            )
 
         if input_data.frequency not in dict(SnapshotSubscription.Frequency.choices):
             return SnapshotSubscriptionResult(
                 ok=False,
                 message="Invalid frequency. Must be 'weekly' or 'monthly'.",
             )
 
-        subscription = SnapshotSubscription.objects.create(
-            user=user,
-            frequency=input_data.frequency,
-            include_chapters=input_data.include_chapters,
-            include_events=input_data.include_events,
-            include_issues=input_data.include_issues,
-            include_posts=input_data.include_posts,
-            include_projects=input_data.include_projects,
-            include_pull_requests=input_data.include_pull_requests,
-            include_releases=input_data.include_releases,
-            include_users=input_data.include_users,
-        )
+        defaults = {
+            "frequency": input_data.frequency,
+            "include_chapters": input_data.include_chapters,
+            "include_events": input_data.include_events,
+            "include_issues": input_data.include_issues,
+            "include_posts": input_data.include_posts,
+            "include_projects": input_data.include_projects,
+            "include_pull_requests": input_data.include_pull_requests,
+            "include_releases": input_data.include_releases,
+            "include_users": input_data.include_users,
+            "is_active": True,
+        }
+
+        try:
+            with transaction.atomic():
+                subscription, created = SnapshotSubscription.objects.get_or_create(
+                    user=user,
+                    defaults=defaults,
+                )
+        except IntegrityError:
+            return SnapshotSubscriptionResult(
+                ok=False,
+                message="Subscription already exists.",
+            )
+
+        if not created:
+            for field_name, value in defaults.items():
+                setattr(subscription, field_name, value)
+            subscription.save()
 
         return SnapshotSubscriptionResult(
             ok=True,
             message="Subscription created successfully.",
             subscription=subscription,
📝 Committable suggestion

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

Suggested change
if SnapshotSubscription.objects.filter(user=user).exists():
return SnapshotSubscriptionResult(
ok=False,
message="Subscription already exists.",
)
if input_data.frequency not in dict(SnapshotSubscription.Frequency.choices):
return SnapshotSubscriptionResult(
ok=False,
message="Invalid frequency. Must be 'weekly' or 'monthly'.",
)
subscription = SnapshotSubscription.objects.create(
user=user,
frequency=input_data.frequency,
include_chapters=input_data.include_chapters,
include_events=input_data.include_events,
include_issues=input_data.include_issues,
include_posts=input_data.include_posts,
include_projects=input_data.include_projects,
include_pull_requests=input_data.include_pull_requests,
include_releases=input_data.include_releases,
include_users=input_data.include_users,
)
user = info.context.request.user
if input_data.frequency not in dict(SnapshotSubscription.Frequency.choices):
return SnapshotSubscriptionResult(
ok=False,
message="Invalid frequency. Must be 'weekly' or 'monthly'.",
)
defaults = {
"frequency": input_data.frequency,
"include_chapters": input_data.include_chapters,
"include_events": input_data.include_events,
"include_issues": input_data.include_issues,
"include_posts": input_data.include_posts,
"include_projects": input_data.include_projects,
"include_pull_requests": input_data.include_pull_requests,
"include_releases": input_data.include_releases,
"include_users": input_data.include_users,
"is_active": True,
}
try:
with transaction.atomic():
subscription, created = SnapshotSubscription.objects.get_or_create(
user=user,
defaults=defaults,
)
except IntegrityError:
return SnapshotSubscriptionResult(
ok=False,
message="Subscription already exists.",
)
if not created:
for field_name, value in defaults.items():
setattr(subscription, field_name, value)
subscription.save()
🤖 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 `@backend/apps/owasp/api/internal/mutations/snapshot_subscription.py` around
lines 63 - 86, The subscription creation logic in SnapshotSubscription mutation
is checking only SnapshotSubscription.objects.filter(user=user).exists(), which
blocks re-subscription after cancel and still allows a race before create().
Update the check to distinguish active vs inactive subscriptions (for example,
based on is_active in the SnapshotSubscription model) so canceled users can
subscribe again, and make the create path resilient to concurrent requests by
handling the one-to-one constraint failure around the subscription creation in
the mutation that builds the SnapshotSubscriptionResult.

Comment on lines +167 to +173
try:
subscription = SnapshotSubscription.objects.get(unsubscribe_token=token)
except (SnapshotSubscription.DoesNotExist, ValueError):
return SnapshotSubscriptionResult(
ok=False,
message="Invalid unsubscribe token.",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

python3 - <<'PY'
import os
import sys
import django
from django.conf import settings

# Minimal Django setup
if not settings.configured:
    settings.configure(
        DEBUG=True,
        DATABASES={
            'default': {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': ':memory:',
            }
        },
        INSTALLED_APPS=[
            'django.contrib.contenttypes',
            'django.contrib.auth',
        ],
        USE_TZ=True,
        SECRET_KEY='test-secret',
    )
    django.setup()

from uuid import UUID
from django.core.exceptions import ValidationError
from django.db import models

class TestModel(models.Model):
    uuid_field = models.UUIDField()
    class Meta:
        app_label = 'test'

# Test the to_python method of UUIDField
field = TestModel._meta.get_field('uuid_field')

# Valid UUID
try:
    result = field.to_python('550e8400-e29b-41d4-a716-446655440000')
    print(f"Valid UUID parsed: {result}")
except Exception as e:
    print(f"Valid UUID raised: {type(e).__name__}: {e}")

# Invalid UUID (malformed)
try:
    result = field.to_python('not-a-uuid')
    print(f"Invalid UUID parsed: {result}")
except Exception as e:
    print(f"Invalid UUID raised: {type(e).__name__}: {e}")

# Test with query simulation
print("\n--- Testing actual ORM lookup simulation ---")
from django.db.models.lookups import Lookup
from django.db.models.sql.query import Query

# Try to see what happens when we try to create a lookup with invalid UUID
try:
    # This simulates what Django does internally when you do .get(uuid_field='bad-uuid')
    # The field's to_python is called before the query
    converted = field.to_python('bad-uuid')
    print(f"Converted: {converted}")
except Exception as e:
    print(f"Conversion raised: {type(e).__name__}: {e}")
    print(f"Is ValidationError? {isinstance(e, ValidationError)}")
PY

Repository: OWASP/Nest

Length of output: 267


🏁 Script executed:

cat -n backend/apps/owasp/api/internal/mutations/snapshot_subscription.py | head -20

Repository: OWASP/Nest

Length of output: 927


🏁 Script executed:

cat -n backend/apps/owasp/api/internal/mutations/snapshot_subscription.py | sed -n '160,180p'

Repository: OWASP/Nest

Length of output: 1078


🏁 Script executed:

cat backend/requirements.txt backend/requirements-dev.txt 2>/dev/null | grep -i django || cat requirements.txt requirements-dev.txt 2>/dev/null | grep -i django || find . -name "pyproject.toml" -o -name "requirements*.txt" | head -5 | xargs grep -i django 2>/dev/null | head -10

Repository: OWASP/Nest

Length of output: 891


🏁 Script executed:

python3 - <<'PY'
# Simulate Django UUIDField behavior without installing Django
# Based on Django source code: django/db/models/fields/__init__.py - UUIDField.to_python()

from uuid import UUID
from dataclasses import dataclass
from typing import Any

# Simulate Django's ValidationError
class ValidationError(Exception):
    pass

def simulate_uuid_field_to_python(value: Any) -> UUID:
    """
    Simulates Django's UUIDField.to_python() method behavior.
    Source: https://github.com/django/django/blob/main/django/db/models/fields/__init__.py
    """
    if value is None:
        return None
    
    if isinstance(value, UUID):
        return value
    
    try:
        return UUID(str(value))
    except (ValueError, AttributeError):
        # Django raises ValidationError here if the value is not a valid UUID string
        raise ValidationError("Invalid UUID format.")

# Test cases
test_cases = [
    ("550e8400-e29b-41d4-a716-446655440000", True),  # Valid UUID
    ("not-a-uuid", False),  # Invalid UUID
    ("", False),  # Empty string
    ("123", False),  # Too short
    (None, True),  # None should return None
]

print("Testing UUID field validation behavior:\n")
for value, should_pass in test_cases:
    try:
        result = simulate_uuid_field_to_python(value)
        status = "PASS" if should_pass else "FAIL (should have raised)"
        print(f"Value: {repr(value):30} -> Parsed: {result} [{status}]")
    except ValidationError as e:
        status = "PASS" if not should_pass else "FAIL (should have parsed)"
        print(f"Value: {repr(value):30} -> ValidationError [{status}]")
    except ValueError as e:
        status = "FAIL (raises ValueError instead of ValidationError)"
        print(f"Value: {repr(value):30} -> ValueError [{status}]")
    except Exception as e:
        print(f"Value: {repr(value):30} -> {type(e).__name__}: {e}")

print("\n" + "="*60)
print("Conclusion:")
print("Django's UUIDField.to_python() raises ValidationError for invalid UUIDs.")
print("The ORM's get() call will trigger this during field conversion.")
print("Therefore, catching ValidationError is necessary to prevent 500 errors.")
PY

Repository: OWASP/Nest

Length of output: 826


🏁 Script executed:

find . -name "snapshot_subscription.py" -path "*/models/*" | head -1 | xargs cat -n | grep -A 5 -B 5 "unsubscribe_token"

Repository: OWASP/Nest

Length of output: 495


Catch the ValidationError Django raises for invalid UUIDs.

An invalid UUID string passed to a UUIDField lookup is coerced by Django's ORM before the query runs. This coercion fails by raising ValidationError, not ValueError. Since the current exception handler only catches ValueError, a malformed unsubscribe token will propagate as an unhandled ValidationError, resulting in a 500 error.

Update the exception block to catch ValidationError:

from django.core.exceptions import ValidationError

try:
    subscription = SnapshotSubscription.objects.get(unsubscribe_token=token)
except (SnapshotSubscription.DoesNotExist, ValidationError):
    return SnapshotSubscriptionResult(
        ok=False,
        message="Invalid unsubscribe 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 `@backend/apps/owasp/api/internal/mutations/snapshot_subscription.py` around
lines 167 - 173, The unsubscribe-token lookup in SnapshotSubscription mutation
only handles SnapshotSubscription.DoesNotExist and ValueError, but invalid UUID
input raises Django’s ValidationError before the query completes. Update the
exception handling in the mutation that calls
SnapshotSubscription.objects.get(unsubscribe_token=token) to also catch
ValidationError, and make sure the needed import from django.core.exceptions is
added so malformed tokens return the existing invalid-token result instead of an
unhandled error.

Comment on lines +34 to +45
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name="snapshot_subscription",
)
frequency = models.CharField(
max_length=10,
choices=Frequency.choices,
default=Frequency.WEEKLY,
)
is_active = models.BooleanField(default=True)
unsubscribe_token = models.UUIDField(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether cancel/unsubscribe deletes SnapshotSubscription rows
# or only sets is_active=False without a reactivation path.

fd -i 'snapshot_subscription*.py' backend

echo
echo "=== Mutation / endpoint implementations touching SnapshotSubscription ==="
rg -n -C3 'SnapshotSubscription|is_active\s*=\s*False|delete\s*\(|unsubscribe_token|reactivat' \
  backend/apps/owasp backend/tests/unit/apps/owasp

echo
echo "=== Focus on the known mutation file ==="
sed -n '1,240p' backend/apps/owasp/api/internal/mutations/snapshot_subscription.py

Repository: OWASP/Nest

Length of output: 50366


Canceled subscriptions cannot be recreated due to flawed existence check.

The current implementation permanently blocks users from re-subscribing after cancellation:

  • Cancellation: Both cancel_snapshot_subscription and unsubscribe_by_token only set is_active = False. The record persists.
  • Creation: create_snapshot_subscription checks SnapshotSubscription.objects.filter(user=user).exists(). This returns True even if the existing record is inactive.

This design conflict prevents a user from toggling their subscription status after the initial creation.

Fix: Update the existence check in create_snapshot_subscription to only block active subscriptions:

if SnapshotSubscription.objects.filter(user=user, is_active=True).exists():
🧰 Tools
🪛 ast-grep (0.44.0)

[info] 38-42: use help_text to document model columns
Context: models.CharField(
max_length=10,
choices=Frequency.choices,
default=Frequency.WEEKLY,
)
Note: [CWE-710] Improper Adherence to Coding Standards.

(model-help-text)

🤖 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 `@backend/apps/owasp/models/snapshot_subscription.py` around lines 34 - 45, The
subscription creation flow is blocking re-subscribes because it treats inactive
records as existing subscriptions. Update the existence check in
create_snapshot_subscription to only consider active SnapshotSubscription rows
by filtering on user and is_active=True, while keeping the
cancellation/unsubscribe behavior in cancel_snapshot_subscription and
unsubscribe_by_token unchanged so users can recreate a subscription after
canceling it.

Comment on lines +12 to +45
def test_admin_configuration(self):
"""Test admin configuration matches expected setup."""
site = AdminSite()
admin = SnapshotSubscriptionAdmin(SnapshotSubscription, site)

assert admin.list_display == (
"user",
"frequency",
"is_active",
"created_at",
"updated_at",
)
assert admin.list_filter == ("frequency", "is_active", "created_at")
assert admin.search_fields == ("user__email", "user__username")
assert admin.raw_id_fields == ("user",)
assert admin.readonly_fields == ("unsubscribe_token", "created_at", "updated_at")

# Check fieldsets structure
assert len(admin.fieldsets) == 3

# Check Content Preferences fieldset
preferences_fieldset = admin.fieldsets[1]
assert preferences_fieldset[0] == "Content Preferences"

assert preferences_fieldset[1]["fields"] == (
"include_chapters",
"include_events",
"include_issues",
"include_posts",
"include_projects",
"include_pull_requests",
"include_releases",
"include_users",
)

Copy link
Copy Markdown
Contributor

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 test for actual admin-site registration.

This only validates SnapshotSubscriptionAdmin's static attributes after manual instantiation. It never checks that importing apps.owasp.admin registers SnapshotSubscription on admin.site, so a broken Line 19 import in backend/apps/owasp/admin/__init__.py or a removed Line 44 admin.site.register(...) would still pass.

Suggested coverage
+from django.contrib import admin
+
 class TestSnapshotSubscriptionAdmin:
     """Test SnapshotSubscriptionAdmin configuration."""
 
+    def test_model_is_registered_on_default_admin_site(self):
+        """Test admin package wiring registers SnapshotSubscription."""
+        import apps.owasp.admin  # noqa: F401
+
+        assert SnapshotSubscription in admin.site._registry
+        assert isinstance(
+            admin.site._registry[SnapshotSubscription],
+            SnapshotSubscriptionAdmin,
+        )
+
     def test_admin_configuration(self):
         """Test admin configuration matches expected setup."""
         site = AdminSite()
         admin = SnapshotSubscriptionAdmin(SnapshotSubscription, site)
📝 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_admin_configuration(self):
"""Test admin configuration matches expected setup."""
site = AdminSite()
admin = SnapshotSubscriptionAdmin(SnapshotSubscription, site)
assert admin.list_display == (
"user",
"frequency",
"is_active",
"created_at",
"updated_at",
)
assert admin.list_filter == ("frequency", "is_active", "created_at")
assert admin.search_fields == ("user__email", "user__username")
assert admin.raw_id_fields == ("user",)
assert admin.readonly_fields == ("unsubscribe_token", "created_at", "updated_at")
# Check fieldsets structure
assert len(admin.fieldsets) == 3
# Check Content Preferences fieldset
preferences_fieldset = admin.fieldsets[1]
assert preferences_fieldset[0] == "Content Preferences"
assert preferences_fieldset[1]["fields"] == (
"include_chapters",
"include_events",
"include_issues",
"include_posts",
"include_projects",
"include_pull_requests",
"include_releases",
"include_users",
)
from django.contrib import admin
class TestSnapshotSubscriptionAdmin:
"""Test SnapshotSubscriptionAdmin configuration."""
def test_model_is_registered_on_default_admin_site(self):
"""Test admin package wiring registers SnapshotSubscription."""
import apps.owasp.admin # noqa: F401
assert SnapshotSubscription in admin.site._registry
assert isinstance(
admin.site._registry[SnapshotSubscription],
SnapshotSubscriptionAdmin,
)
def test_admin_configuration(self):
"""Test admin configuration matches expected setup."""
site = AdminSite()
admin = SnapshotSubscriptionAdmin(SnapshotSubscription, site)
assert admin.list_display == (
"user",
"frequency",
"is_active",
"created_at",
"updated_at",
)
assert admin.list_filter == ("frequency", "is_active", "created_at")
assert admin.search_fields == ("user__email", "user__username")
assert admin.raw_id_fields == ("user",)
assert admin.readonly_fields == ("unsubscribe_token", "created_at", "updated_at")
# Check fieldsets structure
assert len(admin.fieldsets) == 3
# Check Content Preferences fieldset
preferences_fieldset = admin.fieldsets[1]
assert preferences_fieldset[0] == "Content Preferences"
assert preferences_fieldset[1]["fields"] == (
"include_chapters",
"include_events",
"include_issues",
"include_posts",
"include_projects",
"include_pull_requests",
"include_releases",
"include_users",
)
🤖 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 `@backend/tests/unit/apps/owasp/admin/snapshot_subscription_test.py` around
lines 12 - 45, The test only checks SnapshotSubscriptionAdmin directly and
misses whether apps.owasp.admin actually registers the model with admin.site.
Update the test in snapshot_subscription_test to import the admin package/module
that performs registration and assert SnapshotSubscription is present in
admin.site._registry, or that the registry entry is an instance of
SnapshotSubscriptionAdmin, so a broken import or missing admin.site.register
call is caught.

Comment on lines +187 to +195
def test_malformed_token(self, mutations):
"""Test unsubscribe fails with malformed token."""
with patch(
"apps.owasp.api.internal.mutations.snapshot_subscription.SnapshotSubscription.objects"
) as mock_objects:
mock_objects.get.side_effect = ValueError
result = mutations.unsubscribe_by_token(token=MOCK_TOKEN)
assert not result.ok
assert result.message == "Invalid unsubscribe token."

Copy link
Copy Markdown
Contributor

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
# 1. Find the actual mutation implementation
find backend -name "snapshot_subscription.py" -path "*/mutations/*" | head -5

# 2. Check what exceptions Django raises for malformed UUIDs
python3 << 'PY'
import sys
try:
    from django.db import models
    from uuid import UUID
    
    # Check what exception Django UUIDField raises when parsing malformed UUID
    field = models.UUIDField()
    
    # Test malformed UUID string
    try:
        # Django field get_prep_value or from_db_value might raise
        val = field.to_python("not-a-uuid")
        print(f"to_python returned: {val} (type: {type(val).__name__})")
    except Exception as e:
        print(f"to_python raised: {type(e).__name__}: {e}")
    
    try:
        val = field.to_python("00000000-0000-0000-0000-00000000000X")
        print(f"to_python returned for partial: {val} (type: {type(val).__name__})")
    except Exception as e:
        print(f"to_python raised for partial: {type(e).__name__}: {e}")
        
    # Check field validation
    try:
        field.clean("not-a-uuid", None)
    except Exception as e:
        print(f"clean raised: {type(e).__name__}: {e}")
        
except ImportError as e:
    print(f"Django not available in sandbox: {e}")
    print("Will check Django docs via web search instead")
PY

# 3. Let's also check if there are any imports or usage of ValidationError in the project
grep -r "ValidationError" backend/apps/owasp/api/internal/mutations/snapshot_subscription.py 2>/dev/null || echo "No ValidationError found in mutation file"

# 4. Check test file for existing tests
grep -n "inactive" backend/tests/unit/apps/owasp/api/internal/mutations/snapshot_subscription_test.py 2>/dev/null || echo "No 'inactive' test found"
grep -n "resubscribe\|create.*after\|recreate" backend/tests/unit/apps/owasp/api/internal/mutations/snapshot_subscription_test.py 2>/dev/null || echo "No resubscribe tests found"

Repository: OWASP/Nest

Length of output: 581


🏁 Script executed:

#!/bin/bash
# Read the actual mutation implementation
cat -n backend/apps/owasp/api/internal/mutations/snapshot_subscription.py

# Check the test file context around line 187-195 and 197-207
sed -n '180,215p' backend/tests/unit/apps/owasp/api/internal/mutations/snapshot_subscription_test.py | cat -n

Repository: OWASP/Nest

Length of output: 10089


🌐 Web query:

Django UUIDField malformed UUID exception type ValueError or ValidationError

💡 Result:

In Django, when a malformed UUID is provided to a UUIDField, the expected exception is a django.core.exceptions.ValidationError, not a standard Python ValueError [1][2][3]. Django's form-based UUIDField explicitly handles UUID conversion in its to_python method [1][2][3]. It attempts to create a uuid.UUID object from the input and catches the ValueError raised by the underlying Python uuid module, re-raising it as a django.core.exceptions.ValidationError [1][2][3]. Historically, this behavior caused issues in older versions of Django where certain components (such as specific widgets or querysets) expected a ValueError or did not account for the switch to ValidationError, leading to crashes [4][5][6]. Modern Django implementations consistently normalize these validation errors to ValidationError to ensure they can be caught by standard form validation and error-handling logic [1][2][3]. If you are encountering a ValueError instead of a ValidationError, it is often because you are interacting directly with lower-level database ORM fields or external libraries that are calling uuid.UUID directly without wrapping it in Django's validation layer [5][7]. For form-level inputs, you should always expect and catch django.core.exceptions.ValidationError [1][2].

Citations:


🌐 Web query:

Django ORM UUIDField lookup malformed string raises exception

💡 Result:

In Django, when performing an ORM lookup on a UUIDField with a malformed string, the behavior regarding exceptions depends on the specific Django version and the nature of the database backend, but generally, it is designed to raise a validation-related exception [1][2]. Historically, passing an invalid string to a UUIDField lookup could result in different outcomes, such as a ValueError from the Python uuid module, a DataError from the database, or an unhandled exception depending on the context of the query execution [1][3]. Over several releases, Django has improved this behavior to ensure more consistent error handling [1][4][3]. Key points regarding this behavior: 1. Exception Type: When an invalid UUID string is provided to a filter or get call, Django's UUIDField often attempts to normalize or validate the input using its internal to_python method [5][2]. If this validation fails, it typically raises a django.core.exceptions.ValidationError [4][2]. In some internal contexts or specific lookup paths, this may still surface as a ValueError or, if it reaches the database layer without validation, a database-level error like DataError [1][3]. 2. Validation during Lookups: Django's ORM is not always required to pre-validate all input for every lookup type. However, for UUIDField, substantial efforts have been made to ensure that queries fail gracefully or predictably when provided with data that cannot be converted into a valid UUID [1][6]. 3. Development Context: If you are encountering an unexpected exception, it is often because the input string is not a valid hexadecimal UUID (with or without hyphens) [7][2]. If you need to perform fuzzy searching or partial matching (e.g., starts-with), keep in mind that standard lookups like exact and icontains have specific handling for UUID fields, and complex lookups sometimes require custom implementations or explicit string casting to avoid validation issues [8][9][10]. If you are writing code that accepts user-provided strings for UUID filtering, the recommended practice is to validate the string using the Python uuid.UUID class manually before passing it to the ORM query, or to wrap the query in a try-except block that catches ValidationError [2].

Citations:


🏁 Script executed:

python3 << 'PY'
# Verify the exception type we expect in the test
from django.core.exceptions import ValidationError
print(f"ValidationError available: {hasattr(ValidationError, '__module__')}")
# The production code catches ValueError, which might be defensive but Django raises ValidationError.
# The test should reflect the real ORM behavior.
PY

Repository: OWASP/Nest

Length of output: 267


Align test exception with Django ORM behavior and add subscription recreation test.

The test_malformed_token mocks ValueError, but Django’s UUIDField raises django.core.exceptions.ValidationError for malformed UUIDs in standard ORM lookups. Update the test to mock and assert ValidationError to reflect actual production behavior. Additionally, the create_snapshot_subscription logic currently blocks creation if any subscription exists (active or inactive). Add a test_create_after_cancel to verify if users can recreate a subscription after cancelling their previous one.

🤖 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
`@backend/tests/unit/apps/owasp/api/internal/mutations/snapshot_subscription_test.py`
around lines 187 - 195, Update test_malformed_token in
snapshot_subscription_test.py to match Django ORM behavior by mocking
django.core.exceptions.ValidationError instead of ValueError for
SnapshotSubscription.objects.get and keep the same unsubscribe_by_token
assertion. Also add a new test_create_after_cancel around
create_snapshot_subscription to verify a user can create a new subscription
after a previously cancelled/inactive one, so the test covers the intended
recreate flow rather than blocking on any existing record.

Comment on lines +34 to +59
def test_my_subscription_unauthenticated(self):
"""Test mySubscription returns None for unauthenticated user."""
info = mock_info(authenticated=False)
result = self.query.__class__.__dict__["my_subscription"](self.query, info=info)
assert result is None

def test_my_subscription_not_found(self):
"""Test mySubscription returns None when no subscription exists."""
info = mock_info()
with patch(
"apps.owasp.api.internal.queries.snapshot_subscription.SnapshotSubscription.objects"
) as mock_objects:
mock_objects.get.side_effect = SnapshotSubscription.DoesNotExist
result = self.query.__class__.__dict__["my_subscription"](self.query, info=info)
assert result is None

def test_my_subscription_found(self):
"""Test mySubscription returns subscription when it exists."""
info = mock_info()
mock_sub = MagicMock(spec=SnapshotSubscription)
with patch(
"apps.owasp.api.internal.queries.snapshot_subscription.SnapshotSubscription.objects"
) as mock_objects:
mock_objects.get.return_value = mock_sub
result = self.query.__class__.__dict__["my_subscription"](self.query, info=info)
assert result == mock_sub

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how strawberry_django field objects expose their resolver and whether they are callable.
fd -t f 'field.py' --full-path 'strawberry_django' 2>/dev/null | head
rg -nP --type=py -C3 '\b(def __call__|base_resolver|wrapped_func)\b' $(python -c "import strawberry_django, os; print(os.path.dirname(strawberry_django.__file__))" 2>/dev/null) 2>/dev/null | head -50
# Also check existing query tests in the repo for the established invocation pattern.
fd -e py -p 'queries' -p 'tests' | xargs rg -nP -l '__dict__\[' 2>/dev/null | head

Repository: OWASP/Nest

Length of output: 148


Update test invocation to access the underlying resolver via base_resolver.

The tests invoke the resolver using self.query.__class__.__dict__["my_subscription"](self.query, info=info). Since @strawberry_django.field transforms the method into a StrawberryDjangoField descriptor, direct invocation of this descriptor does not execute the resolver logic.

To correctly unit test the resolver logic, access the underlying function via the base_resolver attribute:

# In snapshot_subscription_test.py
field = SnapshotSubscriptionQuery.__dict__["my_subscription"]
# Access the original function (func) from the base_resolver
result = field.base_resolver.func(self.query, info=info)

This ensures the test executes the actual my_subscription implementation.

🤖 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
`@backend/tests/unit/apps/owasp/api/internal/queries/snapshot_subscription_test.py`
around lines 34 - 59, The snapshot subscription tests are calling the Strawberry
Django field descriptor directly instead of the underlying resolver, so they
never execute the real my_subscription logic. Update the tests in
SnapshotSubscriptionQuery to fetch the my_subscription field from the class and
invoke its base_resolver.func with self.query and info, so the resolver
implementation is actually exercised for the authenticated, not found, and found
cases.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 19 files

Confidence score: 2/5

  • In backend/apps/owasp/api/internal/mutations/snapshot_subscription.py, the create subscription path appears to block previously canceled users by only checking record existence, so users can get stuck permanently inactive via GraphQL after canceling — update the logic to allow reactivation (or check active state) before merging.
  • In backend/apps/owasp/api/internal/mutations/snapshot_subscription.py, unsubscribe_by_token does not catch ValidationError for malformed UUID tokens, which can bubble up as an unhandled GraphQL error instead of a controlled response — add UUID validation/error handling and return a safe client-facing error before merging.
Prompt for AI agents (unresolved 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/apps/owasp/api/internal/mutations/snapshot_subscription.py">

<violation number="1" location="backend/apps/owasp/api/internal/mutations/snapshot_subscription.py:63">
P1: Canceled users cannot resubscribe because create checks existence instead of active state/reactivation logic. This leaves users permanently inactive through GraphQL once they cancel.</violation>

<violation number="2" location="backend/apps/owasp/api/internal/mutations/snapshot_subscription.py:169">
P2: `unsubscribe_by_token` misses `ValidationError` handling for malformed UUID tokens. Invalid token format can raise an unhandled exception and return a GraphQL error.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

"""Create a new snapshot subscription for the logged-in user."""
user = info.context.request.user

if SnapshotSubscription.objects.filter(user=user).exists():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Canceled users cannot resubscribe because create checks existence instead of active state/reactivation logic. This leaves users permanently inactive through GraphQL once they cancel.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/apps/owasp/api/internal/mutations/snapshot_subscription.py, line 63:

<comment>Canceled users cannot resubscribe because create checks existence instead of active state/reactivation logic. This leaves users permanently inactive through GraphQL once they cancel.</comment>

<file context>
@@ -0,0 +1,188 @@
+        """Create a new snapshot subscription for the logged-in user."""
+        user = info.context.request.user
+
+        if SnapshotSubscription.objects.filter(user=user).exists():
+            return SnapshotSubscriptionResult(
+                ok=False,
</file context>

"""Unsubscribe using a token from an email link. No auth required."""
try:
subscription = SnapshotSubscription.objects.get(unsubscribe_token=token)
except (SnapshotSubscription.DoesNotExist, ValueError):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: unsubscribe_by_token misses ValidationError handling for malformed UUID tokens. Invalid token format can raise an unhandled exception and return a GraphQL error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/apps/owasp/api/internal/mutations/snapshot_subscription.py, line 169:

<comment>`unsubscribe_by_token` misses `ValidationError` handling for malformed UUID tokens. Invalid token format can raise an unhandled exception and return a GraphQL error.</comment>

<file context>
@@ -0,0 +1,188 @@
+        """Unsubscribe using a token from an email link. No auth required."""
+        try:
+            subscription = SnapshotSubscription.objects.get(unsubscribe_token=token)
+        except (SnapshotSubscription.DoesNotExist, ValueError):
+            return SnapshotSubscriptionResult(
+                ok=False,
</file context>

@HarshitVerma109 HarshitVerma109 marked this pull request as draft June 26, 2026 17:43
@HarshitVerma109 HarshitVerma109 added the gsoc2026:harshitverma109 harshitverma109 GSoC 2026 related work label Jun 27, 2026
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.

1 participant