Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
314 changes: 313 additions & 1 deletion lms/djangoapps/mfe_config_api/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
import ddt
from django.core.cache import cache
from django.conf import settings
from django.test import override_settings
from django.test import SimpleTestCase, override_settings
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase

from lms.djangoapps.mfe_config_api.views import mfe_name_to_app_id

# Default legacy configuration values, used in tests to build a correct expected response
default_legacy_config = {
"COURSE_ABOUT_TWITTER_ACCOUNT": "@YourPlatformTwitterAccount",
Expand Down Expand Up @@ -294,3 +296,313 @@ def side_effect(key, default=None):

# Value in original MFE_CONFIG not overridden by catalog config should be preserved
self.assertEqual(data["PRESERVED_SETTING"], "preserved")


class MfeNameToAppIdTests(SimpleTestCase):
"""Tests for the mfe_name_to_app_id helper."""

def test_simple_name(self):
self.assertEqual(mfe_name_to_app_id("authn"), "org.openedx.frontend.app.authn")

def test_kebab_case_name(self):
self.assertEqual(
mfe_name_to_app_id("learner-dashboard"),
"org.openedx.frontend.app.learnerDashboard",
)

def test_multi_segment_kebab(self):
self.assertEqual(
mfe_name_to_app_id("course-authoring"),
"org.openedx.frontend.app.courseAuthoring",
)

def test_three_segment_kebab(self):
self.assertEqual(
mfe_name_to_app_id("admin-portal-enterprise"),
"org.openedx.frontend.app.adminPortalEnterprise",
)


class FrontendSiteConfigTestCase(APITestCase):
"""Tests for the FrontendSiteConfigView endpoint."""

def setUp(self):
self.url = reverse("mfe_config_api:frontend_site_config")
cache.clear()
return super().setUp()

@override_settings(ENABLE_MFE_CONFIG_API=False)
def test_404_when_disabled(self):
"""API returns 404 when ENABLE_MFE_CONFIG_API is False."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
def test_site_level_keys_translated(self, configuration_helpers_mock):
"""Keys that map to RequiredSiteConfig/OptionalSiteConfig appear at the top level in camelCase."""
mfe_config = {
"SITE_NAME": "Test Site",
"BASE_URL": "https://apps.example.com",
"LMS_BASE_URL": "https://courses.example.com",
"LOGIN_URL": "https://courses.example.com/login",
"LOGOUT_URL": "https://courses.example.com/logout",
"LOGO_URL": "https://courses.example.com/logo.png",
"ACCESS_TOKEN_COOKIE_NAME": "edx-jwt",
"LANGUAGE_PREFERENCE_COOKIE_NAME": "lang-pref",
"USER_INFO_COOKIE_NAME": "edx-user-info",
"CSRF_TOKEN_API_PATH": "/csrf/api/v1/token",
"REFRESH_ACCESS_TOKEN_API_PATH": "/login_refresh",
"SEGMENT_KEY": "abc123",
}

def side_effect(key, default=None):
if key == "MFE_CONFIG":
return mfe_config
if key == "MFE_CONFIG_OVERRIDES":
return {}
return default
configuration_helpers_mock.get_value.side_effect = side_effect

response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()

# RequiredSiteConfig
self.assertEqual(data["siteName"], "Test Site")
self.assertEqual(data["baseUrl"], "https://apps.example.com")
self.assertEqual(data["lmsBaseUrl"], "https://courses.example.com")
self.assertEqual(data["loginUrl"], "https://courses.example.com/login")
self.assertEqual(data["logoutUrl"], "https://courses.example.com/logout")
# OptionalSiteConfig
self.assertEqual(data["headerLogoImageUrl"], "https://courses.example.com/logo.png")
self.assertEqual(data["accessTokenCookieName"], "edx-jwt")
self.assertEqual(data["languagePreferenceCookieName"], "lang-pref")
self.assertEqual(data["userInfoCookieName"], "edx-user-info")
self.assertEqual(data["csrfTokenApiPath"], "/csrf/api/v1/token")
self.assertEqual(data["refreshAccessTokenApiPath"], "/login_refresh")
self.assertEqual(data["segmentKey"], "abc123")

@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
def test_unmapped_keys_in_app_config(self, configuration_helpers_mock):
"""Keys that don't map to SiteConfig fields are included in each app's config."""
mfe_config = {
"LMS_BASE_URL": "https://courses.example.com",
"CREDENTIALS_BASE_URL": "https://credentials.example.com",
"STUDIO_BASE_URL": "https://studio.example.com",
}

def side_effect(key, default=None):
if key == "MFE_CONFIG":
return mfe_config
if key == "MFE_CONFIG_OVERRIDES":
return {"authn": {"SOME_KEY": "value"}}
return default
configuration_helpers_mock.get_value.side_effect = side_effect

response = self.client.get(self.url)
data = response.json()

# Site-level key translated to top level
self.assertEqual(data["lmsBaseUrl"], "https://courses.example.com")
# Unmapped MFE_CONFIG keys appear in commonAppConfig (not at the top level)
self.assertNotIn("CREDENTIALS_BASE_URL", data)
common = data["commonAppConfig"]
self.assertEqual(common["CREDENTIALS_BASE_URL"], "https://credentials.example.com")
self.assertEqual(common["STUDIO_BASE_URL"], "https://studio.example.com")
# Legacy config keys also appear in commonAppConfig
for legacy_key in default_legacy_config:
self.assertIn(legacy_key, common)

@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
def test_apps_from_overrides(self, configuration_helpers_mock):
"""Each MFE_CONFIG_OVERRIDES entry becomes an app with shared base config + overrides."""
mfe_config_overrides = {
"authn": {
"ALLOW_PUBLIC_ACCOUNT_CREATION": True,
"ACTIVATION_EMAIL_SUPPORT_LINK": None,
},
"learner-dashboard": {
"LEARNING_BASE_URL": "http://apps.local.openedx.io:2000",
"ENABLE_PROGRAMS": False,
},
}

def side_effect(key, default=None):
if key == "MFE_CONFIG":
return {
"LMS_BASE_URL": "https://courses.example.com",
"SHARED_SETTING": "shared_value",
}
if key == "MFE_CONFIG_OVERRIDES":
return mfe_config_overrides
return default
configuration_helpers_mock.get_value.side_effect = side_effect

response = self.client.get(self.url)
data = response.json()

self.assertIn("apps", data)
self.assertEqual(len(data["apps"]), 2)

# Shared config (unmapped MFE_CONFIG keys + legacy config) is in commonAppConfig.
common = data["commonAppConfig"]
self.assertEqual(common["SHARED_SETTING"], "shared_value")
for legacy_key in default_legacy_config:
self.assertIn(legacy_key, common)

# Apps should be sorted by MFE name; each carries only its own overrides.
authn = data["apps"][0]
self.assertEqual(authn["appId"], "org.openedx.frontend.app.authn")
self.assertEqual(authn["config"]["ALLOW_PUBLIC_ACCOUNT_CREATION"], True)
self.assertIsNone(authn["config"]["ACTIVATION_EMAIL_SUPPORT_LINK"])
# Shared keys are NOT duplicated into per-app config
self.assertNotIn("SHARED_SETTING", authn["config"])

dashboard = data["apps"][1]
self.assertEqual(dashboard["appId"], "org.openedx.frontend.app.learnerDashboard")
self.assertEqual(dashboard["config"]["LEARNING_BASE_URL"], "http://apps.local.openedx.io:2000")
self.assertEqual(dashboard["config"]["ENABLE_PROGRAMS"], False)
self.assertNotIn("SHARED_SETTING", dashboard["config"])

@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
def test_app_overrides_separate_from_common(self, configuration_helpers_mock):
"""App-specific overrides appear in per-app config; shared keys in commonAppConfig."""
def side_effect(key, default=None):
if key == "MFE_CONFIG":
return {"SOME_KEY": "base_value"}
if key == "MFE_CONFIG_OVERRIDES":
return {"authn": {"SOME_KEY": "overridden_value"}}
return default
configuration_helpers_mock.get_value.side_effect = side_effect

response = self.client.get(self.url)
data = response.json()

self.assertEqual(data["commonAppConfig"]["SOME_KEY"], "base_value")
self.assertEqual(data["apps"][0]["config"]["SOME_KEY"], "overridden_value")

@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
def test_site_level_keys_stripped_from_app_overrides(self, configuration_helpers_mock):
"""Site-level keys in MFE_CONFIG_OVERRIDES are stripped from app config."""
def side_effect(key, default=None):
if key == "MFE_CONFIG":
return {
"LMS_BASE_URL": "https://courses.example.com",
"LOGO_URL": "https://courses.example.com/logo.png",
}
if key == "MFE_CONFIG_OVERRIDES":
return {
"authn": {
"BASE_URL": "https://authn.example.com",
"LOGIN_URL": "https://authn.example.com/login",
"APP_SPECIFIC_KEY": "app_value",
},
}
return default
configuration_helpers_mock.get_value.side_effect = side_effect

response = self.client.get(self.url)
data = response.json()

app_config = data["apps"][0]["config"]
# Site-level keys from overrides must not appear in app config
self.assertNotIn("BASE_URL", app_config)
self.assertNotIn("LOGIN_URL", app_config)
# Non-site-level override keys are kept
self.assertEqual(app_config["APP_SPECIFIC_KEY"], "app_value")
# Site-level keys from overrides also must not appear in commonAppConfig
self.assertNotIn("BASE_URL", data["commonAppConfig"])
self.assertNotIn("LOGIN_URL", data["commonAppConfig"])

@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
def test_no_apps_when_no_overrides(self, configuration_helpers_mock):
"""The apps key is omitted when MFE_CONFIG_OVERRIDES is empty."""
def side_effect(key, default=None):
if key == "MFE_CONFIG":
return {"LMS_BASE_URL": "https://courses.example.com"}
if key == "MFE_CONFIG_OVERRIDES":
return {}
return default
configuration_helpers_mock.get_value.side_effect = side_effect

response = self.client.get(self.url)
data = response.json()

self.assertNotIn("apps", data)
# commonAppConfig is still present with legacy keys
self.assertIn("commonAppConfig", data)
for legacy_key in default_legacy_config:
self.assertIn(legacy_key, data["commonAppConfig"])

@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
def test_unmapped_keys_in_common_app_config_without_overrides(self, configuration_helpers_mock):
"""Unmapped MFE_CONFIG keys appear in commonAppConfig even without overrides."""
def side_effect(key, default=None):
if key == "MFE_CONFIG":
return {
"LMS_BASE_URL": "https://courses.example.com",
"CREDENTIALS_BASE_URL": "https://credentials.example.com",
"STUDIO_BASE_URL": "https://studio.example.com",
}
if key == "MFE_CONFIG_OVERRIDES":
return {}
return default
configuration_helpers_mock.get_value.side_effect = side_effect

response = self.client.get(self.url)
data = response.json()

# Site-level key is promoted to the top level
self.assertEqual(data["lmsBaseUrl"], "https://courses.example.com")
# Unmapped keys are preserved in commonAppConfig
common = data["commonAppConfig"]
self.assertEqual(common["CREDENTIALS_BASE_URL"], "https://credentials.example.com")
self.assertEqual(common["STUDIO_BASE_URL"], "https://studio.example.com")

@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
def test_invalid_override_entry_skipped(self, configuration_helpers_mock):
"""Non-dict override entries are silently skipped."""
mfe_config_overrides = {
"authn": {"SOME_KEY": "value"},
"broken": "not-a-dict",
}

def side_effect(key, default=None):
if key == "MFE_CONFIG":
return {}
if key == "MFE_CONFIG_OVERRIDES":
return mfe_config_overrides
return default
configuration_helpers_mock.get_value.side_effect = side_effect

response = self.client.get(self.url)
data = response.json()

self.assertEqual(len(data["apps"]), 1)
self.assertEqual(data["apps"][0]["appId"], "org.openedx.frontend.app.authn")

def test_from_django_settings(self):
"""When there is no site configuration, the API uses django settings."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()

# settings.MFE_CONFIG in test.py has LANGUAGE_PREFERENCE_COOKIE_NAME and LOGO_URL
self.assertEqual(data.get("languagePreferenceCookieName"), "example-language-preference")
self.assertEqual(data.get("headerLogoImageUrl"), "https://courses.example.com/logo.png")

# Legacy config keys live in commonAppConfig
for legacy_key in default_legacy_config:
self.assertIn(legacy_key, data["commonAppConfig"])

# MFE_CONFIG_OVERRIDES in test.py has mymfe and yourmfe
self.assertIn("apps", data)
app_ids = [app["appId"] for app in data["apps"]]
self.assertIn("org.openedx.frontend.app.mymfe", app_ids)
self.assertIn("org.openedx.frontend.app.yourmfe", app_ids)

# Site-level keys from overrides (LANGUAGE_PREFERENCE_COOKIE_NAME,
# LOGO_URL in test settings) are stripped from per-app config
for app in data["apps"]:
self.assertNotIn("LANGUAGE_PREFERENCE_COOKIE_NAME", app["config"])
self.assertNotIn("LOGO_URL", app["config"])
11 changes: 7 additions & 4 deletions lms/djangoapps/mfe_config_api/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
""" URLs configuration for the mfe api."""
"""URLs configuration for the mfe api."""

from django.urls import path

from lms.djangoapps.mfe_config_api.views import MFEConfigView
from lms.djangoapps.mfe_config_api.views import MFEConfigView, FrontendSiteConfigView

app_name = 'mfe_config_api'
app_name = "mfe_config_api"
urlpatterns = [
path('', MFEConfigView.as_view(), name='config'),
path("", MFEConfigView.as_view(), name="config"),
path(
"/frontend_site", FrontendSiteConfigView.as_view(), name="frontend_site_config"
),
]
Loading
Loading