feat: make ACCOUNT_MICROFRONTEND_URL site aware.

This commit is contained in:
Felipe Bermúdez-Mendoza
2026-03-02 11:43:23 +01:00
committed by Feanil Patel
parent 85e81b32e4
commit b367336d60
22 changed files with 521 additions and 17 deletions

View File

@@ -527,8 +527,12 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
"""
user = request.user
account_microfrontend_url = configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
)
if not UserProfile.objects.filter(user=user).exists():
return redirect(settings.ACCOUNT_MICROFRONTEND_URL)
return redirect(account_microfrontend_url)
if learner_home_mfe_enabled():
return redirect(settings.LEARNER_HOME_MICROFRONTEND_URL)
@@ -633,7 +637,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
"Go to {link_start}your Account Settings{link_end}.")
).format(
link_start=HTML("<a href='{account_setting_page}'>").format(
account_setting_page=settings.ACCOUNT_MICROFRONTEND_URL,
account_setting_page=account_microfrontend_url,
),
link_end=HTML("</a>")
)
@@ -902,7 +906,10 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
except DashboardRenderStarted.RenderInvalidDashboard as exc:
response = render_to_response(exc.dashboard_template, exc.template_context)
except DashboardRenderStarted.RedirectToPage as exc:
response = HttpResponseRedirect(exc.redirect_to or settings.ACCOUNT_MICROFRONTEND_URL)
response = HttpResponseRedirect(
exc.redirect_to or
account_microfrontend_url
)
except DashboardRenderStarted.RenderCustomResponse as exc:
response = exc.response
else:

View File

@@ -3,6 +3,7 @@ Tests for the Third Party Auth REST API
"""
import urllib
from types import SimpleNamespace
from unittest.mock import patch
import ddt
@@ -447,3 +448,24 @@ class TestThirdPartyAuthUserStatusView(ThirdPartyAuthTestMixin, APITestCase):
'connect_url': f'/auth/login/google-oauth2/?auth_entry=account_settings&next={next_url}',
'connected': False, 'id': 'oa2-google-oauth2'
}])
def test_get_uses_site_config_account_mfe_url(self):
"""
The providers API uses site-configured ACCOUNT_MICROFRONTEND_URL in the next link.
"""
self.client.login(username=self.user.username, password=PASSWORD)
siteconf_url = "https://accounts.siteconf.example"
helpers_stub = SimpleNamespace(
get_value=lambda key, default=None, *args, **kwargs:
siteconf_url if key == "ACCOUNT_MICROFRONTEND_URL" else default
)
with patch("common.djangoapps.third_party_auth.api.views.configuration_helpers", new=helpers_stub):
response = self.client.get(self.url)
assert response.status_code == 200
providers = response.data
google = next(p for p in providers if p["id"].endswith("google-oauth2"))
qs = urllib.parse.parse_qs(urllib.parse.urlparse(google["connect_url"]).query)
assert qs.get("next") == [siteconf_url]

View File

@@ -22,6 +22,7 @@ from openedx.core.lib.api.authentication import (
BearerAuthenticationAllowInactiveUser
)
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from common.djangoapps.third_party_auth import pipeline
from common.djangoapps.third_party_auth.api import serializers
from common.djangoapps.third_party_auth.api.permissions import TPA_PERMISSIONS
@@ -468,7 +469,10 @@ class ThirdPartyAuthUserStatusView(APIView):
state.provider.provider_id,
pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS,
# The url the user should be directed to after the auth process has completed.
redirect_url=settings.ACCOUNT_MICROFRONTEND_URL,
redirect_url=configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
),
),
'accepts_logins': state.provider.accepts_logins,
# If the user is connected, sending a POST request to this url removes the connection

View File

@@ -111,7 +111,10 @@ def _get_course_email_context(course):
'course_url': course_url,
'course_image_url': image_url,
'course_end_date': course_end_date,
'account_settings_url': settings.ACCOUNT_MICROFRONTEND_URL,
'account_settings_url': configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
),
'email_settings_url': '{}{}'.format(lms_root_url, reverse('dashboard')),
'logo_url': get_logo_url_for_email(),
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),

View File

@@ -463,6 +463,38 @@ class TestBulkEmailInstructorTask(InstructorTaskCourseTestCase):
assert 'email_settings_url' in result
assert 'platform_name' in result
def test_account_settings_url_uses_site_config_value(self):
"""
If site configuration defines ACCOUNT_MICROFRONTEND_URL, the email context
should use that value.
"""
siteconf_url = "https://accounts.siteconf.example"
with patch(
"lms.djangoapps.bulk_email.tasks.configuration_helpers.get_value",
side_effect=lambda key, default=None, *a, **k:
siteconf_url if key == "ACCOUNT_MICROFRONTEND_URL" else default,
):
ctx = _get_course_email_context(self.course)
assert ctx["account_settings_url"] == siteconf_url
def test_account_settings_url_falls_back_to_settings(self):
"""
If site configuration does not override, fall back to settings.ACCOUNT_MICROFRONTEND_URL.
"""
fallback = "https://accounts.settings.example"
with override_settings(ACCOUNT_MICROFRONTEND_URL=fallback):
with patch(
"lms.djangoapps.bulk_email.tasks.configuration_helpers.get_value",
side_effect=lambda key, default=None, *a, **k: default,
):
ctx = _get_course_email_context(self.course)
assert ctx["account_settings_url"] == fallback
assert ctx["account_settings_url"] == settings.ACCOUNT_MICROFRONTEND_URL
@override_settings(BULK_COURSE_EMAIL_LAST_LOGIN_ELIGIBILITY_PERIOD=1)
def test_ineligible_recipients_filtered_by_last_login(self):
"""

View File

@@ -818,6 +818,56 @@ class ViewsTestCase(BaseViewsTestCase):
response = self.client.get(url)
self.assertRedirects(response, reverse('signin_user') + '?next=' + url)
def test_financial_assistance_form_uses_site_config_account_mfe_url(self):
"""
When site configuration defines ACCOUNT_MICROFRONTEND_URL, the view should
pass that URL in the render context as 'account_settings_url'.
"""
siteconf_url = "https://accounts.siteconf.example"
captured = {}
def fake_render(template_name, context, *args, **kwargs):
captured['context'] = context
return HttpResponse("ok")
with patch.object(views, 'render_to_response', new=fake_render):
with patch.object(
views.configuration_helpers,
'get_value',
side_effect=lambda key, default=None, *a, **k:
siteconf_url if key == "ACCOUNT_MICROFRONTEND_URL" else default,
):
resp = self.client.get(reverse('financial_assistance_form'))
assert resp.status_code == 200
assert 'context' in captured
assert captured['context']['account_settings_url'] == siteconf_url
def test_financial_assistance_form_falls_back_to_settings_for_account_mfe(self):
"""
If site configuration doesn't override, fall back to settings.ACCOUNT_MICROFRONTEND_URL.
"""
fallback = "https://accounts.settings.example"
captured = {}
def fake_render(template_name, context, *args, **kwargs):
captured['context'] = context
return HttpResponse("ok")
with override_settings(ACCOUNT_MICROFRONTEND_URL=fallback):
with patch.object(views, 'render_to_response', new=fake_render):
with patch.object(
views.configuration_helpers,
'get_value',
side_effect=lambda key, default=None, *a, **k: default,
):
resp = self.client.get(reverse('financial_assistance_form'))
assert resp.status_code == 200
assert 'context' in captured
assert captured['context']['account_settings_url'] == fallback
assert captured['context']['account_settings_url'] == settings.ACCOUNT_MICROFRONTEND_URL
# Patching 'lms.djangoapps.courseware.views.views.get_programs' would be ideal,
# but for some unknown reason that patch doesn't seem to be applied.

View File

@@ -2232,7 +2232,10 @@ def financial_assistance_form(request, course_id=None):
'header_text': _get_fa_header(FINANCIAL_ASSISTANCE_HEADER),
'course_id': course_id,
'dashboard_url': reverse('dashboard'),
'account_settings_url': settings.ACCOUNT_MICROFRONTEND_URL,
'account_settings_url': configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
),
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
'user_details': {
'email': user.email,

View File

@@ -22,6 +22,7 @@ from lms.djangoapps.verify_student.message_types import VerificationExpiry
from lms.djangoapps.verify_student.models import ManualVerification, SoftwareSecurePhotoVerification, SSOVerification
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from openedx.core.lib.celery.task_utils import emulate_http_request
@@ -188,7 +189,13 @@ class Command(BaseCommand):
return True
site = Site.objects.get_current()
account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
account_base_url = (
configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
) or
""
).rstrip('/')
message_context = get_base_template_context(site)
message_context.update({
'platform_name': settings.PLATFORM_NAME,

View File

@@ -3,6 +3,7 @@ Tests for django admin command `send_verification_expiry_email` in the verify_st
"""
from contextlib import nullcontext
from datetime import timedelta
from unittest.mock import patch
@@ -302,3 +303,68 @@ class TestSendVerificationExpiryEmail(MockS3Boto3Mixin, TestCase):
assert verifications[0].expiry_email_date is None
assert verifications[1].expiry_email_date is not None
assert mock_ace.send.call_count == 2
def _run_cmd_and_get_verification_link(self, get_value_side_effect, *, fallback=None):
"""
Create one expired, approved verification; run the command with patches;
return the lms_verification_link computed in the message context.
"""
self.create_expired_software_secure_photo_verification()
captured_contexts = []
class FakeVerificationExpiry:
"""Mock message class capturing context for verification expiry tests."""
def __init__(self, context):
captured_contexts.append(context)
def personalize(self, recipient, language=None, user_context=None):
return object()
settings_ctx = (
override_settings(ACCOUNT_MICROFRONTEND_URL=fallback)
if fallback is not None else
nullcontext()
)
module = (
"lms.djangoapps.verify_student.management.commands."
"send_verification_expiry_email"
)
with (
settings_ctx,
patch(f"{module}.VerificationExpiry", new=FakeVerificationExpiry),
patch(f"{module}.ace.send") as mock_send,
patch(
f"{module}.configuration_helpers.get_value",
side_effect=get_value_side_effect,
),
):
call_command("send_verification_expiry_email")
assert mock_send.called, "Expected at least one email to be sent"
assert captured_contexts, ("VerificationExpiry(context=...) should have been called")
return captured_contexts[0]["lms_verification_link"]
def test_account_mfe_link_uses_site_config_value(self):
"""Test verification link uses site-configured ACCOUNT_MICROFRONTEND_URL."""
siteconf_url = "https://accounts.siteconf.example/"
link = self._run_cmd_and_get_verification_link(
get_value_side_effect=lambda key, default=None, *a, **k:
siteconf_url if key == "ACCOUNT_MICROFRONTEND_URL" else default
)
assert link == "https://accounts.siteconf.example/id-verification"
def test_account_mfe_link_falls_back_to_settings(self):
"""Test verification link falls back to settings when site config is absent."""
fallback = "https://accounts.settings.example/"
link = self._run_cmd_and_get_verification_link(
get_value_side_effect=lambda key, default=None, *a, **k: default,
fallback=fallback,
)
assert link == "https://accounts.settings.example/id-verification"

View File

@@ -251,7 +251,13 @@ class IDVerificationService:
Returns a string:
Returns URL for IDV on Account Microfrontend
"""
account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
account_base_url = (
configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
) or
""
).rstrip('/')
location = f'{account_base_url}/id-verification'
if course_id:
location += f'?course_id={quote(str(course_id))}'

View File

@@ -3,8 +3,10 @@ Tests for the service classes in verify_student.
"""
import itertools
from contextlib import nullcontext
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
from urllib.parse import quote
import ddt
from django.conf import settings
@@ -230,6 +232,89 @@ class TestIDVerificationService(ModuleStoreTestCase):
expiration_datetime = IDVerificationService.get_expiration_datetime(user, ['approved'])
assert expiration_datetime == newest.expiration_datetime
def _get_verify_url_with_config(
self,
*,
get_value_side_effect,
course_id=None,
settings_account_mfe=None,
):
"""
Build the IDV URL using a patched configuration helper and optional settings override.
Returns the final URL after the (stubbed) filter runs.
"""
settings_ctx = (
override_settings(ACCOUNT_MICROFRONTEND_URL=settings_account_mfe)
if settings_account_mfe is not None
else nullcontext()
)
with settings_ctx:
with patch(
"lms.djangoapps.verify_student.services.configuration_helpers.get_value",
side_effect=get_value_side_effect,
):
with patch(
"lms.djangoapps.verify_student.services.IDVPageURLRequested.run_filter",
side_effect=lambda url: url,
):
return IDVerificationService.get_verify_location(course_id)
def test_get_verify_location_uses_site_config_value(self):
"""
If site config defines ACCOUNT_MICROFRONTEND_URL (with trailing slash),
the base should come from it and the slash should be stripped.
"""
siteconf_url = "https://accounts.siteconf.example/" # trailing slash on purpose
url = self._get_verify_url_with_config(
get_value_side_effect=lambda key, default=None, *a, **k:
siteconf_url if key == "ACCOUNT_MICROFRONTEND_URL" else default
)
assert url == "https://accounts.siteconf.example/id-verification"
def test_get_verify_location_uses_site_config_value_with_course_id(self):
"""
Same as above, but with a course_id. Ensure proper quoting and base from site config.
"""
course = CourseFactory.create(org='Robot', number='999', display_name='Test Course')
siteconf_url = "https://accounts.siteconf.example/"
url = self._get_verify_url_with_config(
get_value_side_effect=lambda key, default=None, *a, **k:
siteconf_url if key == "ACCOUNT_MICROFRONTEND_URL" else default,
course_id=course.id,
)
expected = f"https://accounts.siteconf.example/id-verification?course_id={quote(str(course.id), safe='')}"
assert url == expected
def test_get_verify_location_falls_back_to_settings(self):
"""
If site config does not override, fall back to settings.ACCOUNT_MICROFRONTEND_URL
(and strip any trailing slash).
"""
fallback = "https://accounts.settings.example/" # trailing slash on purpose
url = self._get_verify_url_with_config(
get_value_side_effect=lambda key, default=None, *a, **k: default,
settings_account_mfe=fallback,
)
assert url == "https://accounts.settings.example/id-verification"
def test_get_verify_location_when_site_config_empty(self):
"""
If site config explicitly returns an empty string, we should still produce a valid
absolute path under the LMS domain.
"""
url = self._get_verify_url_with_config(
get_value_side_effect=lambda key, default=None, *a, **k: ""
)
assert url == "/id-verification"
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
@ddt.ddt

View File

@@ -1757,6 +1757,73 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase, TestVerification
)
self.assertContains(response, 'Result Unknown not understood', status_code=400)
@patch("lms.djangoapps.verify_student.views.send_verification_status_email.delay")
def test_failed_status_reverify_url_uses_site_config_value(self, mock_delay):
"""
When site config defines ACCOUNT_MICROFRONTEND_URL (with a trailing slash),
the reverify_url in the denial email context should use it and strip the slash.
"""
siteconf_url = "https://accounts.siteconf.example/"
access_key = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_ACCESS_KEY"]
with patch(
"lms.djangoapps.verify_student.views.configuration_helpers.get_value",
side_effect=lambda key, default=None, *a, **k:
siteconf_url if key == "ACCOUNT_MICROFRONTEND_URL" else default,
):
data = {
"EdX-ID": self.receipt_id,
"Result": "FAIL",
"Reason": [{"photoIdReasons": ["Not provided"]}],
"MessageType": "Your photo doesn't meet standards.",
}
json_data = json.dumps(data)
response = self.client.post(
reverse("verify_student_results_callback"),
data=json_data,
content_type="application/json",
HTTP_AUTHORIZATION=f"test {access_key}:testing",
HTTP_DATE="testdate",
)
assert response.status_code == 200
assert mock_delay.called
context = mock_delay.call_args[0][0]
assert context["email_vars"]["reverify_url"] == "https://accounts.siteconf.example/id-verification"
@patch("lms.djangoapps.verify_student.views.send_verification_status_email.delay")
def test_failed_status_reverify_url_falls_back_to_settings(self, mock_delay):
"""
If site config doesn't override, reverify_url should come from
settings.ACCOUNT_MICROFRONTEND_URL (with trailing slash stripped).
"""
fallback = "https://accounts.settings.example/"
access_key = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_ACCESS_KEY"]
with override_settings(ACCOUNT_MICROFRONTEND_URL=fallback), patch(
"lms.djangoapps.verify_student.views.configuration_helpers.get_value",
side_effect=lambda key, default=None, *a, **k: default,
):
data = {
"EdX-ID": self.receipt_id,
"Result": "FAIL",
"Reason": [{"photoIdReasons": ["Not provided"]}],
"MessageType": "Your photo doesn't meet standards.",
}
json_data = json.dumps(data)
response = self.client.post(
reverse("verify_student_results_callback"),
data=json_data,
content_type="application/json",
HTTP_AUTHORIZATION=f"test {access_key}:testing",
HTTP_DATE="testdate",
)
assert response.status_code == 200
assert mock_delay.called
context = mock_delay.call_args[0][0]
assert context["email_vars"]["reverify_url"] == "https://accounts.settings.example/id-verification"
class TestReverifyView(TestVerificationBase):
"""

View File

@@ -1128,7 +1128,13 @@ def results_callback(request): # lint-amnesty, pylint: disable=too-many-stateme
log.info("[COSMO-184] Denied verification for receipt_id={receipt_id}.".format(receipt_id=receipt_id))
attempt.deny(json.dumps(reason), error_code=error_code)
account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
account_base_url = (
configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
) or
""
).rstrip('/')
reverify_url = f'{account_base_url}/id-verification'
verification_status_email_vars['reasons'] = reason
verification_status_email_vars['reverify_url'] = reverify_url

View File

@@ -11,6 +11,7 @@ from django.urls import reverse
from django.utils.translation import gettext as _
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.features.enterprise_support.utils import get_enterprise_learner_generic_name, get_enterprise_learner_portal
%>
@@ -45,7 +46,7 @@ should_show_order_history = not enterprise_customer_portal and settings.ORDER_HI
% endif
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${urljoin(settings.PROFILE_MICROFRONTEND_URL, f'{user.username}')}" role="menuitem">${_("Profile")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${settings.ACCOUNT_MICROFRONTEND_URL}" role="menuitem">${_("Account")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${configuration_helpers.get_value('ACCOUNT_MICROFRONTEND_URL', settings.ACCOUNT_MICROFRONTEND_URL)}" role="menuitem">${_("Account")}</a></div>
% if should_show_order_history:
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${settings.ORDER_HISTORY_MICROFRONTEND_URL}" role="menuitem">${_("Order History")}</a></div>
% endif

View File

@@ -8,6 +8,7 @@ from django.utils.translation import gettext as _
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user
from openedx.features.enterprise_support.utils import get_enterprise_learner_generic_name
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
%>
<%
@@ -28,7 +29,7 @@ profile_image_url = get_profile_image_urls_for_user(self.real_user)['medium']
<a data-hj-suppress class="dropdown-toggle" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${username}</a>
<ul role="menu" class="dropdown-menu dropdown-menu-right" id="${_("Usermenu")}" aria-labelledby="dropdownMenuLink" tabindex="-1">
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${reverse('dashboard')}">${_("Dashboard")}</a></li>
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${settings.ACCOUNT_MICROFRONTEND_URL}">${_("Account")}</a></li>
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${configuration_helpers.get_value('ACCOUNT_MICROFRONTEND_URL', settings.ACCOUNT_MICROFRONTEND_URL)}">${_("Account")}</a></li>
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${reverse('logout')}">${_("Sign Out")}</a></li>
</ul>
</div>
@@ -36,7 +37,7 @@ profile_image_url = get_profile_image_urls_for_user(self.real_user)['medium']
</div>
<ul role="menu" class="nav flex-column align-items-center">
<li role="presentation" class="nav-item nav-item-open-collapsed-only collapse"><a role="menuitem" href="${reverse('dashboard')}">${_("Dashboard")}</a></li>
<li role="presentation" class="nav-item nav-item-open-collapsed-only"><a role="menuitem" href="${settings.ACCOUNT_MICROFRONTEND_URL}">${_("Account")}</a></li>
<li role="presentation" class="nav-item nav-item-open-collapsed-only"><a role="menuitem" href="${configuration_helpers.get_value('ACCOUNT_MICROFRONTEND_URL', settings.ACCOUNT_MICROFRONTEND_URL)}">${_("Account")}</a></li>
<li role="presentation" class="nav-item nav-item-open-collapsed-only"><a role="menuitem" href="${reverse('logout')}">${_("Sign Out")}</a></li>
</ul>
% else:

View File

@@ -199,6 +199,38 @@ class TestContextFunctions(ModuleStoreTestCase):
assert_list_equal(context['email_digest_updates'], expected_digest_updates)
assert_list_equal(context['email_content'], expected_email_content)
def test_email_template_context_notification_settings_url_uses_site_config(self):
"""
When site configuration defines ACCOUNT_MICROFRONTEND_URL (with a trailing slash),
the context should build notification_settings_url from it and strip the slash.
"""
siteconf_url = "https://accounts.siteconf.example/"
with patch(
"openedx.core.djangoapps.notifications.email.utils.configuration_helpers.get_value",
side_effect=lambda key, default=None, *a, **k:
siteconf_url if key == "ACCOUNT_MICROFRONTEND_URL" else default,
):
ctx = create_email_template_context(self.user.username)
assert ctx["notification_settings_url"] == "https://accounts.siteconf.example/#notifications"
def test_email_template_context_notification_settings_url_falls_back_to_settings(self):
"""
If site config doesn't override, the context should fall back to
settings.ACCOUNT_MICROFRONTEND_URL (also stripping any trailing slash).
"""
fallback = "https://accounts.settings.example/"
with override_settings(ACCOUNT_MICROFRONTEND_URL=fallback):
with patch(
"openedx.core.djangoapps.notifications.email.utils.configuration_helpers.get_value",
side_effect=lambda key, default=None, *a, **k: default,
):
ctx = create_email_template_context(self.user.username)
assert ctx["notification_settings_url"] == "https://accounts.settings.example/#notifications"
class TestWaffleFlag(ModuleStoreTestCase):
"""

View File

@@ -101,7 +101,13 @@ def create_email_template_context(username):
'channel': 'email',
'value': False
}
account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
account_base_url = (
configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
) or
""
).rstrip('/')
return {
"platform_name": settings.PLATFORM_NAME,
"mailing_address": settings.CONTACT_MAILING_ADDRESS,

View File

@@ -9,6 +9,7 @@ from django.conf import settings
from django.utils.translation import gettext as _
from openedx.core.djangolib.markup import HTML
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from common.djangoapps.util.date_utils import DEFAULT_SHORT_DATE_FORMAT, strftime_localized
from common.djangoapps.util.password_policy_validators import validate_password
@@ -97,7 +98,10 @@ def enforce_compliance_on_login(user, password):
platform_name=settings.PLATFORM_NAME,
deadline=strftime_localized(deadline, DEFAULT_SHORT_DATE_FORMAT),
anchor_tag_open=HTML('<a href="{account_settings_url}">').format(
account_settings_url=settings.ACCOUNT_MICROFRONTEND_URL
account_settings_url=configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
),
),
anchor_tag_close=HTML('</a>')
)

View File

@@ -199,3 +199,56 @@ class TestCompliance(TestCase):
assert date1 == _get_compliance_deadline_for_user(staff)
assert date1 == _get_compliance_deadline_for_user(privileged)
assert date1 == _get_compliance_deadline_for_user(user)
def test_warning_link_uses_site_config_account_mfe_url(self):
"""
When site config defines ACCOUNT_MICROFRONTEND_URL, the warning text should
use that value for the Account Settings anchor href.
"""
user = UserFactory()
password = "irrelevant"
siteconf_url = "https://accounts.siteconf.example"
with patch(
"openedx.core.djangoapps.password_policy.compliance._check_user_compliance",
return_value=False,
), patch(
"openedx.core.djangoapps.password_policy.compliance._get_compliance_deadline_for_user",
return_value=datetime.now(ZoneInfo("UTC")) + timedelta(days=1),
), patch(
"openedx.core.djangoapps.password_policy.compliance.configuration_helpers.get_value",
side_effect=lambda key, default=None, *a, **k:
siteconf_url if key == "ACCOUNT_MICROFRONTEND_URL" else default,
):
with pytest.raises(NonCompliantPasswordWarning) as excinfo:
enforce_compliance_on_login(user, password)
msg = str(excinfo.value)
assert f'<a href="{siteconf_url}">Account Settings</a>' in msg
def test_warning_link_falls_back_to_settings_account_mfe_url(self):
"""
If site config doesn't override, the warning text should fall back to
settings.ACCOUNT_MICROFRONTEND_URL for the anchor href.
"""
user = UserFactory()
password = "irrelevant"
fallback = "https://accounts.settings.example"
with override_settings(ACCOUNT_MICROFRONTEND_URL=fallback):
with patch(
"openedx.core.djangoapps.password_policy.compliance._check_user_compliance",
return_value=False,
), patch(
"openedx.core.djangoapps.password_policy.compliance._get_compliance_deadline_for_user",
return_value=datetime.now(ZoneInfo("UTC")) + timedelta(days=1),
), patch(
"openedx.core.djangoapps.password_policy.compliance.configuration_helpers.get_value",
# Simulate "no override": return default argument
side_effect=lambda key, default=None, *a, **k: default,
):
with pytest.raises(NonCompliantPasswordWarning) as excinfo:
enforce_compliance_on_login(user, password)
msg = str(excinfo.value)
assert f'<a href="{fallback}">Account Settings</a>' in msg

View File

@@ -6,6 +6,8 @@ from django.urls import path, re_path, include
from django.views.generic import RedirectView
from rest_framework import routers
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from . import views as user_api_views
from .models import UserPreference
@@ -17,7 +19,12 @@ urlpatterns = [
# This redirect is needed for backward compatibility with the old URL structure for the authentication
# workflows using third-party authentication providers until the authentication workflows fully support
# the URL structure with MFEs.
re_path(r'^account(?:/settings)?/?$', RedirectView.as_view(url=settings.ACCOUNT_MICROFRONTEND_URL)),
re_path(r'^account(?:/settings)?/?$', RedirectView.as_view(
url=configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
)),
),
path('user_api/v1/', include(USER_API_ROUTER.urls)),
re_path(
fr'^user_api/v1/preferences/(?P<pref_key>{UserPreference.KEY_REGEX})/users/$',

View File

@@ -23,6 +23,7 @@ from openedx.core.djangoapps.oauth_dispatch.api import create_dot_access_token
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_from_token
from openedx.core.djangoapps.user_api.accounts.utils import retrieve_last_sitewide_block_completed
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from common.djangoapps.util.json_request import JsonResponse
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user
@@ -245,7 +246,10 @@ def _get_user_info_cookie_data(request, user):
# External sites will need to have fallback mechanisms to handle this case
# (most likely just hiding the links).
try:
header_urls['account_settings'] = settings.ACCOUNT_MICROFRONTEND_URL
header_urls['account_settings'] = configuration_helpers.get_value(
'ACCOUNT_MICROFRONTEND_URL',
settings.ACCOUNT_MICROFRONTEND_URL,
)
header_urls['learner_profile'] = urljoin(settings.PROFILE_MICROFRONTEND_URL, f'/u/{user.username}')
except NoReverseMatch:
pass

View File

@@ -8,7 +8,7 @@ from unittest.mock import MagicMock, patch
from urllib.parse import urljoin
from django.conf import settings
from django.http import HttpResponse
from django.test import RequestFactory, TestCase
from django.test import RequestFactory, TestCase, override_settings
from django.urls import reverse
from edx_rest_framework_extensions.auth.jwt.decoder import jwt_decode_handler
from edx_rest_framework_extensions.auth.jwt.middleware import JwtAuthCookieMiddleware
@@ -154,3 +154,41 @@ class CookieTests(TestCase):
self._assert_cookies_present(response, cookies_api.JWT_COOKIE_NAMES)
self._assert_consistent_expires(response, num_of_unique_expires=1)
self._assert_recreate_jwt_from_cookies(response, can_recreate=True)
@skip_unless_lms
def test_user_info_cookie_account_settings_uses_site_config_value(self):
"""
When site config overrides ACCOUNT_MICROFRONTEND_URL, the user info cookie
should use that exact value for 'account_settings'.
"""
siteconf_url = "https://accounts.siteconf.example"
with patch(
"openedx.core.djangoapps.user_authn.cookies.configuration_helpers.get_value",
side_effect=lambda key, default=None, *a, **k:
siteconf_url if key == "ACCOUNT_MICROFRONTEND_URL" else default,
):
data = cookies_api._get_user_info_cookie_data(self.request, self.user) # pylint: disable=protected-access
assert data["header_urls"]["account_settings"] == siteconf_url
@skip_unless_lms
def test_user_info_cookie_account_settings_falls_back_to_settings(self):
"""
If site config does not override, the user info cookie should fall back to
settings.ACCOUNT_MICROFRONTEND_URL for 'account_settings'.
"""
fallback = "https://accounts.settings.example"
with override_settings(ACCOUNT_MICROFRONTEND_URL=fallback):
with patch(
"openedx.core.djangoapps.user_authn.cookies.configuration_helpers.get_value",
# Simulate "no override": return the provided default
side_effect=lambda key, default=None, *a, **k: default,
):
data = cookies_api._get_user_info_cookie_data( # pylint: disable=protected-access
self.request,
self.user,
)
assert data["header_urls"]["account_settings"] == fallback