Skip to content
8 changes: 7 additions & 1 deletion backend/src/apps/owasp/utils/score_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@ def recalculate_all(self) -> dict[str, Any]:
pending_score.github_user.login,
)
failed_certificates.append((pending_score.github_user.login, e))
except Exception as e:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What's the reason for this wide exception catch here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added this earlier because some tests were failing and removing them was causing the coverage to drop but while checking it again i found the issue and fixed it so the tests are no longer failing

logger.exception(
"Unexpected certificate processing error for user %s",
pending_score.github_user.login,
)
failed_certificates.append((pending_score.github_user.login, e))
pending_scores.clear()
contribution_scores.clear()

Expand Down Expand Up @@ -383,7 +389,7 @@ def recalculate_user(self, user: User) -> dict[str, str | int | bool]:
},
)

Certificate.issue_certificate(user, total_score, tier)
Certificate.issue_certificate(user, total_score, TierChoices(tier))

logger.info(
"Recalculated score for %s: %s points (%s)",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Tests for Certificate GraphQL node."""

from unittest.mock import Mock

from apps.owasp.api.internal.nodes.certificate import CertificateNode
from tests.unit.apps.common.graphql_node_base_test import GraphQLNodeBaseTest


class TestCertificateNode(GraphQLNodeBaseTest):
"""Test cases for CertificateNode class."""

def test_node_fields(self):
"""Test node has expected fields."""
field_names = {field.name for field in CertificateNode.__strawberry_definition__.fields}
expected_field_names = {
"github_user",
"id",
"is_verified",
"issued_at",
"score",
"tier",
}
assert field_names == expected_field_names

def test_tier_resolver(self):
"""Test tier resolver returns human-readable display name."""
mock_cert = Mock()
mock_cert.get_tier_display.return_value = "Level 1"

field = self._get_field_by_name("tier", CertificateNode)
result = field.base_resolver.wrapped_func(None, mock_cert)

assert result == "Level 1"
mock_cert.get_tier_display.assert_called_once()

def test_is_verified_resolver_active(self):
"""Test is_verified returns True when certificate is not revoked."""
mock_cert = Mock()
mock_cert.is_revoked = False

field = self._get_field_by_name("is_verified", CertificateNode)
result = field.base_resolver.wrapped_func(None, mock_cert)

assert result is True

def test_is_verified_resolver_revoked(self):
"""Test is_verified returns False when certificate is revoked."""
mock_cert = Mock()
mock_cert.is_revoked = True

field = self._get_field_by_name("is_verified", CertificateNode)
result = field.base_resolver.wrapped_func(None, mock_cert)

assert result is False

def test_github_user_resolver(self):
"""Test github_user resolver returns the related github_user instance."""
mock_user = Mock()
mock_cert = Mock()
mock_cert.github_user = mock_user

field = self._get_field_by_name("github_user", CertificateNode)
result = field.base_resolver.wrapped_func(None, mock_cert)

assert result == mock_user
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from unittest.mock import MagicMock, patch

import pytest
from django.core.exceptions import ValidationError

from apps.owasp.api.internal.queries.certificate import CertificateQuery
from apps.owasp.models.crp.certificate import Certificate


class TestCertificateQuery:
"""Test suite for CertificateQuery."""

def test_has_strawberry_definition(self):
"""Test that CertificateQuery has valid Strawberry field definitions."""
assert hasattr(CertificateQuery, "__strawberry_definition__")
field_names = [field.name for field in CertificateQuery.__strawberry_definition__.fields]
assert "certificate" in field_names
assert "my_certificate" in field_names
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

@patch("apps.owasp.models.crp.certificate.Certificate.objects.select_related")
def test_certificate_found(self, mock_select_related):
"""Test certificate resolution when certificate exists."""
mock_cert = MagicMock(spec=Certificate)
mock_select_related.return_value.get.return_value = mock_cert

result = CertificateQuery().certificate("CERT12345678")

mock_select_related.assert_called_once_with("github_user")
mock_select_related.return_value.get.assert_called_once_with(id="CERT12345678")
assert result == mock_cert

@pytest.mark.parametrize(
"exception",
[
Certificate.DoesNotExist(),
ValidationError("Invalid ID format"),
ValueError("Invalid value"),
],
)
@patch("apps.owasp.models.crp.certificate.Certificate.objects.select_related")
def test_certificate_not_found_or_invalid(self, mock_select_related, exception):
"""Test certificate resolution returns None when not found or on validation error."""
mock_select_related.return_value.get.side_effect = exception

result = CertificateQuery().certificate("INVALID_ID")

assert result is None

def test_my_certificate_user_without_github_user(self):
"""Test my_certificate returns None when user has no github_user."""
info = MagicMock()
info.context.request.user = MagicMock(spec=[]) # user has no github_user attr

result = CertificateQuery().my_certificate(info)

assert result is None

@patch("apps.owasp.models.crp.certificate.Certificate.objects.select_related")
def test_my_certificate_returns_latest_active_certificate(self, mock_select_related):
"""Test my_certificate returns the user's latest active certificate."""
info = MagicMock()
mock_github_user = MagicMock()
info.context.request.user.github_user = mock_github_user

mock_cert = MagicMock(spec=Certificate)
mock_qs = MagicMock()
mock_select_related.return_value = mock_qs
mock_qs.filter.return_value = mock_qs
mock_qs.order_by.return_value = mock_qs
mock_qs.first.return_value = mock_cert

result = CertificateQuery().my_certificate(info)

mock_select_related.assert_called_once_with("github_user")
mock_qs.filter.assert_called_once_with(github_user=mock_github_user, is_revoked=False)
mock_qs.order_by.assert_called_once_with("-issued_at")
assert result == mock_cert
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from io import StringIO
from unittest.mock import MagicMock, patch

import pytest
from django.core.management import call_command
from django.core.management.base import CommandError

COMMAND_PATH = "apps.owasp.management.commands.owasp_crp_recalculate_scores"


class TestOwaspCrpRecalculateScores:
"""Test suite for the owasp_crp_recalculate_scores management command."""

@patch(f"{COMMAND_PATH}.ContributionScoreCalculator")
def test_handle_success(self, mock_calculator_class):
"""Test successful score recalculation with no failures."""
mock_calculator = MagicMock()
mock_calculator.recalculate_all.return_value = {
"total": 10,
"created": 3,
"updated": 7,
"failed_count": 0,
}
mock_calculator_class.return_value = mock_calculator

out = StringIO()
call_command("owasp_crp_recalculate_scores", stdout=out)

mock_calculator_class.assert_called_once()
mock_calculator.recalculate_all.assert_called_once()

output = out.getvalue()
assert "Starting score recalculation for all users..." in output
assert "Score recalculation complete:" in output
assert "- Total users: 10" in output
Comment thread
anurag2787 marked this conversation as resolved.
assert "- Created: 3" in output
assert "- Updated: 7" in output
assert "- Failed: 0" in output

@patch(f"{COMMAND_PATH}.ContributionScoreCalculator")
def test_handle_with_failures(self, mock_calculator_class):
"""Test score recalculation when certificate issuance failures occur."""
mock_calculator = MagicMock()
mock_calculator.recalculate_all.return_value = {
"total": 5,
"created": 1,
"updated": 2,
"failed_count": 2,
"failures": [("alice", "Certificate error"), ("bob", "Network error")],
}
mock_calculator_class.return_value = mock_calculator

out = StringIO()
with pytest.raises(CommandError, match=r"Failed to issue certificates for 2 user\(s\)"):
call_command("owasp_crp_recalculate_scores", stdout=out)

output = out.getvalue()
assert "Failed to issue certificates for: alice, bob" in output

@patch(f"{COMMAND_PATH}.ContributionScoreCalculator")
def test_handle_with_failures_no_failures_list(self, mock_calculator_class):
"""Test score recalculation when failed_count > 0 but failures key is missing."""
mock_calculator = MagicMock()
mock_calculator.recalculate_all.return_value = {
"total": 2,
"created": 0,
"updated": 1,
"failed_count": 1,
}
mock_calculator_class.return_value = mock_calculator

out = StringIO()
with pytest.raises(CommandError, match=r"Failed to issue certificates for 1 user\(s\)"):
call_command("owasp_crp_recalculate_scores", stdout=out)

output = out.getvalue()
assert "Failed to issue certificates for: " in output
Empty file.
114 changes: 114 additions & 0 deletions backend/tests/unit/apps/owasp/models/crp/certificate_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from unittest.mock import MagicMock, patch
Comment thread
anurag2787 marked this conversation as resolved.

import pytest

from apps.github.models.user import User
from apps.owasp.exceptions import CertificateIssuanceError
from apps.owasp.models.crp.certificate import (
CERTIFICATE_ID_ALPHABET,
CERTIFICATE_ID_LENGTH,
Certificate,
generate_certificate_id,
)
from apps.owasp.models.crp.recognition_enums import TierChoices

MODEL_PATH = "apps.owasp.models.crp.certificate"


class TestCertificateModel:
"""Test suite for Certificate model."""

def test_generate_certificate_id(self):
"""Test generate_certificate_id produces a 12-char string from ALPHABET."""
cert_id = generate_certificate_id()
assert len(cert_id) == CERTIFICATE_ID_LENGTH
assert all(c in CERTIFICATE_ID_ALPHABET for c in cert_id)

def test_str_representation_active(self):
"""Test __str__ for active certificate."""
user = User(login="john_doe")
cert = Certificate(github_user=user, tier=TierChoices.LEVEL_1, is_revoked=False)

assert str(cert) == "john_doe - LEVEL_1 Certificate (Active)"

def test_str_representation_revoked(self):
"""Test __str__ for revoked certificate."""
user = User(login="jane_doe")
cert = Certificate(github_user=user, tier=TierChoices.LEVEL_2, is_revoked=True)

assert str(cert) == "jane_doe - LEVEL_2 Certificate (Revoked)"

@patch("django.db.transaction.Atomic.__enter__", return_value=None)
@patch("django.db.transaction.Atomic.__exit__", return_value=None)
@patch(f"{MODEL_PATH}.Certificate.objects")
@patch(f"{MODEL_PATH}.User.objects")
def test_issue_certificate_already_exists(
self, mock_user_objects, mock_cert_objects, mock_exit, mock_enter
Comment thread
coderabbitai[bot] marked this conversation as resolved.
):
"""Test issue_certificate returns early if active certificate already exists."""
user = User(id=1, login="john_doe")
mock_user_objects.select_for_update.return_value.get.return_value = user
mock_cert_objects.filter.return_value.exists.return_value = True

Certificate.issue_certificate(user, 150, TierChoices.LEVEL_2)

mock_cert_objects.filter.assert_called_once_with(
github_user=user, tier=TierChoices.LEVEL_2, is_revoked=False
)

@patch("django.db.transaction.Atomic.__enter__", return_value=None)
@patch("django.db.transaction.Atomic.__exit__", return_value=None)
@patch(f"{MODEL_PATH}.Certificate.objects")
@patch(f"{MODEL_PATH}.CertificateProviderFactory")
@patch(f"{MODEL_PATH}.User.objects")
def test_issue_certificate_provider_resolution_error(
self, mock_user_objects, mock_factory, mock_cert_objects, mock_exit, mock_enter
):
"""Test issue_certificate raises on provider resolution error."""
user = User(id=1, login="john_doe")
mock_user_objects.select_for_update.return_value.get.return_value = user
mock_cert_objects.filter.return_value.exists.return_value = False
mock_factory.get_provider.side_effect = ValueError("Unknown provider")

with pytest.raises(CertificateIssuanceError):
Certificate.issue_certificate(user, 150, TierChoices.LEVEL_2)

@patch("django.db.transaction.Atomic.__enter__", return_value=None)
@patch("django.db.transaction.Atomic.__exit__", return_value=None)
@patch(f"{MODEL_PATH}.Certificate.objects")
@patch(f"{MODEL_PATH}.CertificateProviderFactory")
@patch(f"{MODEL_PATH}.User.objects")
def test_issue_certificate_provider_issuance_exception(
self, mock_user_objects, mock_factory, mock_cert_objects, mock_exit, mock_enter
):
"""Test issue_certificate raises when provider issuance fails."""
user = User(id=1, login="test_user")
mock_user_objects.select_for_update.return_value.get.return_value = user
mock_cert_objects.filter.return_value.exists.return_value = False

mock_provider = MagicMock()
mock_provider.issue_certificate.side_effect = RuntimeError("PDF generation failed")
mock_factory.get_provider.return_value = mock_provider

with pytest.raises(CertificateIssuanceError):
Certificate.issue_certificate(user, 150, TierChoices.LEVEL_2)

@patch("django.db.transaction.Atomic.__enter__", return_value=None)
@patch("django.db.transaction.Atomic.__exit__", return_value=None)
@patch(f"{MODEL_PATH}.Certificate.objects")
@patch(f"{MODEL_PATH}.CertificateProviderFactory")
@patch(f"{MODEL_PATH}.User.objects")
def test_issue_certificate_success(
self, mock_user_objects, mock_factory, mock_cert_objects, mock_exit, mock_enter
):
"""Test successful certificate issuance."""
user = User(id=1, login="test_user")
mock_user_objects.select_for_update.return_value.get.return_value = user
mock_cert_objects.filter.return_value.exists.return_value = False

mock_provider = MagicMock()
mock_factory.get_provider.return_value = mock_provider

Certificate.issue_certificate(user, 150, TierChoices.LEVEL_2)

mock_provider.issue_certificate.assert_called_once_with(user, 150, TierChoices.LEVEL_2)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from apps.github.models.user import User
from apps.owasp.models.crp.contribution_score import ContributionScore
from apps.owasp.models.crp.recognition_enums import TierChoices


class TestContributionScoreModel:
"""Test suite for ContributionScore model."""

def test_str_representation(self):
"""Test __str__ for ContributionScore."""
user = User(login="alice_dev")
score = ContributionScore(github_user=user, tier=TierChoices.LEVEL_3, value=350)

assert str(score) == "alice_dev - LEVEL_3 (350 points)"
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from datetime import date

from apps.github.models.user import User
from apps.owasp.models.crp.leaderboard_snapshot import LeaderboardSnapshot


class TestLeaderboardSnapshotModel:
"""Test suite for LeaderboardSnapshot model."""

def test_str_representation(self):
"""Test __str__ for LeaderboardSnapshot."""
user = User(login="bob_coder")
snapshot = LeaderboardSnapshot(
github_user=user,
global_rank=5,
project_rank=2,
chapter_rank=1,
snapshot_date=date(2026, 8, 1),
)

assert (
str(snapshot)
== "bob_coder - Global Rank: 5, Project Rank: 2, Chapter Rank: 1 (2026-08-01)"
)
Loading
Loading