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

@@ -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