Skip to content

Commit 0b3b5d6

Browse files
authored
Add unit tests (#3)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 2d00c7f commit 0b3b5d6

10 files changed

Lines changed: 148 additions & 3 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Run unit tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-24.04
12+
13+
strategy:
14+
max-parallel: 4
15+
matrix:
16+
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Python ${{ matrix.python-version }}
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: ${{ matrix.python-version }}
26+
27+
- name: Install dependencies
28+
run: make dev
29+
30+
- name: Validate code format
31+
run: make check
32+
33+
- name: Run tests
34+
run: make test

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Changelog
22
=========
33

4+
v0.1.1 (2025-03-31)
5+
-------------------
6+
7+
- Add unit tests.
8+
49
v0.1.0 (2025-03-31)
510
-------------------
611

Makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
PYTHON_EXE?=python3
1010
VENV_LOCATION=.venv
1111
ACTIVATE?=. ${VENV_LOCATION}/bin/activate;
12-
MANAGE=${VENV_LOCATION}/bin/python manage.py
1312
DOCS_LOCATION=./docs
1413

1514
virtualenv:
@@ -39,7 +38,7 @@ clean:
3938

4039
test:
4140
@echo "-> Run the test suite"
42-
${MANAGE} test --noinput --parallel auto
41+
@${ACTIVATE} pytest
4342

4443
dist:
4544
@echo "-> Build source and wheel distributions"

django_altcha/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class AltchaWidget(HiddenInput):
3838

3939
def __init__(self, options, *args, **kwargs):
4040
"""Initialize the ALTCHA widget with provided options from the field."""
41-
self.options = options
41+
self.options = options or {}
4242
super().__init__(*args, **kwargs)
4343

4444
def get_context(self, name, value, attrs):

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ keywords = ["captcha", "django", "widget", "form", "altcha"]
3737
dev = [
3838
"build",
3939
"ruff",
40+
"pytest-django",
4041
]
4142
docs = [
4243
"Sphinx>=5.0.2",

pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
DJANGO_SETTINGS_MODULE = tests.settings
3+
pythonpath = .

tests/__init__.py

Whitespace-only changes.

tests/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
INSTALLED_APPS = [
2+
"django_altcha",
3+
]
4+
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3"}}

tests/test_field.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# SPDX-License-Identifier: MIT
4+
# See https://github.com/aboutcode-org/django-altcha for support or download.
5+
# See https://aboutcode.org for more information about AboutCode FOSS projects.
6+
#
7+
8+
from unittest import mock
9+
10+
from django import forms
11+
from django.test import TestCase
12+
13+
from django_altcha import AltchaField, AltchaWidget
14+
15+
16+
class AltchaFieldTest(TestCase):
17+
def setUp(self):
18+
class TestForm(forms.Form):
19+
altcha_field = AltchaField()
20+
21+
self.form_class = TestForm
22+
23+
def test_field_renders_widget(self):
24+
form = self.form_class()
25+
self.assertIsInstance(form.fields["altcha_field"].widget, AltchaWidget)
26+
27+
def test_field_with_missing_value_raises_required_error(self):
28+
form = self.form_class(data={})
29+
self.assertFalse(form.is_valid())
30+
self.assertIn("altcha_field", form.errors)
31+
self.assertEqual(
32+
form.errors["altcha_field"][0], "ALTCHA CAPTCHA token is missing."
33+
)
34+
35+
@mock.patch("altcha.verify_solution")
36+
def test_field_validation_calls_altcha_verify_solution(self, mock_verify_solution):
37+
mock_verify_solution.return_value = (True, None)
38+
form = self.form_class(data={"altcha_field": "valid_token"})
39+
self.assertTrue(form.is_valid())
40+
mock_verify_solution.assert_called_once_with(
41+
payload="valid_token",
42+
hmac_key=mock.ANY,
43+
check_expires=False,
44+
)
45+
46+
@mock.patch("altcha.verify_solution")
47+
def test_field_validation_fails_with_invalid_token(self, mock_verify_solution):
48+
mock_verify_solution.return_value = (False, "Invalid token")
49+
form = self.form_class(data={"altcha_field": "invalid_token"})
50+
self.assertFalse(form.is_valid())
51+
self.assertIn("altcha_field", form.errors)
52+
self.assertEqual(form.errors["altcha_field"][0], "Invalid CAPTCHA token.")
53+
54+
@mock.patch("altcha.verify_solution")
55+
def test_field_validation_handles_exception(self, mock_verify_solution):
56+
mock_verify_solution.side_effect = Exception("Verification failed")
57+
form = self.form_class(data={"altcha_field": "some_token"})
58+
self.assertFalse(form.is_valid())
59+
self.assertIn("altcha_field", form.errors)
60+
self.assertEqual(
61+
form.errors["altcha_field"][0], "Failed to process CAPTCHA token"
62+
)

tests/test_widget.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# SPDX-License-Identifier: MIT
4+
# See https://github.com/aboutcode-org/django-altcha for support or download.
5+
# See https://aboutcode.org for more information about AboutCode FOSS projects.
6+
#
7+
8+
import json
9+
10+
from django.test import TestCase
11+
12+
from django_altcha import AltchaWidget
13+
14+
15+
class AltchaWidgetTest(TestCase):
16+
def test_widget_initialization_with_default_options(self):
17+
widget = AltchaWidget(options=None)
18+
self.assertNotIn("challengeurl", widget.options)
19+
self.assertNotIn("challengejson", widget.options)
20+
self.assertNotIn("auto", widget.options)
21+
22+
def test_widget_initialization_with_custom_options(self):
23+
options = {
24+
"auto": "onload",
25+
"delay": 500,
26+
}
27+
widget = AltchaWidget(options)
28+
self.assertEqual(widget.options["auto"], "onload")
29+
self.assertEqual(widget.options["delay"], 500)
30+
31+
def test_widget_generates_challengejson_if_no_challengeurl(self):
32+
widget = AltchaWidget(options={}) # Pass an empty dictionary
33+
context = widget.get_context(name="test", value=None, attrs={})
34+
35+
challengejson = json.loads(context["widget"]["altcha_options"]["challengejson"])
36+
self.assertEqual("SHA-256", challengejson["algorithm"])
37+
self.assertEqual(64, len(challengejson["challenge"]))

0 commit comments

Comments
 (0)