diff --git a/app/app/settings.py b/app/app/settings.py index 89cfde3..dab5211 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -12,6 +12,8 @@ from pathlib import Path +from drf_yasg.openapi import Contact, Info + from app import env # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -70,8 +72,11 @@ # Application definition INSTALLED_APPS = [ + 'drf_yasg', 'rest_framework', + 'doc', + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -162,3 +167,25 @@ # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +# drf-yasg settings +# https://drf-yasg.readthedocs.io/en/stable/settings.html +SWAGGER_SETTINGS = { + # NOTE: DEFAULT_INFO 설정을 해두어야 `generate_swagger` management command를 사용할 수 있음에 유의. + "DEFAULT_INFO": "app.settings.OPEN_API_INFO", +} + +REDOC_SETTINGS = { + "LAZY_RENDERING": True, +} + +OPEN_API_INFO = Info( + title="Time Limit Exceeded :: Authentication API", + default_version='v1', + description=( + "This API provides authentication endpoints and related features for the Time Limit Exceeded application, " + "including user login, registration, and token management." + ), + contact=Contact(email="hepheir@gmail.com"), +) diff --git a/app/app/urls.py b/app/app/urls.py index 93599f7..83df3d1 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -15,8 +15,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), + path('doc/', include('doc.urls')), ] diff --git a/app/doc/__init__.py b/app/doc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/doc/apps.py b/app/doc/apps.py new file mode 100644 index 0000000..9f8fe2b --- /dev/null +++ b/app/doc/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DocConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'doc' diff --git a/app/doc/test_urls.py b/app/doc/test_urls.py new file mode 100644 index 0000000..df6c66e --- /dev/null +++ b/app/doc/test_urls.py @@ -0,0 +1,58 @@ +from django.test import TestCase, override_settings +from rest_framework import status + + +class DocEndpointsDebugModeTestCase(TestCase): + """DEBUG=True일 때 문서 엔드포인트 접근 가능 여부 테스트""" + + @override_settings(DEBUG=True) + def test_swagger_ui_accessible_in_debug_mode(self): + """DEBUG 모드에서 Swagger UI 접근 가능""" + response = self.client.get('/doc/swagger/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @override_settings(DEBUG=True) + def test_redoc_ui_accessible_in_debug_mode(self): + """DEBUG 모드에서 ReDoc UI 접근 가능""" + response = self.client.get('/doc/redoc/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @override_settings(DEBUG=True) + def test_schema_json_accessible_in_debug_mode(self): + """DEBUG 모드에서 스키마 JSON 접근 가능""" + response = self.client.get('/doc/swagger.json/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @override_settings(DEBUG=True) + def test_schema_yaml_accessible_in_debug_mode(self): + """DEBUG 모드에서 스키마 YAML 접근 가능""" + response = self.client.get('/doc/swagger.yaml/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class DocEndpointsProductionModeTestCase(TestCase): + """DEBUG=False일 때 문서 엔드포인트 접근 제한 테스트""" + + @override_settings(DEBUG=False, ALLOWED_HOSTS=['testserver']) + def test_swagger_ui_not_accessible_in_production(self): + """프로덕션 모드에서 Swagger UI 접근 불가""" + response = self.client.get('/doc/swagger/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + @override_settings(DEBUG=False, ALLOWED_HOSTS=['testserver']) + def test_redoc_ui_not_accessible_in_production(self): + """프로덕션 모드에서 ReDoc UI 접근 불가""" + response = self.client.get('/doc/redoc/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + @override_settings(DEBUG=False, ALLOWED_HOSTS=['testserver']) + def test_schema_json_not_accessible_in_production(self): + """프로덕션 모드에서 스키마 JSON 접근 불가""" + response = self.client.get('/doc/swagger.json/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + @override_settings(DEBUG=False, ALLOWED_HOSTS=['testserver']) + def test_schema_yaml_not_accessible_in_production(self): + """프로덕션 모드에서 스키마 YAML 접근 불가""" + response = self.client.get('/doc/swagger.yaml/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/app/doc/urls.py b/app/doc/urls.py new file mode 100644 index 0000000..7e60f73 --- /dev/null +++ b/app/doc/urls.py @@ -0,0 +1,26 @@ +""" +URL configuration for doc app. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path + +from . import views + + +urlpatterns = [ + path('swagger/', views.schema_format_view), + path('swagger/', views.swagger_view), + path('redoc/', views.redoc_view), +] diff --git a/app/doc/views.py b/app/doc/views.py new file mode 100644 index 0000000..c6be006 --- /dev/null +++ b/app/doc/views.py @@ -0,0 +1,24 @@ +from django.conf import settings +from drf_yasg.views import get_schema_view +from rest_framework.permissions import BasePermission + + +class IsDebug(BasePermission): + """ + Permission class that allows access only when Django's DEBUG mode is enabled. + Useful for restricting certain endpoints to development environments. + """ + def has_permission(self, request, view): + return settings.DEBUG + + +SchemaView = get_schema_view( + info=settings.OPEN_API_INFO, + permission_classes=[IsDebug], + public=True, +) + + +redoc_view = SchemaView.with_ui('redoc') +swagger_view = SchemaView.with_ui('swagger') +schema_format_view = SchemaView.without_ui() diff --git a/requirements.txt b/requirements.txt index bec54d2..85a5301 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ django djangorestframework +drf-yasg pytest-django python-dotenv