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
diff --git a/Makefile b/Makefile
index cbd8b5c..e592bf7 100644
--- a/Makefile
+++ b/Makefile
@@ -23,8 +23,8 @@ functionaltest:
2323
24clean:24clean:
25 @echo "Cleaning files"25 @echo "Cleaning files"
26 @if [ -d ./.tox ] ; then rm -r ./.tox ; fi26 @rm -rf ./.tox
27 @if [ -d ./.pytest_cache ] ; then rm -r ./.pytest_cache ; fi27 @rm -rf ./.pytest_cache
2828
29# The targets below don't depend on a file29# The targets below don't depend on a file
30.PHONY: lint test unittest functionaltest clean help30.PHONY: lint test unittest functionaltest clean help
diff --git a/README.md b/README.md
index e69de29..004a714 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,9 @@
1# Overview
2
3Deploy your own content distribution network (CDN).
4
5
6# Usage
7
8
9# TODO
diff --git a/config.yaml b/config.yaml
index 8d4f5d1..36d48c3 100644
--- a/config.yaml
+++ b/config.yaml
@@ -11,3 +11,15 @@ options:
11 description: |11 description: |
12 A comma-separated list of nagios servicegroups.12 A comma-separated list of nagios servicegroups.
13 If left empty, the nagios_context will be used as the servicegroup13 If left empty, the nagios_context will be used as the servicegroup
14 sites:
15 default: ""
16 type: string
17 description: |
18 YAML-formatted virtual hosts/sites. e.g.
19 site1.local:
20 server:
21 server_name: site1.local
22 location:
23 path: /
24 proxy_pass: http://localhost:8081
25 proxy_set_header: Host "site1.local"
diff --git a/layer.yaml b/layer.yaml
index 3a60daa..253b93a 100644
--- a/layer.yaml
+++ b/layer.yaml
@@ -2,4 +2,10 @@ includes:
2 - layer:basic2 - layer:basic
3 - layer:apt3 - layer:apt
4 - layer:nagios4 - layer:nagios
5repo: lp:cdn-charm5 - layer:status
6repo: lp:content-cache-charm
7options:
8 basic:
9 packages:
10 - haproxy
11 - nginx
diff --git a/lib/nginx.py b/lib/nginx.py
6new file mode 10064412new file mode 100644
index 0000000..ac2f0eb
--- /dev/null
+++ b/lib/nginx.py
@@ -0,0 +1,83 @@
1import os
2
3NGINX_SITE_BASE_PATH = '/etc/nginx'
4INDENT = ' '*4
5
6
7class NginxConf:
8
9 def __init__(self, sites_base_path=NGINX_SITE_BASE_PATH):
10 self._sites_path = os.path.join(sites_base_path, 'sites-available')
11
12 # Expose sites_path as a property to allow mocking in indirect calls to
13 # this class.
14 @property
15 def sites_path(self):
16 return self._sites_path
17
18 def write_site(self, site, new):
19 fname = os.path.join(self.sites_path, site)
20 # Check if contents changed
21 try:
22 with open(fname, 'r', encoding='utf-8') as f:
23 current = f.read()
24 except FileNotFoundError:
25 current = ''
26 if new == current:
27 return False
28 with open(fname, 'w', encoding='utf-8') as f:
29 f.write(new)
30 return True
31
32 def sync_sites(self, sites):
33 changed = False
34 for site in os.listdir(self.sites_path):
35 available = os.path.join(self.sites_path, site)
36 enabled = os.path.join(os.path.dirname(self.sites_path), 'sites-enabled', site)
37 if site not in sites:
38 changed = True
39 try:
40 os.remove(available)
41 os.remove(enabled)
42 except FileNotFoundError:
43 pass
44 elif not os.path.exists(enabled):
45 changed = True
46 os.symlink(available, enabled)
47
48 return changed
49
50 def render(self, conf):
51 output = []
52 for key in conf.keys():
53 if key == 'server':
54 output.append(self._render_server(conf[key]))
55 else:
56 output.append('{key} {value};'
57 .format(key=key, value=conf[key]))
58 return '\n'.join(output)
59
60 def _render_server(self, conf):
61 output = ['\nserver {']
62 for key in conf.keys():
63 if key == 'location':
64 output.append(self._render_location(conf[key]))
65 else:
66 output.append('{indent}{key} {value};'
67 .format(indent=INDENT, key=key, value=conf[key]))
68 for log in ['access_log', 'error_log']:
69 if log not in conf:
70 output.append('{indent}{key} /var/log/nginx/{site}-access.log;'
71 .format(indent=INDENT, key=log, site=conf['server_name']))
72 output.append('}\n')
73 return '\n'.join(output)
74
75 def _render_location(self, conf):
76 output = ['\n{}location {} {{'.format(INDENT, conf['path'])]
77 for key in conf.keys():
78 if key == 'path':
79 continue
80 output.append('{indent}{indent}{key} {value};'
81 .format(indent=INDENT, key=key, value=conf[key]))
82 output.append('{}}}\n'.format(INDENT))
83 return '\n'.join(output)
diff --git a/metadata.yaml b/metadata.yaml
index 43eba49..ceba5c2 100644
--- a/metadata.yaml
+++ b/metadata.yaml
@@ -1,10 +1,14 @@
1name: cdn1name: content-cache
2summary: Content distribution network (CDN)2summary: Content / Frontend Cache
3maintainer: CDN Charmers <cdn-charmers@lists.launchpad.net>3maintainer: Content Cache Charmers <content-cache-charmers@lists.launchpad.net>
4description: |4description: |
5 Deploy CDN5 Installs Nginx and HAProxy as a highly available web accelerator
6 with TLS support. Useful for providing local mirrors of HTTP servers
7 and building content delivery networks (CDN).
6tags:8tags:
7 - cache-proxy9 - cache-proxy
10 - content-cache
11 - web-cache
8 - ops12 - ops
9series:13series:
10 - bionic14 - bionic
diff --git a/pytest.ini b/pytest.ini
11new file mode 10064415new file mode 100644
index 0000000..39da980
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,6 @@
1[pytest]
2filterwarnings =
3 # https://github.com/juju/charm-helpers/issues/294
4 ignore:.*inspect.getargspec\(\) is deprecated.*:DeprecationWarning
5 # https://github.com/juju/charm-helpers/issues/293
6 ignore:.*dist\(\) and linux_distribution\(\) functions are deprecated:PendingDeprecationWarning
diff --git a/reactive/cdn.py b/reactive/cdn.py
0deleted file mode 1006447deleted file mode 100644
index e69de29..0000000
--- a/reactive/cdn.py
+++ /dev/null
diff --git a/reactive/content_cache.py b/reactive/content_cache.py
1new file mode 1006448new file mode 100644
index 0000000..d620521
--- /dev/null
+++ b/reactive/content_cache.py
@@ -0,0 +1,86 @@
1import yaml
2
3from charms import reactive
4from charms.layer import status
5from charmhelpers.core import hookenv, host
6from lib import nginx
7
8
9@reactive.hook('upgrade-charm')
10def upgrade_charm():
11 status.maintenance('forcing reconfiguration on upgrade-charm')
12 reactive.clear_flag('content_cache.active')
13 reactive.clear_flag('content_cache.installed')
14 reactive.clear_flag('content_cache.haproxy.configured')
15 reactive.clear_flag('content_cache.nginx.configured')
16
17
18@reactive.when_not('content_cache.installed')
19def install():
20 reactive.clear_flag('content_cache.active')
21
22 reactive.clear_flag('content_cache.haproxy.configured')
23 reactive.clear_flag('content_cache.nginx.configured')
24 reactive.set_flag('content_cache.installed')
25
26
27@reactive.when('config.changed')
28def config_changed():
29 reactive.clear_flag('content_cache.haproxy.configured')
30 reactive.clear_flag('content_cache.nginx.configured')
31
32
33@reactive.when('content_cache.nginx.configured', 'content_cache.haproxy.configured')
34@reactive.when_not('content_cache.active')
35def set_active():
36 # XXX: Add more info such as nginx and haproxy status
37 status.active('ready')
38 reactive.set_flag('content_cache.active')
39
40
41def service_start_or_restart(name):
42 if host.service_running(name):
43 status.maintenance('Restarting {}...'.format(name))
44 host.service_restart(name)
45 else:
46 status.maintenance('Starting {}...'.format(name))
47 host.service_start(name)
48
49
50@reactive.when_not('content_cache.nginx.configured')
51def configure_nginx():
52 config = hookenv.config()
53
54 if not config.get('sites'):
55 status.blocked('requires list of sites to configure')
56 reactive.clear_flag('content_cache.active')
57 return
58
59 ngx_conf = nginx.NginxConf()
60 conf = yaml.safe_load(config.get('sites'))
61 changed = False
62 for site in conf.keys():
63 if ngx_conf.write_site(site, ngx_conf.render(conf[site])):
64 hookenv.log('Wrote out new configs for site: {}'.format(site))
65 changed = True
66 if ngx_conf.sync_sites(conf.keys()):
67 hookenv.log('Enabled sites: {}'.format(' '.join(conf.keys())))
68 changed = True
69 if changed:
70 service_start_or_restart('nginx')
71
72 reactive.set_flag('content_cache.nginx.configured')
73
74
75@reactive.when_not('content_cache.haproxy.configured')
76def configure_haproxy():
77 config = hookenv.config()
78
79 if not config.get('sites'):
80 status.blocked('requires list of sites to configure')
81 reactive.clear_flag('content_cache.active')
82 return
83
84 # TODO: Configure up and start/restart HAProxy
85
86 reactive.set_flag('content_cache.haproxy.configured')
diff --git a/tests/functional/test_cdn.py b/tests/functional/test_content_cache.py
0similarity index 86%87similarity index 86%
1rename from tests/functional/test_cdn.py88rename from tests/functional/test_cdn.py
2rename to tests/functional/test_content_cache.py89rename to tests/functional/test_content_cache.py
index 70a79da..e5660c3 100644
--- a/tests/functional/test_cdn.py
+++ b/tests/functional/test_content_cache.py
@@ -21,7 +21,7 @@ async def model():
21async def apps(model):21async def apps(model):
22 apps = []22 apps = []
23 for entry in series:23 for entry in series:
24 app = model.applications['cdn-{}'.format(entry)]24 app = model.applications['content_cache-{}'.format(entry)]
25 apps.append(app)25 apps.append(app)
26 return apps26 return apps
2727
@@ -35,15 +35,15 @@ async def units(apps):
3535
3636
37@pytest.mark.parametrize('series', series)37@pytest.mark.parametrize('series', series)
38async def test_cdn_deploy(model, series):38async def test_content_cache_deploy(model, series):
39 # Starts a deploy for each series39 # Starts a deploy for each series
40 await model.deploy('{}/builds/cdn'.format(juju_repository),40 await model.deploy('{}/builds/content_cache'.format(juju_repository),
41 series=series,41 series=series,
42 application_name='cdn-{}'.format(series))42 application_name='content_cache-{}'.format(series))
43 assert True43 assert True
4444
4545
46async def test_cdn_status(apps, model):46async def test_content_cache_status(apps, model):
47 # Verifies status for all deployed series of the charm47 # Verifies status for all deployed series of the charm
48 for app in apps:48 for app in apps:
49 await model.block_until(lambda: app.status == 'active')49 await model.block_until(lambda: app.status == 'active')
diff --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
50new file mode 10064450new file mode 100644
index 0000000..f5b318d
--- /dev/null
+++ b/tests/unit/files/nginx_config_rendered_test_output-site1.local.txt
@@ -0,0 +1,19 @@
1proxy_cache_path /var/cache/nginx/site1.local use_temp_path=off levels=1:2 keys_zone=site1-cache:10m max_size=1g;
2
3server {
4 server_name site1.local;
5 listen 6081;
6
7 location / {
8 proxy_pass http://localhost:8081;
9 proxy_set_header Host "site1.local";
10 proxy_cache valid 200 1d;
11 proxy_cache_lock on;
12 proxy_cache_min_uses 5;
13 proxy_cache_revalidate on;
14 proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
15 }
16
17 access_log /var/log/nginx/site1.local-access.log;
18 error_log /var/log/nginx/site1.local-access.log;
19}
diff --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
0new file mode 10064420new file mode 100644
index 0000000..a76e005
--- /dev/null
+++ b/tests/unit/files/nginx_config_rendered_test_output-site2.local.txt
@@ -0,0 +1,14 @@
1proxy_cache_path /var/cache/nginx/site2.local use_temp_path=off levels=1:2 keys_zone=site2-cache:10m max_size=1g;
2
3server {
4 server_name site2.local;
5 listen 6082;
6 access_log /var/log/nginx/site2.local-access.log;
7 error_log /var/log/nginx/site2.local-error.log;;
8
9 location / {
10 proxy_pass http://localhost:8082;
11 proxy_set_header Host "site2.local";
12 }
13
14}
diff --git a/tests/unit/files/nginx_config_test_config.txt b/tests/unit/files/nginx_config_test_config.txt
0new file mode 10064415new file mode 100644
index 0000000..9087fae
--- /dev/null
+++ b/tests/unit/files/nginx_config_test_config.txt
@@ -0,0 +1,25 @@
1site1.local:
2 proxy_cache_path: /var/cache/nginx/site1.local use_temp_path=off levels=1:2 keys_zone=site1-cache:10m max_size=1g
3 server:
4 server_name: site1.local
5 listen: 6081
6 location:
7 path: /
8 proxy_pass: http://localhost:8081
9 proxy_set_header: Host "site1.local"
10 proxy_cache: valid 200 1d
11 proxy_cache_lock: 'on'
12 proxy_cache_min_uses: 5
13 proxy_cache_revalidate: 'on'
14 proxy_cache_use_stale: error timeout updating http_500 http_502 http_503 http_504
15site2.local:
16 proxy_cache_path: /var/cache/nginx/site2.local use_temp_path=off levels=1:2 keys_zone=site2-cache:10m max_size=1g
17 server:
18 server_name: site2.local
19 listen: 6082
20 access_log: /var/log/nginx/site2.local-access.log
21 error_log: /var/log/nginx/site2.local-error.log;
22 location:
23 path: /
24 proxy_pass: http://localhost:8082
25 proxy_set_header: Host "site2.local"
diff --git a/tests/unit/test_cdn.py b/tests/unit/test_cdn.py
0deleted file mode 10064426deleted file mode 100644
index 6342df8..0000000
--- a/tests/unit/test_cdn.py
+++ /dev/null
@@ -1,18 +0,0 @@
1import shutil
2import tempfile
3import unittest
4
5
6class TestCDN(unittest.TestCase):
7 def setUp(self):
8 self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
9
10 def tearDown(self):
11 shutil.rmtree(self.tmpdir)
12
13 def test_test(self):
14 self.assertTrue(True)
15
16
17if __name__ == '__main__':
18 unittest.main()
diff --git a/tests/unit/test_content_cache.py b/tests/unit/test_content_cache.py
19new file mode 1006440new file mode 100644
index 0000000..2366df8
--- /dev/null
+++ b/tests/unit/test_content_cache.py
@@ -0,0 +1,152 @@
1import os
2import shutil
3import sys
4import tempfile
5import unittest
6from unittest import mock
7
8# Add path to where our reactive layer lives and import.
9sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
10# We also need to mock up charms.layer so we can run unit tests without having
11# to build the charm and pull in layers such as layer-status.
12sys.modules['charms.layer'] = mock.MagicMock()
13from charms.layer import status # NOQA: E402
14from reactive import content_cache # NOQA: E402
15
16
17class TestCharm(unittest.TestCase):
18 def setUp(self):
19 self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
20 self.addCleanup(shutil.rmtree, self.tmpdir)
21
22 self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
23
24 patcher = mock.patch('charmhelpers.core.hookenv.log')
25 self.mock_log = patcher.start()
26 self.addCleanup(patcher.stop)
27 self.mock_log.return_value = ''
28
29 patcher = mock.patch('charmhelpers.core.hookenv.charm_dir')
30 self.mock_charm_dir = patcher.start()
31 self.addCleanup(patcher.stop)
32 self.mock_charm_dir.return_value = self.charm_dir
33
34 patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
35 self.mock_local_unit = patcher.start()
36 self.addCleanup(patcher.stop)
37 self.mock_local_unit.return_value = 'mock-content-cache/0'
38
39 patcher = mock.patch('charmhelpers.core.hookenv.config')
40 self.mock_config = patcher.start()
41 self.addCleanup(patcher.stop)
42 self.mock_config.return_value = {}
43
44 @mock.patch('charms.reactive.clear_flag')
45 def test_hook_upgrade_charm_flags(self, clear_flag):
46 '''Test correct flags set via upgrade-charm hook'''
47 status.maintenance.reset_mock()
48 content_cache.upgrade_charm()
49 self.assertFalse(status.maintenance.assert_called())
50 expected = [mock.call('content_cache.active'),
51 mock.call('content_cache.installed'),
52 mock.call('content_cache.haproxy.configured'),
53 mock.call('content_cache.nginx.configured')]
54 self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
55
56 @mock.patch('charms.reactive.clear_flag')
57 @mock.patch('charms.reactive.set_flag')
58 def test_hook_install_flags(self, set_flag, clear_flag):
59 '''Test correct flags are set via install charm hook'''
60 content_cache.install()
61 expected = [mock.call('content_cache.installed')]
62 self.assertFalse(set_flag.assert_has_calls(expected, any_order=True))
63
64 expected = [mock.call('content_cache.active'),
65 mock.call('content_cache.haproxy.configured'),
66 mock.call('content_cache.nginx.configured')]
67 self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
68
69 @mock.patch('charms.reactive.clear_flag')
70 def test_hook_config_changed_flags(self, clear_flag):
71 '''Test correct flags are set via config-changed charm hook'''
72 content_cache.config_changed()
73 expected = [mock.call('content_cache.haproxy.configured'),
74 mock.call('content_cache.nginx.configured')]
75 self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
76
77 @mock.patch('charms.reactive.set_flag')
78 def test_hook_set_active(self, set_flag):
79 status.active.reset_mock()
80 content_cache.set_active()
81 self.assertFalse(status.active.assert_called())
82 self.assertFalse(set_flag.assert_called_once_with('content_cache.active'))
83
84 @mock.patch('charmhelpers.core.host.service_running')
85 @mock.patch('charmhelpers.core.host.service_restart')
86 @mock.patch('charmhelpers.core.host.service_start')
87 def test_service_start_or_restart_running(self, service_start, service_restart, service_running):
88 '''Test service restarted when already running'''
89 service_running.return_value = True
90 status.active.reset_mock()
91 content_cache.service_start_or_restart('someservice')
92 self.assertFalse(status.maintenance.assert_called())
93 self.assertFalse(service_start.assert_not_called())
94 self.assertFalse(service_restart.assert_called_once_with('someservice'))
95
96 @mock.patch('charmhelpers.core.host.service_running')
97 @mock.patch('charmhelpers.core.host.service_restart')
98 @mock.patch('charmhelpers.core.host.service_start')
99 def test_service_start_or_restart_stopped(self, service_start, service_restart, service_running):
100 '''Test service started up when not running/stopped'''
101 service_running.return_value = False
102 status.active.reset_mock()
103 content_cache.service_start_or_restart('someservice')
104 self.assertFalse(status.maintenance.assert_called())
105 self.assertFalse(service_start.assert_called_once_with('someservice'))
106 self.assertFalse(service_restart.assert_not_called())
107
108 @mock.patch('charms.reactive.clear_flag')
109 def test_configure_nginx_no_sites(self, clear_flag):
110 '''Test correct flags are set when no sites defined to configure Nginx'''
111 status.blocked.reset_mock()
112 content_cache.configure_nginx()
113 self.assertFalse(status.blocked.assert_called())
114 self.assertFalse(clear_flag.assert_called_once_with('content_cache.active'))
115
116 @mock.patch('reactive.content_cache.service_start_or_restart')
117 def test_configure_nginx_sites(self, service_start_or_restart):
118 '''Test configuration of Nginx sites'''
119 with open('tests/unit/files/nginx_config_test_config.txt', 'r', encoding='utf-8') as f:
120 ngx_config = f.read()
121 self.mock_config.return_value = {'sites': ngx_config}
122
123 with mock.patch('lib.nginx.NginxConf.sites_path', new_callable=mock.PropertyMock) as mock_site_path:
124 mock_site_path.return_value = os.path.join(self.tmpdir, 'sites-available')
125 # sites-available and sites-enabled won't exist in our temp dir
126 os.mkdir(os.path.join(self.tmpdir, 'sites-available'))
127 os.mkdir(os.path.join(self.tmpdir, 'sites-enabled'))
128 content_cache.configure_nginx()
129 self.assertFalse(service_start_or_restart.assert_called_once_with('nginx'))
130
131 # Re-run with same set of sites, no change so shouldn't need to restart Nginx
132 service_start_or_restart.reset_mock()
133 content_cache.configure_nginx()
134 self.assertFalse(service_start_or_restart.assert_not_called())
135
136 @mock.patch('charms.reactive.clear_flag')
137 def test_configure_haproxy_no_sites(self, clear_flag):
138 status.blocked.reset_mock()
139 content_cache.configure_haproxy()
140 self.assertFalse(status.blocked.assert_called())
141 self.assertFalse(clear_flag.assert_called_once_with('content_cache.active'))
142
143 @mock.patch('reactive.content_cache.service_start_or_restart')
144 def test_configure_haproxy_sites(self, service_start_or_restart):
145 with open('tests/unit/files/nginx_config_test_config.txt', 'r', encoding='utf-8') as f:
146 ngx_config = f.read()
147 self.mock_config.return_value = {'sites': ngx_config}
148 content_cache.configure_haproxy()
149
150
151if __name__ == '__main__':
152 unittest.main()
diff --git a/tests/unit/test_nginx.py b/tests/unit/test_nginx.py
0new file mode 100644153new file mode 100644
index 0000000..7cec4e6
--- /dev/null
+++ b/tests/unit/test_nginx.py
@@ -0,0 +1,83 @@
1import os
2import shutil
3import sys
4import tempfile
5import unittest
6import yaml
7
8sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
9from lib import nginx # NOQA: E402
10
11
12class TestLibNginx(unittest.TestCase):
13 def setUp(self):
14 self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
15 self.addCleanup(shutil.rmtree, self.tmpdir)
16 self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
17
18 def test_nginx_config_sites_path(self):
19 sites_path = '/etc/nginx/sites-available'
20 ngx_conf = nginx.NginxConf()
21 self.assertEqual(ngx_conf.sites_path, sites_path)
22
23 def test_nginx_config_render(self):
24 '''Test parsing a YAML-formatted list of sites'''
25
26 ngx_conf = nginx.NginxConf()
27
28 with open('tests/unit/files/nginx_config_test_config.txt', 'r', encoding='utf-8') as f:
29 conf = yaml.safe_load(f.read())
30
31 # From the given YAML-formatted list of sites, check that each individual
32 # Nginx config rendered matches what's in tests/unit/files.
33 for site in conf.keys():
34 output_file = 'tests/unit/files/nginx_config_rendered_test_output-{}.txt'.format(site)
35 with open(output_file, 'r', encoding='utf-8') as f:
36 output = f.read()
37 self.assertEqual(output, ngx_conf.render(conf[site]))
38
39 def test_nginx_config_write_sites(self):
40 '''Test writing out sites to individual Nginx site config files'''
41 ngx_conf = nginx.NginxConf(self.tmpdir)
42 os.mkdir(os.path.join(self.tmpdir, 'sites-available'))
43 os.mkdir(os.path.join(self.tmpdir, 'sites-enabled'))
44
45 with open('tests/unit/files/nginx_config_rendered_test_output-site1.local.txt', 'r', encoding='utf-8') as f:
46 conf = f.read()
47
48 self.assertTrue(ngx_conf.write_site('site1.local', conf))
49 # Write again with same contents, this time it should return 'False'
50 # as there's no change, thus no need to restart/reload Nginx.
51 self.assertFalse(ngx_conf.write_site('site1.local', conf))
52
53 # Compare what's been written out matches what's in tests/unit/files.
54 with open(os.path.join(self.tmpdir, 'sites-available', 'site1.local'), 'r', encoding='utf-8') as f:
55 output = f.read()
56 self.assertEqual(conf, output)
57
58 def test_nginx_config_sync_sites(self):
59 '''Test cleanup of stale sites and that sites are enabled'''
60 ngx_conf = nginx.NginxConf(self.tmpdir)
61 os.mkdir(os.path.join(self.tmpdir, 'sites-available'))
62 os.mkdir(os.path.join(self.tmpdir, 'sites-enabled'))
63
64 with open('tests/unit/files/nginx_config_rendered_test_output-site1.local.txt', 'r', encoding='utf-8') as f:
65 conf = f.read()
66
67 # Write out an extra site config to test cleaning it up.
68 for site in ['site1.local', 'site2.local']:
69 ngx_conf.write_site(site, conf)
70 ngx_conf.write_site('site3.local', conf)
71
72 # Clean up anything that's not site1 and site2.
73 self.assertTrue(ngx_conf.sync_sites(['site1.local', 'site2.local']))
74 # Check to make sure site1 still exists and is symlinked in site-senabled.
75 self.assertTrue(os.path.exists(os.path.join(self.tmpdir, 'sites-available', 'site1.local')))
76 self.assertTrue(os.path.islink(os.path.join(self.tmpdir, 'sites-enabled', 'site1.local')))
77 # Only two sites, site3.local shouldn't exist.
78 self.assertFalse(os.path.exists(os.path.join(self.tmpdir, 'sites-available', 'site3.local')))
79 self.assertFalse(os.path.exists(os.path.join(self.tmpdir, 'sites-enabled', 'site3.local')))
80
81
82if __name__ == '__main__':
83 unittest.main()
diff --git a/tox.ini b/tox.ini
index ee5dbeb..d6b7675 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,7 +9,7 @@ setenv =
9 PYTHONPATH = .9 PYTHONPATH = .
1010
11[testenv:unit]11[testenv:unit]
12commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term12commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term-missing --cov-branch
13deps = -r{toxinidir}/tests/unit/requirements.txt13deps = -r{toxinidir}/tests/unit/requirements.txt
14 -r{toxinidir}/requirements.txt14 -r{toxinidir}/requirements.txt
15setenv = PYTHONPATH={toxinidir}/lib15setenv = PYTHONPATH={toxinidir}/lib
@@ -32,6 +32,5 @@ exclude =
32 .git,32 .git,
33 __pycache__,33 __pycache__,
34 .tox,34 .tox,
35 hooks/install.d/,
36max-line-length = 12035max-line-length = 120
37max-complexity = 1036max-complexity = 10

Subscribers

People subscribed via source and target branches