diff --git a/AUTHORS b/AUTHORS index 4ebe787cd..aa469f8ce 100644 --- a/AUTHORS +++ b/AUTHORS @@ -85,6 +85,7 @@ Jun Zhou Kaleb Porter Kristian Rune Larsen Lazaros Toumanidis +lrq315 Ludwig Hähne Łukasz Skarżyński Madison Swain-Bowden diff --git a/CHANGELOG.md b/CHANGELOG.md index a29772c13..18fecb608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed * #1252 Fix crash when 'client' is in token request body * #1496 Fix error when Bearer token string is empty but preceded by `Bearer` keyword. +* #1628 Fix inaccurate help_text on client_secret field of Application model diff --git a/Dockerfile b/Dockerfile index 8828cead5..789be1dc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,15 +9,9 @@ FROM python:3.11.6-slim as builder ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 -ENV DEBUG=False -ENV ALLOWED_HOSTS="*" -ENV TEMPLATES_DIRS="/data/templates" -ENV STATIC_ROOT="/data/static" -ENV DATABASE_URL="sqlite:////data/db.sqlite3" - RUN apt-get update # Build Deps -RUN apt-get install -y --no-install-recommends gcc libc-dev python3-dev git openssh-client libpq-dev file libev-dev +RUN apt-get install -y --no-install-recommends gcc libc-dev python3-dev git openssh-client libpq-dev file libev-dev gettext # bundle code in a virtual env to make copying to the final image without all the upstream stuff easier. RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" @@ -28,7 +22,8 @@ COPY . /code WORKDIR /code/tests/app/idp RUN pip install -r requirements.txt RUN pip install gunicorn -RUN python manage.py collectstatic --noinput +RUN cd /code/oauth2_provider && django-admin compilemessages +RUN STATIC_ROOT="static" python manage.py collectstatic --noinput @@ -47,8 +42,8 @@ ENV SENTRY_RELEASE=${GIT_SHA1} # disable debug mode, but allow all hosts by default when running in docker ENV DEBUG=False ENV ALLOWED_HOSTS="*" -ENV TEMPLATES_DIRS="/data/templates" -ENV STATIC_ROOT="/data/static" +ENV TEMPLATES_DIRS="/code/tests/app/idp/templates" +ENV STATIC_ROOT="/code/tests/app/idp/static" ENV DATABASE_URL="sqlite:////data/db.sqlite3" @@ -57,9 +52,6 @@ ENV DATABASE_URL="sqlite:////data/db.sqlite3" COPY --from=builder /opt/venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" COPY --from=builder /code /code -RUN mkdir -p /data/static /data/templates -COPY --from=builder /code/tests/app/idp/static /data/static -COPY --from=builder /code/tests/app/idp/templates /data/templates WORKDIR /code/tests/app/idp RUN apt-get update && apt-get install -y \ diff --git a/docs/contributing.rst b/docs/contributing.rst index a633419e2..291da24d6 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -380,18 +380,18 @@ Development with astral uv package and project manager. We have experimental support for `astral uv `__. It provides an improved developer experience over vanilla virtualenv/venv and pip by managing multiple python versions, virtual environments and dependencies in a more efficient way. The ``uv run`` command automatically -syncs dependencies and python version before running the command, saving multiple steps when +syncs dependencies and python version before running the command, saving multiple steps when working on multiple branches with different dependencies. You can use uv sync to set up your environment and install dependencies and run python:: -... code-block:: bash +.. code-block:: bash uv sync # checks deps, installs virtualenv and dependencies as necessary uv run ... # runs command in the uv environment, syncs deps and python version first if necessary To run tox uv use `tox uv `__:: -... code-block:: bash +.. code-block:: bash uv tool install tox --with tox-uv # use uv to install tox --version # validate you are using the installed tox tox r -e py312 # will use uv diff --git a/oauth2_provider/admin.py b/oauth2_provider/admin.py index dd636184c..7bf61af26 100644 --- a/oauth2_provider/admin.py +++ b/oauth2_provider/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.contrib.auth import get_user_model +from oauth2_provider.forms import ApplicationForm from oauth2_provider.models import ( get_access_token_admin_class, get_access_token_model, @@ -19,6 +20,7 @@ class ApplicationAdmin(admin.ModelAdmin): + form = ApplicationForm list_display = ("pk", "name", "user", "client_type", "authorization_grant_type") list_filter = ("client_type", "authorization_grant_type", "skip_authorization") radio_fields = { @@ -28,6 +30,9 @@ class ApplicationAdmin(admin.ModelAdmin): search_fields = ("name",) + (("user__email",) if has_email else ()) raw_id_fields = ("user",) + class Media: + js = ("oauth2_provider/admin/application_form.js",) + class AccessTokenAdmin(admin.ModelAdmin): list_display = ("token", "user", "application", "expires") diff --git a/oauth2_provider/forms.py b/oauth2_provider/forms.py index 113ab3f53..85e39747e 100644 --- a/oauth2_provider/forms.py +++ b/oauth2_provider/forms.py @@ -1,4 +1,48 @@ from django import forms +from django.utils.encoding import force_str +from django.utils.translation import gettext_lazy as _ + +from .models import get_application_model + + +HASHED_WARNING_TEXT = _("Hashed on Save. Copy it now if this is a new secret.") + + +def get_application_form_class(): + application_model = get_application_model() + + class ApplicationForm(forms.ModelForm): + """ + Form for Application model with dynamic help_text for client_secret field + based on hash_client_secret value. + """ + + class Meta: + model = application_model + fields = ( + "name", + "client_id", + "client_secret", + "hash_client_secret", + "client_type", + "authorization_grant_type", + "redirect_uris", + "post_logout_redirect_uris", + "allowed_origins", + "algorithm", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields.get("client_secret").widget.attrs.setdefault( + "data-hashed-warning", force_str(HASHED_WARNING_TEXT) + ) + + return ApplicationForm + + +ApplicationForm = get_application_form_class() class AllowForm(forms.Form): diff --git a/oauth2_provider/locale/zh_Hans/LC_MESSAGES/django.po b/oauth2_provider/locale/zh_Hans/LC_MESSAGES/django.po new file mode 100644 index 000000000..ad7b23468 --- /dev/null +++ b/oauth2_provider/locale/zh_Hans/LC_MESSAGES/django.po @@ -0,0 +1,241 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-12-16 09:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: oauth2_provider/forms.py:8 +msgid "Hashed on Save. Copy it now if this is a new secret." +msgstr "保存时会进行哈希处理。如果这是一个新的密钥,请立即复制并保存。" + +#: oauth2_provider/models.py:87 +msgid "Confidential" +msgstr "" + +#: oauth2_provider/models.py:88 +msgid "Public" +msgstr "" + +#: oauth2_provider/models.py:98 +msgid "Authorization code" +msgstr "" + +#: oauth2_provider/models.py:99 +msgid "Device Code" +msgstr "" + +#: oauth2_provider/models.py:100 +msgid "Implicit" +msgstr "" + +#: oauth2_provider/models.py:101 +msgid "Resource owner password-based" +msgstr "" + +#: oauth2_provider/models.py:102 +msgid "Client credentials" +msgstr "" + +#: oauth2_provider/models.py:103 +msgid "OpenID connect hybrid" +msgstr "" + +#: oauth2_provider/models.py:110 +msgid "No OIDC support" +msgstr "" + +#: oauth2_provider/models.py:111 +msgid "RSA with SHA-2 256" +msgstr "" + +#: oauth2_provider/models.py:112 +msgid "HMAC with SHA-2 256" +msgstr "" + +#: oauth2_provider/models.py:127 +msgid "Allowed URIs list, space separated" +msgstr "" + +#: oauth2_provider/models.py:131 +msgid "Allowed Post Logout URIs list, space separated" +msgstr "" + +#: oauth2_provider/models.py:141 +msgid "Client secret for authentication" +msgstr "" + +#: oauth2_provider/models.py:152 +msgid "Allowed origins list to enable CORS, space separated" +msgstr "" + +#: oauth2_provider/models.py:232 +#, python-brace-format +msgid "redirect_uris cannot be empty with grant_type {grant_type}" +msgstr "" + +#: oauth2_provider/models.py:249 +msgid "You must set OIDC_RSA_PRIVATE_KEY to use RSA algorithm" +msgstr "" + +#: oauth2_provider/models.py:258 +msgid "You cannot use HS256 with public grants or clients" +msgstr "" + +#: oauth2_provider/models.py:674 +msgid "Authorized" +msgstr "" + +#: oauth2_provider/models.py:675 +msgid "Authorization pending" +msgstr "" + +#: oauth2_provider/models.py:676 +msgid "Expired" +msgstr "" + +#: oauth2_provider/models.py:677 +msgid "Denied" +msgstr "" + +#: oauth2_provider/oauth2_validators.py:249 +msgid "The access token is invalid." +msgstr "" + +#: oauth2_provider/oauth2_validators.py:256 +msgid "The access token has expired." +msgstr "" + +#: oauth2_provider/oauth2_validators.py:263 +msgid "The access token is valid but does not have enough scope." +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_confirm_delete.html:6 +msgid "Are you sure to delete the application" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_confirm_delete.html:12 +#: oauth2_provider/templates/oauth2_provider/authorize.html:29 +msgid "Cancel" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_confirm_delete.html:13 +#: oauth2_provider/templates/oauth2_provider/application_detail.html:53 +#: oauth2_provider/templates/oauth2_provider/authorized-token-delete.html:7 +msgid "Delete" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:10 +msgid "Client id" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:15 +msgid "Client secret" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:20 +msgid "Hash client secret" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:21 +msgid "yes,no" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:25 +msgid "Client type" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:30 +msgid "Authorization Grant Type" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:35 +msgid "Redirect Uris" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:40 +msgid "Post Logout Redirect Uris" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:45 +msgid "Allowed Origins" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:51 +#: oauth2_provider/templates/oauth2_provider/application_form.html:38 +msgid "Go Back" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:52 +msgid "Edit" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_form.html:9 +msgid "Edit application" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_form.html:40 +msgid "Save" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_list.html:6 +msgid "Your applications" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_list.html:14 +msgid "New Application" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_list.html:17 +msgid "No applications defined" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_list.html:17 +msgid "Click here" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_list.html:17 +msgid "if you want to register a new one" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/application_registration_form.html:5 +msgid "Register a new application" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/authorize.html:8 +#: oauth2_provider/templates/oauth2_provider/authorize.html:30 +msgid "Authorize" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/authorize.html:17 +msgid "Application requires the following permissions" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/authorized-token-delete.html:6 +msgid "Are you sure you want to delete this token?" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/authorized-tokens.html:6 +msgid "Tokens" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/authorized-tokens.html:11 +msgid "revoke" +msgstr "" + +#: oauth2_provider/templates/oauth2_provider/authorized-tokens.html:19 +msgid "There are no authorized tokens yet." +msgstr "" diff --git a/oauth2_provider/migrations/0014_alter_help_text.py b/oauth2_provider/migrations/0014_alter_help_text.py new file mode 100644 index 000000000..4857021ec --- /dev/null +++ b/oauth2_provider/migrations/0014_alter_help_text.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.9 on 2025-12-14 17:02 + +import oauth2_provider.generators +import oauth2_provider.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oauth2_provider', '0013_alter_application_authorization_grant_type_device'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='client_secret', + field=oauth2_provider.models.ClientSecretField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, help_text='Client secret for authentication', max_length=255), + ), + ] diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index 523ade289..26ec6a6a1 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -138,7 +138,7 @@ class AbstractApplication(models.Model): blank=True, default=generate_client_secret, db_index=True, - help_text=_("Hashed on Save. Copy it now if this is a new secret."), + help_text=_("Client secret for authentication"), ) hash_client_secret = models.BooleanField(default=True) name = models.CharField(max_length=255, blank=True) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index a202a6a82..3879b8c13 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -217,13 +217,16 @@ def _load_application(self, client_id, request): if request.client: # check for cached client, to save the db hit if this has already been loaded if not isinstance(request.client, Application): - # resetting request.client (client_id=%r): not an Application, something else set request.client erroneously + # resetting request.client (client_id=%r): + # not an Application, something else set request.client erroneously request.client = None elif request.client.client_id != client_id: - # resetting request.client (client_id=%r): request.client.client_id does not match the given client_id + # resetting request.client (client_id=%r): + # request.client.client_id does not match the given client_id request.client = None elif not request.client.is_usable(request): - # resetting request.client (client_id=%r): request.client is a valid Application, but is not usable + # resetting request.client (client_id=%r): + # request.client is a valid Application, but is not usable request.client = None else: # request.client is a valid Application, reusing it diff --git a/oauth2_provider/static/oauth2_provider/admin/application_form.js b/oauth2_provider/static/oauth2_provider/admin/application_form.js new file mode 100644 index 000000000..b843d1f56 --- /dev/null +++ b/oauth2_provider/static/oauth2_provider/admin/application_form.js @@ -0,0 +1,32 @@ +(() => { + 'use strict'; + document.addEventListener('DOMContentLoaded', () => { + const hashField = document.getElementById('id_hash_client_secret'); + const clientSecretField = document.getElementById('id_client_secret'); + if (!hashField || !clientSecretField) return; + + // Find help text container - works for both admin and application_form.html + const helpBox = document.querySelector('#id_client_secret_helptext > div') || + document.getElementById('help-id_client_secret'); + if (!helpBox) return; + + const warningText = clientSecretField.getAttribute('data-hashed-warning'); + + let hint = document.getElementById('client-secret-hash-hint'); + if (!hint) { + hint = document.createElement('span'); + hint.id = 'client-secret-hash-hint'; + hint.style.marginLeft = '6px'; + hint.style.color = '#ba2121'; + hint.style.fontWeight = '600'; + helpBox.appendChild(hint); + } + + const update = () => { + hint.textContent = hashField.checked ? warningText : ''; + }; + + hashField.addEventListener('change', update); + update(); + }); +})(); diff --git a/oauth2_provider/templates/oauth2_provider/application_form.html b/oauth2_provider/templates/oauth2_provider/application_form.html index 7d8c07989..cda25ead2 100644 --- a/oauth2_provider/templates/oauth2_provider/application_form.html +++ b/oauth2_provider/templates/oauth2_provider/application_form.html @@ -1,6 +1,6 @@ {% extends "oauth2_provider/base.html" %} -{% load i18n %} +{% load i18n static %} {% block content %}
@@ -16,6 +16,9 @@

{{ field }} + {% if field.help_text %} + {{ field.help_text }} + {% endif %} {% for error in field.errors %} {{ error }} {% endfor %} @@ -39,4 +42,5 @@

+ {% endblock %} diff --git a/oauth2_provider/views/application.py b/oauth2_provider/views/application.py index b896c45e3..0f66388af 100644 --- a/oauth2_provider/views/application.py +++ b/oauth2_provider/views/application.py @@ -1,8 +1,8 @@ from django.contrib.auth.mixins import LoginRequiredMixin -from django.forms.models import modelform_factory from django.urls import reverse_lazy from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView +from ..forms import get_application_form_class from ..models import get_application_model @@ -11,8 +11,6 @@ class ApplicationOwnerIsUserMixin(LoginRequiredMixin): This mixin is used to provide an Application queryset filtered by the current request.user. """ - fields = "__all__" - def get_queryset(self): return get_application_model().objects.filter(user=self.request.user) @@ -28,21 +26,7 @@ def get_form_class(self): """ Returns the form class for the application model """ - return modelform_factory( - get_application_model(), - fields=( - "name", - "client_id", - "client_secret", - "hash_client_secret", - "client_type", - "authorization_grant_type", - "redirect_uris", - "post_logout_redirect_uris", - "allowed_origins", - "algorithm", - ), - ) + return get_application_form_class() def form_valid(self, form): form.instance.user = self.request.user @@ -89,18 +73,4 @@ def get_form_class(self): """ Returns the form class for the application model """ - return modelform_factory( - get_application_model(), - fields=( - "name", - "client_id", - "client_secret", - "hash_client_secret", - "client_type", - "authorization_grant_type", - "redirect_uris", - "post_logout_redirect_uris", - "allowed_origins", - "algorithm", - ), - ) + return get_application_form_class() diff --git a/tests/app/idp/idp/apps.py b/tests/app/idp/idp/apps.py index 63a7f442f..f6e8291ab 100644 --- a/tests/app/idp/idp/apps.py +++ b/tests/app/idp/idp/apps.py @@ -3,7 +3,7 @@ def cors_allow_origin(sender, request, **kwargs): - origin = request.headers.get('Origin') + origin = request.headers.get("Origin") return ( request.path == "/o/userinfo/" @@ -11,12 +11,12 @@ def cors_allow_origin(sender, request, **kwargs): or request.path == "/o/.well-known/openid-configuration" or request.path == "/o/.well-known/openid-configuration/" # this is for testing the device authorization flow in the example rp. - # You would not normally have a browser-based client do this and shoudn't + # You would not normally have a browser-based client do this and shouldn't # open the following endpoints to CORS requests in a production environment. - or (origin == 'http://localhost:5173' and request.path == "/o/device-authorization") - or (origin == 'http://localhost:5173' and request.path == "/o/device-authorization/") - or (origin == 'http://localhost:5173' and request.path == "/o/token") - or (origin == 'http://localhost:5173' and request.path == "/o/token/") + or (origin == "http://localhost:5173" and request.path == "/o/device-authorization") + or (origin == "http://localhost:5173" and request.path == "/o/device-authorization/") + or (origin == "http://localhost:5173" and request.path == "/o/token") + or (origin == "http://localhost:5173" and request.path == "/o/token/") ) diff --git a/tests/app/idp/idp/settings.py b/tests/app/idp/idp/settings.py index 679407604..d0bf088dd 100644 --- a/tests/app/idp/idp/settings.py +++ b/tests/app/idp/idp/settings.py @@ -121,6 +121,7 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -141,6 +142,7 @@ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", + "django.template.context_processors.i18n", "django.contrib.messages.context_processors.messages", ], }, @@ -188,6 +190,18 @@ USE_TZ = True +LANGUAGES = [ + ("en", "English"), + ("es", "Español"), + ("fa", "فارسی"), + ("fr", "Français"), + ("ja", "日本語"), + ("pt", "Português"), + ("pt-br", "Português (Brasil)"), + ("tr", "Türkçe"), + ("zh-hans", "简体中文"), +] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ diff --git a/tests/app/idp/idp/urls.py b/tests/app/idp/idp/urls.py index 8be65c35b..49bbd5c5c 100644 --- a/tests/app/idp/idp/urls.py +++ b/tests/app/idp/idp/urls.py @@ -15,14 +15,24 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from django.conf import settings from django.contrib import admin from django.urls import include, path from django.views.generic import TemplateView +from django.views.static import serve urlpatterns = [ - path('', TemplateView.as_view(template_name='home/index.html'), name='home'), # Maps the root URL to your home_view + path( + "", TemplateView.as_view(template_name="home/index.html"), name="home" + ), # Maps the root URL to your home_view path("admin/", admin.site.urls), path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")), path("accounts/", include("django.contrib.auth.urls")), ] + +# just for local gunicorn +if not settings.DEBUG: + urlpatterns += [ + path(f"{settings.STATIC_URL.strip('/')}", serve, {"document_root": settings.STATIC_ROOT}), + ] diff --git a/tests/app/rp/Dockerfile b/tests/app/rp/Dockerfile index a719a1eb4..87fdb37a7 100644 --- a/tests/app/rp/Dockerfile +++ b/tests/app/rp/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine AS builder +FROM node:22-alpine AS builder WORKDIR /app COPY package*.json . RUN npm ci @@ -6,7 +6,7 @@ COPY . . RUN npm run build RUN npm prune --production -FROM node:18-alpine +FROM node:22-alpine WORKDIR /app COPY --from=builder /app/build build/ COPY --from=builder /app/node_modules node_modules/ diff --git a/tests/test_application_forms.py b/tests/test_application_forms.py new file mode 100644 index 000000000..6e10b6d07 --- /dev/null +++ b/tests/test_application_forms.py @@ -0,0 +1,57 @@ +from django.contrib.auth import get_user_model +from django.urls import reverse +from django.utils.encoding import force_str + +from oauth2_provider.forms import HASHED_WARNING_TEXT, ApplicationForm +from oauth2_provider.models import get_application_model + +from .common_testing import OAuth2ProviderTestCase as TestCase + + +Application = get_application_model() +UserModel = get_user_model() + + +class TestApplicationForm(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = UserModel.objects.create_user("test_user", "test@example.com", "123456") + cls.app = Application.objects.create( + name="Test App", + user=cls.user, + client_type=Application.CLIENT_CONFIDENTIAL, + authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, + hash_client_secret=True, + ) + + def test_has_warning_attribute_with_client_secret_field(self): + form = ApplicationForm() + client_secret_field = form.fields.get("client_secret") + + self.assertIn("data-hashed-warning", client_secret_field.widget.attrs) + + def test_application_views_contain_required_elements(self): + """ + Test that application views contain all elements needed for JavaScript to work. + + Note: The span with dynamic warning text is created by JavaScript at runtime. + This test just verifies the prerequisites (data attribute, JS file, DOM elements). + """ + self.client.login(username="test_user", password="123456") + + test_cases = [ + ("register", reverse("oauth2_provider:register")), + ("update", reverse("oauth2_provider:update", args=(self.app.pk,))), + ] + + for view_name, url in test_cases: + with self.subTest(view=view_name): + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + self.assertContains(response, 'id="id_hash_client_secret"') + self.assertContains(response, 'id="id_client_secret"') + self.assertContains(response, "data-hashed-warning") + self.assertContains(response, "oauth2_provider/admin/application_form.js") + # Warning text should be in data-hashed-warning attribute + self.assertContains(response, force_str(HASHED_WARNING_TEXT), count=1) diff --git a/tests/test_oauth2_validators.py b/tests/test_oauth2_validators.py index 3fb292060..c412cd2a5 100644 --- a/tests/test_oauth2_validators.py +++ b/tests/test_oauth2_validators.py @@ -228,7 +228,7 @@ def test_load_application_uses_cached_when_request_has_valid_client_matching_cli self.assertIs(self.request.client, self.application) def test_load_application_succeeds_when_request_has_invalid_client_valid_client_id(self): - self.request.client = 'invalid_client' + self.request.client = "invalid_client" application = self.validator._load_application("client_id", self.request) self.assertEqual(application, self.application) self.assertEqual(self.request.client, self.application)