charm-keystone-saml-mellon/unit_tests/test_lib_charm_openstack_keystone_saml_mellon.py
Hervé Beraud 0225c61e76 Use unittest.mock instead of mock
The mock third party library was needed for mock support in py2
runtimes. Since we now only support py36 and later, we can use the
standard lib unittest.mock module instead.

Note that https://github.com/openstack/charms.openstack is used during tests
and he need `mock`, unfortunatelly it doesn't declare `mock` in its
requirements so it retrieve mock from other charm project (cross dependency).
So we depend on charms.openstack first and when
Ib1ed5b598a52375e29e247db9ab4786df5b6d142 will be merged then CI
will pass without errors.

Depends-On: Ib1ed5b598a52375e29e247db9ab4786df5b6d142
Change-Id: Ib7e61c0293e398b831fbf8a6ade02bf833b78948
2021-12-15 14:04:23 +00:00

401 lines
16 KiB
Python

# Copyright 2019 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import urllib.error
from unittest import mock
import charms_openstack.test_utils as test_utils
import charm.openstack.keystone_saml_mellon as keystone_saml_mellon
def FakeConfig(init_dict):
def _config(key=None):
return init_dict[key] if key else init_dict
return _config
def FakeResourceGet(init_dict):
def _config(key=None):
return init_dict[key] if key else init_dict
return _config
class Helper(test_utils.PatchHelper):
def setUp(self):
super().setUp()
self.patch_release(
keystone_saml_mellon.KeystoneSAMLMellonCharm.release)
self.endpoint = mock.MagicMock()
self.idp_name = "samltest"
self.protocol_name = "mapped"
self.user_facing_name = "samltest.id"
self.nameid_formats = "fake:name:id:format1,fake:name:id:format2"
self.test_config = {
"idp-name": self.idp_name,
"protocol-name": self.protocol_name,
"user-facing-name": self.user_facing_name,
"nameid-formats": self.nameid_formats,
"subject-confirmation-data-address-check": False,
"idp-metadata-url": None
}
self.resources = {
"idp-metadata": "/path/to/idp-metadata.xml",
"sp-private-key": "/path/to/sp-private-key.pem",
"sp-signing-keyinfo": "/path/to/sp-signing-keyinfo.xml"
}
self.patch_object(keystone_saml_mellon.hookenv, 'config',
side_effect=FakeConfig(self.test_config))
self.patch_object(keystone_saml_mellon.hookenv, 'resource_get',
side_effect=FakeResourceGet(self.resources))
self.patch_object(
keystone_saml_mellon.hookenv, 'application_version_set')
self.patch_object(keystone_saml_mellon.hookenv, 'status_set')
self.patch_object(keystone_saml_mellon.ch_host, 'mkdir')
self.patch_object(keystone_saml_mellon.core.templating, 'render')
self.template_loader = mock.MagicMock()
self.patch_object(keystone_saml_mellon.os_templating, 'get_loader',
return_value=self.template_loader)
self.patch_object(
keystone_saml_mellon.KeystoneSAMLMellonCharm,
'application_version',
return_value="1.0.0")
self.patch_object(
keystone_saml_mellon.KeystoneSAMLMellonCharm, 'render_configs')
self.patch_object(keystone_saml_mellon, 'os')
self.patch_object(keystone_saml_mellon, 'subprocess')
self.patch(
"builtins.open", new_callable=mock.mock_open(), name="open")
self.file = mock.MagicMock()
self.fileobj = mock.MagicMock()
self.fileobj.__enter__.return_value = self.file
self.open.return_value = self.fileobj
class TestKeystoneSAMLMellonConfigurationAdapter(Helper):
def setUp(self):
super().setUp()
self.hostname = "keystone-sp.local"
self.port = "5000"
self.tls_enabled = True
self.endpoint_data = {
"hostname": self.hostname,
"port": self.port,
"tls-enabled": self.tls_enabled,
}
self.endpoint.all_joined_units.received.get.side_effect = (
FakeConfig(self.endpoint_data))
self.base_url = "https://{}:{}".format(self.hostname, self.port)
def test_validation_errors(self):
errors = {"idp-metadata": "Bad XML"}
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
ksmca._validation_errors = errors
self.assertEqual(ksmca.validation_errors, errors)
def test_remote_id_attribute(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(ksmca.remote_id_attribute, "MELLON_IDP")
def test_idp_metadata_file(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.idp_metadata_file, keystone_saml_mellon.IDP_METADATA)
def test_sp_metadata_file(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.sp_metadata_file, keystone_saml_mellon.SP_METADATA)
def test_sp_private_key_file(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.sp_private_key_file, keystone_saml_mellon.SP_PRIVATE_KEY)
def test_sp_idp_path(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.sp_idp_path,
'/v3/OS-FEDERATION/identity_providers/{}'.format(self.idp_name))
def test_sp_protocol_path(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.sp_protocol_path,
'{}/protocols/{}'.format(ksmca.sp_idp_path, self.protocol_name))
def test_sp_auth_path(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.sp_auth_path, '{}/auth'.format(ksmca.sp_protocol_path))
def test_mellon_endpoint_path(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.mellon_endpoint_path, '{}/mellon'.format(ksmca.sp_auth_path))
def test_websso_auth_idp_protocol_path(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.websso_auth_idp_protocol_path,
('/v3/auth/OS-FEDERATION/identity_providers/{}/protocols/{}/websso'
.format(self.idp_name, self.protocol_name)))
def test_sp_post_response_path(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.sp_post_response_path,
'{}/postResponse'.format(ksmca.mellon_endpoint_path))
def test_sp_logout_path(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.sp_logout_path,
'{}/logout'.format(ksmca.mellon_endpoint_path))
def test_mellon_subject_confirmation_data_address_check(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.mellon_subject_confirmation_data_address_check,
'Off')
def test_supported_nameid_formats(self):
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(
ksmca.supported_nameid_formats, self.nameid_formats.split(","))
def test_idp_metadata(self):
self.os.path.exists.return_value = True
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
# Valid XML
self.idp_metadata_xml = (
"<?xml version='1.0' encoding='UTF-8'?>"
"<EntityDescriptor entityID='https://samltest.id/saml/idp'> "
"</EntityDescriptor>")
self.file.read.return_value = self.idp_metadata_xml
self.assertEqual(ksmca.idp_metadata, self.idp_metadata_xml)
self.open.assert_called_with(self.resources["idp-metadata"],
'r', encoding='utf-8')
# Invalid XML
ksmca._idp_metadata = None
self.file.read.return_value = "INVALID XML"
self.assertEqual(ksmca.idp_metadata, None)
self.assertEqual(
ksmca._validation_errors,
{"idp-metadata": ksmca.IDP_METADATA_INVALID})
def test_no_resources(self):
resources = {
"idp-metadata": False,
"sp-private-key": False,
"sp-signing-keyinfo": False
}
self.patch_object(keystone_saml_mellon.hookenv, 'resource_get',
side_effect=FakeResourceGet(resources))
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
self.assertEqual(ksmca.idp_metadata, None)
self.assertEqual(
ksmca._validation_errors,
{"idp-metadata": ksmca.IDP_METADATA_NOT_PROVIDED})
self.assertEqual(ksmca.sp_signing_keyinfo, None)
self.assertEqual(
ksmca._validation_errors,
{"idp-metadata": ksmca.IDP_METADATA_NOT_PROVIDED,
"sp-signing-keyinfo": ksmca.SP_SIGNING_KEYINFO_NOT_PROVIDED})
self.assertEqual(ksmca.sp_private_key, None)
self.assertEqual(
ksmca._validation_errors,
{"idp-metadata": ksmca.IDP_METADATA_NOT_PROVIDED,
"sp-signing-keyinfo": ksmca.SP_SIGNING_KEYINFO_NOT_PROVIDED,
"sp-private-key": ksmca.SP_PRIVATE_KEY_NOT_PROVIDED})
def test_idp_metadata_url(self):
self.test_config.update(
{'idp-metadata-url': 'https://samltest.id/saml/idp'}
)
self.patch_object(keystone_saml_mellon.hookenv, 'config',
side_effect=FakeConfig(self.test_config))
resources = {
"idp-metadata": False,
"sp-private-key": False,
"sp-signing-keyinfo": False
}
self.patch_object(keystone_saml_mellon.hookenv, 'resource_get',
side_effect=FakeResourceGet(resources))
response = mock.MagicMock()
idp_meta_xml = (
"<?xml version='1.0' encoding='UTF-8'?>"
"<EntityDescriptor entityID='https://samltest.id/saml/idp'> "
"</EntityDescriptor>"
)
response.read.return_value = idp_meta_xml.encode('utf-8')
self.patch_object(keystone_saml_mellon.urllib.request, 'urlopen',
return_value=response)
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
# Valid XML
self.assertEqual(ksmca.idp_metadata, idp_meta_xml)
# Invalid XML
response = mock.MagicMock()
response.read.return_value = "foobar42".encode('utf-8')
self.patch_object(keystone_saml_mellon.urllib.request, 'urlopen',
return_value=response)
ksmca._idp_metadata = None
self.assertEqual(ksmca.idp_metadata, None)
self.assertEqual(
ksmca._validation_errors,
{"idp-metadata": ksmca.IDP_METADATA_INVALID})
# Invalid URL
self.patch_object(keystone_saml_mellon.urllib.request, 'urlopen',
side_effect=urllib.error.URLError('invalid URL'))
self.assertEqual(ksmca.idp_metadata, None)
self.assertEqual(
ksmca._validation_errors,
{"idp-metadata": ksmca.IDP_METADATA_URL_ERROR + ': invalid URL'})
def test_sp_signing_keyinfo(self):
self.os.path.exists.return_value = True
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
# Valid XML
self.sp_signing_keyinfo_xml = (
"<?xml version='1.0' encoding='UTF-8'?>"
"<ds:KeyInfo xmlns:ds='http://www.w3.org/2000/09/xmldsig#'>"
"<ds:X509Data> <ds:X509Certificate> </ds:X509Certificate>"
"</ds:X509Data> </ds:KeyInfo>")
self.file.read.return_value = self.sp_signing_keyinfo_xml
self.assertEqual(ksmca.sp_signing_keyinfo, self.sp_signing_keyinfo_xml)
self.open.assert_called_with(self.resources["sp-signing-keyinfo"])
# Inalid XML
ksmca._sp_signing_keyinfo = None
self.file.read.return_value = "INVALID XML"
self.assertEqual(ksmca.sp_signing_keyinfo, "")
self.assertEqual(
ksmca._validation_errors,
{"sp-signing-keyinfo": ksmca.SP_SIGNING_KEYINFO_INVALID})
def test_sp_private_key(self):
self.os.path.exists.return_value = True
ksmca = keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter()
# Valid Key
self.sp_private_key_pem = ("""
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBANLUtlT9JMQ/RcGEipW6MBtUoFBGMOclUmOpP1BbaFJoBn19J0UG
STj29M9nDLDRdfP0O/JiisG6ejxmO0A0xTsCAwEAAQJBAKT0IKRmW3ngN2etl/CF
+FWp5LRp9qEjJk8rgIoSupCdvuT0Q6XLk/ygHeiBYcKTf2pT/PWjQxg1pD7So5K8
YcECIQD5SKfItJ5YC9mD+6H28UqQATPehRPhQEEFIl/lJCrFgwIhANiC14XvcuWc
xMy1Lcc5lFkrB+b+oWVKJyMpNTHgXivpAiEAqh0FurZfNDBp8GJgpbcFrf3UGq7v
4RBLDqjljeY/decCIEk3/lDCCFYULQ2ZW9Da7Qs2nSaGB+isKg4e+mlSmiY5AiEA
lAoUNjDHWBOlyXziqZiufMURqbPPbRkEjWwN8G2r15A=
-----END RSA PRIVATE KEY-----
""")
self.file.read.return_value = self.sp_private_key_pem
self.assertEqual(ksmca.sp_private_key, self.sp_private_key_pem)
self.open.assert_called_with(self.resources["sp-private-key"])
# Invalid Key
ksmca._sp_private_key = None
self.file.read.return_value = "INVALID PEM KEY"
self.assertEqual(ksmca.sp_private_key, '')
self.assertEqual(
ksmca._validation_errors,
{"sp-private-key": ksmca.SP_PRIVATE_KEY_INVALID})
class TestKeystoneSAMLMellonCharm(Helper):
def setUp(self):
super().setUp()
self.patch_object(
keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter,
'idp_metadata')
self.patch_object(
keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter,
'sp_private_key')
self.patch_object(
keystone_saml_mellon.KeystoneSAMLMellonConfigurationAdapter,
'sp_signing_keyinfo')
self.idp_metadata.return_value = self.resources["idp-metadata"]
self.idp_metadata.__bool__.return_value = True
self.sp_private_key.return_value = self.resources["sp-private-key"]
self.sp_private_key.__bool__.return_value = True
self.sp_signing_keyinfo.return_value = self.resources[
"sp-signing-keyinfo"]
self.sp_signing_keyinfo.__bool__.return_value = True
def test_configuration_complete(self):
ksm = keystone_saml_mellon.KeystoneSAMLMellonCharm()
self.assertTrue(ksm.configuration_complete())
# One option not ready
self.sp_signing_keyinfo.__bool__.return_value = False
self.assertFalse(ksm.configuration_complete())
def test_custom_assess_status_check(self):
ksm = keystone_saml_mellon.KeystoneSAMLMellonCharm()
self.assertEqual(
ksm.custom_assess_status_check(),
(None, None))
# One option not ready
self.status_set.reset_mock()
self.sp_signing_keyinfo.__bool__.return_value = False
ksm.options._validation_errors = {"idp-metadata": "malformed"}
self.assertEqual(
ksm.custom_assess_status_check(),
("blocked",
"Configuration is incomplete. idp-metadata: malformed"))
def test_render_config(self):
ksm = keystone_saml_mellon.KeystoneSAMLMellonCharm()
ksm.render_config()
self.assertEqual(self.render_configs.call_count, 1)
self.assertEqual(self.render.call_count, 2)
def test_remove_config(self):
self.os.path.exists.return_value = True
ksm = keystone_saml_mellon.KeystoneSAMLMellonCharm()
ksm.remove_config()
self.assertEqual(self.os.path.exists.call_count, 4)
self.assertEqual(self.os.unlink.call_count, 4)
def test_enable_module(self):
ksm = keystone_saml_mellon.KeystoneSAMLMellonCharm()
ksm.enable_module()
self.subprocess.check_call.assert_called_once_with(
['a2enmod', 'auth_mellon'])
def test_disable_module(self):
ksm = keystone_saml_mellon.KeystoneSAMLMellonCharm()
ksm.disable_module()
self.subprocess.check_call.assert_called_once_with(
['a2dismod', 'auth_mellon'])