# SPDX-License-Identifier: AGPL-3.0-or-later """ Test the Let's Encrypt component for managing certificates. """ from unittest.mock import call, patch import pytest from plinth.modules.letsencrypt.components import LetsEncrypt from plinth.modules.names.components import DomainName, DomainType @pytest.fixture(name='empty_letsencrypt_list', autouse=True) def fixture_empty_letsencrypt_list(): """Remove all entries from Let's Encrypt component list.""" LetsEncrypt._all = {} @pytest.fixture(name='component') def fixture_component(): """Create a new component for testing.""" return LetsEncrypt( 'test-component', domains=['valid.example', 'invalid.example'], daemons=['test-daemon'], should_copy_certificates=True, private_key_path='/etc/test-app/{domain}/private.path', certificate_path='/etc/test-app/{domain}/certificate.path', user_owner='test-user', group_owner='test-group', managing_app='test-app') @pytest.fixture(name='try_restart') def fixture_try_restart(): """Patch and return service.try_restart privileged call.""" with patch('plinth.privileged.service.try_restart') as try_restart: yield try_restart @pytest.fixture(name='copy_certificate') def fixture_copy_certificate(): """Patch and return privileged.copy_certificate call.""" with patch('plinth.modules.letsencrypt.privileged.copy_certificate' ) as copy_certificate: yield copy_certificate @pytest.fixture(name='compare_certificate') def fixture_compare_certificate(): """Patch and return privileged.compare_certificate call.""" with patch('plinth.modules.letsencrypt.privileged.compare_certificate' ) as compare_certificate: yield compare_certificate @pytest.fixture(name='get_status') def fixture_get_status(): """Return patched letsencrypt.get_status() method.""" domains = ['valid.example'] with patch('plinth.modules.letsencrypt.get_status') as get_status: get_status.return_value = { 'domains': { domain: { 'lineage': '/etc/letsencrypt/live/' + domain, 'validity': 'valid', } for domain in domains } } yield get_status @pytest.fixture(name='domain_list') def fixture_domain_list(): """Return patch DomainName.list() method.""" method = 'plinth.modules.names.components.DomainName.list' with patch(method) as domain_list: DomainType._all = {} DomainType('domain-type-1', 'type-1', 'url1', False) DomainType('domain-type-2', 'type-2', 'url1', True) domain1 = DomainName('domain-name-1', 'invalid1.example', 'domain-type-1', '__all__') domain2 = DomainName('domain-name-2', 'valid.example', 'domain-type-2', '__all__') domain3 = DomainName('domain-name-3', 'invalid2.example', 'domain-type-2', '__all__') domain_list.return_value = [domain1, domain2, domain3] yield domain_list def test_init_without_arguments(): """Test that component is initialized with defaults properly.""" component = LetsEncrypt('test-component') assert component.component_id == 'test-component' assert component.domains is None assert component.daemons is None assert not component.should_copy_certificates assert component.private_key_path is None assert component.certificate_path is None assert component.user_owner is None assert component.group_owner is None assert component.managing_app is None assert len(component._all) == 1 assert component._all['test-component'] == component def test_init(component): """Test initializing the component.""" assert component.domains == ['valid.example', 'invalid.example'] assert component.daemons == ['test-daemon'] assert component.should_copy_certificates assert component.private_key_path == '/etc/test-app/{domain}/private.path' assert component.certificate_path == \ '/etc/test-app/{domain}/certificate.path' assert component.user_owner == 'test-user' assert component.group_owner == 'test-group' assert component.managing_app == 'test-app' def test_init_values(): """Test initializing with invalid values.""" properties = { 'private_key_path': 'test-private-key-path', 'certificate_path': 'test-certificate-path', 'user_owner': 'test-user', 'group_owner': 'test-group', 'managing_app': 'test-app' } LetsEncrypt('test-component', should_copy_certificates=True, **properties) for key in properties: new_properties = dict(properties) new_properties[key] = None with pytest.raises(ValueError): LetsEncrypt('test-component', should_copy_certificates=True, **new_properties) def test_domains(): """Test getting domains.""" component = LetsEncrypt('test-component', domains=lambda: ['test-domains']) assert component.domains == ['test-domains'] def test_list(): """Test listing components.""" component1 = LetsEncrypt('test-component1') component2 = LetsEncrypt('test-component2') assert set(LetsEncrypt.list()) == {component1, component2} def _assert_copy_certificate_called(component, copy_certificate, domains): """Check that copy certificate calls have been made properly.""" expected_calls = [] for domain, domain_status in domains.items(): if domain_status == 'valid': source_private_key_path = \ '/etc/letsencrypt/live/{}/privkey.pem'.format(domain) source_certificate_path = \ '/etc/letsencrypt/live/{}/fullchain.pem'.format(domain) else: source_private_key_path = '/etc/ssl/private/ssl-cert-snakeoil.key' source_certificate_path = '/etc/ssl/certs/ssl-cert-snakeoil.pem' private_key_path = '/etc/test-app/{}/private.path'.format(domain) certificate_path = '/etc/test-app/{}/certificate.path'.format(domain) expected_call = call(component.managing_app, str(source_private_key_path), str(source_certificate_path), private_key_path, certificate_path, component.user_owner, component.group_owner) expected_calls.append(expected_call) copy_certificate.assert_has_calls(expected_calls, any_order=True) def _assert_restarted_daemons(daemons, try_restart): """Check that a call has restarted the daemons of a component.""" expected_calls = [call(daemon) for daemon in daemons] try_restart.assert_has_calls(expected_calls, any_order=True) def test_setup_certificates(copy_certificate, try_restart, get_status, component): """Test that initial copying of certs for an app works.""" component.setup_certificates() _assert_copy_certificate_called(component, copy_certificate, { 'valid.example': 'valid', 'invalid.example': 'invalid' }) _assert_restarted_daemons(component.daemons, try_restart) def test_setup_certificates_without_copy(copy_certificate, try_restart, get_status, component): """Test that initial copying of certs for an app works.""" component.should_copy_certificates = False component.setup_certificates() _assert_copy_certificate_called(component, copy_certificate, {}) _assert_restarted_daemons(component.daemons, try_restart) def test_setup_certificates_with_app_domains(copy_certificate, try_restart, get_status, component): """Test that initial copying of certs for an app works.""" component._domains = ['irrelevant1.example', 'irrelevant2.example'] component.setup_certificates( app_domains=['valid.example', 'invalid.example']) _assert_copy_certificate_called(component, copy_certificate, { 'valid.example': 'valid', 'invalid.example': 'invalid' }) _assert_restarted_daemons(component.daemons, try_restart) def test_setup_certificates_with_all_domains(domain_list, copy_certificate, try_restart, get_status, component): """Test that initial copying for certs works when app domains is '*'.""" component._domains = '*' component.setup_certificates() _assert_copy_certificate_called( component, copy_certificate, { 'valid.example': 'valid', 'invalid1.example': 'invalid', 'invalid2.example': 'invalid' }) _assert_restarted_daemons(component.daemons, try_restart) def _assert_compare_certificate_called(component, compare_certificate, domains): """Check that compare certificate was called properly.""" expected_calls = [] for domain in domains: source_private_key_path = \ '/etc/letsencrypt/live/{}/privkey.pem'.format(domain) source_certificate_path = \ '/etc/letsencrypt/live/{}/fullchain.pem'.format(domain) private_key_path = '/etc/test-app/{}/private.path'.format(domain) certificate_path = '/etc/test-app/{}/certificate.path'.format(domain) expected_call = call(component.managing_app, str(source_private_key_path), str(source_certificate_path), private_key_path, certificate_path) expected_calls.append(expected_call) compare_certificate.assert_has_calls(expected_calls, any_order=True) def test_get_status(component, compare_certificate, get_status): """Test that getting domain status works.""" compare_certificate.return_value = True assert component.get_status() == { 'valid.example': 'valid', 'invalid.example': 'self-signed' } _assert_compare_certificate_called(component, compare_certificate, ['valid.example']) def test_get_status_outdate_copy(component, compare_certificate, get_status): """Test that getting domain status works with outdated copy.""" compare_certificate.return_value = False assert component.get_status() == { 'valid.example': 'outdated-copy', 'invalid.example': 'self-signed' } _assert_compare_certificate_called(component, compare_certificate, ['valid.example']) def test_get_status_without_copy(component, get_status): """Test that getting domain status works without copying.""" component.should_copy_certificates = False assert component.get_status() == { 'valid.example': 'valid', 'invalid.example': 'self-signed' } def test_on_certificate_obtained(copy_certificate, try_restart, component): """Test that certificate obtained event handler works.""" component.on_certificate_obtained(['valid.example', 'irrelevant.example'], '/etc/letsencrypt/live/valid.example/') _assert_copy_certificate_called(component, copy_certificate, { 'valid.example': 'valid', }) _assert_restarted_daemons(component.daemons, try_restart) def test_on_certificate_obtained_with_all_domains(copy_certificate, try_restart, component): """Test that certificate obtained event handler works for app with all domains. """ component._domains = '*' component.on_certificate_obtained(['valid.example'], '/etc/letsencrypt/live/valid.example/') _assert_copy_certificate_called(component, copy_certificate, { 'valid.example': 'valid', }) _assert_restarted_daemons(component.daemons, try_restart) def test_on_certificate_obtained_irrelevant(copy_certificate, try_restart, component): """Test that certificate obtained event handler works with irrelevant domain. """ component.on_certificate_obtained( ['irrelevant.example'], '/etc/letsencrypt/live/irrelevant.example/') _assert_copy_certificate_called(component, copy_certificate, {}) _assert_restarted_daemons([], try_restart) def test_on_certificate_obtained_without_copy(copy_certificate, try_restart, component): """Test that certificate obtained event handler works without copying.""" component.should_copy_certificates = False component.on_certificate_obtained(['valid.example'], '/etc/letsencrypt/live/valid.example/') _assert_copy_certificate_called(component, copy_certificate, {}) _assert_restarted_daemons(component.daemons, try_restart) def test_on_certificate_renewed(copy_certificate, try_restart, component): """Test that certificate renewed event handler works.""" component.on_certificate_renewed(['valid.example', 'irrelevant.example'], '/etc/letsencrypt/live/valid.example/') _assert_copy_certificate_called(component, copy_certificate, { 'valid.example': 'valid', }) _assert_restarted_daemons(component.daemons, try_restart) def test_on_certificate_renewed_irrelevant(copy_certificate, try_restart, component): """Test that certificate renewed event handler works for irrelevant domains. """ component.on_certificate_renewed( ['irrelevant.example'], '/etc/letsencrypt/live/irrelevant.example/') _assert_copy_certificate_called(component, copy_certificate, {}) _assert_restarted_daemons([], try_restart) def test_on_certificate_renewed_without_copy(copy_certificate, try_restart, component): """Test that certificate renewed event handler works without copying.""" component.should_copy_certificates = False component.on_certificate_renewed(['valid.example'], '/etc/letsencrypt/live/valid.example/') _assert_copy_certificate_called(component, copy_certificate, {}) _assert_restarted_daemons(component.daemons, try_restart) def test_on_certificate_revoked(copy_certificate, try_restart, component): """Test that certificate revoked event handler works.""" component.on_certificate_revoked(['valid.example', 'irrelevant.example'], '/etc/letsencrypt/live/valid.example/') _assert_copy_certificate_called(component, copy_certificate, { 'valid.example': 'invalid', }) _assert_restarted_daemons(component.daemons, try_restart) def test_on_certificate_revoked_irrelevant(copy_certificate, try_restart, component): """Test that certificate revoked event handler works for irrelevant domains. """ component.on_certificate_revoked( ['irrelevant.example'], '/etc/letsencrypt/live/irrelevant.example/') _assert_copy_certificate_called(component, copy_certificate, {}) _assert_restarted_daemons([], try_restart) def test_on_certificate_revoked_without_copy(copy_certificate, try_restart, component): """Test that certificate revoked event handler works without copying.""" component.should_copy_certificates = False component.on_certificate_revoked(['valid.example'], '/etc/letsencrypt/live/valid.example/') _assert_copy_certificate_called(component, copy_certificate, {}) _assert_restarted_daemons(component.daemons, try_restart) def test_on_certificate_deleted(copy_certificate, try_restart, component): """Test that certificate deleted event handler works.""" component.on_certificate_deleted(['valid.example', 'irrelevant.example'], '/etc/letsencrypt/live/valid.example/') _assert_copy_certificate_called(component, copy_certificate, { 'valid.example': 'invalid', }) _assert_restarted_daemons(component.daemons, try_restart) def test_on_certificate_deleted_irrelevant(copy_certificate, try_restart, component): """Test that certificate deleted event handler works for irrelevant domains. """ component.on_certificate_deleted( ['irrelevant.example'], '/etc/letsencrypt/live/irrelevant.example/') _assert_copy_certificate_called(component, copy_certificate, {}) _assert_restarted_daemons([], try_restart) def test_on_certificate_deleted_without_copy(copy_certificate, try_restart, component): """Test that certificate deleted event handler works without copying.""" component.should_copy_certificates = False component.on_certificate_deleted(['valid.example'], '/etc/letsencrypt/live/valid.example/') _assert_copy_certificate_called(component, copy_certificate, {}) _assert_restarted_daemons(component.daemons, try_restart)