File: test_components.py

package info (click to toggle)
freedombox 26.2
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 82,976 kB
  • sloc: python: 48,504; javascript: 1,736; xml: 481; makefile: 290; sh: 167; php: 32
file content (440 lines) | stat: -rw-r--r-- 17,021 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# 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, {})