Merge ~aieri/charm-sudo-pair:functional_fixtures into ~sudo-pair-charmers/charm-sudo-pair:master

Proposed by Andrea Ieri
Status: Merged
Approved by: Andrea Ieri
Approved revision: 5fd51e354cfec8039f2fc45eb855138fa8062669
Merge reported by: Andrea Ieri
Merged at revision: 5fd51e354cfec8039f2fc45eb855138fa8062669
Proposed branch: ~aieri/charm-sudo-pair:functional_fixtures
Merge into: ~sudo-pair-charmers/charm-sudo-pair:master
Diff against target: 411 lines (+237/-122)
2 files modified
tests/functional/conftest.py (+164/-0)
tests/functional/test_deploy.py (+73/-122)
Reviewer Review Type Date Requested Status
Giuseppe Petralia Approve
Review via email: mp+358899@code.launchpad.net

Commit message

Rewrite of fixtures and tests

Description of the change

The fixtures in conftest.py are generic enough to be moved to a test template

To post a comment you must log in.
Revision history for this message
Giuseppe Petralia (peppepetra) wrote :

Looks good to me. All tests pass and the code is nice and clean. I agree with Andrea about conftest.py, that can be merged into charm-template into the functional folder.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
2new file mode 100644
3index 0000000..d5de8f4
4--- /dev/null
5+++ b/tests/functional/conftest.py
6@@ -0,0 +1,164 @@
7+#!/usr/bin/python3
8+
9+import pytest
10+import json
11+import uuid
12+import asyncio
13+import juju
14+from juju.controller import Controller
15+from juju.model import Model
16+from juju.errors import JujuError
17+
18+STAT_FILE = "python3 -c \"import json; import os; s=os.stat('%s'); print(json.dumps({'uid': s.st_uid, 'gid': s.st_gid, 'mode': oct(s.st_mode), 'size': s.st_size}))\"" # noqa: E501
19+
20+
21+@pytest.yield_fixture(scope='module')
22+def event_loop(request):
23+ '''Override the default pytest event loop to allow for broaded scoped
24+ fixtures'''
25+ loop = asyncio.get_event_loop_policy().new_event_loop()
26+ asyncio.set_event_loop(loop)
27+ loop.set_debug(True)
28+ yield loop
29+ loop.close()
30+ asyncio.set_event_loop(None)
31+
32+
33+@pytest.fixture(scope='module')
34+async def controller():
35+ '''Connect to the current controller'''
36+ controller = Controller()
37+ await controller.connect_current()
38+ yield controller
39+ await controller.disconnect()
40+
41+
42+@pytest.fixture(scope='module')
43+async def model(controller):
44+ '''This model lives only for the duration of the test'''
45+ model_name = str(uuid.uuid4())
46+ model = await controller.add_model(model_name)
47+ yield model
48+ await model.disconnect()
49+ await controller.destroy_model(model_name)
50+ while model_name in await controller.list_models():
51+ await asyncio.sleep(1)
52+
53+
54+@pytest.fixture(scope='module')
55+async def current_model():
56+ '''Returns the current model, does not create or destroy it'''
57+ model = Model()
58+ await model.connect_current()
59+ yield model
60+ await model.disconnect()
61+
62+
63+@pytest.fixture
64+async def get_app(model):
65+ '''Returns the application requested'''
66+ async def _get_app(name):
67+ try:
68+ return model.applications[name]
69+ except KeyError:
70+ raise JujuError("Cannot find application {}".format(name))
71+ return _get_app
72+
73+
74+@pytest.fixture
75+async def get_unit(model):
76+ '''Returns the requested <app_name>/<unit_number> unit'''
77+ async def _get_unit(name):
78+ try:
79+ (app_name, unit_number) = name.split('/')
80+ return model.applications[app_name].units[unit_number]
81+ except (KeyError, ValueError):
82+ raise JujuError("Cannot find unit {}".format(name))
83+ return _get_unit
84+
85+
86+@pytest.fixture
87+async def get_entity(model, get_unit, get_app):
88+ '''Returns a unit or an application'''
89+ async def _get_entity(name):
90+ try:
91+ return await get_unit(name)
92+ except JujuError:
93+ try:
94+ return await get_app(name)
95+ except JujuError as e:
96+ raise JujuError("Cannot find entity {}".format(name))
97+ return _get_entity
98+
99+
100+@pytest.fixture
101+async def run_command(get_unit):
102+ '''
103+ Runs a command on a unit.
104+
105+ :param cmd: Command to be run
106+ :param target: Unit object or unit name string
107+ '''
108+ async def _run_command(cmd, target):
109+ unit = (
110+ target
111+ if type(target) is juju.unit.Unit
112+ else await get_unit(target)
113+ )
114+ action = await unit.run(cmd)
115+ return action.results
116+ return _run_command
117+
118+
119+@pytest.fixture
120+async def file_stat(run_command):
121+ '''
122+ Runs stat on a file
123+
124+ :param path: File path
125+ :param target: Unit object or unit name string
126+ '''
127+ async def _file_stat(path, target):
128+ cmd = STAT_FILE % path
129+ results = await run_command(cmd, target)
130+ return json.loads(results['Stdout'])
131+ return _file_stat
132+
133+
134+@pytest.fixture
135+async def file_contents(run_command):
136+ '''
137+ Returns the contents of a file
138+
139+ :param path: File path
140+ :param target: Unit object or unit name string
141+ '''
142+ async def _file_contents(path, target):
143+ cmd = 'cat {}'.format(path)
144+ results = await run_command(cmd, target)
145+ return results['Stdout']
146+ return _file_contents
147+
148+
149+@pytest.fixture
150+async def reconfigure_app(get_app, model):
151+ '''Applies a different config to the requested app'''
152+ async def _reconfigure_app(cfg, target):
153+ application = (
154+ target
155+ if type(target) is juju.application.Application
156+ else await get_app(target)
157+ )
158+ await application.set_config(cfg)
159+ await application.get_config()
160+ await model.block_until(lambda: application.status == 'active')
161+ return _reconfigure_app
162+
163+
164+@pytest.fixture
165+async def create_group(run_command):
166+ '''Creates the UNIX group specified'''
167+ async def _create_group(group_name, target):
168+ cmd = "sudo groupadd %s" % group_name
169+ await run_command(cmd, target)
170+ return _create_group
171diff --git a/tests/functional/test_deploy.py b/tests/functional/test_deploy.py
172index 28ac196..6ce4ec3 100644
173--- a/tests/functional/test_deploy.py
174+++ b/tests/functional/test_deploy.py
175@@ -1,38 +1,33 @@
176 #!/usr/bin/python3.6
177
178-from juju.model import Model
179-import asyncio
180-import json
181 import pytest
182
183-
184-STAT_FILE = "python3 -c \"import json; import os; s=os.stat('%s'); print(json.dumps({'uid': s.st_uid, 'gid': s.st_gid, 'mode': oct(s.st_mode), 'size': s.st_size}))\""
185-
186-FILE_CONTENT = "python3 -c \"print(open('%s').read())\""
187-
188 pytestmark = pytest.mark.asyncio
189+SERIES = ['xenial', 'bionic']
190
191+############
192+# FIXTURES #
193+############
194
195-@pytest.fixture
196-async def model():
197- model = Model()
198- await model.connect_current()
199- yield model
200- await model.disconnect()
201
202+# This fixture shouldn't really be in conftest.py since it's specific to this
203+# charm
204+@pytest.fixture(scope='module',
205+ params=SERIES)
206+async def deploy_app(request, model):
207+ '''Deploys the sudo_pair app as a subordinate of ubuntu'''
208+ release = request.param
209
210-@pytest.fixture
211-async def deploy_app(model):
212 await model.deploy(
213 'ubuntu',
214- application_name='ubuntu',
215- series='bionic',
216+ application_name='ubuntu-' + release,
217+ series=release,
218 channel='stable'
219 )
220 sudo_pair_app = await model.deploy(
221 'local:',
222- application_name='sudo-pair',
223- series='bionic',
224+ application_name='sudo-pair-' + release,
225+ series=release,
226 num_units=0,
227 config={
228 'bypass_cmds': '/bin/ls',
229@@ -41,124 +36,80 @@ async def deploy_app(model):
230 }
231 )
232 await model.add_relation(
233- 'ubuntu',
234- 'sudo-pair'
235+ 'ubuntu-' + release,
236+ 'sudo-pair-' + release
237 )
238
239 await model.block_until(lambda: sudo_pair_app.status == 'active')
240- return sudo_pair_app
241-
242-
243-@pytest.fixture
244-async def get_app(model):
245- for app in model.applications:
246- if app == 'sudo-pair':
247- return model.applications[app]
248-
249-
250-@pytest.fixture
251-async def get_unit(model):
252- for app in model.applications:
253- if app == 'ubuntu':
254- return model.applications[app].units[0]
255-
256-
257-@pytest.fixture
258-async def run_command(get_unit):
259- async def make_run_command(cmd):
260- action = await get_unit.run(cmd)
261- return action.results
262- return make_run_command
263-
264-
265-@pytest.fixture
266-async def file_stat(run_command):
267- async def make_file_stat(path):
268- cmd = STAT_FILE % path
269- results = await run_command(cmd)
270- return json.loads(results['Stdout'])
271- return make_file_stat
272-
273+ yield sudo_pair_app
274+ # no need to cleanup since the model will be be torn down at the end of the
275+ # testing
276
277-@pytest.fixture
278-async def file_contents(run_command):
279- async def make_file_contents(path):
280- cmd = FILE_CONTENT % path
281- results = await run_command(cmd)
282- return results['Stdout']
283- return make_file_contents
284
285+@pytest.fixture(scope='module')
286+async def unit(deploy_app):
287+ '''Returns the sudo_pair unit we've deployed'''
288+ return deploy_app.units.pop()
289
290-@pytest.fixture
291-async def reconfigure_app(get_app):
292- async def make_reconfigure_app(cfg):
293- await get_app.set_config(cfg)
294- await get_app.get_config()
295- await asyncio.sleep(10)
296- return make_reconfigure_app
297-
298-
299-@pytest.fixture
300-async def create_group(run_command):
301- async def make_create_group(group_name):
302- cmd = "sudo groupadd %s" % group_name
303- await run_command(cmd)
304- return make_create_group
305+#########
306+# TESTS #
307+#########
308
309
310 async def test_deploy(deploy_app):
311- status = deploy_app.status
312- assert status == 'active'
313-
314-
315-async def test_sudo_pair_lib(file_stat):
316- sudo_pair_lib_stat = await file_stat("/usr/lib/sudo/sudo_pair.so")
317- assert sudo_pair_lib_stat['size'] > 0
318- assert sudo_pair_lib_stat['gid'] == 0
319- assert sudo_pair_lib_stat['uid'] == 0
320- assert sudo_pair_lib_stat['mode'] == '0o100644'
321-
322-
323-async def test_sudo_approve(file_stat, file_contents):
324- sudo_approve_path = '/usr/bin/sudo_approve'
325- sudo_approve_stat = await file_stat(sudo_approve_path)
326- assert sudo_approve_stat['size'] > 0
327- assert sudo_approve_stat['gid'] == 0
328- assert sudo_approve_stat['uid'] == 0
329- assert sudo_approve_stat['mode'] == '0o100755'
330-
331-
332-async def test_sudo_prompt(file_stat):
333- for prompt_type in ['user', 'pair']:
334- sudo_prompt_stat = await file_stat('/etc/sudo_pair.prompt.' + prompt_type)
335- assert sudo_prompt_stat['size'] > 0
336- assert sudo_prompt_stat['gid'] == 0
337- assert sudo_prompt_stat['uid'] == 0
338- assert sudo_prompt_stat['mode'] == '0o100644'
339-
340-
341-async def test_socket_dir(file_stat):
342- dir_stat = await file_stat('/var/run/sudo_pair')
343- assert dir_stat['gid'] == 0
344- assert dir_stat['uid'] == 0
345- assert dir_stat['mode'] == '0o40644'
346-
347-
348-async def test_sudoers(file_contents):
349- sudoers_content = await file_contents("/etc/sudoers")
350+ assert deploy_app.status == 'active'
351+
352+
353+@pytest.mark.parametrize("path,expected_stat", [
354+ ('/usr/lib/sudo/sudo_pair.so', {
355+ 'gid': 0,
356+ 'uid': 0,
357+ 'mode': '0o100644'}),
358+ ('/usr/bin/sudo_approve', {
359+ 'gid': 0,
360+ 'uid': 0,
361+ 'mode': '0o100755'}),
362+ ('/etc/sudo_pair.prompt.user', {
363+ 'gid': 0,
364+ 'uid': 0,
365+ 'mode': '0o100644'}),
366+ ('/etc/sudo_pair.prompt.pair', {
367+ 'gid': 0,
368+ 'uid': 0,
369+ 'mode': '0o100644'}),
370+ ('/var/run/sudo_pair', {
371+ 'gid': 0,
372+ 'uid': 0,
373+ 'mode': '0o40644'})
374+ ])
375+async def test_stats(path, expected_stat, unit, file_stat):
376+ test_stat = await file_stat(path, unit)
377+ assert test_stat['size'] > 0
378+ assert test_stat['gid'] == expected_stat['gid']
379+ assert test_stat['uid'] == expected_stat['uid']
380+ assert test_stat['mode'] == expected_stat['mode']
381+
382+
383+async def test_sudoers(file_contents, unit):
384+ sudoers_content = await file_contents("/etc/sudoers", unit)
385 assert 'Defaults log_output' in sudoers_content
386
387
388-async def test_sudoers_bypass_conf(file_contents):
389- sudoers_bypass_content = await file_contents("/etc/sudoers.d/91-bypass-sudopair-cmds")
390+async def test_sudoers_bypass_conf(file_contents, unit):
391+ path = "/etc/sudoers.d/91-bypass-sudopair-cmds"
392+ sudoers_bypass_content = await file_contents(path=path,
393+ target=unit)
394 content = '%warthogs ALL = (ALL) NOLOG_OUTPUT: /bin/ls'
395 assert content in sudoers_bypass_content
396
397
398-async def test_reconfigure(reconfigure_app, file_contents, file_stat):
399- auto_approve = "false"
400+async def test_reconfigure(reconfigure_app, file_contents, unit, deploy_app):
401+ '''Change a charm config parameter and verify that it has been propagated to
402+ the unit'''
403 sudo_approve_path = '/usr/bin/sudo_approve'
404- await reconfigure_app({'auto_approve': auto_approve})
405- sudo_approve_content = await file_contents(sudo_approve_path)
406+ await reconfigure_app(cfg={'auto_approve': 'false'},
407+ target=deploy_app)
408+ sudo_approve_content = await file_contents(path=sudo_approve_path,
409+ target=unit)
410 new_content = 'echo "You can\'t approve your own session."'
411 assert new_content in sudo_approve_content

Subscribers

People subscribed via source and target branches