From 10a9ecabe2a41a0d42372d7773747c4e49910e21 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Thu, 10 Sep 2026 12:51:24 -0400 Subject: [PATCH 1/4] feat: default the authn MFE (logistration) on Flip ENABLE_AUTHN_MICROFRONTEND to default True so vanilla installs serve the frontend-app-authn login/registration experience by default, matching Tutor deployments which already enable it. Operators can still opt out with EDXAPP_ENABLE_AUTHN_MFE=False. Enterprise/SAML/TPA learners are unaffected: the has_external_provider guard in login_form.py keeps them on the legacy page until the authn MFE renders those flows (openedx/frontend-app-authn#1691). Part of the legacy logistration removal (openedx/public-engineering#81), tracked in openedx/openedx-platform#38936. Co-Authored-By: Claude Opus 4.8 --- lms/envs/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index f7a6f15558cb..7c67541a9957 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -629,7 +629,7 @@ # .. toggle_name: settings.ENABLE_AUTHN_MICROFRONTEND # .. toggle_implementation: DjangoSetting -# .. toggle_default: False +# .. toggle_default: True # .. toggle_description: Supports staged rollout of a new micro-frontend-based implementation of the logistration. # .. toggle_use_cases: temporary, open_edx # .. toggle_creation_date: 2020-09-08 @@ -637,7 +637,7 @@ # .. toggle_tickets: 'https://github.com/openedx/edx-platform/pull/24908' # .. toggle_warning: Also set settings.AUTHN_MICROFRONTEND_URL for rollout. This temporary feature # toggle does not have a target removal date. -ENABLE_AUTHN_MICROFRONTEND = os.environ.get("EDXAPP_ENABLE_AUTHN_MFE", False) +ENABLE_AUTHN_MICROFRONTEND = os.environ.get("EDXAPP_ENABLE_AUTHN_MFE", True) # .. toggle_name: settings.ENABLE_CATALOG_MICROFRONTEND # .. toggle_implementation: DjangoSetting From a151eff1878c3e729b21e74795c03c0afa4117fe Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 14 Sep 2026 10:07:41 -0400 Subject: [PATCH 2/4] test: stop fetching the login page in unrelated redirect assertions These tests all assert the same incidental thing: an unauthenticated request gets bounced to the login page. None of them care about what the login page then renders, but assertRedirects fetches the redirect target and requires a 200 from it by default, so they were all coupled to it anyway. With the authn MFE on by default /login answers a 302 and every one of them fails with "Couldn't retrieve redirection page '/login'". Pass fetch_redirect_response=False so they assert the redirect target and stop there. That is the behavior each of these tests is actually about, and it holds whichever logistration frontend is in use. course_wiki gets the same treatment in a different shape. It followed the whole chain and checked the last hop, which now loops forever: the test settings point AUTHN_MICROFRONTEND_URL at http://authn-mfe, and the test client resolves that host against the same URLconf, so /login forwards to itself until Django raises RedirectCycleError. There is no chain to follow in the first place - WikiAccessMiddleware redirects straight to signin_user (lms/djangoapps/course_wiki/middleware.py:55) - so assert on the first Location instead. The theming test loses its LMS/Studio branch on target_status_code for the same reason; both sides now redirect onward. Co-Authored-By: Claude Opus 5 (1M context) --- lms/djangoapps/course_wiki/tests/tests.py | 9 +++------ lms/djangoapps/courseware/tests/test_course_survey.py | 3 ++- lms/djangoapps/courseware/tests/test_views.py | 2 +- lms/djangoapps/learner_dashboard/tests/test_programs.py | 6 ++++-- lms/djangoapps/support/tests/test_views.py | 2 +- lms/djangoapps/teams/tests/test_views.py | 2 +- .../djangoapps/cache_toolbox/tests/test_middleware.py | 2 +- openedx/core/djangoapps/theming/tests/test_views.py | 4 +--- 8 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lms/djangoapps/course_wiki/tests/tests.py b/lms/djangoapps/course_wiki/tests/tests.py index 42d14baab469..d0fa9fafcf54 100644 --- a/lms/djangoapps/course_wiki/tests/tests.py +++ b/lms/djangoapps/course_wiki/tests/tests.py @@ -166,14 +166,11 @@ def test_redirect_when_not_logged_in(self): self.logout() course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'}) - # When not logged in, we should get a 302 + # When not logged in, we should get a 302 to the login page. Don't follow the + # redirect: the login page may itself redirect on to the authn MFE. resp = self.client.get(course_wiki_page, follow=False) assert resp.status_code == 302 - - # and end up at the login page - resp = self.client.get(course_wiki_page, follow=True) - target_url, __ = resp.redirect_chain[-1] - assert reverse('signin_user') in target_url + assert reverse('signin_user') in resp['Location'] @override_settings(ALLOW_WIKI_ROOT_ACCESS=True) def test_create_wiki_with_long_course_id(self): diff --git a/lms/djangoapps/courseware/tests/test_course_survey.py b/lms/djangoapps/courseware/tests/test_course_survey.py index df89031b0b47..f7db1e2b3879 100644 --- a/lms/djangoapps/courseware/tests/test_course_survey.py +++ b/lms/djangoapps/courseware/tests/test_course_survey.py @@ -149,7 +149,8 @@ def test_anonymous_user_visiting_course_with_survey(self): ) self.assertRedirects( resp, - f'/login?next=/courses/{quote(str(self.course.id))}/courseware' + f'/login?next=/courses/{quote(str(self.course.id))}/courseware', + fetch_redirect_response=False ) def test_visiting_course_with_existing_answers(self): diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index b7ca242a4c4e..a655b2f87c3a 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -803,7 +803,7 @@ def test_financial_assistance_login_required(self): ): self.client.logout() response = self.client.get(url) - self.assertRedirects(response, reverse('signin_user') + '?next=' + url) + self.assertRedirects(response, reverse('signin_user') + '?next=' + url, fetch_redirect_response=False) def test_financial_assistance_form_uses_site_config_account_mfe_url(self): """ diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py index 3036c37f4c86..fec11905753b 100644 --- a/lms/djangoapps/learner_dashboard/tests/test_programs.py +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -102,7 +102,8 @@ def test_login_required(self, mock_get_programs): response = self.client.get(self.url) self.assertRedirects( response, - '{}?next={}'.format(reverse('signin_user'), self.url) + '{}?next={}'.format(reverse('signin_user'), self.url), + fetch_redirect_response=False ) self.client.login(username=self.user.username, password=self.TEST_PASSWORD) @@ -267,7 +268,8 @@ def test_login_required(self, mock_get_programs, mock_get_pathways): response = self.client.get(self.url) self.assertRedirects( response, - '{}?next={}'.format(reverse('signin_user'), self.url) + '{}?next={}'.format(reverse('signin_user'), self.url), + fetch_redirect_response=False ) self.client.login(username=self.user.username, password=self.TEST_PASSWORD) diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index 7e2e04ea1f68..a1dfa59529a2 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -240,7 +240,7 @@ def test_require_login(self, url_name): login_url=reverse("signin_user"), original_url=quote(url), ) - self.assertRedirects(response, redirect_url) + self.assertRedirects(response, redirect_url, fetch_redirect_response=False) class SupportViewIndexTests(SupportViewTestCase): diff --git a/lms/djangoapps/teams/tests/test_views.py b/lms/djangoapps/teams/tests/test_views.py index bfb17002bc07..ba7b3b85c712 100644 --- a/lms/djangoapps/teams/tests/test_views.py +++ b/lms/djangoapps/teams/tests/test_views.py @@ -84,7 +84,7 @@ def test_anonymous(self): anonymous_client = APIClient() response = anonymous_client.get(self.teams_url) redirect_url = f'{settings.LOGIN_URL}?next={quote(self.teams_url)}' - self.assertRedirects(response, redirect_url) + self.assertRedirects(response, redirect_url, fetch_redirect_response=False) def test_not_enrolled_not_staff(self): """ Verifies that a student who is not enrolled cannot access the team dashboard. """ diff --git a/openedx/core/djangoapps/cache_toolbox/tests/test_middleware.py b/openedx/core/djangoapps/cache_toolbox/tests/test_middleware.py index e3efcfaa2913..4cf32766a98c 100644 --- a/openedx/core/djangoapps/cache_toolbox/tests/test_middleware.py +++ b/openedx/core/djangoapps/cache_toolbox/tests/test_middleware.py @@ -43,7 +43,7 @@ def test_session_change_lms(self): response = self.client.get(dashboard_url) redirect_url = reverse('signin_user') + '?next=' + dashboard_url - self.assertRedirects(response, redirect_url, target_status_code=200) + self.assertRedirects(response, redirect_url, fetch_redirect_response=False) mock_set_custom_attribute.assert_any_call('failed_session_verification', True) @skip_unless_cms diff --git a/openedx/core/djangoapps/theming/tests/test_views.py b/openedx/core/djangoapps/theming/tests/test_views.py index 992bf92db249..ceeda106028a 100644 --- a/openedx/core/djangoapps/theming/tests/test_views.py +++ b/openedx/core/djangoapps/theming/tests/test_views.py @@ -27,15 +27,13 @@ def test_preview_theme_access(self): """ # Anonymous users get redirected to the login page response = self.client.get(THEMING_ADMIN_URL) - # Studio login redirects to LMS login - expected_target_status_code = 200 if settings.ROOT_URLCONF == 'lms.urls' else 302 self.assertRedirects( response, '{login_url}?next={url}'.format( # noqa: UP032 login_url=settings.LOGIN_URL, url=THEMING_ADMIN_URL, ), - target_status_code=expected_target_status_code + fetch_redirect_response=False ) # Logged in non-global staff get a 404 From 284c0eb8be02449f34fb9dc4ec28c0119679bf2f Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 14 Sep 2026 10:09:44 -0400 Subject: [PATCH 3/4] test: pin the legacy logistration tests to ENABLE_AUTHN_MICROFRONTEND=False These tests are about the legacy combined login/registration page itself - its rendered context, its TPA provider list and hinted-login dialog, the Register link that ALLOW_PUBLIC_ACCOUNT_CREATION hides, the filters that fire while it renders, the activation redirects, and the reset link the recover_account command mails out. They assert on that page's content, so they only make sense with the flag that serves it. Flipping the default sent /login, /register and /password_assistance on to the authn MFE and turned all of them into "302 != 200". Pin the flag off at the class (or method, for the one-off in the support tests) so they keep covering the legacy page, which is still reachable: operators can opt out with EDXAPP_ENABLE_AUTHN_MFE=False, ?skip_authn_mfe short-circuits the redirect, and SAML/TPA learners stay on it via the has_external_provider guard in login_form.py. The MFE-path cases in these same classes already carry their own per-method override and keep winning over the class one. These pins come out when the legacy page is deleted, along with the tests. Part of openedx/public-engineering#81, tracked in openedx/openedx-platform#38936. Co-Authored-By: Claude Opus 5 (1M context) --- .../student/management/tests/test_recover_account.py | 5 +++++ common/djangoapps/student/tests/test_activate_account.py | 4 ++++ lms/djangoapps/support/tests/test_views.py | 4 ++++ .../core/djangoapps/user_authn/views/tests/test_filters.py | 1 + .../djangoapps/user_authn/views/tests/test_logistration.py | 6 +++++- 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/student/management/tests/test_recover_account.py b/common/djangoapps/student/management/tests/test_recover_account.py index bd1cbf3a8e9b..ee070ee3caed 100644 --- a/common/djangoapps/student/management/tests/test_recover_account.py +++ b/common/djangoapps/student/management/tests/test_recover_account.py @@ -20,9 +20,14 @@ LOGGER_NAME = 'common.djangoapps.student.management.commands.recover_account' +@override_settings(ENABLE_AUTHN_MICROFRONTEND=False) class RecoverAccountTests(TestCase): """ Test account recovery and exception handling + + The reset link the command mails out points at the authn MFE when + ENABLE_AUTHN_MICROFRONTEND is on, so the legacy-path cases pin it off and + test_authn_mfe_url_in_reset_link turns it back on. """ request_factory = RequestFactory() diff --git a/common/djangoapps/student/tests/test_activate_account.py b/common/djangoapps/student/tests/test_activate_account.py index 24c658e17eb8..59e5bbf77a91 100644 --- a/common/djangoapps/student/tests/test_activate_account.py +++ b/common/djangoapps/student/tests/test_activate_account.py @@ -20,6 +20,10 @@ @skip_unless_lms @ddt.ddt +# Activation redirects and messaging differ depending on whether logistration is served by +# the legacy page or the authn MFE. Pin the flag off for the legacy cases; the MFE cases +# turn it back on per-method. +@override_settings(ENABLE_AUTHN_MICROFRONTEND=False) class TestActivateAccount(TestCase): """Tests for account creation""" diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index a1dfa59529a2..438421652722 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -125,9 +125,13 @@ def test_get_contact_us_redirect_if_undefined_zendesk_url(self): response = self.client.get(url) assert response.status_code == 302 + @override_settings(ENABLE_AUTHN_MICROFRONTEND=False) def test_get_password_assistance(self): """ Tests password assistance + + /password_assistance is the legacy logistration page in reset mode, so it only + renders when the authn MFE is off. """ # Ensure that user is not logged in if they need # password assistance. diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_filters.py b/openedx/core/djangoapps/user_authn/views/tests/test_filters.py index 41fe122bfdc8..6a093b27840a 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_filters.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_filters.py @@ -466,6 +466,7 @@ def test_registration_form_without_filter_configuration(self): @skip_unless_lms +@override_settings(ENABLE_AUTHN_MICROFRONTEND=False) class LogistrationPageFiltersTest(UserAPITestCase): """ Tests for the Open edX Filters associated with the legacy logistration page. diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py index 8fc1500e95f3..32422f52800b 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py @@ -25,7 +25,10 @@ @skip_unless_lms @ddt.ddt -@override_settings(EMBARGO=True) +# ENABLE_AUTHN_MICROFRONTEND defaults to True, which sends /login and /register on to the +# authn MFE. Most of this class covers the legacy page these URLs still render when the MFE +# is off, so pin the flag here; the handful of MFE-redirect tests re-enable it per-method. +@override_settings(EMBARGO=True, ENABLE_AUTHN_MICROFRONTEND=False) class LoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMixin, ModuleStoreTestCase): """ Tests for Login and Registration. """ USERNAME = "bob" @@ -417,6 +420,7 @@ def test_browser_language_dialent(self): @skip_unless_lms +@override_settings(ENABLE_AUTHN_MICROFRONTEND=False) class AccountCreationTestCaseWithSiteOverrides(SiteMixin, TestCase): """ Test cases for Feature flag ALLOW_PUBLIC_ACCOUNT_CREATION which when From 822af4ad1c6561520c0fb229ccc31024628927d5 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 14 Sep 2026 10:10:12 -0400 Subject: [PATCH 4/4] temp: disable fail-fast on the unit-test matrix The matrix has no fail-fast setting, so it takes the GitHub default of true: the first shard to fail cancels the other nine. On run 34504727200 that meant lms-3 and lms-5 reported 12 failures while lms-1, lms-2, lms-4, cms-2 and all three shared-with-* shards were killed mid-run, hiding at least another 70 in common/ and openedx/ - the densest area for this change. Chasing them one shard per push is slow. Turn fail-fast off so every shard finishes and we get the whole list in a single run. Revert this commit before the PR merges. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/unit-tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index bc63f274d9a4..65dabbdd6cf4 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -21,6 +21,11 @@ jobs: name: ${{ matrix.shard_name }}(py=${{ matrix.python-version }},dj=${{ matrix.django-version }},mongo=${{ matrix.mongo-version }}) runs-on: ${{ matrix.os-version }} strategy: + # TEMPORARY - revert before merge. + # The default (fail-fast: true) cancels every other shard as soon as one fails, so a + # change that breaks tests broadly only ever reports the first shard's failures. Let + # every shard finish so we get the full list in one run. + fail-fast: false matrix: python-version: - "3.12"