88from io import StringIO
99
1010from unittest import mock
11+ from ddt import ddt , data , unpack
12+ from django .contrib .sites .models import Site
1113from django .core .management import call_command
1214from django .core .management .base import CommandError
1315from requests import exceptions
1618from openedx .core .djangolib .testing .utils import CacheIsolationTestCase , skip_unless_lms
1719from common .djangoapps .third_party_auth .tests .factories import SAMLConfigurationFactory , SAMLProviderConfigFactory
1820
21+ from common .djangoapps .third_party_auth .models import SAMLProviderConfig
22+
1923
2024def mock_get (status_code = 200 ):
2125 """
@@ -45,6 +49,7 @@ def _(url=None, *args, **kwargs): # lint-amnesty, pylint: disable=keyword-arg-b
4549
4650
4751@skip_unless_lms
52+ @ddt
4853class TestSAMLCommand (CacheIsolationTestCase ):
4954 """
5055 Test django management command for fetching saml metadata.
@@ -58,12 +63,17 @@ def setUp(self):
5863 super ().setUp ()
5964
6065 self .stdout = StringIO ()
66+ self .site = Site .objects .get_current ()
6167
6268 # We are creating SAMLConfiguration instance here so that there is always at-least one
6369 # disabled saml configuration instance, this is done to verify that disabled configurations are
6470 # not processed.
65- SAMLConfigurationFactory .create (enabled = False , site__domain = 'testserver.fake' , site__name = 'testserver.fake' )
66- SAMLProviderConfigFactory .create (
71+ self .saml_config = SAMLConfigurationFactory .create (
72+ enabled = False ,
73+ site__domain = 'testserver.fake' ,
74+ site__name = 'testserver.fake'
75+ )
76+ self .provider_config = SAMLProviderConfigFactory .create (
6777 site__domain = 'testserver.fake' ,
6878 site__name = 'testserver.fake' ,
6979 slug = 'test-shib' ,
@@ -72,6 +82,44 @@ def setUp(self):
7282 metadata_source = 'https://www.testshib.org/metadata/testshib-providers.xml' ,
7383 )
7484
85+ def _setup_test_configs_for_fix_references (self ):
86+ """
87+ Helper method to create SAML configurations for fix-references tests.
88+
89+ Returns tuple of (old_config, new_config, provider_config)
90+
91+ Using a separate method keeps test data isolated. Including these configs in
92+ setUp would create 3 provider configs for all tests, breaking tests that expect
93+ specific provider counts or try to access non-existent test XML files.
94+ """
95+ # Create a SAML config that will be outdated after the new config is created
96+ old_config = SAMLConfigurationFactory .create (
97+ enabled = False ,
98+ site = self .site ,
99+ slug = 'test-config' ,
100+ entity_id = 'https://old.example.com'
101+ )
102+
103+ # Create newer config with same slug
104+ new_config = SAMLConfigurationFactory .create (
105+ enabled = True ,
106+ site = self .site ,
107+ slug = 'test-config' ,
108+ entity_id = 'https://updated.example.com'
109+ )
110+
111+ # Create a provider config that references the old config for fix-references tests
112+ test_provider_config = SAMLProviderConfigFactory .create (
113+ site = self .site ,
114+ slug = 'test-provider' ,
115+ name = 'Test Provider' ,
116+ entity_id = 'https://test.provider/idp/shibboleth' ,
117+ metadata_source = 'https://test.provider/metadata.xml' ,
118+ saml_configuration = old_config
119+ )
120+
121+ return old_config , new_config , test_provider_config
122+
75123 def __create_saml_configurations__ (self , saml_config = None , saml_provider_config = None ):
76124 """
77125 Helper method to create SAMLConfiguration and AMLProviderConfig.
@@ -101,11 +149,11 @@ def test_raises_command_error_for_invalid_arguments(self):
101149 This test would fail with an error if ValueError is raised.
102150 """
103151 # Call `saml` command without any argument so that it raises a CommandError
104- with self .assertRaisesMessage (CommandError , "Command can only be used with '--pull' option." ):
152+ with self .assertRaisesMessage (CommandError , "Command must be used with '--pull' or '--fix-references ' option." ):
105153 call_command ("saml" )
106154
107155 # Call `saml` command without any argument so that it raises a CommandError
108- with self .assertRaisesMessage (CommandError , "Command can only be used with '--pull' option." ):
156+ with self .assertRaisesMessage (CommandError , "Command must be used with '--pull' or '--fix-references ' option." ):
109157 call_command ("saml" , pull = False )
110158
111159 def test_no_saml_configuration (self ):
@@ -285,3 +333,60 @@ def test_xml_parse_exceptions(self, mocked_get):
285333 with self .assertRaisesRegex (CommandError , "XMLSyntaxError:" ):
286334 call_command ("saml" , pull = True , stdout = self .stdout )
287335 assert expected in self .stdout .getvalue ()
336+
337+ @data (
338+ (True , '[DRY RUN]' , 'should not update provider configs' ),
339+ (False , '' , 'should create new provider config for new version' )
340+ )
341+ @unpack
342+ def test_fix_references (self , dry_run , expected_output_marker , test_description ):
343+ """
344+ Test the --fix-references command with and without --dry-run option.
345+
346+ Args:
347+ dry_run (bool): Whether to run with --dry-run flag
348+ expected_output_marker (str): Expected marker in output
349+ test_description (str): Description of what the test should do
350+ """
351+ old_config , new_config , test_provider_config = self ._setup_test_configs_for_fix_references ()
352+ new_config_id = new_config .id
353+ original_config_id = old_config .id
354+
355+ out = StringIO ()
356+ if dry_run :
357+ call_command ('saml' , '--fix-references' , '--dry-run' , stdout = out )
358+ else :
359+ call_command ('saml' , '--fix-references' , stdout = out )
360+
361+ output = out .getvalue ()
362+
363+ self .assertIn ('test-provider' , output )
364+ if expected_output_marker :
365+ self .assertIn (expected_output_marker , output )
366+
367+ test_provider_config .refresh_from_db ()
368+
369+ if dry_run :
370+ # For dry run, ensure the provider config was NOT updated
371+ self .assertEqual (
372+ test_provider_config .saml_configuration_id ,
373+ original_config_id ,
374+ "Provider config should not be updated in dry run mode"
375+ )
376+ else :
377+ # For actual run, check that a new provider config was created
378+ new_provider = SAMLProviderConfig .objects .filter (
379+ site = self .site ,
380+ slug = 'test-provider' ,
381+ saml_configuration_id = new_config_id
382+ ).exclude (id = test_provider_config .id ).first ()
383+
384+ self .assertIsNotNone (new_provider , "New provider config should be created" )
385+ self .assertEqual (new_provider .saml_configuration_id , new_config_id )
386+
387+ # Original provider config should still reference the old config
388+ self .assertEqual (
389+ test_provider_config .saml_configuration_id ,
390+ original_config_id ,
391+ "Original provider config should still reference old config"
392+ )
0 commit comments