prune_captcha 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- captcha_prune/__init__.py +0 -0
- captcha_prune/asgi.py +16 -0
- captcha_prune/migrations/0001_initial.py +29 -0
- captcha_prune/migrations/0002_remove_captcha_created_at_remove_captcha_pos_x_and_more.py +73 -0
- captcha_prune/migrations/__init__.py +0 -0
- captcha_prune/models.py +32 -0
- captcha_prune/payloads.py +6 -0
- captcha_prune/settings.py +130 -0
- captcha_prune/urls.py +10 -0
- captcha_prune/views.py +45 -0
- captcha_prune/wsgi.py +16 -0
- commons/base_model.py +9 -0
- commons/decorators.py +41 -0
- prune_captcha-1.0.0.dist-info/METADATA +91 -0
- prune_captcha-1.0.0.dist-info/RECORD +21 -0
- prune_captcha-1.0.0.dist-info/WHEEL +5 -0
- prune_captcha-1.0.0.dist-info/entry_points.txt +2 -0
- prune_captcha-1.0.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/captcha_prune/__init__.py +0 -0
- tests/captcha_prune/test_views.py +67 -0
File without changes
|
captcha_prune/asgi.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
"""
|
2
|
+
ASGI config for captcha_prune project.
|
3
|
+
|
4
|
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
5
|
+
|
6
|
+
For more information on this file, see
|
7
|
+
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
8
|
+
"""
|
9
|
+
|
10
|
+
import os
|
11
|
+
|
12
|
+
from django.core.asgi import get_asgi_application
|
13
|
+
|
14
|
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'captcha_prune.settings')
|
15
|
+
|
16
|
+
application = get_asgi_application()
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Generated by Django 5.2 on 2025-04-28 10:19
|
2
|
+
|
3
|
+
import uuid
|
4
|
+
from django.db import migrations, models
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
initial = True
|
10
|
+
|
11
|
+
dependencies = [
|
12
|
+
]
|
13
|
+
|
14
|
+
operations = [
|
15
|
+
migrations.CreateModel(
|
16
|
+
name='Captcha',
|
17
|
+
fields=[
|
18
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
19
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
20
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
21
|
+
('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
|
22
|
+
('pos_x', models.IntegerField()),
|
23
|
+
('pos_y', models.IntegerField()),
|
24
|
+
],
|
25
|
+
options={
|
26
|
+
'abstract': False,
|
27
|
+
},
|
28
|
+
),
|
29
|
+
]
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# Generated by Django 5.2 on 2025-04-29 07:55
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
dependencies = [
|
8
|
+
("captcha_prune", "0001_initial"),
|
9
|
+
]
|
10
|
+
|
11
|
+
operations = [
|
12
|
+
migrations.RemoveField(
|
13
|
+
model_name="captcha",
|
14
|
+
name="created_at",
|
15
|
+
),
|
16
|
+
migrations.RemoveField(
|
17
|
+
model_name="captcha",
|
18
|
+
name="pos_x",
|
19
|
+
),
|
20
|
+
migrations.RemoveField(
|
21
|
+
model_name="captcha",
|
22
|
+
name="pos_y",
|
23
|
+
),
|
24
|
+
migrations.RemoveField(
|
25
|
+
model_name="captcha",
|
26
|
+
name="updated_at",
|
27
|
+
),
|
28
|
+
migrations.AddField(
|
29
|
+
model_name="captcha",
|
30
|
+
name="height",
|
31
|
+
field=models.IntegerField(default=200),
|
32
|
+
),
|
33
|
+
migrations.AddField(
|
34
|
+
model_name="captcha",
|
35
|
+
name="piece_height",
|
36
|
+
field=models.IntegerField(default=50),
|
37
|
+
),
|
38
|
+
migrations.AddField(
|
39
|
+
model_name="captcha",
|
40
|
+
name="piece_pos_x",
|
41
|
+
field=models.IntegerField(blank=True, null=True),
|
42
|
+
),
|
43
|
+
migrations.AddField(
|
44
|
+
model_name="captcha",
|
45
|
+
name="piece_pos_y",
|
46
|
+
field=models.IntegerField(blank=True, null=True),
|
47
|
+
),
|
48
|
+
migrations.AddField(
|
49
|
+
model_name="captcha",
|
50
|
+
name="piece_width",
|
51
|
+
field=models.IntegerField(default=80),
|
52
|
+
),
|
53
|
+
migrations.AddField(
|
54
|
+
model_name="captcha",
|
55
|
+
name="pos_x_solution",
|
56
|
+
field=models.IntegerField(blank=True, null=True),
|
57
|
+
),
|
58
|
+
migrations.AddField(
|
59
|
+
model_name="captcha",
|
60
|
+
name="pos_y_solution",
|
61
|
+
field=models.IntegerField(blank=True, null=True),
|
62
|
+
),
|
63
|
+
migrations.AddField(
|
64
|
+
model_name="captcha",
|
65
|
+
name="precision",
|
66
|
+
field=models.IntegerField(default=2),
|
67
|
+
),
|
68
|
+
migrations.AddField(
|
69
|
+
model_name="captcha",
|
70
|
+
name="width",
|
71
|
+
field=models.IntegerField(default=350),
|
72
|
+
),
|
73
|
+
]
|
File without changes
|
captcha_prune/models.py
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
import random
|
2
|
+
import uuid
|
3
|
+
|
4
|
+
from django.db import models
|
5
|
+
|
6
|
+
|
7
|
+
class Captcha(models.Model):
|
8
|
+
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
9
|
+
width = models.IntegerField(default=350)
|
10
|
+
height = models.IntegerField(default=200)
|
11
|
+
pos_x_solution = models.IntegerField(null=True, blank=True)
|
12
|
+
pos_y_solution = models.IntegerField(null=True, blank=True)
|
13
|
+
piece_pos_x = models.IntegerField(null=True, blank=True)
|
14
|
+
piece_pos_y = models.IntegerField(null=True, blank=True)
|
15
|
+
piece_width = models.IntegerField(default=80)
|
16
|
+
piece_height = models.IntegerField(default=50)
|
17
|
+
precision = models.IntegerField(default=2)
|
18
|
+
|
19
|
+
def save(self, *args, **kwargs):
|
20
|
+
if self.pos_x_solution is None:
|
21
|
+
self.pos_x_solution = random.randint(0, self.width - self.piece_width)
|
22
|
+
|
23
|
+
if self.pos_y_solution is None:
|
24
|
+
self.pos_y_solution = random.randint(0, self.height - self.piece_height)
|
25
|
+
|
26
|
+
if self.piece_pos_x is None:
|
27
|
+
self.piece_pos_x = random.randint(0, self.width - self.piece_width)
|
28
|
+
|
29
|
+
if self.piece_pos_y is None:
|
30
|
+
self.piece_pos_y = random.randint(0, self.height - self.piece_height)
|
31
|
+
|
32
|
+
super().save(*args, **kwargs)
|
@@ -0,0 +1,130 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
4
|
+
|
5
|
+
|
6
|
+
class EnvSettings(BaseSettings):
|
7
|
+
model_config = SettingsConfigDict(env_file=".env", extra="allow")
|
8
|
+
|
9
|
+
postgres_db: str
|
10
|
+
postgres_user: str
|
11
|
+
postgres_password: str
|
12
|
+
postgres_host: str
|
13
|
+
postgres_port: str
|
14
|
+
|
15
|
+
|
16
|
+
env_settings = EnvSettings()
|
17
|
+
|
18
|
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
19
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
20
|
+
|
21
|
+
|
22
|
+
# Quick-start development settings - unsuitable for production
|
23
|
+
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
24
|
+
|
25
|
+
# SECURITY WARNING: keep the secret key used in production secret!
|
26
|
+
SECRET_KEY = "django-insecure-dj-w%-kno*r5w=e_&l1sl3)9kl%m25(3ikoc=z_%p5-0d5472%"
|
27
|
+
|
28
|
+
# SECURITY WARNING: don't run with debug turned on in production!
|
29
|
+
DEBUG = True
|
30
|
+
|
31
|
+
ALLOWED_HOSTS = []
|
32
|
+
|
33
|
+
|
34
|
+
# Application definition
|
35
|
+
|
36
|
+
INSTALLED_APPS = [
|
37
|
+
"django.contrib.admin",
|
38
|
+
"django.contrib.auth",
|
39
|
+
"django.contrib.contenttypes",
|
40
|
+
"django.contrib.sessions",
|
41
|
+
"django.contrib.messages",
|
42
|
+
"django.contrib.staticfiles",
|
43
|
+
"captcha_prune",
|
44
|
+
]
|
45
|
+
|
46
|
+
MIDDLEWARE = [
|
47
|
+
"django.middleware.security.SecurityMiddleware",
|
48
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
49
|
+
"django.middleware.common.CommonMiddleware",
|
50
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
51
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
52
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
53
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
54
|
+
]
|
55
|
+
|
56
|
+
ROOT_URLCONF = "captcha_prune.urls"
|
57
|
+
|
58
|
+
TEMPLATES = [
|
59
|
+
{
|
60
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
61
|
+
"DIRS": [],
|
62
|
+
"APP_DIRS": True,
|
63
|
+
"OPTIONS": {
|
64
|
+
"context_processors": [
|
65
|
+
"django.template.context_processors.request",
|
66
|
+
"django.contrib.auth.context_processors.auth",
|
67
|
+
"django.contrib.messages.context_processors.messages",
|
68
|
+
],
|
69
|
+
},
|
70
|
+
},
|
71
|
+
]
|
72
|
+
|
73
|
+
WSGI_APPLICATION = "captcha_prune.wsgi.application"
|
74
|
+
|
75
|
+
|
76
|
+
# Database
|
77
|
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
78
|
+
|
79
|
+
DATABASES = {
|
80
|
+
"default": {
|
81
|
+
"ENGINE": "django.db.backends.postgresql",
|
82
|
+
"NAME": env_settings.postgres_db,
|
83
|
+
"USER": env_settings.postgres_user,
|
84
|
+
"PASSWORD": env_settings.postgres_password,
|
85
|
+
"HOST": env_settings.postgres_host,
|
86
|
+
"PORT": env_settings.postgres_port,
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
|
91
|
+
# Password validation
|
92
|
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
93
|
+
|
94
|
+
AUTH_PASSWORD_VALIDATORS = [
|
95
|
+
{
|
96
|
+
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
97
|
+
},
|
98
|
+
{
|
99
|
+
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
100
|
+
},
|
101
|
+
{
|
102
|
+
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
103
|
+
},
|
104
|
+
{
|
105
|
+
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
106
|
+
},
|
107
|
+
]
|
108
|
+
|
109
|
+
|
110
|
+
# Internationalization
|
111
|
+
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
112
|
+
|
113
|
+
LANGUAGE_CODE = "en-us"
|
114
|
+
|
115
|
+
TIME_ZONE = "UTC"
|
116
|
+
|
117
|
+
USE_I18N = True
|
118
|
+
|
119
|
+
USE_TZ = True
|
120
|
+
|
121
|
+
|
122
|
+
# Static files (CSS, JavaScript, Images)
|
123
|
+
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
124
|
+
|
125
|
+
STATIC_URL = "static/"
|
126
|
+
|
127
|
+
# Default primary key field type
|
128
|
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
129
|
+
|
130
|
+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
captcha_prune/urls.py
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
from django.urls import path
|
2
|
+
|
3
|
+
from captcha_prune.views import create_captcha_view, verify_captcha_view
|
4
|
+
|
5
|
+
app_name = "captcha"
|
6
|
+
|
7
|
+
urlpatterns = [
|
8
|
+
path("", create_captcha_view, name="create-captcha"),
|
9
|
+
path("<str:uuid>/", verify_captcha_view, name="verify-captcha"),
|
10
|
+
]
|
captcha_prune/views.py
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
from django.http import HttpRequest, HttpResponse, JsonResponse
|
2
|
+
from django.shortcuts import get_object_or_404
|
3
|
+
from django.views.decorators.csrf import csrf_exempt
|
4
|
+
from django.views.decorators.http import require_GET, require_POST
|
5
|
+
|
6
|
+
from captcha_prune.models import Captcha
|
7
|
+
from captcha_prune.payloads import PuzzleAnswerPayload
|
8
|
+
from commons.decorators import use_payload
|
9
|
+
|
10
|
+
|
11
|
+
@require_POST
|
12
|
+
@csrf_exempt
|
13
|
+
def create_captcha_view(request: HttpRequest) -> HttpResponse:
|
14
|
+
captcha = Captcha.objects.create()
|
15
|
+
return JsonResponse(
|
16
|
+
status=201,
|
17
|
+
data={
|
18
|
+
"uuid": str(captcha.uuid),
|
19
|
+
"width": captcha.width,
|
20
|
+
"height": captcha.height,
|
21
|
+
"piece_width": captcha.piece_width,
|
22
|
+
"piece_height": captcha.piece_height,
|
23
|
+
"pos_x_solution": captcha.pos_x_solution,
|
24
|
+
"pos_y_solution": captcha.pos_y_solution,
|
25
|
+
"piece_pos_x": captcha.piece_pos_x,
|
26
|
+
"piece_pos_y": captcha.piece_pos_y,
|
27
|
+
},
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
@require_GET
|
32
|
+
@use_payload(PuzzleAnswerPayload)
|
33
|
+
def verify_captcha_view(
|
34
|
+
request: HttpRequest, uuid: str, payload: PuzzleAnswerPayload
|
35
|
+
) -> HttpResponse:
|
36
|
+
captcha = get_object_or_404(Captcha, uuid=uuid)
|
37
|
+
pos_x_answer = payload.pos_x_answer
|
38
|
+
pos_y_answer = payload.pos_y_answer
|
39
|
+
if (
|
40
|
+
abs(captcha.pos_x_solution - pos_x_answer) <= captcha.precision
|
41
|
+
and abs(captcha.pos_y_solution - pos_y_answer) <= captcha.precision
|
42
|
+
):
|
43
|
+
return HttpResponse(status=304)
|
44
|
+
else:
|
45
|
+
return HttpResponse(status=401)
|
captcha_prune/wsgi.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
"""
|
2
|
+
WSGI config for captcha_prune project.
|
3
|
+
|
4
|
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
5
|
+
|
6
|
+
For more information on this file, see
|
7
|
+
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
8
|
+
"""
|
9
|
+
|
10
|
+
import os
|
11
|
+
|
12
|
+
from django.core.wsgi import get_wsgi_application
|
13
|
+
|
14
|
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'captcha_prune.settings')
|
15
|
+
|
16
|
+
application = get_wsgi_application()
|
commons/base_model.py
ADDED
commons/decorators.py
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
import functools
|
2
|
+
from typing import get_origin
|
3
|
+
|
4
|
+
from django.http import HttpResponse
|
5
|
+
from pydantic import ValidationError
|
6
|
+
|
7
|
+
|
8
|
+
def use_payload(payload_class):
|
9
|
+
def decorator(func):
|
10
|
+
@functools.wraps(func)
|
11
|
+
def wrapper(request, *args, **kwargs):
|
12
|
+
data = {}
|
13
|
+
|
14
|
+
if request.method == "POST":
|
15
|
+
body = request.POST
|
16
|
+
elif request.method == "GET":
|
17
|
+
body = request.GET
|
18
|
+
else:
|
19
|
+
raise NotImplementedError
|
20
|
+
|
21
|
+
for field_name, field in payload_class.model_fields.items():
|
22
|
+
url_name = field.alias if field.alias else field_name
|
23
|
+
is_list_field = get_origin(field.annotation) is list
|
24
|
+
url_value = (
|
25
|
+
body.getlist(url_name) if is_list_field else body.get(url_name)
|
26
|
+
)
|
27
|
+
if url_value is not None:
|
28
|
+
data[url_name] = url_value
|
29
|
+
|
30
|
+
try:
|
31
|
+
payload = payload_class.model_validate(data)
|
32
|
+
except ValidationError as e:
|
33
|
+
return HttpResponse(
|
34
|
+
e.json(), headers={"content_type": "application/json"}, status=400
|
35
|
+
)
|
36
|
+
|
37
|
+
return func(request, *args, payload=payload, **kwargs)
|
38
|
+
|
39
|
+
return wrapper
|
40
|
+
|
41
|
+
return decorator
|
@@ -0,0 +1,91 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: prune_captcha
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: A tool to protect formulaire from spam.
|
5
|
+
Author-email: Arnout <bastien@prune.sh>
|
6
|
+
Project-URL: Made_by, https://prune.sh/
|
7
|
+
Keywords: captcha,django,code-quality
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: <4.0,>=3.12
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
Requires-Dist: django>=5.2
|
14
|
+
Requires-Dist: psycopg2-binary>=2.9.10
|
15
|
+
Requires-Dist: pydantic>=2.11.4
|
16
|
+
Requires-Dist: pydantic-settings>=2.9.1
|
17
|
+
|
18
|
+
# Prune's Captcha
|
19
|
+
|
20
|
+
## What is it for?
|
21
|
+
|
22
|
+
Captcha helps prevent robots from spamming using your forms.
|
23
|
+
|
24
|
+
## Prerequisites
|
25
|
+
|
26
|
+
- To be installed on a Prune Django project that uses poetry or UV
|
27
|
+
|
28
|
+
## UV project
|
29
|
+
|
30
|
+
### Installation
|
31
|
+
|
32
|
+
Run the following command in the console:
|
33
|
+
|
34
|
+
```bash
|
35
|
+
uv add captcha_prune
|
36
|
+
```
|
37
|
+
|
38
|
+
### Running the captcha
|
39
|
+
|
40
|
+
To run the package, simply enter in the console:
|
41
|
+
|
42
|
+
```bash
|
43
|
+
captcha_prune
|
44
|
+
```
|
45
|
+
|
46
|
+
### Updating the captcha
|
47
|
+
|
48
|
+
Don't hesitate to regularly run `uv sync --upgrade`, as the captcha evolves with time and our practices!
|
49
|
+
|
50
|
+
## Poetry project
|
51
|
+
|
52
|
+
### Installation
|
53
|
+
|
54
|
+
Run the following command:
|
55
|
+
|
56
|
+
```bash
|
57
|
+
poetry add prune_captcha
|
58
|
+
```
|
59
|
+
|
60
|
+
### Running the captcha
|
61
|
+
|
62
|
+
```bash
|
63
|
+
poetry run prune_captcha
|
64
|
+
```
|
65
|
+
|
66
|
+
### Updating the captcha
|
67
|
+
|
68
|
+
Don't hesitate to regularly run `poetry update`, as the captcha evolves with time and our practices!
|
69
|
+
|
70
|
+
## Captcha Integration
|
71
|
+
|
72
|
+
Once the project is launched, the application's URLs need to be linked to the form.
|
73
|
+
|
74
|
+
- When making a **GET** request to the form page, the API must be called via the **creation endpoint**. The response will contain the necessary information to display the captcha.
|
75
|
+
- When submitting the form via **POST**, the API must be called via the **verification endpoint**. The response will indicate whether the captcha is valid or not.
|
76
|
+
|
77
|
+
## Captcha Display
|
78
|
+
|
79
|
+
To display the captcha correctly, use the data received in the response from the creation request. This data includes:
|
80
|
+
|
81
|
+
- the captcha's width and height,
|
82
|
+
- the piece's width and height,
|
83
|
+
- the current position of the piece,
|
84
|
+
- the target position (where the piece should be placed),
|
85
|
+
- the expected precision (captcha difficulty level).
|
86
|
+
|
87
|
+
# Available Versions
|
88
|
+
|
89
|
+
| Version | Date | Notes |
|
90
|
+
| ------- | ---------- | -------------------------------------- |
|
91
|
+
| 1.0.0 | 2025-03-14 | First version of the `captcha` package |
|
@@ -0,0 +1,21 @@
|
|
1
|
+
captcha_prune/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
captcha_prune/asgi.py,sha256=wi2rRLFltXOZyve8mAB_E8udaFyONOf5N42WrWIQX8M,403
|
3
|
+
captcha_prune/models.py,sha256=3IwEWBl5ykZEivE5FGU0jUZKZGVkhvCuMh1HFehjYFU,1201
|
4
|
+
captcha_prune/payloads.py,sha256=PwR48xKg9YEgGgoJmGe7ywwFPCqM4gumomyFunn-Ems,115
|
5
|
+
captcha_prune/settings.py,sha256=YEZHEjYYYK-cH3gSLIR3a7g4Bypnuf_5eMTTcm719CE,3395
|
6
|
+
captcha_prune/urls.py,sha256=DeCJTjJA0krW27KoHafvh3GBRr7fJ80E2lVHEWYSsW4,271
|
7
|
+
captcha_prune/views.py,sha256=BzjQfXLwYx2YSWODrnGpokqU8XeC-9QORivzWKxYofY,1539
|
8
|
+
captcha_prune/wsgi.py,sha256=fukA_iiCT4FFzcJ0QX2Zr_HNddqdq-ln3ien2UWcCTA,403
|
9
|
+
captcha_prune/migrations/0001_initial.py,sha256=QMTaeSIxPociSrV4r89zfsbxMwMu4qpL16LT3yPgcaw,831
|
10
|
+
captcha_prune/migrations/0002_remove_captcha_created_at_remove_captcha_pos_x_and_more.py,sha256=xyyE_G3t8kJqFvKuNWFRTyTyyD1kzvfeSgbkTQ9DO8A,2099
|
11
|
+
captcha_prune/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
+
commons/base_model.py,sha256=q1Q7lgtvN_t6ujhEiSsNNXb_ay0qY6FbfK0Zm2Hdgp0,213
|
13
|
+
commons/decorators.py,sha256=nU_3SyxENcWDdxo03VyNYEvoevplC6ig7BFogY8apNs,1306
|
14
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
tests/captcha_prune/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
tests/captcha_prune/test_views.py,sha256=h-Mrdiprh71s7kIJPYAt6xv4G_S7vp51WCpAzy4CZ6U,2364
|
17
|
+
prune_captcha-1.0.0.dist-info/METADATA,sha256=bUB-rBL654MCCPo0kmTGlwmCHtfkA-DDr56hPjpkALI,2447
|
18
|
+
prune_captcha-1.0.0.dist-info/WHEEL,sha256=ooBFpIzZCPdw3uqIQsOo4qqbA4ZRPxHnOH7peeONza0,91
|
19
|
+
prune_captcha-1.0.0.dist-info/entry_points.txt,sha256=_7zxh1tub_wWQu9oo5vcYIJAEQMPmTj0ekh0oXJ_8FM,52
|
20
|
+
prune_captcha-1.0.0.dist-info/top_level.txt,sha256=EB4h0WF_YGF7kAWB0XtqmuloOhkL5pC71CzgEVYam7w,28
|
21
|
+
prune_captcha-1.0.0.dist-info/RECORD,,
|
tests/__init__.py
ADDED
File without changes
|
File without changes
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import uuid
|
2
|
+
|
3
|
+
from django.test import TestCase
|
4
|
+
from django.urls import reverse
|
5
|
+
|
6
|
+
from captcha_prune.models import Captcha
|
7
|
+
|
8
|
+
|
9
|
+
class CreateCaptchaViewTestCase(TestCase):
|
10
|
+
def test_create_captcha_with_GET_method(self):
|
11
|
+
response = self.client.get(reverse("create-captcha"))
|
12
|
+
self.assertEqual(response.status_code, 405)
|
13
|
+
|
14
|
+
def test_create_captcha_with_POST_method(self):
|
15
|
+
response = self.client.post(reverse("create-captcha"))
|
16
|
+
self.assertEqual(response.status_code, 201)
|
17
|
+
|
18
|
+
data = response.json()
|
19
|
+
captcha = Captcha.objects.get(uuid=data["uuid"])
|
20
|
+
self.assertIsNotNone(captcha)
|
21
|
+
|
22
|
+
|
23
|
+
class VerifyCaptchaViewTestCase(TestCase):
|
24
|
+
def setUp(self):
|
25
|
+
self.captcha = Captcha.objects.create()
|
26
|
+
self.pos_x_solution = self.captcha.pos_x_solution
|
27
|
+
self.pos_y_solution = self.captcha.pos_y_solution
|
28
|
+
|
29
|
+
def test_verify_captcha_with_POST_method(self):
|
30
|
+
response = self.client.post(
|
31
|
+
reverse("verify-captcha", kwargs={"uuid": str(uuid.uuid4())}),
|
32
|
+
data={
|
33
|
+
"pos_x_answer": self.pos_x_solution,
|
34
|
+
"pos_y_answer": self.pos_y_solution,
|
35
|
+
},
|
36
|
+
)
|
37
|
+
self.assertEqual(response.status_code, 405)
|
38
|
+
|
39
|
+
def test_verify_captcha_with_bad_uuid(self):
|
40
|
+
response = self.client.get(
|
41
|
+
reverse("verify-captcha", kwargs={"uuid": str(uuid.uuid4())}),
|
42
|
+
data={
|
43
|
+
"pos_x_answer": self.pos_x_solution,
|
44
|
+
"pos_y_answer": self.pos_y_solution,
|
45
|
+
},
|
46
|
+
)
|
47
|
+
self.assertEqual(response.status_code, 404)
|
48
|
+
|
49
|
+
def test_verify_captcha_with_good_uuid_but_bad_answer(self):
|
50
|
+
response = self.client.get(
|
51
|
+
reverse("verify-captcha", kwargs={"uuid": str(self.captcha.uuid)}),
|
52
|
+
data={
|
53
|
+
"pos_x_answer": self.pos_x_solution - 20,
|
54
|
+
"pos_y_answer": self.pos_y_solution - 20,
|
55
|
+
},
|
56
|
+
)
|
57
|
+
self.assertEqual(response.status_code, 401)
|
58
|
+
|
59
|
+
def test_verify_captcha_with_good_uuid_and_good_answer(self):
|
60
|
+
response = self.client.get(
|
61
|
+
reverse("verify-captcha", kwargs={"uuid": str(self.captcha.uuid)}),
|
62
|
+
data={
|
63
|
+
"pos_x_answer": self.pos_x_solution,
|
64
|
+
"pos_y_answer": self.pos_y_solution,
|
65
|
+
},
|
66
|
+
)
|
67
|
+
self.assertEqual(response.status_code, 304)
|