Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b4f565b
cherry pick claim related files
rudransh-shrivastava Jun 20, 2026
731811c
display spinner when fetching data, show toast for reorder, fixes
rudransh-shrivastava Jun 20, 2026
1af6996
backend: update claim mutations to return claim object
rudransh-shrivastava Jun 20, 2026
bd4a54a
select claim/claims in mutations
rudransh-shrivastava Jun 21, 2026
e80f85e
avoid refetching queries on mutation, use returned claim object
rudransh-shrivastava Jun 21, 2026
a15bf82
add timeout to toasts
rudransh-shrivastava Jun 21, 2026
ab08b9c
remove unused queries temporarily
rudransh-shrivastava Jun 21, 2026
3d5f326
reduce queries network calls by merging queries
rudransh-shrivastava Jun 21, 2026
625db7b
use status enum instead of strings
rudransh-shrivastava Jun 21, 2026
867c94f
add claim type
rudransh-shrivastava Jun 21, 2026
974318e
backend: add has_evidence
rudransh-shrivastava Jun 21, 2026
3d65c52
add has evidence label
rudransh-shrivastava Jun 21, 2026
88af465
add unit tests for bod dashboard page
rudransh-shrivastava Jun 21, 2026
b5e6c6d
add tests
rudransh-shrivastava Jun 21, 2026
497af6c
add e2e tests
rudransh-shrivastava Jun 21, 2026
00ae269
undo backend change
rudransh-shrivastava Jun 21, 2026
79d1a7c
bot comments
rudransh-shrivastava Jun 21, 2026
bb605c7
bot comments
rudransh-shrivastava Jun 21, 2026
93a7d07
improve skip condition
rudransh-shrivastava Jun 21, 2026
6370b08
backend: annotate has_evidence
rudransh-shrivastava Jun 21, 2026
4157b4e
Merge branch 'feature/bod-candidate-transparency' into feature/bod-cl…
rudransh-shrivastava Jun 21, 2026
f0112e9
apply bot comments
rudransh-shrivastava Jun 22, 2026
d68477c
improve breadcrumbs
rudransh-shrivastava Jun 22, 2026
7b56553
backend: add test for annotate has evidence
rudransh-shrivastava Jun 22, 2026
6fe2194
update code
rudransh-shrivastava Jun 22, 2026
c6ea05b
apply bot suggestions
rudransh-shrivastava Jun 22, 2026
fd34abb
fix tests
rudransh-shrivastava Jun 23, 2026
6fbd993
improve test coverage
rudransh-shrivastava Jun 23, 2026
8f4fe72
fix e2e test
rudransh-shrivastava Jun 23, 2026
640208f
fmt frontend files
rudransh-shrivastava Jun 23, 2026
55466a9
fix sonar qube and bot issues
rudransh-shrivastava Jun 23, 2026
b502f9b
fix double click on keypress
rudransh-shrivastava Jun 23, 2026
ee4005b
update code
rudransh-shrivastava Jun 24, 2026
bcc61fa
add disableAnimation to Button
rudransh-shrivastava Jun 24, 2026
9c34d81
cherry pick evidence related files
rudransh-shrivastava Jun 24, 2026
2caede4
update pnpm lock
rudransh-shrivastava Jun 24, 2026
29097a0
add back missing query and generate
rudransh-shrivastava Jun 24, 2026
a2073d2
fix skip: condition
rudransh-shrivastava Jun 24, 2026
4808531
remove refetchQueries and use mutation returned object
rudransh-shrivastava Jun 25, 2026
1c3c434
fix validation by removing .
rudransh-shrivastava Jun 25, 2026
1ba8581
fix isSyncing bug
rudransh-shrivastava Jun 25, 2026
3eff335
move access denied display up
rudransh-shrivastava Jun 25, 2026
719325c
fix titles
rudransh-shrivastava Jun 25, 2026
71fa26b
update code
rudransh-shrivastava Jun 25, 2026
abc3ec6
remove double queries
rudransh-shrivastava Jun 25, 2026
1de83ec
add unit tests
rudransh-shrivastava Jun 26, 2026
2c6eb8b
add e2e tests
rudransh-shrivastava Jun 26, 2026
bebd592
bot comments
rudransh-shrivastava Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class ClaimResult:
ok: bool
code: str | None = None
message: str | None = None
claim: BoardCandidateClaimNode | None = None


def _validate_reorder_claims(
Expand Down Expand Up @@ -167,7 +168,7 @@ def create_board_candidate_claim(
)

try:
BoardCandidateClaim.objects.create(
claim = BoardCandidateClaim.objects.create(
board=board,
candidate=candidate,
description=input_data.description,
Expand All @@ -194,7 +195,12 @@ def create_board_candidate_claim(
message=" ".join(messages),
)

return ClaimResult(ok=True, code="SUCCESS", message="Claim created successfully.")
return ClaimResult(
ok=True,
code="SUCCESS",
message="Claim created successfully.",
claim=claim,
)

@strawberry.mutation(permission_classes=[IsAuthenticated])
@transaction.atomic
Expand Down Expand Up @@ -250,7 +256,12 @@ def update_board_candidate_claim(
message=" ".join(messages),
)

return ClaimResult(ok=True, code="SUCCESS", message="Claim updated successfully.")
return ClaimResult(
ok=True,
code="SUCCESS",
message="Claim updated successfully.",
claim=claim,
)

@strawberry.mutation(permission_classes=[IsAuthenticated])
@transaction.atomic
Expand Down Expand Up @@ -302,7 +313,12 @@ def discard_board_candidate_claim(
message=" ".join(messages),
)

return ClaimResult(ok=True, code="SUCCESS", message="Claim discarded successfully.")
return ClaimResult(
ok=True,
code="SUCCESS",
message="Claim discarded successfully.",
claim=claim,
)

@strawberry.mutation(permission_classes=[IsAuthenticated])
@transaction.atomic
Expand Down Expand Up @@ -366,6 +382,7 @@ def submit_board_candidate_claim(
ok=True,
code="SUCCESS",
message="Claim submitted successfully.",
claim=claim,
)

return result
Expand Down Expand Up @@ -425,7 +442,12 @@ def withdraw_board_candidate_claim(
message=" ".join(messages),
)

return ClaimResult(ok=True, code="SUCCESS", message="Claim withdrawn successfully.")
return ClaimResult(
ok=True,
code="SUCCESS",
message="Claim withdrawn successfully.",
claim=claim,
)

@strawberry.mutation(permission_classes=[IsAuthenticated])
@transaction.atomic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ def created_at(self, root: BoardCandidateClaim) -> datetime:
"""Resolve claim creation date."""
return root.nest_created_at

@strawberry_django.field
def has_evidence(self, root: BoardCandidateClaim) -> bool:
"""Resolve whether the claim has any evidence."""
if hasattr(root, "evidence_exists"):
return root.evidence_exists
return root.evidences.filter(is_removed=False).exists()

@strawberry_django.field
def status(self, root: BoardCandidateClaim) -> ClaimStatusEnum:
"""Resolve claim status as GraphQL enum."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import strawberry
import strawberry_django
from django.db.models import Exists, OuterRef

from apps.owasp.api.internal.nodes.board_candidate_claim import BoardCandidateClaimNode
from apps.owasp.models.board_candidate_claim import BoardCandidateClaim
from apps.owasp.models.board_candidate_claim_evidence import BoardCandidateClaimEvidence


@strawberry.type
Expand Down Expand Up @@ -32,10 +34,20 @@ def board_candidate_claims(
and user.github_user is not None
and user.github_user.login == login
)
claims = BoardCandidateClaim.objects.filter(
board__year=year,
candidate__member__login=login,
).order_by("order", "nest_created_at")
claims = (
BoardCandidateClaim.objects.filter(
board__year=year,
candidate__member__login=login,
)
.annotate(
evidence_exists=Exists(
BoardCandidateClaimEvidence.objects.filter(
claim=OuterRef("pk"), is_removed=False
)
),
)
.order_by("order", "nest_created_at")
)

if not is_self:
claims = claims.filter(status=BoardCandidateClaim.Status.APPROVED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def test_discard_claim_success(self, mock_claim_model):

assert result.ok
assert result.code == "SUCCESS"
assert result.claim is claim
assert claim.status == BoardCandidateClaim.Status.DISCARDED
claim.save.assert_called_once()

Expand Down Expand Up @@ -130,6 +131,7 @@ def test_submit_claim_success(self, mock_claim_model):

assert result.ok
assert result.code == "SUCCESS"
assert result.claim is claim
assert claim.status == BoardCandidateClaim.Status.SUBMITTED
claim.save.assert_called_once()

Expand Down Expand Up @@ -210,6 +212,7 @@ def test_withdraw_claim_success_submitted(self, mock_timezone, mock_claim_model)

assert result.ok
assert result.code == "SUCCESS"
assert result.claim is claim
assert claim.status == BoardCandidateClaim.Status.WITHDRAWN
assert claim.withdrawn_reason == "No longer relevant"
assert claim.withdrawn_at == now
Expand Down Expand Up @@ -238,6 +241,7 @@ def test_withdraw_claim_success_approved(self, mock_timezone, mock_claim_model):

assert result.ok
assert result.code == "SUCCESS"
assert result.claim is claim
assert claim.status == BoardCandidateClaim.Status.WITHDRAWN
assert claim.withdrawn_reason == "No longer relevant"
assert claim.withdrawn_at == now
Expand Down Expand Up @@ -548,6 +552,7 @@ def test_create_claim_success(self, mock_claim_model, mock_board_model):
)
assert result.ok
assert result.code == "SUCCESS"
assert result.claim is not None

@patch("apps.owasp.api.internal.mutations.board_candidate_claim.BoardOfDirectors")
def test_create_claim_board_not_found(self, mock_board_model):
Expand Down Expand Up @@ -688,6 +693,7 @@ def test_update_claim_success(self, mock_claim_model):

assert result.ok
assert result.code == "SUCCESS"
assert result.claim is claim
assert claim.name == input_data.name
assert claim.description == input_data.description
claim.save.assert_called_once()
Expand All @@ -713,6 +719,7 @@ def test_update_claim_partial(self, mock_claim_model):

assert result.ok
assert result.code == "SUCCESS"
assert result.claim is claim
assert claim.name == "Updated Name"
claim.save.assert_called_once_with(update_fields=["name", "key"])

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests for BoardCandidateClaim GraphQL node."""

from unittest.mock import Mock

from apps.owasp.api.internal.nodes.board_candidate_claim import BoardCandidateClaimNode
from tests.unit.apps.common.graphql_node_base_test import GraphQLNodeBaseTest

Expand All @@ -15,6 +17,7 @@ def test_node_fields(self):
"_id",
"created_at",
"description",
"has_evidence",
"is_locked",
"key",
"name",
Expand All @@ -25,3 +28,32 @@ def test_node_fields(self):
"withdrawn_reason",
}
assert field_names == expected_field_names

def test_has_evidence_returns_true_when_evidence_exists(self):
mock_claim = Mock()
mock_claim.evidence_exists = True

field = self._get_field_by_name("has_evidence", BoardCandidateClaimNode)
result = field.base_resolver.wrapped_func(None, mock_claim)

assert result

def test_has_evidence_returns_false_when_no_evidence(self):
mock_claim = Mock()
mock_claim.evidence_exists = False

field = self._get_field_by_name("has_evidence", BoardCandidateClaimNode)
result = field.base_resolver.wrapped_func(None, mock_claim)

assert not result

def test_has_evidence_falls_back_to_evidences_filter_when_annotation_missing(self):
mock_claim = Mock(spec=[])
mock_claim.evidences = Mock()
mock_claim.evidences.filter.return_value.exists.return_value = True

field = self._get_field_by_name("has_evidence", BoardCandidateClaimNode)
result = field.base_resolver.wrapped_func(None, mock_claim)

mock_claim.evidences.filter.assert_called_once_with(is_removed=False)
assert result
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def test_board_candidate_claims_self(self, mock_claim_model):

claims = [MagicMock(), MagicMock()]
mock_qs = MagicMock()
mock_qs.annotate.return_value = mock_qs
mock_qs.order_by.return_value = claims
Comment on lines +29 to 30

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.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Assert the evidence_exists annotation, not just the chain.

These tests now only stub annotate(), so they will still pass if board_candidate_claims stops annotating and the dashboard falls back to one .exists() query per claim. Please assert that annotate is called with an evidence_exists kwarg to lock in the optimization contract.

Also applies to: 52-53, 73-74

🤖 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/board_candidate_claim_test.py`
around lines 29 - 30, The board candidate claim tests currently only stub the
queryset chain and do not verify the optimization contract. In the
`board_candidate_claims` test cases, assert that `annotate()` is called with an
`evidence_exists` keyword argument (along with the existing queryset chaining)
so the tests fail if the annotation is removed and the code falls back to
per-claim `.exists()` queries. Use the existing `mock_qs` setup in
`test_board_candidate_claims` to check the `annotate` call directly.

mock_claim_model.objects.filter.return_value = mock_qs

Expand All @@ -48,6 +49,7 @@ def test_board_candidate_claims_non_self_filters_approved(self, mock_claim_model
info = _make_info(user)

base_qs = MagicMock()
base_qs.annotate.return_value = base_qs
ordered_qs = MagicMock()
filtered_qs = MagicMock()
ordered_qs.filter.return_value = filtered_qs
Expand All @@ -68,6 +70,7 @@ def test_board_candidate_claims_anonymous_filters_approved(self, mock_claim_mode
info = _make_info(user)

base_qs = MagicMock()
base_qs.annotate.return_value = base_qs
ordered_qs = MagicMock()
filtered_qs = MagicMock()
ordered_qs.filter.return_value = filtered_qs
Expand Down
32 changes: 32 additions & 0 deletions e2e/helpers/mockAuthCookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const authCookies = [
{
name: 'csrftoken',
value: 'abc123',
domain: 'localhost',
path: '/',
},
{
name: 'nest.session-id',
value: 'test-session-id',
domain: 'localhost',
path: '/',
},
{
name: 'next-auth.csrf-token',
value: 'test-csrf-token',
domain: 'localhost',
path: '/',
},
{
name: 'next-auth.callback-url',
value: '/',
domain: 'localhost',
path: '/',
},
{
name: 'next-auth.session-token',
value: 'test-session-token',
domain: 'localhost',
path: '/',
},
]
42 changes: 42 additions & 0 deletions e2e/helpers/mockClaimAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { authCookies } from './mockAuthCookies'

export const mockClaimAuth = async (page, mockData, login = 'testuser', operationNames?: string[]) => {
await page.route('**/api/auth/session', async (route) => {
await route.fulfill({
status: 200,
json: {
accessToken: 'test-access-token',
expires: '2125-08-28T01:33:56.550Z',
user: {
isOwaspStaff: false,
login,
},
},
})
})
await page.route('**/graphql/', async (route, request) => {
const postData = request.postDataJSON()
Comment on lines +17 to +18

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 | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\nUsages of mockClaimAuth:\n'
rg -n --type=ts --type=tsx '\bmockClaimAuth\s*\(' e2e frontend

printf '\nPotential multipart/upload GraphQL paths:\n'
rg -n --type=ts --type=tsx 'createUploadLink|multipart/form-data|FormData|Upload|postDataJSON\(' e2e frontend

Repository: OWASP/Nest

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Usages of mockClaimAuth ==="
rg -n '\bmockClaimAuth\s*\(' --type ts --type ts --glob "*test*" --glob "*.spec.ts" --glob "*e2e*" e2e frontend || true

echo "=== Potential multipart/upload GraphQL patterns ==="
rg -n 'createUploadLink|multipart/form-data|new FormData|Upload' --type ts e2e frontend || true

echo "=== Check mockClaimAuth definition ==="
cat -n e2e/helpers/mockClaimAuth.ts | head -40

Repository: OWASP/Nest

Length of output: 4932


Fix postDataJSON() crash on multipart file uploads in mockClaimAuth.

The mockClaimAuth helper unconditionally calls request.postDataJSON() on line 18. Since the frontend uses UploadHttpLink for evidence flows, requests involving files are sent as multipart/form-data. Playwright throws an error when calling postDataJSON() on non-JSON bodies, causing tests like BoardCandidateClaimEvidenceCreate.spec.ts to fail before the operationName filter runs.

Gate the JSON parsing behind a content-type check:

const headers = request.headers()
const contentType = headers['content-type'] || ''
let postData

if (contentType.includes('application/json')) {
  postData = request.postDataJSON()
} else {
  // For multipart uploads, parse body as text and extract operationName manually
  postData = { operationName: null } 
  // Alternative: skip operationName check for non-JSON or parse text body if needed
}

Update the route handler to conditionally access postData only when safe.

🤖 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 `@e2e/helpers/mockClaimAuth.ts` around lines 17 - 18, `mockClaimAuth` is
unconditionally parsing request bodies as JSON, which crashes on multipart
`UploadHttpLink` uploads before the `operationName` check. Update the
`page.route('**/graphql/', ...)` handler to inspect `request.headers()` first,
only call `request.postDataJSON()` when the content type is JSON, and safely
skip or specially handle multipart requests so evidence upload tests do not
fail.

if (postData.operationName === 'SyncDjangoSession') {
await route.fulfill({
status: 200,
json: {
data: {
githubAuth: {
message: 'test message',
ok: true,
user: { isOwaspStaff: false },
},
},
},
})
} else if (operationNames && postData.operationName && !operationNames.includes(postData.operationName)) {
await route.abort('aborted')
} else {
await route.fulfill({
status: 200,
json: { data: mockData },
})
}
})
await page.context().addCookies(authCookies)
}
35 changes: 3 additions & 32 deletions e2e/helpers/mockDashboardCookies.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { authCookies } from './mockAuthCookies'

export const mockDashboardCookies = async (page, mockDashboardData, isOwaspStaff) => {
await page.route('**/api/auth/session', async (route) => {
await route.fulfill({
Expand Down Expand Up @@ -36,36 +38,5 @@ export const mockDashboardCookies = async (page, mockDashboardData, isOwaspStaff
})
}
})
await page.context().addCookies([
{
name: 'csrftoken',
value: 'abc123',
domain: 'localhost',
path: '/',
},
{
name: 'nest.session-id',
value: 'test-session-id',
domain: 'localhost',
path: '/',
},
{
name: 'next-auth.csrf-token',
value: 'test-csrf-token',
domain: 'localhost',
path: '/',
},
{
name: 'next-auth.callback-url',
value: '/',
domain: 'localhost',
path: '/',
},
{
name: 'next-auth.session-token',
value: 'test-session-token',
domain: 'localhost',
path: '/',
},
])
await page.context().addCookies(authCookies)
}
Loading
Loading