django-bolt 0.1.0__cp310-abi3-win_amd64.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.
Potentially problematic release.
This version of django-bolt might be problematic. Click here for more details.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.pyd +0 -0
- django_bolt/admin/__init__.py +25 -0
- django_bolt/admin/admin_detection.py +179 -0
- django_bolt/admin/asgi_bridge.py +267 -0
- django_bolt/admin/routes.py +91 -0
- django_bolt/admin/static.py +155 -0
- django_bolt/admin/static_routes.py +111 -0
- django_bolt/api.py +1011 -0
- django_bolt/apps.py +7 -0
- django_bolt/async_collector.py +228 -0
- django_bolt/auth/README.md +464 -0
- django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
- django_bolt/auth/__init__.py +84 -0
- django_bolt/auth/backends.py +236 -0
- django_bolt/auth/guards.py +224 -0
- django_bolt/auth/jwt_utils.py +212 -0
- django_bolt/auth/revocation.py +286 -0
- django_bolt/auth/token.py +335 -0
- django_bolt/binding.py +363 -0
- django_bolt/bootstrap.py +77 -0
- django_bolt/cli.py +133 -0
- django_bolt/compression.py +104 -0
- django_bolt/decorators.py +159 -0
- django_bolt/dependencies.py +128 -0
- django_bolt/error_handlers.py +305 -0
- django_bolt/exceptions.py +294 -0
- django_bolt/health.py +129 -0
- django_bolt/logging/__init__.py +6 -0
- django_bolt/logging/config.py +357 -0
- django_bolt/logging/middleware.py +296 -0
- django_bolt/management/__init__.py +1 -0
- django_bolt/management/commands/__init__.py +0 -0
- django_bolt/management/commands/runbolt.py +427 -0
- django_bolt/middleware/__init__.py +32 -0
- django_bolt/middleware/compiler.py +131 -0
- django_bolt/middleware/middleware.py +247 -0
- django_bolt/openapi/__init__.py +23 -0
- django_bolt/openapi/config.py +196 -0
- django_bolt/openapi/plugins.py +439 -0
- django_bolt/openapi/routes.py +152 -0
- django_bolt/openapi/schema_generator.py +581 -0
- django_bolt/openapi/spec/__init__.py +68 -0
- django_bolt/openapi/spec/base.py +74 -0
- django_bolt/openapi/spec/callback.py +24 -0
- django_bolt/openapi/spec/components.py +72 -0
- django_bolt/openapi/spec/contact.py +21 -0
- django_bolt/openapi/spec/discriminator.py +25 -0
- django_bolt/openapi/spec/encoding.py +67 -0
- django_bolt/openapi/spec/enums.py +41 -0
- django_bolt/openapi/spec/example.py +36 -0
- django_bolt/openapi/spec/external_documentation.py +21 -0
- django_bolt/openapi/spec/header.py +132 -0
- django_bolt/openapi/spec/info.py +50 -0
- django_bolt/openapi/spec/license.py +28 -0
- django_bolt/openapi/spec/link.py +66 -0
- django_bolt/openapi/spec/media_type.py +51 -0
- django_bolt/openapi/spec/oauth_flow.py +36 -0
- django_bolt/openapi/spec/oauth_flows.py +28 -0
- django_bolt/openapi/spec/open_api.py +87 -0
- django_bolt/openapi/spec/operation.py +105 -0
- django_bolt/openapi/spec/parameter.py +147 -0
- django_bolt/openapi/spec/path_item.py +78 -0
- django_bolt/openapi/spec/paths.py +27 -0
- django_bolt/openapi/spec/reference.py +38 -0
- django_bolt/openapi/spec/request_body.py +38 -0
- django_bolt/openapi/spec/response.py +48 -0
- django_bolt/openapi/spec/responses.py +44 -0
- django_bolt/openapi/spec/schema.py +678 -0
- django_bolt/openapi/spec/security_requirement.py +28 -0
- django_bolt/openapi/spec/security_scheme.py +69 -0
- django_bolt/openapi/spec/server.py +34 -0
- django_bolt/openapi/spec/server_variable.py +32 -0
- django_bolt/openapi/spec/tag.py +32 -0
- django_bolt/openapi/spec/xml.py +44 -0
- django_bolt/pagination.py +669 -0
- django_bolt/param_functions.py +49 -0
- django_bolt/params.py +337 -0
- django_bolt/request_parsing.py +128 -0
- django_bolt/responses.py +214 -0
- django_bolt/router.py +48 -0
- django_bolt/serialization.py +193 -0
- django_bolt/status_codes.py +321 -0
- django_bolt/testing/__init__.py +10 -0
- django_bolt/testing/client.py +274 -0
- django_bolt/testing/helpers.py +93 -0
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +1 -0
- django_bolt/tests/admin_tests/conftest.py +6 -0
- django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
- django_bolt/tests/admin_tests/urls.py +9 -0
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +570 -0
- django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
- django_bolt/tests/cbv/test_class_views_features.py +1173 -0
- django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
- django_bolt/tests/conftest.py +165 -0
- django_bolt/tests/test_action_decorator.py +399 -0
- django_bolt/tests/test_auth_secret_key.py +83 -0
- django_bolt/tests/test_decorator_syntax.py +159 -0
- django_bolt/tests/test_error_handling.py +481 -0
- django_bolt/tests/test_file_response.py +192 -0
- django_bolt/tests/test_global_cors.py +172 -0
- django_bolt/tests/test_guards_auth.py +441 -0
- django_bolt/tests/test_guards_integration.py +303 -0
- django_bolt/tests/test_health.py +283 -0
- django_bolt/tests/test_integration_validation.py +400 -0
- django_bolt/tests/test_json_validation.py +536 -0
- django_bolt/tests/test_jwt_auth.py +327 -0
- django_bolt/tests/test_jwt_token.py +458 -0
- django_bolt/tests/test_logging.py +837 -0
- django_bolt/tests/test_logging_merge.py +419 -0
- django_bolt/tests/test_middleware.py +492 -0
- django_bolt/tests/test_middleware_server.py +230 -0
- django_bolt/tests/test_model_viewset.py +323 -0
- django_bolt/tests/test_models.py +24 -0
- django_bolt/tests/test_pagination.py +1258 -0
- django_bolt/tests/test_parameter_validation.py +178 -0
- django_bolt/tests/test_syntax.py +626 -0
- django_bolt/tests/test_testing_utilities.py +163 -0
- django_bolt/tests/test_testing_utilities_simple.py +123 -0
- django_bolt/tests/test_viewset_unified.py +346 -0
- django_bolt/typing.py +273 -0
- django_bolt/views.py +1110 -0
- django_bolt-0.1.0.dist-info/METADATA +629 -0
- django_bolt-0.1.0.dist-info/RECORD +128 -0
- django_bolt-0.1.0.dist-info/WHEEL +4 -0
- django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pytest configuration for Django-Bolt tests.
|
|
3
|
+
|
|
4
|
+
Ensures Django settings are properly reset between tests.
|
|
5
|
+
Provides utilities for subprocess-based testing.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import pathlib
|
|
9
|
+
import signal
|
|
10
|
+
import socket
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import logging
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
# Suppress httpx INFO logs during tests
|
|
18
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def pytest_configure(config):
|
|
22
|
+
"""Configure Django settings for pytest-django."""
|
|
23
|
+
import django
|
|
24
|
+
from django.conf import settings
|
|
25
|
+
|
|
26
|
+
# Skip configuration if DJANGO_SETTINGS_MODULE is already set
|
|
27
|
+
# This allows specific test modules to use their own Django settings
|
|
28
|
+
if os.getenv("DJANGO_SETTINGS_MODULE"):
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
if not settings.configured:
|
|
32
|
+
# Configure with all apps including admin to support all tests
|
|
33
|
+
# The admin apps don't significantly impact non-admin tests
|
|
34
|
+
settings.configure(
|
|
35
|
+
DEBUG=True,
|
|
36
|
+
SECRET_KEY='test-secret-key-global',
|
|
37
|
+
ALLOWED_HOSTS=['*'],
|
|
38
|
+
INSTALLED_APPS=[
|
|
39
|
+
'django.contrib.admin',
|
|
40
|
+
'django.contrib.auth',
|
|
41
|
+
'django.contrib.contenttypes',
|
|
42
|
+
'django.contrib.sessions',
|
|
43
|
+
'django.contrib.messages',
|
|
44
|
+
'django.contrib.staticfiles',
|
|
45
|
+
'django_bolt',
|
|
46
|
+
],
|
|
47
|
+
MIDDLEWARE=[
|
|
48
|
+
'django.middleware.security.SecurityMiddleware',
|
|
49
|
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
50
|
+
'django.middleware.common.CommonMiddleware',
|
|
51
|
+
'django.middleware.csrf.CsrfViewMiddleware',
|
|
52
|
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
53
|
+
'django.contrib.messages.middleware.MessageMiddleware',
|
|
54
|
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
55
|
+
],
|
|
56
|
+
ROOT_URLCONF='django_bolt.tests.admin_tests.urls',
|
|
57
|
+
TEMPLATES=[
|
|
58
|
+
{
|
|
59
|
+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
60
|
+
'DIRS': [],
|
|
61
|
+
'APP_DIRS': True,
|
|
62
|
+
'OPTIONS': {
|
|
63
|
+
'context_processors': [
|
|
64
|
+
'django.template.context_processors.debug',
|
|
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
|
+
DATABASES={
|
|
73
|
+
'default': {
|
|
74
|
+
'ENGINE': 'django.db.backends.sqlite3',
|
|
75
|
+
'NAME': ':memory:',
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
USE_TZ=True,
|
|
79
|
+
LANGUAGE_CODE='en-us',
|
|
80
|
+
TIME_ZONE='UTC',
|
|
81
|
+
USE_I18N=True,
|
|
82
|
+
STATIC_URL='/static/',
|
|
83
|
+
DEFAULT_AUTO_FIELD='django.db.models.BigAutoField',
|
|
84
|
+
)
|
|
85
|
+
# Setup Django apps so ExceptionReporter works
|
|
86
|
+
django.setup()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.fixture(scope='session')
|
|
90
|
+
def django_db_setup(django_db_setup, django_db_blocker):
|
|
91
|
+
"""
|
|
92
|
+
Ensure database migrations are run before any tests that use the database.
|
|
93
|
+
This creates the auth_user table and other Django core tables.
|
|
94
|
+
Also creates test model tables (Article, etc.).
|
|
95
|
+
"""
|
|
96
|
+
from django.core.management import call_command
|
|
97
|
+
from django.db import connection
|
|
98
|
+
|
|
99
|
+
with django_db_blocker.unblock():
|
|
100
|
+
# Run migrations to create all necessary tables
|
|
101
|
+
call_command('migrate', '--run-syncdb', verbosity=0)
|
|
102
|
+
|
|
103
|
+
# Create test model tables manually since they're not in migrations
|
|
104
|
+
with connection.schema_editor() as schema_editor:
|
|
105
|
+
from django_bolt.tests.test_models import Article
|
|
106
|
+
schema_editor.create_model(Article)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def spawn_process(command):
|
|
110
|
+
"""Spawn a subprocess in a new process group"""
|
|
111
|
+
import platform
|
|
112
|
+
if platform.system() == "Windows":
|
|
113
|
+
process = subprocess.Popen(
|
|
114
|
+
command,
|
|
115
|
+
shell=True,
|
|
116
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
|
117
|
+
stdout=subprocess.PIPE,
|
|
118
|
+
stderr=subprocess.PIPE
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
process = subprocess.Popen(
|
|
122
|
+
command,
|
|
123
|
+
preexec_fn=os.setsid,
|
|
124
|
+
stdout=subprocess.PIPE,
|
|
125
|
+
stderr=subprocess.PIPE
|
|
126
|
+
)
|
|
127
|
+
return process
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def kill_process(process):
|
|
131
|
+
"""Kill a subprocess and its process group"""
|
|
132
|
+
import platform
|
|
133
|
+
if platform.system() == "Windows":
|
|
134
|
+
try:
|
|
135
|
+
process.send_signal(signal.CTRL_BREAK_EVENT)
|
|
136
|
+
except:
|
|
137
|
+
pass
|
|
138
|
+
try:
|
|
139
|
+
process.kill()
|
|
140
|
+
except:
|
|
141
|
+
pass
|
|
142
|
+
else:
|
|
143
|
+
try:
|
|
144
|
+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
145
|
+
except ProcessLookupError:
|
|
146
|
+
pass
|
|
147
|
+
except:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def wait_for_server(host, port, timeout=15):
|
|
152
|
+
"""Wait for server to be reachable"""
|
|
153
|
+
start_time = time.time()
|
|
154
|
+
while time.time() - start_time < timeout:
|
|
155
|
+
try:
|
|
156
|
+
sock = socket.create_connection((host, port), timeout=2)
|
|
157
|
+
sock.close()
|
|
158
|
+
return True
|
|
159
|
+
except Exception:
|
|
160
|
+
time.sleep(0.5)
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Django configuration is now handled by pytest-django
|
|
165
|
+
# via pytest_configure above
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for @action decorator with ViewSets.
|
|
3
|
+
|
|
4
|
+
This test suite verifies that the @action decorator works correctly:
|
|
5
|
+
- Instance-level actions (detail=True)
|
|
6
|
+
- Collection-level actions (detail=False)
|
|
7
|
+
- Multiple HTTP methods on single action
|
|
8
|
+
- Custom path parameter
|
|
9
|
+
- Auth/guards inheritance from class-level
|
|
10
|
+
"""
|
|
11
|
+
import pytest
|
|
12
|
+
import msgspec
|
|
13
|
+
from django_bolt import BoltAPI, ViewSet, action
|
|
14
|
+
from django_bolt.testing import TestClient
|
|
15
|
+
from .test_models import Article
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# --- Fixtures ---
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def api():
|
|
22
|
+
"""Create a fresh BoltAPI instance for each test."""
|
|
23
|
+
return BoltAPI()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# --- Schemas ---
|
|
27
|
+
|
|
28
|
+
class ArticleSchema(msgspec.Struct):
|
|
29
|
+
"""Article schema."""
|
|
30
|
+
id: int
|
|
31
|
+
title: str
|
|
32
|
+
content: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ArticleCreateSchema(msgspec.Struct):
|
|
36
|
+
"""Schema for creating articles."""
|
|
37
|
+
title: str
|
|
38
|
+
content: str
|
|
39
|
+
author: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# --- Tests ---
|
|
43
|
+
|
|
44
|
+
@pytest.mark.django_db(transaction=True)
|
|
45
|
+
def test_action_decorator_detail_true(api):
|
|
46
|
+
"""Test @action with detail=True (instance-level action)."""
|
|
47
|
+
|
|
48
|
+
@api.viewset("/articles")
|
|
49
|
+
class ArticleViewSet(ViewSet):
|
|
50
|
+
queryset = Article.objects.all()
|
|
51
|
+
serializer_class = ArticleSchema
|
|
52
|
+
|
|
53
|
+
async def list(self, request):
|
|
54
|
+
"""List articles."""
|
|
55
|
+
articles = []
|
|
56
|
+
async for article in await self.get_queryset():
|
|
57
|
+
articles.append(ArticleSchema(
|
|
58
|
+
id=article.id,
|
|
59
|
+
title=article.title,
|
|
60
|
+
content=article.content
|
|
61
|
+
))
|
|
62
|
+
return articles
|
|
63
|
+
|
|
64
|
+
async def retrieve(self, request, pk: int):
|
|
65
|
+
"""Retrieve single article."""
|
|
66
|
+
article = await self.get_object(pk)
|
|
67
|
+
return ArticleSchema(
|
|
68
|
+
id=article.id,
|
|
69
|
+
title=article.title,
|
|
70
|
+
content=article.content
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@action(methods=["POST"], detail=True)
|
|
74
|
+
async def publish(self, request, pk: int):
|
|
75
|
+
"""Publish an article. POST /articles/{pk}/publish"""
|
|
76
|
+
article = await self.get_object(pk)
|
|
77
|
+
article.is_published = True
|
|
78
|
+
await article.asave()
|
|
79
|
+
return {"published": True, "article_id": pk}
|
|
80
|
+
|
|
81
|
+
# Create test article
|
|
82
|
+
article = Article.objects.create(
|
|
83
|
+
title="Test Article",
|
|
84
|
+
content="Test content",
|
|
85
|
+
author="Test Author",
|
|
86
|
+
is_published=False
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
client = TestClient(api)
|
|
90
|
+
|
|
91
|
+
# Test the custom action
|
|
92
|
+
response = client.post(f"/articles/{article.id}/publish")
|
|
93
|
+
assert response.status_code == 200
|
|
94
|
+
data = response.json()
|
|
95
|
+
assert data["published"] is True
|
|
96
|
+
assert data["article_id"] == article.id
|
|
97
|
+
|
|
98
|
+
# Verify article was published
|
|
99
|
+
article.refresh_from_db()
|
|
100
|
+
assert article.is_published is True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.django_db(transaction=True)
|
|
104
|
+
def test_action_decorator_detail_false(api):
|
|
105
|
+
"""Test @action with detail=False (collection-level action)."""
|
|
106
|
+
|
|
107
|
+
@api.viewset("/articles")
|
|
108
|
+
class ArticleViewSet(ViewSet):
|
|
109
|
+
queryset = Article.objects.all()
|
|
110
|
+
serializer_class = ArticleSchema
|
|
111
|
+
|
|
112
|
+
async def list(self, request):
|
|
113
|
+
"""List articles."""
|
|
114
|
+
articles = []
|
|
115
|
+
async for article in await self.get_queryset():
|
|
116
|
+
articles.append(ArticleSchema(
|
|
117
|
+
id=article.id,
|
|
118
|
+
title=article.title,
|
|
119
|
+
content=article.content
|
|
120
|
+
))
|
|
121
|
+
return articles
|
|
122
|
+
|
|
123
|
+
@action(methods=["GET"], detail=False)
|
|
124
|
+
async def published(self, request):
|
|
125
|
+
"""Get published articles. GET /articles/published"""
|
|
126
|
+
articles = []
|
|
127
|
+
async for article in Article.objects.filter(is_published=True):
|
|
128
|
+
articles.append(ArticleSchema(
|
|
129
|
+
id=article.id,
|
|
130
|
+
title=article.title,
|
|
131
|
+
content=article.content
|
|
132
|
+
))
|
|
133
|
+
return articles
|
|
134
|
+
|
|
135
|
+
# Create test articles
|
|
136
|
+
Article.objects.create(
|
|
137
|
+
title="Published 1",
|
|
138
|
+
content="Content 1",
|
|
139
|
+
author="Author 1",
|
|
140
|
+
is_published=True
|
|
141
|
+
)
|
|
142
|
+
Article.objects.create(
|
|
143
|
+
title="Draft",
|
|
144
|
+
content="Content 2",
|
|
145
|
+
author="Author 2",
|
|
146
|
+
is_published=False
|
|
147
|
+
)
|
|
148
|
+
Article.objects.create(
|
|
149
|
+
title="Published 2",
|
|
150
|
+
content="Content 3",
|
|
151
|
+
author="Author 3",
|
|
152
|
+
is_published=True
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
client = TestClient(api)
|
|
156
|
+
|
|
157
|
+
# Test the custom action
|
|
158
|
+
response = client.get("/articles/published")
|
|
159
|
+
assert response.status_code == 200
|
|
160
|
+
data = response.json()
|
|
161
|
+
assert len(data) == 2
|
|
162
|
+
assert all(article["title"].startswith("Published") for article in data)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@pytest.mark.django_db(transaction=True)
|
|
166
|
+
def test_action_decorator_multiple_methods(api):
|
|
167
|
+
"""Test @action with multiple HTTP methods."""
|
|
168
|
+
|
|
169
|
+
class StatusUpdate(msgspec.Struct):
|
|
170
|
+
"""Schema for status update."""
|
|
171
|
+
is_published: bool
|
|
172
|
+
|
|
173
|
+
@api.viewset("/articles")
|
|
174
|
+
class ArticleViewSet(ViewSet):
|
|
175
|
+
queryset = Article.objects.all()
|
|
176
|
+
serializer_class = ArticleSchema
|
|
177
|
+
|
|
178
|
+
async def list(self, request):
|
|
179
|
+
"""List articles."""
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
@action(methods=["GET"], detail=True, path="status")
|
|
183
|
+
async def get_status(self, request, pk: int):
|
|
184
|
+
"""GET /articles/{pk}/status - Get article status"""
|
|
185
|
+
article = await self.get_object(pk)
|
|
186
|
+
return {"is_published": article.is_published}
|
|
187
|
+
|
|
188
|
+
@action(methods=["POST"], detail=True, path="status")
|
|
189
|
+
async def update_status(self, request, pk: int, data: StatusUpdate):
|
|
190
|
+
"""POST /articles/{pk}/status - Update article status"""
|
|
191
|
+
article = await self.get_object(pk)
|
|
192
|
+
article.is_published = data.is_published
|
|
193
|
+
await article.asave()
|
|
194
|
+
return {"updated": True, "is_published": article.is_published}
|
|
195
|
+
|
|
196
|
+
# Create test article
|
|
197
|
+
article = Article.objects.create(
|
|
198
|
+
title="Test Article",
|
|
199
|
+
content="Test content",
|
|
200
|
+
author="Test Author",
|
|
201
|
+
is_published=False
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
client = TestClient(api)
|
|
205
|
+
|
|
206
|
+
# Test GET
|
|
207
|
+
response = client.get(f"/articles/{article.id}/status")
|
|
208
|
+
assert response.status_code == 200
|
|
209
|
+
data = response.json()
|
|
210
|
+
assert data["is_published"] is False
|
|
211
|
+
|
|
212
|
+
# Test POST
|
|
213
|
+
response = client.post(f"/articles/{article.id}/status", json={"is_published": True})
|
|
214
|
+
assert response.status_code == 200
|
|
215
|
+
data = response.json()
|
|
216
|
+
assert data["updated"] is True
|
|
217
|
+
assert data["is_published"] is True
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@pytest.mark.django_db(transaction=True)
|
|
221
|
+
def test_action_decorator_custom_path(api):
|
|
222
|
+
"""Test @action with custom path parameter."""
|
|
223
|
+
|
|
224
|
+
@api.viewset("/articles")
|
|
225
|
+
class ArticleViewSet(ViewSet):
|
|
226
|
+
queryset = Article.objects.all()
|
|
227
|
+
serializer_class = ArticleSchema
|
|
228
|
+
|
|
229
|
+
async def list(self, request):
|
|
230
|
+
"""List articles."""
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
@action(methods=["POST"], detail=True, path="custom-action-name")
|
|
234
|
+
async def some_method_name(self, request, pk: int):
|
|
235
|
+
"""POST /articles/{pk}/custom-action-name"""
|
|
236
|
+
return {"action": "custom-action-name", "article_id": pk}
|
|
237
|
+
|
|
238
|
+
client = TestClient(api)
|
|
239
|
+
|
|
240
|
+
# Create test article
|
|
241
|
+
article = Article.objects.create(
|
|
242
|
+
title="Test Article",
|
|
243
|
+
content="Test content",
|
|
244
|
+
author="Test Author"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Test custom path (not method name)
|
|
248
|
+
response = client.post(f"/articles/{article.id}/custom-action-name")
|
|
249
|
+
assert response.status_code == 200
|
|
250
|
+
data = response.json()
|
|
251
|
+
assert data["action"] == "custom-action-name"
|
|
252
|
+
assert data["article_id"] == article.id
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@pytest.mark.django_db(transaction=True)
|
|
256
|
+
def test_action_decorator_with_api_view_raises_error(api):
|
|
257
|
+
"""Test that @action raises error when used with api.view() instead of api.viewset()."""
|
|
258
|
+
|
|
259
|
+
# This should raise an error because api.view() doesn't support @action
|
|
260
|
+
with pytest.raises(ValueError, match="uses @action decorator.*api.viewset"):
|
|
261
|
+
@api.view("/articles", methods=["GET"])
|
|
262
|
+
class ArticleViewSet(ViewSet):
|
|
263
|
+
async def get(self, request):
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
@action(methods=["POST"], detail=False)
|
|
267
|
+
async def custom_action(self, request):
|
|
268
|
+
return {"ok": True}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@pytest.mark.django_db(transaction=True)
|
|
272
|
+
def test_action_decorator_defaults_to_function_name(api):
|
|
273
|
+
"""Test that @action uses function name as default path."""
|
|
274
|
+
|
|
275
|
+
@api.viewset("/articles")
|
|
276
|
+
class ArticleViewSet(ViewSet):
|
|
277
|
+
queryset = Article.objects.all()
|
|
278
|
+
serializer_class = ArticleSchema
|
|
279
|
+
|
|
280
|
+
async def list(self, request):
|
|
281
|
+
"""List articles."""
|
|
282
|
+
return []
|
|
283
|
+
|
|
284
|
+
@action(methods=["POST"], detail=True)
|
|
285
|
+
async def archive(self, request, pk: int):
|
|
286
|
+
"""POST /articles/{pk}/archive"""
|
|
287
|
+
return {"archived": True, "article_id": pk}
|
|
288
|
+
|
|
289
|
+
client = TestClient(api)
|
|
290
|
+
|
|
291
|
+
# Create test article
|
|
292
|
+
article = Article.objects.create(
|
|
293
|
+
title="Test Article",
|
|
294
|
+
content="Test content",
|
|
295
|
+
author="Test Author"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Test action at /articles/{pk}/archive (function name)
|
|
299
|
+
response = client.post(f"/articles/{article.id}/archive")
|
|
300
|
+
assert response.status_code == 200
|
|
301
|
+
data = response.json()
|
|
302
|
+
assert data["archived"] is True
|
|
303
|
+
assert data["article_id"] == article.id
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@pytest.mark.django_db(transaction=True)
|
|
307
|
+
def test_action_decorator_with_query_params(api):
|
|
308
|
+
"""Test @action with query parameters."""
|
|
309
|
+
|
|
310
|
+
@api.viewset("/articles")
|
|
311
|
+
class ArticleViewSet(ViewSet):
|
|
312
|
+
queryset = Article.objects.all()
|
|
313
|
+
serializer_class = ArticleSchema
|
|
314
|
+
|
|
315
|
+
async def list(self, request):
|
|
316
|
+
"""List articles."""
|
|
317
|
+
return []
|
|
318
|
+
|
|
319
|
+
@action(methods=["GET"], detail=False)
|
|
320
|
+
async def search(self, request, query: str, limit: int = 10):
|
|
321
|
+
"""GET /articles/search?query=xxx&limit=5"""
|
|
322
|
+
articles = []
|
|
323
|
+
async for article in Article.objects.filter(title__icontains=query)[:limit]:
|
|
324
|
+
articles.append(ArticleSchema(
|
|
325
|
+
id=article.id,
|
|
326
|
+
title=article.title,
|
|
327
|
+
content=article.content
|
|
328
|
+
))
|
|
329
|
+
return {"query": query, "limit": limit, "results": articles}
|
|
330
|
+
|
|
331
|
+
# Create test articles
|
|
332
|
+
Article.objects.create(title="Python Guide", content="Content", author="Author")
|
|
333
|
+
Article.objects.create(title="Django Tutorial", content="Content", author="Author")
|
|
334
|
+
Article.objects.create(title="Python Basics", content="Content", author="Author")
|
|
335
|
+
|
|
336
|
+
client = TestClient(api)
|
|
337
|
+
|
|
338
|
+
# Test with query params
|
|
339
|
+
response = client.get("/articles/search?query=Python&limit=5")
|
|
340
|
+
assert response.status_code == 200
|
|
341
|
+
data = response.json()
|
|
342
|
+
assert data["query"] == "Python"
|
|
343
|
+
assert data["limit"] == 5
|
|
344
|
+
assert len(data["results"]) == 2
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@pytest.mark.django_db(transaction=True)
|
|
348
|
+
def test_action_decorator_invalid_method(api):
|
|
349
|
+
"""Test that @action raises error for invalid HTTP methods."""
|
|
350
|
+
|
|
351
|
+
with pytest.raises(ValueError, match="Invalid HTTP method"):
|
|
352
|
+
@action(methods=["INVALID"], detail=True)
|
|
353
|
+
async def some_action(self, request, pk: int):
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@pytest.mark.django_db(transaction=True)
|
|
358
|
+
def test_action_decorator_with_different_lookup_fields(api):
|
|
359
|
+
"""Test @action respects custom lookup_field."""
|
|
360
|
+
|
|
361
|
+
@api.viewset("/articles")
|
|
362
|
+
class ArticleViewSet(ViewSet):
|
|
363
|
+
queryset = Article.objects.all()
|
|
364
|
+
serializer_class = ArticleSchema
|
|
365
|
+
lookup_field = 'id' # Explicitly set to 'id' instead of default 'pk'
|
|
366
|
+
|
|
367
|
+
async def list(self, request):
|
|
368
|
+
"""List articles."""
|
|
369
|
+
return []
|
|
370
|
+
|
|
371
|
+
async def retrieve(self, request, id: int):
|
|
372
|
+
"""Retrieve article by id."""
|
|
373
|
+
article = await self.get_object(id=id)
|
|
374
|
+
return ArticleSchema(
|
|
375
|
+
id=article.id,
|
|
376
|
+
title=article.title,
|
|
377
|
+
content=article.content
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
@action(methods=["POST"], detail=True)
|
|
381
|
+
async def feature(self, request, id: int):
|
|
382
|
+
"""POST /articles/{id}/feature"""
|
|
383
|
+
return {"featured": True, "article_id": id}
|
|
384
|
+
|
|
385
|
+
client = TestClient(api)
|
|
386
|
+
|
|
387
|
+
# Create test article
|
|
388
|
+
article = Article.objects.create(
|
|
389
|
+
title="Test Article",
|
|
390
|
+
content="Test content",
|
|
391
|
+
author="Test Author"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Test action with custom lookup field
|
|
395
|
+
response = client.post(f"/articles/{article.id}/feature")
|
|
396
|
+
assert response.status_code == 200
|
|
397
|
+
data = response.json()
|
|
398
|
+
assert data["featured"] is True
|
|
399
|
+
assert data["article_id"] == article.id
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test that JWT authentication uses Django SECRET_KEY when not specified
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
from django_bolt import BoltAPI
|
|
6
|
+
from django_bolt.auth import JWTAuthentication
|
|
7
|
+
from django_bolt.auth import IsAuthenticated
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_jwt_auth_uses_django_secret_key():
|
|
11
|
+
"""Test that JWTAuthentication uses Django SECRET_KEY when secret not provided"""
|
|
12
|
+
# Django is already configured by pytest-django with SECRET_KEY='test-secret-key-global'
|
|
13
|
+
from django.conf import settings
|
|
14
|
+
|
|
15
|
+
# Create JWT auth without explicit secret
|
|
16
|
+
auth = JWTAuthentication() # No secret specified
|
|
17
|
+
|
|
18
|
+
# Should use Django's SECRET_KEY
|
|
19
|
+
assert auth.secret == settings.SECRET_KEY
|
|
20
|
+
print("✓ JWTAuthentication uses Django SECRET_KEY when not specified")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_jwt_auth_explicit_secret_overrides():
|
|
24
|
+
"""Test that explicit secret overrides Django SECRET_KEY"""
|
|
25
|
+
from django.conf import settings
|
|
26
|
+
|
|
27
|
+
# Create with explicit secret
|
|
28
|
+
auth = JWTAuthentication(secret="custom-secret")
|
|
29
|
+
|
|
30
|
+
# Should use the explicit secret, not Django's
|
|
31
|
+
assert auth.secret == 'custom-secret'
|
|
32
|
+
assert auth.secret != settings.SECRET_KEY
|
|
33
|
+
print("✓ Explicit secret overrides Django SECRET_KEY")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_route_with_django_secret():
|
|
37
|
+
"""Test that route-level auth uses Django SECRET_KEY"""
|
|
38
|
+
from django.conf import settings
|
|
39
|
+
|
|
40
|
+
api = BoltAPI()
|
|
41
|
+
|
|
42
|
+
@api.get(
|
|
43
|
+
"/protected",
|
|
44
|
+
auth=[JWTAuthentication()], # No secret - should use Django's
|
|
45
|
+
guards=[IsAuthenticated()]
|
|
46
|
+
)
|
|
47
|
+
async def protected_endpoint():
|
|
48
|
+
return {"message": "Protected"}
|
|
49
|
+
|
|
50
|
+
# Check that metadata has Django SECRET_KEY
|
|
51
|
+
handler_id = 0
|
|
52
|
+
if handler_id in api._handler_middleware:
|
|
53
|
+
metadata = api._handler_middleware[handler_id]
|
|
54
|
+
auth_backends = metadata.get('auth_backends', [])
|
|
55
|
+
assert len(auth_backends) > 0
|
|
56
|
+
assert auth_backends[0]['secret'] == settings.SECRET_KEY
|
|
57
|
+
print("✓ Route-level JWT auth uses Django SECRET_KEY")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_global_auth_with_django_secret():
|
|
61
|
+
"""Test global auth configuration with Django SECRET_KEY"""
|
|
62
|
+
from django.conf import settings
|
|
63
|
+
|
|
64
|
+
# Set auth classes (settings already configured by pytest-django)
|
|
65
|
+
settings.BOLT_AUTHENTICATION_CLASSES = [
|
|
66
|
+
JWTAuthentication() # No secret - should use Django's
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
from django_bolt.auth import get_default_authentication_classes
|
|
70
|
+
|
|
71
|
+
auth_classes = get_default_authentication_classes()
|
|
72
|
+
assert len(auth_classes) > 0
|
|
73
|
+
assert auth_classes[0].secret == settings.SECRET_KEY
|
|
74
|
+
print("✓ Global auth configuration uses Django SECRET_KEY")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
test_jwt_auth_uses_django_secret_key()
|
|
79
|
+
test_jwt_auth_explicit_secret_overrides()
|
|
80
|
+
test_route_with_django_secret()
|
|
81
|
+
test_global_auth_with_django_secret()
|
|
82
|
+
|
|
83
|
+
print("\n✅ All Django SECRET_KEY integration tests passed!")
|