feat: make ACCOUNT_MICROFRONTEND_URL site aware.
This commit is contained in:
committed by
Feanil Patel
parent
85e81b32e4
commit
b367336d60
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))}'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user