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