mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-28 08:03:36 +00:00
- Instead of restarting them. Tests: - Changing a domain name leads restarting of services postfix/dovecot services. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
441 lines
17 KiB
Python
441 lines
17 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""
|
|
Test the Let's Encrypt component for managing certificates.
|
|
"""
|
|
|
|
import contextlib
|
|
import random
|
|
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."""
|
|
reload_daemons = random.choice([True, False])
|
|
component = 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', reload_daemons=reload_daemons)
|
|
assert component.reload_daemons == reload_daemons
|
|
return component
|
|
|
|
|
|
@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 not component.reload_daemons
|
|
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)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _assert_restarted_daemons(component, daemons=None):
|
|
"""Check that a call has restarted the daemons of a component."""
|
|
daemons = daemons if daemons is not None else component.daemons
|
|
|
|
expected_calls = [call(daemon) for daemon in daemons]
|
|
with patch('plinth.privileged.service.try_reload_or_restart'
|
|
) as try_reload_or_restart, patch(
|
|
'plinth.privileged.service.try_restart') as try_restart:
|
|
yield
|
|
|
|
if component.reload_daemons:
|
|
try_reload_or_restart.assert_has_calls(expected_calls,
|
|
any_order=True)
|
|
try_restart.assert_not_called()
|
|
else:
|
|
try_restart.assert_has_calls(expected_calls, any_order=True)
|
|
try_reload_or_restart.assert_not_called()
|
|
|
|
|
|
def test_setup_certificates(copy_certificate, get_status, component):
|
|
"""Test that initial copying of certs for an app works."""
|
|
with _assert_restarted_daemons(component):
|
|
component.setup_certificates()
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {
|
|
'valid.example': 'valid',
|
|
'invalid.example': 'invalid'
|
|
})
|
|
|
|
|
|
def test_setup_certificates_without_copy(copy_certificate, get_status,
|
|
component):
|
|
"""Test that initial copying of certs for an app works."""
|
|
component.should_copy_certificates = False
|
|
with _assert_restarted_daemons(component):
|
|
component.setup_certificates()
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {})
|
|
|
|
|
|
def test_setup_certificates_with_app_domains(copy_certificate, get_status,
|
|
component):
|
|
"""Test that initial copying of certs for an app works."""
|
|
component._domains = ['irrelevant1.example', 'irrelevant2.example']
|
|
with _assert_restarted_daemons(component):
|
|
component.setup_certificates(
|
|
app_domains=['valid.example', 'invalid.example'])
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {
|
|
'valid.example': 'valid',
|
|
'invalid.example': 'invalid'
|
|
})
|
|
|
|
|
|
def test_setup_certificates_with_all_domains(domain_list, copy_certificate,
|
|
get_status, component):
|
|
"""Test that initial copying for certs works when app domains is '*'."""
|
|
component._domains = '*'
|
|
with _assert_restarted_daemons(component):
|
|
component.setup_certificates()
|
|
|
|
_assert_copy_certificate_called(
|
|
component, copy_certificate, {
|
|
'valid.example': 'valid',
|
|
'invalid1.example': 'invalid',
|
|
'invalid2.example': 'invalid'
|
|
})
|
|
|
|
|
|
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, component):
|
|
"""Test that certificate obtained event handler works."""
|
|
with _assert_restarted_daemons(component):
|
|
component.on_certificate_obtained(
|
|
['valid.example', 'irrelevant.example'],
|
|
'/etc/letsencrypt/live/valid.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {
|
|
'valid.example': 'valid',
|
|
})
|
|
|
|
|
|
def test_on_certificate_obtained_with_all_domains(copy_certificate, component):
|
|
"""Test that certificate obtained event handler works for app with
|
|
all domains.
|
|
"""
|
|
component._domains = '*'
|
|
with _assert_restarted_daemons(component):
|
|
component.on_certificate_obtained(
|
|
['valid.example'], '/etc/letsencrypt/live/valid.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {
|
|
'valid.example': 'valid',
|
|
})
|
|
|
|
|
|
def test_on_certificate_obtained_irrelevant(copy_certificate, component):
|
|
"""Test that certificate obtained event handler works with
|
|
irrelevant domain.
|
|
"""
|
|
with _assert_restarted_daemons(component, []):
|
|
component.on_certificate_obtained(
|
|
['irrelevant.example'],
|
|
'/etc/letsencrypt/live/irrelevant.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {})
|
|
|
|
|
|
def test_on_certificate_obtained_without_copy(copy_certificate, component):
|
|
"""Test that certificate obtained event handler works without copying."""
|
|
component.should_copy_certificates = False
|
|
with _assert_restarted_daemons(component):
|
|
component.on_certificate_obtained(
|
|
['valid.example'], '/etc/letsencrypt/live/valid.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {})
|
|
|
|
|
|
def test_on_certificate_renewed(copy_certificate, component):
|
|
"""Test that certificate renewed event handler works."""
|
|
with _assert_restarted_daemons(component):
|
|
component.on_certificate_renewed(
|
|
['valid.example', 'irrelevant.example'],
|
|
'/etc/letsencrypt/live/valid.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {
|
|
'valid.example': 'valid',
|
|
})
|
|
|
|
|
|
def test_on_certificate_renewed_irrelevant(copy_certificate, component):
|
|
"""Test that cert renewed event handler works for irrelevant domains."""
|
|
with _assert_restarted_daemons(component, []):
|
|
component.on_certificate_renewed(
|
|
['irrelevant.example'],
|
|
'/etc/letsencrypt/live/irrelevant.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {})
|
|
|
|
|
|
def test_on_certificate_renewed_without_copy(copy_certificate, component):
|
|
"""Test that certificate renewed event handler works without copying."""
|
|
component.should_copy_certificates = False
|
|
with _assert_restarted_daemons(component):
|
|
component.on_certificate_renewed(
|
|
['valid.example'], '/etc/letsencrypt/live/valid.example/')
|
|
_assert_copy_certificate_called(component, copy_certificate, {})
|
|
|
|
|
|
def test_on_certificate_revoked(copy_certificate, component):
|
|
"""Test that certificate revoked event handler works."""
|
|
with _assert_restarted_daemons(component):
|
|
component.on_certificate_revoked(
|
|
['valid.example', 'irrelevant.example'],
|
|
'/etc/letsencrypt/live/valid.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {
|
|
'valid.example': 'invalid',
|
|
})
|
|
|
|
|
|
def test_on_certificate_revoked_irrelevant(copy_certificate, component):
|
|
"""Test that certificate revoked event handler works for
|
|
irrelevant domains.
|
|
"""
|
|
with _assert_restarted_daemons(component, []):
|
|
component.on_certificate_revoked(
|
|
['irrelevant.example'],
|
|
'/etc/letsencrypt/live/irrelevant.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {})
|
|
|
|
|
|
def test_on_certificate_revoked_without_copy(copy_certificate, component):
|
|
"""Test that certificate revoked event handler works without copying."""
|
|
component.should_copy_certificates = False
|
|
with _assert_restarted_daemons(component):
|
|
component.on_certificate_revoked(
|
|
['valid.example'], '/etc/letsencrypt/live/valid.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {})
|
|
|
|
|
|
def test_on_certificate_deleted(copy_certificate, component):
|
|
"""Test that certificate deleted event handler works."""
|
|
with _assert_restarted_daemons(component):
|
|
component.on_certificate_deleted(
|
|
['valid.example', 'irrelevant.example'],
|
|
'/etc/letsencrypt/live/valid.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {
|
|
'valid.example': 'invalid',
|
|
})
|
|
|
|
|
|
def test_on_certificate_deleted_irrelevant(copy_certificate, component):
|
|
"""Test that certificate deleted event handler works for
|
|
irrelevant domains.
|
|
"""
|
|
with _assert_restarted_daemons(component, []):
|
|
component.on_certificate_deleted(
|
|
['irrelevant.example'],
|
|
'/etc/letsencrypt/live/irrelevant.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {})
|
|
|
|
|
|
def test_on_certificate_deleted_without_copy(copy_certificate, component):
|
|
"""Test that certificate deleted event handler works without copying."""
|
|
component.should_copy_certificates = False
|
|
with _assert_restarted_daemons(component):
|
|
component.on_certificate_deleted(
|
|
['valid.example'], '/etc/letsencrypt/live/valid.example/')
|
|
|
|
_assert_copy_certificate_called(component, copy_certificate, {})
|