Merge ~hloeung/content-cache-charm:master into content-cache-charm:master

Proposed by Haw Loeung
Status: Merged
Approved by: Haw Loeung
Approved revision: edb4467b113921a80de960c33be4096c73febd89
Merged at revision: 6dc158dda107948ed5c41d42dbfe81a82f59b66d
Proposed branch: ~hloeung/content-cache-charm:master
Merge into: content-cache-charm:master
Diff against target: 689 lines (+512/-32)
16 files modified
Makefile (+2/-2)
README.md (+9/-0)
config.yaml (+12/-0)
dev/null (+0/-18)
layer.yaml (+7/-1)
lib/nginx.py (+83/-0)
metadata.yaml (+8/-4)
pytest.ini (+6/-0)
reactive/content_cache.py (+86/-0)
tests/functional/test_content_cache.py (+5/-5)
tests/unit/files/nginx_config_rendered_test_output-site1.local.txt (+19/-0)
tests/unit/files/nginx_config_rendered_test_output-site2.local.txt (+14/-0)
tests/unit/files/nginx_config_test_config.txt (+25/-0)
tests/unit/test_content_cache.py (+152/-0)
tests/unit/test_nginx.py (+83/-0)
tox.ini (+1/-2)
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Haw Loeung Needs Resubmitting
Review via email: mp+363822@code.launchpad.net

Commit message

Initial charm

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Haw Loeung (hloeung) :
review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change cannot be self approved, setting status to needs review.

Revision history for this message
Stuart Bishop (stub) wrote :

This all looks good, and nice to see someone chasing complete test coverage.

I think I found two bugs, commented on inline, but neither dificult to fix.

The rest is stylistic stuff. Most of them are just suggestions on alternative unittest.mock asserts; it is a very fat API and the docs take some getting used to.

I do think it is worth adding layer:status to your layer.yaml, as it will make the charm easier to maintain and, since this is a rare example of a tested charm, avoids some false positives in the tests. Particularly, there will be no need to ensure that an important status like 'blocked' was the last status set, and instead we can rely on layer:status ensuring that the important status is presented to the user when the hook finishes.

In general, I think it is its better for tests to focus on particular important results and only test those, rather than that the implementation exactly matches what the test expects. A good test is one where the implementation can be changed, and if the results are the same, the test passes unmodified. Mocks don't make this easy, and it is worth thinking about if you are best off with exact matches (eg. these flags where set, and only these flags, and in this order), or less fragile tests (eg. these flags were set, and maybe some others, and order doesn't matter).

Assuming this is still a work in progress, it is worth creating a new branch. Future merge proposals can use this branch as the dependent branch, and only the changes displayed for review.

review: Approve
Revision history for this message
Haw Loeung (hloeung) wrote :

First round of fixes. Will continue with more.

Revision history for this message
Haw Loeung (hloeung) wrote :

Second found of fixes done.

Revision history for this message
Haw Loeung (hloeung) wrote :

Another round of fixes.

Revision history for this message
Haw Loeung (hloeung) :
Revision history for this message
Haw Loeung (hloeung) wrote :

Last round of fixes to address all of Stuart's points.

Revision history for this message
Haw Loeung (hloeung) :
review: Needs Resubmitting
Revision history for this message
Stuart Bishop (stub) wrote :

Yup, all good.

You may be able to replace the status reset_mocks with a single one, doing charms.layer.reset_mock() in the TestCase's setUp method.

It would be possible to target the status mock by installing the MagicMock at charms.layer.status instead of charms.layer. However, what you have may be more useful as pretty much anything that comes from charms.layer will need to be mocked.

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 6dc158dda107948ed5c41d42dbfe81a82f59b66d

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/Makefile b/Makefile
2index cbd8b5c..e592bf7 100644
3--- a/Makefile
4+++ b/Makefile
5@@ -23,8 +23,8 @@ functionaltest:
6
7 clean:
8 @echo "Cleaning files"
9- @if [ -d ./.tox ] ; then rm -r ./.tox ; fi
10- @if [ -d ./.pytest_cache ] ; then rm -r ./.pytest_cache ; fi
11+ @rm -rf ./.tox
12+ @rm -rf ./.pytest_cache
13
14 # The targets below don't depend on a file
15 .PHONY: lint test unittest functionaltest clean help
16diff --git a/README.md b/README.md
17index e69de29..004a714 100644
18--- a/README.md
19+++ b/README.md
20@@ -0,0 +1,9 @@
21+# Overview
22+
23+Deploy your own content distribution network (CDN).
24+
25+
26+# Usage
27+
28+
29+# TODO
30diff --git a/config.yaml b/config.yaml
31index 8d4f5d1..36d48c3 100644
32--- a/config.yaml
33+++ b/config.yaml
34@@ -11,3 +11,15 @@ options:
35 description: |
36 A comma-separated list of nagios servicegroups.
37 If left empty, the nagios_context will be used as the servicegroup
38+ sites:
39+ default: ""
40+ type: string
41+ description: |
42+ YAML-formatted virtual hosts/sites. e.g.
43+ site1.local:
44+ server:
45+ server_name: site1.local
46+ location:
47+ path: /
48+ proxy_pass: http://localhost:8081
49+ proxy_set_header: Host "site1.local"
50diff --git a/layer.yaml b/layer.yaml
51index 3a60daa..253b93a 100644
52--- a/layer.yaml
53+++ b/layer.yaml
54@@ -2,4 +2,10 @@ includes:
55 - layer:basic
56 - layer:apt
57 - layer:nagios
58-repo: lp:cdn-charm
59+ - layer:status
60+repo: lp:content-cache-charm
61+options:
62+ basic:
63+ packages:
64+ - haproxy
65+ - nginx
66diff --git a/lib/nginx.py b/lib/nginx.py
67new file mode 100644
68index 0000000..ac2f0eb
69--- /dev/null
70+++ b/lib/nginx.py
71@@ -0,0 +1,83 @@
72+import os
73+
74+NGINX_SITE_BASE_PATH = '/etc/nginx'
75+INDENT = ' '*4
76+
77+
78+class NginxConf:
79+
80+ def __init__(self, sites_base_path=NGINX_SITE_BASE_PATH):
81+ self._sites_path = os.path.join(sites_base_path, 'sites-available')
82+
83+ # Expose sites_path as a property to allow mocking in indirect calls to
84+ # this class.
85+ @property
86+ def sites_path(self):
87+ return self._sites_path
88+
89+ def write_site(self, site, new):
90+ fname = os.path.join(self.sites_path, site)
91+ # Check if contents changed
92+ try:
93+ with open(fname, 'r', encoding='utf-8') as f:
94+ current = f.read()
95+ except FileNotFoundError:
96+ current = ''
97+ if new == current:
98+ return False
99+ with open(fname, 'w', encoding='utf-8') as f:
100+ f.write(new)
101+ return True
102+
103+ def sync_sites(self, sites):
104+ changed = False
105+ for site in os.listdir(self.sites_path):
106+ available = os.path.join(self.sites_path, site)
107+ enabled = os.path.join(os.path.dirname(self.sites_path), 'sites-enabled', site)
108+ if site not in sites:
109+ changed = True
110+ try:
111+ os.remove(available)
112+ os.remove(enabled)
113+ except FileNotFoundError:
114+ pass
115+ elif not os.path.exists(enabled):
116+ changed = True
117+ os.symlink(available, enabled)
118+
119+ return changed
120+
121+ def render(self, conf):
122+ output = []
123+ for key in conf.keys():
124+ if key == 'server':
125+ output.append(self._render_server(conf[key]))
126+ else:
127+ output.append('{key} {value};'
128+ .format(key=key, value=conf[key]))
129+ return '\n'.join(output)
130+
131+ def _render_server(self, conf):
132+ output = ['\nserver {']
133+ for key in conf.keys():
134+ if key == 'location':
135+ output.append(self._render_location(conf[key]))
136+ else:
137+ output.append('{indent}{key} {value};'
138+ .format(indent=INDENT, key=key, value=conf[key]))
139+ for log in ['access_log', 'error_log']:
140+ if log not in conf:
141+ output.append('{indent}{key} /var/log/nginx/{site}-access.log;'
142+ .format(indent=INDENT, key=log, site=conf['server_name']))
143+ output.append('}\n')
144+ return '\n'.join(output)
145+
146+ def _render_location(self, conf):
147+ output = ['\n{}location {} {{'.format(INDENT, conf['path'])]
148+ for key in conf.keys():
149+ if key == 'path':
150+ continue
151+ output.append('{indent}{indent}{key} {value};'
152+ .format(indent=INDENT, key=key, value=conf[key]))
153+ output.append('{}}}\n'.format(INDENT))
154+ return '\n'.join(output)
155diff --git a/metadata.yaml b/metadata.yaml
156index 43eba49..ceba5c2 100644
157--- a/metadata.yaml
158+++ b/metadata.yaml
159@@ -1,10 +1,14 @@
160-name: cdn
161-summary: Content distribution network (CDN)
162-maintainer: CDN Charmers <cdn-charmers@lists.launchpad.net>
163+name: content-cache
164+summary: Content / Frontend Cache
165+maintainer: Content Cache Charmers <content-cache-charmers@lists.launchpad.net>
166 description: |
167- Deploy CDN
168+ Installs Nginx and HAProxy as a highly available web accelerator
169+ with TLS support. Useful for providing local mirrors of HTTP servers
170+ and building content delivery networks (CDN).
171 tags:
172 - cache-proxy
173+ - content-cache
174+ - web-cache
175 - ops
176 series:
177 - bionic
178diff --git a/pytest.ini b/pytest.ini
179new file mode 100644
180index 0000000..39da980
181--- /dev/null
182+++ b/pytest.ini
183@@ -0,0 +1,6 @@
184+[pytest]
185+filterwarnings =
186+ # https://github.com/juju/charm-helpers/issues/294
187+ ignore:.*inspect.getargspec\(\) is deprecated.*:DeprecationWarning
188+ # https://github.com/juju/charm-helpers/issues/293
189+ ignore:.*dist\(\) and linux_distribution\(\) functions are deprecated:PendingDeprecationWarning
190diff --git a/reactive/cdn.py b/reactive/cdn.py
191deleted file mode 100644
192index e69de29..0000000
193--- a/reactive/cdn.py
194+++ /dev/null
195diff --git a/reactive/content_cache.py b/reactive/content_cache.py
196new file mode 100644
197index 0000000..d620521
198--- /dev/null
199+++ b/reactive/content_cache.py
200@@ -0,0 +1,86 @@
201+import yaml
202+
203+from charms import reactive
204+from charms.layer import status
205+from charmhelpers.core import hookenv, host
206+from lib import nginx
207+
208+
209+@reactive.hook('upgrade-charm')
210+def upgrade_charm():
211+ status.maintenance('forcing reconfiguration on upgrade-charm')
212+ reactive.clear_flag('content_cache.active')
213+ reactive.clear_flag('content_cache.installed')
214+ reactive.clear_flag('content_cache.haproxy.configured')
215+ reactive.clear_flag('content_cache.nginx.configured')
216+
217+
218+@reactive.when_not('content_cache.installed')
219+def install():
220+ reactive.clear_flag('content_cache.active')
221+
222+ reactive.clear_flag('content_cache.haproxy.configured')
223+ reactive.clear_flag('content_cache.nginx.configured')
224+ reactive.set_flag('content_cache.installed')
225+
226+
227+@reactive.when('config.changed')
228+def config_changed():
229+ reactive.clear_flag('content_cache.haproxy.configured')
230+ reactive.clear_flag('content_cache.nginx.configured')
231+
232+
233+@reactive.when('content_cache.nginx.configured', 'content_cache.haproxy.configured')
234+@reactive.when_not('content_cache.active')
235+def set_active():
236+ # XXX: Add more info such as nginx and haproxy status
237+ status.active('ready')
238+ reactive.set_flag('content_cache.active')
239+
240+
241+def service_start_or_restart(name):
242+ if host.service_running(name):
243+ status.maintenance('Restarting {}...'.format(name))
244+ host.service_restart(name)
245+ else:
246+ status.maintenance('Starting {}...'.format(name))
247+ host.service_start(name)
248+
249+
250+@reactive.when_not('content_cache.nginx.configured')
251+def configure_nginx():
252+ config = hookenv.config()
253+
254+ if not config.get('sites'):
255+ status.blocked('requires list of sites to configure')
256+ reactive.clear_flag('content_cache.active')
257+ return
258+
259+ ngx_conf = nginx.NginxConf()
260+ conf = yaml.safe_load(config.get('sites'))
261+ changed = False
262+ for site in conf.keys():
263+ if ngx_conf.write_site(site, ngx_conf.render(conf[site])):
264+ hookenv.log('Wrote out new configs for site: {}'.format(site))
265+ changed = True
266+ if ngx_conf.sync_sites(conf.keys()):
267+ hookenv.log('Enabled sites: {}'.format(' '.join(conf.keys())))
268+ changed = True
269+ if changed:
270+ service_start_or_restart('nginx')
271+
272+ reactive.set_flag('content_cache.nginx.configured')
273+
274+
275+@reactive.when_not('content_cache.haproxy.configured')
276+def configure_haproxy():
277+ config = hookenv.config()
278+
279+ if not config.get('sites'):
280+ status.blocked('requires list of sites to configure')
281+ reactive.clear_flag('content_cache.active')
282+ return
283+
284+ # TODO: Configure up and start/restart HAProxy
285+
286+ reactive.set_flag('content_cache.haproxy.configured')
287diff --git a/tests/functional/test_cdn.py b/tests/functional/test_content_cache.py
288similarity index 86%
289rename from tests/functional/test_cdn.py
290rename to tests/functional/test_content_cache.py
291index 70a79da..e5660c3 100644
292--- a/tests/functional/test_cdn.py
293+++ b/tests/functional/test_content_cache.py
294@@ -21,7 +21,7 @@ async def model():
295 async def apps(model):
296 apps = []
297 for entry in series:
298- app = model.applications['cdn-{}'.format(entry)]
299+ app = model.applications['content_cache-{}'.format(entry)]
300 apps.append(app)
301 return apps
302
303@@ -35,15 +35,15 @@ async def units(apps):
304
305
306 @pytest.mark.parametrize('series', series)
307-async def test_cdn_deploy(model, series):
308+async def test_content_cache_deploy(model, series):
309 # Starts a deploy for each series
310- await model.deploy('{}/builds/cdn'.format(juju_repository),
311+ await model.deploy('{}/builds/content_cache'.format(juju_repository),
312 series=series,
313- application_name='cdn-{}'.format(series))
314+ application_name='content_cache-{}'.format(series))
315 assert True
316
317
318-async def test_cdn_status(apps, model):
319+async def test_content_cache_status(apps, model):
320 # Verifies status for all deployed series of the charm
321 for app in apps:
322 await model.block_until(lambda: app.status == 'active')
323diff --git a/tests/unit/files/nginx_config_rendered_test_output-site1.local.txt b/tests/unit/files/nginx_config_rendered_test_output-site1.local.txt
324new file mode 100644
325index 0000000..f5b318d
326--- /dev/null
327+++ b/tests/unit/files/nginx_config_rendered_test_output-site1.local.txt
328@@ -0,0 +1,19 @@
329+proxy_cache_path /var/cache/nginx/site1.local use_temp_path=off levels=1:2 keys_zone=site1-cache:10m max_size=1g;
330+
331+server {
332+ server_name site1.local;
333+ listen 6081;
334+
335+ location / {
336+ proxy_pass http://localhost:8081;
337+ proxy_set_header Host "site1.local";
338+ proxy_cache valid 200 1d;
339+ proxy_cache_lock on;
340+ proxy_cache_min_uses 5;
341+ proxy_cache_revalidate on;
342+ proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
343+ }
344+
345+ access_log /var/log/nginx/site1.local-access.log;
346+ error_log /var/log/nginx/site1.local-access.log;
347+}
348diff --git a/tests/unit/files/nginx_config_rendered_test_output-site2.local.txt b/tests/unit/files/nginx_config_rendered_test_output-site2.local.txt
349new file mode 100644
350index 0000000..a76e005
351--- /dev/null
352+++ b/tests/unit/files/nginx_config_rendered_test_output-site2.local.txt
353@@ -0,0 +1,14 @@
354+proxy_cache_path /var/cache/nginx/site2.local use_temp_path=off levels=1:2 keys_zone=site2-cache:10m max_size=1g;
355+
356+server {
357+ server_name site2.local;
358+ listen 6082;
359+ access_log /var/log/nginx/site2.local-access.log;
360+ error_log /var/log/nginx/site2.local-error.log;;
361+
362+ location / {
363+ proxy_pass http://localhost:8082;
364+ proxy_set_header Host "site2.local";
365+ }
366+
367+}
368diff --git a/tests/unit/files/nginx_config_test_config.txt b/tests/unit/files/nginx_config_test_config.txt
369new file mode 100644
370index 0000000..9087fae
371--- /dev/null
372+++ b/tests/unit/files/nginx_config_test_config.txt
373@@ -0,0 +1,25 @@
374+site1.local:
375+ proxy_cache_path: /var/cache/nginx/site1.local use_temp_path=off levels=1:2 keys_zone=site1-cache:10m max_size=1g
376+ server:
377+ server_name: site1.local
378+ listen: 6081
379+ location:
380+ path: /
381+ proxy_pass: http://localhost:8081
382+ proxy_set_header: Host "site1.local"
383+ proxy_cache: valid 200 1d
384+ proxy_cache_lock: 'on'
385+ proxy_cache_min_uses: 5
386+ proxy_cache_revalidate: 'on'
387+ proxy_cache_use_stale: error timeout updating http_500 http_502 http_503 http_504
388+site2.local:
389+ proxy_cache_path: /var/cache/nginx/site2.local use_temp_path=off levels=1:2 keys_zone=site2-cache:10m max_size=1g
390+ server:
391+ server_name: site2.local
392+ listen: 6082
393+ access_log: /var/log/nginx/site2.local-access.log
394+ error_log: /var/log/nginx/site2.local-error.log;
395+ location:
396+ path: /
397+ proxy_pass: http://localhost:8082
398+ proxy_set_header: Host "site2.local"
399diff --git a/tests/unit/test_cdn.py b/tests/unit/test_cdn.py
400deleted file mode 100644
401index 6342df8..0000000
402--- a/tests/unit/test_cdn.py
403+++ /dev/null
404@@ -1,18 +0,0 @@
405-import shutil
406-import tempfile
407-import unittest
408-
409-
410-class TestCDN(unittest.TestCase):
411- def setUp(self):
412- self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
413-
414- def tearDown(self):
415- shutil.rmtree(self.tmpdir)
416-
417- def test_test(self):
418- self.assertTrue(True)
419-
420-
421-if __name__ == '__main__':
422- unittest.main()
423diff --git a/tests/unit/test_content_cache.py b/tests/unit/test_content_cache.py
424new file mode 100644
425index 0000000..2366df8
426--- /dev/null
427+++ b/tests/unit/test_content_cache.py
428@@ -0,0 +1,152 @@
429+import os
430+import shutil
431+import sys
432+import tempfile
433+import unittest
434+from unittest import mock
435+
436+# Add path to where our reactive layer lives and import.
437+sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
438+# We also need to mock up charms.layer so we can run unit tests without having
439+# to build the charm and pull in layers such as layer-status.
440+sys.modules['charms.layer'] = mock.MagicMock()
441+from charms.layer import status # NOQA: E402
442+from reactive import content_cache # NOQA: E402
443+
444+
445+class TestCharm(unittest.TestCase):
446+ def setUp(self):
447+ self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
448+ self.addCleanup(shutil.rmtree, self.tmpdir)
449+
450+ self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
451+
452+ patcher = mock.patch('charmhelpers.core.hookenv.log')
453+ self.mock_log = patcher.start()
454+ self.addCleanup(patcher.stop)
455+ self.mock_log.return_value = ''
456+
457+ patcher = mock.patch('charmhelpers.core.hookenv.charm_dir')
458+ self.mock_charm_dir = patcher.start()
459+ self.addCleanup(patcher.stop)
460+ self.mock_charm_dir.return_value = self.charm_dir
461+
462+ patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
463+ self.mock_local_unit = patcher.start()
464+ self.addCleanup(patcher.stop)
465+ self.mock_local_unit.return_value = 'mock-content-cache/0'
466+
467+ patcher = mock.patch('charmhelpers.core.hookenv.config')
468+ self.mock_config = patcher.start()
469+ self.addCleanup(patcher.stop)
470+ self.mock_config.return_value = {}
471+
472+ @mock.patch('charms.reactive.clear_flag')
473+ def test_hook_upgrade_charm_flags(self, clear_flag):
474+ '''Test correct flags set via upgrade-charm hook'''
475+ status.maintenance.reset_mock()
476+ content_cache.upgrade_charm()
477+ self.assertFalse(status.maintenance.assert_called())
478+ expected = [mock.call('content_cache.active'),
479+ mock.call('content_cache.installed'),
480+ mock.call('content_cache.haproxy.configured'),
481+ mock.call('content_cache.nginx.configured')]
482+ self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
483+
484+ @mock.patch('charms.reactive.clear_flag')
485+ @mock.patch('charms.reactive.set_flag')
486+ def test_hook_install_flags(self, set_flag, clear_flag):
487+ '''Test correct flags are set via install charm hook'''
488+ content_cache.install()
489+ expected = [mock.call('content_cache.installed')]
490+ self.assertFalse(set_flag.assert_has_calls(expected, any_order=True))
491+
492+ expected = [mock.call('content_cache.active'),
493+ mock.call('content_cache.haproxy.configured'),
494+ mock.call('content_cache.nginx.configured')]
495+ self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
496+
497+ @mock.patch('charms.reactive.clear_flag')
498+ def test_hook_config_changed_flags(self, clear_flag):
499+ '''Test correct flags are set via config-changed charm hook'''
500+ content_cache.config_changed()
501+ expected = [mock.call('content_cache.haproxy.configured'),
502+ mock.call('content_cache.nginx.configured')]
503+ self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
504+
505+ @mock.patch('charms.reactive.set_flag')
506+ def test_hook_set_active(self, set_flag):
507+ status.active.reset_mock()
508+ content_cache.set_active()
509+ self.assertFalse(status.active.assert_called())
510+ self.assertFalse(set_flag.assert_called_once_with('content_cache.active'))
511+
512+ @mock.patch('charmhelpers.core.host.service_running')
513+ @mock.patch('charmhelpers.core.host.service_restart')
514+ @mock.patch('charmhelpers.core.host.service_start')
515+ def test_service_start_or_restart_running(self, service_start, service_restart, service_running):
516+ '''Test service restarted when already running'''
517+ service_running.return_value = True
518+ status.active.reset_mock()
519+ content_cache.service_start_or_restart('someservice')
520+ self.assertFalse(status.maintenance.assert_called())
521+ self.assertFalse(service_start.assert_not_called())
522+ self.assertFalse(service_restart.assert_called_once_with('someservice'))
523+
524+ @mock.patch('charmhelpers.core.host.service_running')
525+ @mock.patch('charmhelpers.core.host.service_restart')
526+ @mock.patch('charmhelpers.core.host.service_start')
527+ def test_service_start_or_restart_stopped(self, service_start, service_restart, service_running):
528+ '''Test service started up when not running/stopped'''
529+ service_running.return_value = False
530+ status.active.reset_mock()
531+ content_cache.service_start_or_restart('someservice')
532+ self.assertFalse(status.maintenance.assert_called())
533+ self.assertFalse(service_start.assert_called_once_with('someservice'))
534+ self.assertFalse(service_restart.assert_not_called())
535+
536+ @mock.patch('charms.reactive.clear_flag')
537+ def test_configure_nginx_no_sites(self, clear_flag):
538+ '''Test correct flags are set when no sites defined to configure Nginx'''
539+ status.blocked.reset_mock()
540+ content_cache.configure_nginx()
541+ self.assertFalse(status.blocked.assert_called())
542+ self.assertFalse(clear_flag.assert_called_once_with('content_cache.active'))
543+
544+ @mock.patch('reactive.content_cache.service_start_or_restart')
545+ def test_configure_nginx_sites(self, service_start_or_restart):
546+ '''Test configuration of Nginx sites'''
547+ with open('tests/unit/files/nginx_config_test_config.txt', 'r', encoding='utf-8') as f:
548+ ngx_config = f.read()
549+ self.mock_config.return_value = {'sites': ngx_config}
550+
551+ with mock.patch('lib.nginx.NginxConf.sites_path', new_callable=mock.PropertyMock) as mock_site_path:
552+ mock_site_path.return_value = os.path.join(self.tmpdir, 'sites-available')
553+ # sites-available and sites-enabled won't exist in our temp dir
554+ os.mkdir(os.path.join(self.tmpdir, 'sites-available'))
555+ os.mkdir(os.path.join(self.tmpdir, 'sites-enabled'))
556+ content_cache.configure_nginx()
557+ self.assertFalse(service_start_or_restart.assert_called_once_with('nginx'))
558+
559+ # Re-run with same set of sites, no change so shouldn't need to restart Nginx
560+ service_start_or_restart.reset_mock()
561+ content_cache.configure_nginx()
562+ self.assertFalse(service_start_or_restart.assert_not_called())
563+
564+ @mock.patch('charms.reactive.clear_flag')
565+ def test_configure_haproxy_no_sites(self, clear_flag):
566+ status.blocked.reset_mock()
567+ content_cache.configure_haproxy()
568+ self.assertFalse(status.blocked.assert_called())
569+ self.assertFalse(clear_flag.assert_called_once_with('content_cache.active'))
570+
571+ @mock.patch('reactive.content_cache.service_start_or_restart')
572+ def test_configure_haproxy_sites(self, service_start_or_restart):
573+ with open('tests/unit/files/nginx_config_test_config.txt', 'r', encoding='utf-8') as f:
574+ ngx_config = f.read()
575+ self.mock_config.return_value = {'sites': ngx_config}
576+ content_cache.configure_haproxy()
577+
578+
579+if __name__ == '__main__':
580+ unittest.main()
581diff --git a/tests/unit/test_nginx.py b/tests/unit/test_nginx.py
582new file mode 100644
583index 0000000..7cec4e6
584--- /dev/null
585+++ b/tests/unit/test_nginx.py
586@@ -0,0 +1,83 @@
587+import os
588+import shutil
589+import sys
590+import tempfile
591+import unittest
592+import yaml
593+
594+sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
595+from lib import nginx # NOQA: E402
596+
597+
598+class TestLibNginx(unittest.TestCase):
599+ def setUp(self):
600+ self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
601+ self.addCleanup(shutil.rmtree, self.tmpdir)
602+ self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
603+
604+ def test_nginx_config_sites_path(self):
605+ sites_path = '/etc/nginx/sites-available'
606+ ngx_conf = nginx.NginxConf()
607+ self.assertEqual(ngx_conf.sites_path, sites_path)
608+
609+ def test_nginx_config_render(self):
610+ '''Test parsing a YAML-formatted list of sites'''
611+
612+ ngx_conf = nginx.NginxConf()
613+
614+ with open('tests/unit/files/nginx_config_test_config.txt', 'r', encoding='utf-8') as f:
615+ conf = yaml.safe_load(f.read())
616+
617+ # From the given YAML-formatted list of sites, check that each individual
618+ # Nginx config rendered matches what's in tests/unit/files.
619+ for site in conf.keys():
620+ output_file = 'tests/unit/files/nginx_config_rendered_test_output-{}.txt'.format(site)
621+ with open(output_file, 'r', encoding='utf-8') as f:
622+ output = f.read()
623+ self.assertEqual(output, ngx_conf.render(conf[site]))
624+
625+ def test_nginx_config_write_sites(self):
626+ '''Test writing out sites to individual Nginx site config files'''
627+ ngx_conf = nginx.NginxConf(self.tmpdir)
628+ os.mkdir(os.path.join(self.tmpdir, 'sites-available'))
629+ os.mkdir(os.path.join(self.tmpdir, 'sites-enabled'))
630+
631+ with open('tests/unit/files/nginx_config_rendered_test_output-site1.local.txt', 'r', encoding='utf-8') as f:
632+ conf = f.read()
633+
634+ self.assertTrue(ngx_conf.write_site('site1.local', conf))
635+ # Write again with same contents, this time it should return 'False'
636+ # as there's no change, thus no need to restart/reload Nginx.
637+ self.assertFalse(ngx_conf.write_site('site1.local', conf))
638+
639+ # Compare what's been written out matches what's in tests/unit/files.
640+ with open(os.path.join(self.tmpdir, 'sites-available', 'site1.local'), 'r', encoding='utf-8') as f:
641+ output = f.read()
642+ self.assertEqual(conf, output)
643+
644+ def test_nginx_config_sync_sites(self):
645+ '''Test cleanup of stale sites and that sites are enabled'''
646+ ngx_conf = nginx.NginxConf(self.tmpdir)
647+ os.mkdir(os.path.join(self.tmpdir, 'sites-available'))
648+ os.mkdir(os.path.join(self.tmpdir, 'sites-enabled'))
649+
650+ with open('tests/unit/files/nginx_config_rendered_test_output-site1.local.txt', 'r', encoding='utf-8') as f:
651+ conf = f.read()
652+
653+ # Write out an extra site config to test cleaning it up.
654+ for site in ['site1.local', 'site2.local']:
655+ ngx_conf.write_site(site, conf)
656+ ngx_conf.write_site('site3.local', conf)
657+
658+ # Clean up anything that's not site1 and site2.
659+ self.assertTrue(ngx_conf.sync_sites(['site1.local', 'site2.local']))
660+ # Check to make sure site1 still exists and is symlinked in site-senabled.
661+ self.assertTrue(os.path.exists(os.path.join(self.tmpdir, 'sites-available', 'site1.local')))
662+ self.assertTrue(os.path.islink(os.path.join(self.tmpdir, 'sites-enabled', 'site1.local')))
663+ # Only two sites, site3.local shouldn't exist.
664+ self.assertFalse(os.path.exists(os.path.join(self.tmpdir, 'sites-available', 'site3.local')))
665+ self.assertFalse(os.path.exists(os.path.join(self.tmpdir, 'sites-enabled', 'site3.local')))
666+
667+
668+if __name__ == '__main__':
669+ unittest.main()
670diff --git a/tox.ini b/tox.ini
671index ee5dbeb..d6b7675 100644
672--- a/tox.ini
673+++ b/tox.ini
674@@ -9,7 +9,7 @@ setenv =
675 PYTHONPATH = .
676
677 [testenv:unit]
678-commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term
679+commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term-missing --cov-branch
680 deps = -r{toxinidir}/tests/unit/requirements.txt
681 -r{toxinidir}/requirements.txt
682 setenv = PYTHONPATH={toxinidir}/lib
683@@ -32,6 +32,5 @@ exclude =
684 .git,
685 __pycache__,
686 .tox,
687- hooks/install.d/,
688 max-line-length = 120
689 max-complexity = 10

Subscribers

People subscribed via source and target branches