Merge ~peppepetra/charm-sudo-pair:fix-tests into ~sudo-pair-charmers/charm-sudo-pair:master

Proposed by Giuseppe Petralia
Status: Merged
Approved by: Giuseppe Petralia
Approved revision: 51a606515c12e0087a05ebb350794530d4f51987
Merged at revision: 51a606515c12e0087a05ebb350794530d4f51987
Proposed branch: ~peppepetra/charm-sudo-pair:fix-tests
Merge into: ~sudo-pair-charmers/charm-sudo-pair:master
Diff against target: 841 lines (+214/-130)
12 files modified
.gitignore (+33/-7)
Makefile (+25/-22)
actions/actions.py (+2/-2)
lib/libsudopair.py (+22/-2)
reactive/sudo_pair.py (+1/-1)
tests/functional/conftest.py (+23/-21)
tests/functional/requirements.txt (+1/-0)
tests/functional/test_deploy.py (+62/-51)
tests/unit/conftest.py (+3/-2)
tests/unit/requirements.txt (+1/-0)
tests/unit/test_libsudopair.py (+18/-4)
tox.ini (+23/-18)
Reviewer Review Type Date Requested Status
Alvaro Uria (community) Approve
Review via email: mp+379381@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Alvaro Uria (aluria) wrote :

MP lgtm (only code review, I haven't run make...)
Added a comment inline to possibly remove "make submodules". Thank you.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.gitignore b/.gitignore
index a437a87..eda8bea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,33 @@
1__pycache__1# Byte-compiled / optimized / DLL files
2*~2__pycache__/
3*.swp3*.py[cod]
4builds/4*$py.class
5.idea5
6.tox6# Log files
7/repo-info7*.log
8
9.tox/
10src/.tox/
11.coverage
12
13# vi
14.*.swp
15
16# pycharm
17.idea/
18.unit-state.db
19src/.unit-state.db
20
21# version data
22repo-info
23
24
25# reports
26report/*
27src/report/*
28
29# virtual env
30venv/*
31
32# builds
33builds/*
8\ No newline at end of file34\ No newline at end of file
diff --git a/Makefile b/Makefile
index 6c2a104..c7506b2 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,9 @@
1PROJECTPATH = $(dir $(realpath $(firstword $(MAKEFILE_LIST))))1PROJECTPATH = $(dir $(realpath $(MAKEFILE_LIST)))
2ifndef JUJU_REPOSITORY2DIRNAME = $(notdir $(PROJECTPATH:%/=%))
3 JUJU_REPOSITORY := $(shell pwd)3
4 $(warning Warning JUJU_REPOSITORY was not set, defaulting to $(JUJU_REPOSITORY))4ifndef CHARM_BUILD_DIR
5 CHARM_BUILD_DIR := /tmp/$(DIRNAME)-builds
6 $(warning Warning CHARM_BUILD_DIR was not set, defaulting to $(CHARM_BUILD_DIR))
5endif7endif
68
7help:9help:
@@ -9,42 +11,43 @@ help:
9 @echo ""11 @echo ""
10 @echo " make help - show this text"12 @echo " make help - show this text"
11 @echo " make lint - run flake8"13 @echo " make lint - run flake8"
12 @echo " make test - run the unittests and lint"14 @echo " make test - run the functional tests, unittests and lint"
13 @echo " make unittest - run the tests defined in the unit subdirectory"15 @echo " make unittest - run the tests defined in the unittest subdirectory"
14 @echo " make functional - run the tests defined in the functional subdirectory"16 @echo " make functional - run the tests defined in the functional subdirectory"
15 @echo " make release - build the charm"17 @echo " make release - build the charm"
16 @echo " make clean - remove unneeded files"18 @echo " make clean - remove unneeded files"
17 @echo ""19 @echo ""
1820
19submodules:
20 @echo "Cloning submodules"
21 @git submodule update --init --recursive
22
23lint:21lint:
24 @echo "Running flake8"22 @echo "Running flake8"
25 @tox -e lint23 @tox -e lint
2624
27test: unittest functional lint25test: lint unittest functional
26
27functional: build
28 @PYTEST_KEEP_MODEL=$(PYTEST_KEEP_MODEL) \
29 PYTEST_CLOUD_NAME=$(PYTEST_CLOUD_NAME) \
30 PYTEST_CLOUD_REGION=$(PYTEST_CLOUD_REGION) \
31 tox -e functional
2832
29unittest:33unittest:
30 @tox -e unit34 @tox -e unit
3135
32functional: build
33 @tox -e functional
34
35build:36build:
36 @echo "Building charm to base directory $(JUJU_REPOSITORY)"37 @echo "Building charm to base directory $(CHARM_BUILD_DIR)"
37 @-git describe --tags > $(PROJECTPATH)/repo-info38 @-git describe --tags > ./repo-info
38 @LAYER_PATH=./layers INTERFACE_PATH=./interfaces\39 @CHARM_LAYERS_DIR=./layers CHARM_INTERFACES_DIR=./interfaces TERM=linux\
39 JUJU_REPOSITORY=$(JUJU_REPOSITORY) charm build ./ --force40 charm build --output-dir $(CHARM_BUILD_DIR) $(PROJECTPATH) --force
4041
41release: clean build42release: clean build
42 @echo "Charm is built at $(JUJU_REPOSITORY)/builds"43 @echo "Charm is built at $(CHARM_BUILD_DIR)/builds"
4344
44clean:45clean:
45 @echo "Cleaning files"46 @echo "Cleaning files"
46 @rm -rf $(PROJECTPATH)/.tox47 @find $(PROJECTPATH) -iname __pycache__ -exec rm -r {} +
47 @rm -rf $(PROJECTPATH)/.pytest_cache48 @if [ -d $(CHARM_BUILD_DIR)/builds ] ; then rm -r $(CHARM_BUILD_DIR)/builds ; fi
49 @if [ -d $(PROJECTPATH)/.tox ] ; then rm -r $(PROJECTPATH)/.tox ; fi
50 @if [ -d $(PROJECTPATH)/.pytest_cache ] ; then rm -r $(PROJECTPATH)/.pytest_cache ; fi
4851
49# The targets below don't depend on a file52# The targets below don't depend on a file
50.PHONY: lint test unittest functional build release clean help submodules53.PHONY: lint test unittest functional build release clean help
diff --git a/actions/actions.py b/actions/actions.py
index 7dd03d8..ccf3ffc 100755
--- a/actions/actions.py
+++ b/actions/actions.py
@@ -16,7 +16,7 @@
16import os16import os
17import sys17import sys
1818
19from charmhelpers.core.hookenv import action_set, action_fail19from charmhelpers.core.hookenv import action_fail, action_set
2020
2121
22sys.path.append("lib")22sys.path.append("lib")
@@ -25,7 +25,7 @@ import libsudopair # NOQA
2525
2626
27def remove():27def remove():
28 """Action to remove sudo-pair config and binaries"""28 """Remove sudo-pair config and binaries."""
29 sph = libsudopair.SudoPairHelper()29 sph = libsudopair.SudoPairHelper()
30 sph.deconfigure()30 sph.deconfigure()
31 action_set({"message": "Successfully removed sudo-pair config and binaries"})31 action_set({"message": "Successfully removed sudo-pair config and binaries"})
diff --git a/lib/libsudopair.py b/lib/libsudopair.py
index 095e9a4..05d1f9d 100644
--- a/lib/libsudopair.py
+++ b/lib/libsudopair.py
@@ -1,9 +1,11 @@
1import grp1import grp
2import os2import os
3from charmhelpers.core import host, hookenv, templating3
4from charmhelpers.core import hookenv, host, templating
45
56
6def check_valid_group(group_name):7def check_valid_group(group_name):
8 """Check that a group exists."""
7 try:9 try:
8 grp.getgrnam(group_name)10 grp.getgrnam(group_name)
9 return True11 return True
@@ -12,12 +14,14 @@ def check_valid_group(group_name):
1214
1315
14def group_id(group_name):16def group_id(group_name):
17 """Check that a group exists."""
15 return grp.getgrnam(group_name).gr_gid18 return grp.getgrnam(group_name).gr_gid
1619
1720
18def group_names_to_group_ids(group_names):21def group_names_to_group_ids(group_names):
19 """22 """
20 From Group Names comma-separated list to Group Ids23 Return comma-separated list of Group Ids.
24
21 :param group_names: i.e. "root,user1,user2"25 :param group_names: i.e. "root,user1,user2"
22 :return gids: i.e. "0,1001,1002"26 :return gids: i.e. "0,1001,1002"
23 """27 """
@@ -26,6 +30,7 @@ def group_names_to_group_ids(group_names):
2630
2731
28def copy_file(source, destination, owner, group, perms):32def copy_file(source, destination, owner, group, perms):
33 """Copy a file on the unit."""
29 if destination is not None:34 if destination is not None:
30 target_dir = os.path.dirname(destination)35 target_dir = os.path.dirname(destination)
31 if not os.path.exists(target_dir):36 if not os.path.exists(target_dir):
@@ -37,7 +42,10 @@ def copy_file(source, destination, owner, group, perms):
3742
3843
39class SudoPairHelper(object):44class SudoPairHelper(object):
45 """Configure sudo-pair."""
46
40 def __init__(self):47 def __init__(self):
48 """Retrieve charm config and set defaults."""
41 self.charm_config = hookenv.config()49 self.charm_config = hookenv.config()
42 self.binary_path = '/usr/bin/sudo_approve'50 self.binary_path = '/usr/bin/sudo_approve'
43 self.sudo_conf_path = '/etc/sudo.conf'51 self.sudo_conf_path = '/etc/sudo.conf'
@@ -58,6 +66,7 @@ class SudoPairHelper(object):
58 self.sudo_approve_perms = 0o75566 self.sudo_approve_perms = 0o755
5967
60 def get_config(self):68 def get_config(self):
69 """Return config as a dict."""
61 config = {70 config = {
62 'binary_path': self.binary_path,71 'binary_path': self.binary_path,
63 'user_prompt_path': self.user_prompt_path,72 'user_prompt_path': self.user_prompt_path,
@@ -71,41 +80,51 @@ class SudoPairHelper(object):
71 return config80 return config
7281
73 def set_charm_config(self, charm_config):82 def set_charm_config(self, charm_config):
83 """Update configuration."""
74 self.charm_config = charm_config84 self.charm_config = charm_config
7585
76 def render_sudo_conf(self):86 def render_sudo_conf(self):
87 """Render sudo.conf file."""
77 return templating.render('sudo.conf.tmpl', self.sudo_conf_path, self.get_config(),88 return templating.render('sudo.conf.tmpl', self.sudo_conf_path, self.get_config(),
78 perms=self.sudo_conf_perms, owner=self.owner, group=self.group)89 perms=self.sudo_conf_perms, owner=self.owner, group=self.group)
7990
80 def create_socket_dir(self):91 def create_socket_dir(self):
92 """Create socket dir."""
81 host.mkdir(self.socket_dir, perms=self.socket_dir_perms, owner=self.owner, group=self.group)93 host.mkdir(self.socket_dir, perms=self.socket_dir_perms, owner=self.owner, group=self.group)
8294
83 def create_tmpfiles_conf(self):95 def create_tmpfiles_conf(self):
96 """Create temporary conf file."""
84 with open(self.tmpfiles_conf, "w") as f:97 with open(self.tmpfiles_conf, "w") as f:
85 f.write("d {} 0755 - - -\n".format(self.socket_dir))98 f.write("d {} 0755 - - -\n".format(self.socket_dir))
8699
87 def install_sudo_pair_so(self):100 def install_sudo_pair_so(self):
101 """Install sudo-pair lib."""
88 sudo_pair_lib = os.path.join(hookenv.charm_dir(), 'files', 'sudo_pair.so')102 sudo_pair_lib = os.path.join(hookenv.charm_dir(), 'files', 'sudo_pair.so')
89 copy_file(sudo_pair_lib, self.sudo_lib_path, self.owner, self.group, self.sudo_pair_so_perms)103 copy_file(sudo_pair_lib, self.sudo_lib_path, self.owner, self.group, self.sudo_pair_so_perms)
90104
91 def copy_user_prompt(self):105 def copy_user_prompt(self):
106 """Copy user prompt on the unit."""
92 prompt_file = os.path.join(hookenv.charm_dir(), 'files', 'sudo.prompt.user')107 prompt_file = os.path.join(hookenv.charm_dir(), 'files', 'sudo.prompt.user')
93 copy_file(prompt_file, self.user_prompt_path, self.owner, self.group, self.prompt_perms)108 copy_file(prompt_file, self.user_prompt_path, self.owner, self.group, self.prompt_perms)
94109
95 def copy_pair_prompt(self):110 def copy_pair_prompt(self):
111 """Copy pair prompt on the unit."""
96 prompt_file = os.path.join(hookenv.charm_dir(), 'files', 'sudo.prompt.pair')112 prompt_file = os.path.join(hookenv.charm_dir(), 'files', 'sudo.prompt.pair')
97 copy_file(prompt_file, self.pair_prompt_path, self.owner, self.group, self.prompt_perms)113 copy_file(prompt_file, self.pair_prompt_path, self.owner, self.group, self.prompt_perms)
98114
99 def copy_sudoers(self):115 def copy_sudoers(self):
116 """Copy sudoers file on the unit."""
100 sudoers_file = os.path.join(hookenv.charm_dir(), 'files', 'sudoers')117 sudoers_file = os.path.join(hookenv.charm_dir(), 'files', 'sudoers')
101 copy_file(sudoers_file, self.sudoers_path, self.owner, self.group, self.sudoers_perms)118 copy_file(sudoers_file, self.sudoers_path, self.owner, self.group, self.sudoers_perms)
102119
103 def render_sudo_approve(self):120 def render_sudo_approve(self):
121 """Render sudo-approve file."""
104 hookenv.log("Rendering sudo_approve.tmpl to {}".format(self.binary_path))122 hookenv.log("Rendering sudo_approve.tmpl to {}".format(self.binary_path))
105 return templating.render('sudo_approve.tmpl', self.binary_path, self.get_config(),123 return templating.render('sudo_approve.tmpl', self.binary_path, self.get_config(),
106 perms=self.sudo_approve_perms, owner=self.owner, group=self.group)124 perms=self.sudo_approve_perms, owner=self.owner, group=self.group)
107125
108 def render_bypass_cmds(self):126 def render_bypass_cmds(self):
127 """Render bypass command file."""
109 if self.get_config()['bypass_cmds'] != "" and self.get_config()['bypass_group'] != "":128 if self.get_config()['bypass_cmds'] != "" and self.get_config()['bypass_group'] != "":
110 hookenv.log("Render bypass cmds to {}".format(self.sudoers_bypass_path))129 hookenv.log("Render bypass cmds to {}".format(self.sudoers_bypass_path))
111 return templating.render('91-bypass-sudopair-cmds.tmpl', self.sudoers_bypass_path,130 return templating.render('91-bypass-sudopair-cmds.tmpl', self.sudoers_bypass_path,
@@ -113,6 +132,7 @@ class SudoPairHelper(object):
113 return None132 return None
114133
115 def deconfigure(self):134 def deconfigure(self):
135 """Remove sudo-pair configuration."""
116 paths = [136 paths = [
117 self.sudo_conf_path,137 self.sudo_conf_path,
118 self.sudo_lib_path,138 self.sudo_lib_path,
diff --git a/reactive/sudo_pair.py b/reactive/sudo_pair.py
index be557f8..974dd66 100644
--- a/reactive/sudo_pair.py
+++ b/reactive/sudo_pair.py
@@ -1,6 +1,6 @@
1from charms.reactive import when, when_not, set_state, remove_state, hook
2from charmhelpers.core import hookenv1from charmhelpers.core import hookenv
32
3from charms.reactive import hook, remove_state, set_state, when, when_not
44
5from libsudopair import SudoPairHelper5from libsudopair import SudoPairHelper
66
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
index 7279748..aa42a09 100644
--- a/tests/functional/conftest.py
+++ b/tests/functional/conftest.py
@@ -2,21 +2,23 @@
22
3import asyncio3import asyncio
4import json4import json
5import juju
6import os5import os
7import pytest
8import uuid6import uuid
7
8import juju
9from juju.controller import Controller9from juju.controller import Controller
10from juju.errors import JujuError10from juju.errors import JujuError
11from juju.model import Model11from juju.model import Model
1212
13import pytest
14
15
13STAT_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: E50116STAT_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
1417
1518
16@pytest.yield_fixture(scope='module')19@pytest.yield_fixture(scope='module')
17def event_loop(request):20def event_loop(request):
18 '''Override the default pytest event loop to allow for broaded scoped21 """Override the default pytest event loop to allow for broaded scopedv fixtures."""
19 fixtures'''
20 loop = asyncio.get_event_loop_policy().new_event_loop()22 loop = asyncio.get_event_loop_policy().new_event_loop()
21 asyncio.set_event_loop(loop)23 asyncio.set_event_loop(loop)
22 loop.set_debug(True)24 loop.set_debug(True)
@@ -27,7 +29,7 @@ def event_loop(request):
2729
28@pytest.fixture(scope='module')30@pytest.fixture(scope='module')
29async def controller():31async def controller():
30 '''Connect to the current controller'''32 """Connect to the current controller."""
31 controller = Controller()33 controller = Controller()
32 await controller.connect_current()34 await controller.connect_current()
33 yield controller35 yield controller
@@ -36,7 +38,7 @@ async def controller():
3638
37@pytest.fixture(scope='module')39@pytest.fixture(scope='module')
38async def model(controller):40async def model(controller):
39 '''This model lives only for the duration of the test'''41 """Create a model that lives only for the duration of the test."""
40 model_name = "functest-{}".format(uuid.uuid4())42 model_name = "functest-{}".format(uuid.uuid4())
41 model = await controller.add_model(model_name)43 model = await controller.add_model(model_name)
42 yield model44 yield model
@@ -50,7 +52,7 @@ async def model(controller):
5052
51@pytest.fixture(scope='module')53@pytest.fixture(scope='module')
52async def current_model():54async def current_model():
53 '''Returns the current model, does not create or destroy it'''55 """Return the current model, does not create or destroy it."""
54 model = Model()56 model = Model()
55 await model.connect_current()57 await model.connect_current()
56 yield model58 yield model
@@ -59,7 +61,7 @@ async def current_model():
5961
60@pytest.fixture62@pytest.fixture
61async def get_app(model):63async def get_app(model):
62 '''Returns the application requested'''64 """Return the application requested."""
63 async def _get_app(name):65 async def _get_app(name):
64 try:66 try:
65 return model.applications[name]67 return model.applications[name]
@@ -70,7 +72,7 @@ async def get_app(model):
7072
71@pytest.fixture73@pytest.fixture
72async def get_unit(model):74async def get_unit(model):
73 '''Returns the requested <app_name>/<unit_number> unit'''75 """Return the requested <app_name>/<unit_number> unit."""
74 async def _get_unit(name):76 async def _get_unit(name):
75 try:77 try:
76 (app_name, unit_number) = name.split('/')78 (app_name, unit_number) = name.split('/')
@@ -82,7 +84,7 @@ async def get_unit(model):
8284
83@pytest.fixture85@pytest.fixture
84async def get_entity(model, get_unit, get_app):86async def get_entity(model, get_unit, get_app):
85 '''Returns a unit or an application'''87 """Return a unit or an application."""
86 async def _get_entity(name):88 async def _get_entity(name):
87 try:89 try:
88 return await get_unit(name)90 return await get_unit(name)
@@ -96,12 +98,12 @@ async def get_entity(model, get_unit, get_app):
9698
97@pytest.fixture99@pytest.fixture
98async def run_command(get_unit):100async def run_command(get_unit):
99 '''101 """
100 Runs a command on a unit.102 Run a command on a unit.
101103
102 :param cmd: Command to be run104 :param cmd: Command to be run
103 :param target: Unit object or unit name string105 :param target: Unit object or unit name string
104 '''106 """
105 async def _run_command(cmd, target):107 async def _run_command(cmd, target):
106 unit = (108 unit = (
107 target109 target
@@ -115,12 +117,12 @@ async def run_command(get_unit):
115117
116@pytest.fixture118@pytest.fixture
117async def file_stat(run_command):119async def file_stat(run_command):
118 '''120 """
119 Runs stat on a file121 Run stat on a file.
120122
121 :param path: File path123 :param path: File path
122 :param target: Unit object or unit name string124 :param target: Unit object or unit name string
123 '''125 """
124 async def _file_stat(path, target):126 async def _file_stat(path, target):
125 cmd = STAT_FILE % path127 cmd = STAT_FILE % path
126 results = await run_command(cmd, target)128 results = await run_command(cmd, target)
@@ -130,12 +132,12 @@ async def file_stat(run_command):
130132
131@pytest.fixture133@pytest.fixture
132async def file_contents(run_command):134async def file_contents(run_command):
133 '''135 """
134 Returns the contents of a file136 Return the contents of a file.
135137
136 :param path: File path138 :param path: File path
137 :param target: Unit object or unit name string139 :param target: Unit object or unit name string
138 '''140 """
139 async def _file_contents(path, target):141 async def _file_contents(path, target):
140 cmd = 'cat {}'.format(path)142 cmd = 'cat {}'.format(path)
141 results = await run_command(cmd, target)143 results = await run_command(cmd, target)
@@ -145,7 +147,7 @@ async def file_contents(run_command):
145147
146@pytest.fixture148@pytest.fixture
147async def reconfigure_app(get_app, model):149async def reconfigure_app(get_app, model):
148 '''Applies a different config to the requested app'''150 """Apply a different config to the requested app."""
149 async def _reconfigure_app(cfg, target):151 async def _reconfigure_app(cfg, target):
150 application = (152 application = (
151 target153 target
@@ -160,7 +162,7 @@ async def reconfigure_app(get_app, model):
160162
161@pytest.fixture163@pytest.fixture
162async def create_group(run_command):164async def create_group(run_command):
163 '''Creates the UNIX group specified'''165 """Create the UNIX group specified."""
164 async def _create_group(group_name, target):166 async def _create_group(group_name, target):
165 cmd = "sudo groupadd %s" % group_name167 cmd = "sudo groupadd %s" % group_name
166 await run_command(cmd, target)168 await run_command(cmd, target)
diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt
index b9815c2..1da5a06 100644
--- a/tests/functional/requirements.txt
+++ b/tests/functional/requirements.txt
@@ -2,5 +2,6 @@ juju
2requests2requests
3pytest3pytest
4pytest-asyncio4pytest-asyncio
5pytest-cov
5mock6mock
6flake87flake8
diff --git a/tests/functional/test_deploy.py b/tests/functional/test_deploy.py
index 5c8e690..cdb5ae4 100644
--- a/tests/functional/test_deploy.py
+++ b/tests/functional/test_deploy.py
@@ -1,38 +1,67 @@
1#!/usr/bin/python3.61#!/usr/bin/python3.6
2import asyncio2
3import os3import os
4
4import pytest5import pytest
56
6pytestmark = pytest.mark.asyncio7pytestmark = pytest.mark.asyncio
78
9charm_build_dir = os.getenv('CHARM_BUILD_DIR', '..').rstrip('/')
10
11
12sources = [('local', '{}/builds/sudo-pair'.format(charm_build_dir))]
13
14series = ['xenial',
15 'bionic',
16 ]
817
9def get_series():
10 series = os.getenv('test_series', 'xenial bionic').strip()
11 return series.split()
1218
13############19############
14# FIXTURES #20# FIXTURES #
15############21############
1622
1723
18# This fixture shouldn't really be in conftest.py since it's specific to this24@pytest.fixture(params=series)
19# charm25def series(request):
20@pytest.fixture(scope='module',26 """Return ubuntu version (i.e. xenial) in use in the test."""
21 params=get_series())27 return request.param
22async def deploy_app(request, model):28
23 '''Deploys the sudo_pair app as a subordinate of ubuntu'''29
24 release = request.param30@pytest.fixture(params=sources, ids=[s[0] for s in sources])
31def source(request):
32 """Return source of the charm under test (i.e. local, cs)."""
33 return request.param
34
35
36@pytest.fixture
37async def app(model, series, source):
38 """Return application of the charm under test."""
39 app_name = 'sudo-pair-{}'.format(series)
40 return await model._wait_for_new('application', app_name)
41
42
43@pytest.fixture
44async def unit(app):
45 """Return the sudo_pair unit we've deployed."""
46 return app.units[0]
47
48
49#########
50# TESTS #
51#########
2552
53async def test_deploy_app(model, series, source):
54 """Deploy the sudo_pair app as a subordinate of ubuntu."""
26 await model.deploy(55 await model.deploy(
27 'ubuntu',56 'ubuntu',
28 application_name='ubuntu-' + release,57 application_name='ubuntu-' + series,
29 series=release,58 series=series,
30 channel='stable'59 channel='stable'
31 )60 )
32 sudo_pair_app = await model.deploy(61 sudo_pair_app = await model.deploy(
33 '{}/builds/sudo-pair'.format(os.getenv('JUJU_REPOSITORY')),62 source[1],
34 application_name='sudo-pair-' + release,63 application_name='sudo-pair-' + series,
35 series=release,64 series=series,
36 num_units=0,65 num_units=0,
37 config={66 config={
38 'bypass_cmds': '/bin/ls',67 'bypass_cmds': '/bin/ls',
@@ -41,27 +70,17 @@ async def deploy_app(request, model):
41 }70 }
42 )71 )
43 await model.add_relation(72 await model.add_relation(
44 'ubuntu-{}:juju-info'.format(release),73 'ubuntu-{}:juju-info'.format(series),
45 'sudo-pair-{}:juju-info'.format(release))74 'sudo-pair-{}:juju-info'.format(series))
4675
47 await model.block_until(lambda: sudo_pair_app.status == 'active')76 await model.block_until(lambda: sudo_pair_app.status == 'active')
48 yield sudo_pair_app
49 # no need to cleanup since the model will be be torn down at the end of the77 # no need to cleanup since the model will be be torn down at the end of the
50 # testing78 # testing
5179
5280
53@pytest.fixture(scope='module')81async def test_status(app):
54async def unit(deploy_app):82 """Check that the app is in active state."""
55 '''Returns the sudo_pair unit we've deployed'''83 assert app.status == 'active'
56 return deploy_app.units.pop()
57
58#########
59# TESTS #
60#########
61
62
63async def test_deploy(deploy_app):
64 assert deploy_app.status == 'active'
6584
6685
67@pytest.mark.parametrize("path,expected_stat", [86@pytest.mark.parametrize("path,expected_stat", [
@@ -86,6 +105,7 @@ async def test_deploy(deploy_app):
86 'uid': 0,105 'uid': 0,
87 'mode': '0o40644'})])106 'mode': '0o40644'})])
88async def test_stats(path, expected_stat, unit, file_stat):107async def test_stats(path, expected_stat, unit, file_stat):
108 """Check that created files have the correct permissions."""
89 test_stat = await file_stat(path, unit)109 test_stat = await file_stat(path, unit)
90 assert test_stat['size'] > 0110 assert test_stat['size'] > 0
91 assert test_stat['gid'] == expected_stat['gid']111 assert test_stat['gid'] == expected_stat['gid']
@@ -94,11 +114,13 @@ async def test_stats(path, expected_stat, unit, file_stat):
94114
95115
96async def test_sudoers(file_contents, unit):116async def test_sudoers(file_contents, unit):
117 """Check the content of sudoers file."""
97 sudoers_content = await file_contents("/etc/sudoers", unit)118 sudoers_content = await file_contents("/etc/sudoers", unit)
98 assert 'Defaults log_output' in sudoers_content119 assert 'Defaults log_output' in sudoers_content
99120
100121
101async def test_sudoers_bypass_conf(file_contents, unit):122async def test_sudoers_bypass_conf(file_contents, unit):
123 """Check the content of sudoers bypass command file."""
102 path = "/etc/sudoers.d/91-bypass-sudopair-cmds"124 path = "/etc/sudoers.d/91-bypass-sudopair-cmds"
103 sudoers_bypass_content = await file_contents(path=path,125 sudoers_bypass_content = await file_contents(path=path,
104 target=unit)126 target=unit)
@@ -106,41 +128,30 @@ async def test_sudoers_bypass_conf(file_contents, unit):
106 assert content in sudoers_bypass_content128 assert content in sudoers_bypass_content
107129
108130
109async def test_reconfigure(reconfigure_app, file_contents, unit, deploy_app):131async def test_reconfigure(reconfigure_app, file_contents, unit, app):
110 '''Change a charm config parameter and verify that it has been propagated to132 """Change a charm config parameter and verify that it has been propagated to the unit."""
111 the unit'''
112 sudo_approve_path = '/usr/bin/sudo_approve'133 sudo_approve_path = '/usr/bin/sudo_approve'
113 await reconfigure_app(cfg={'auto_approve': 'false'},134 await reconfigure_app(cfg={'auto_approve': 'false'},
114 target=deploy_app)135 target=app)
115 sudo_approve_content = await file_contents(path=sudo_approve_path,136 sudo_approve_content = await file_contents(path=sudo_approve_path,
116 target=unit)137 target=unit)
117 new_content = 'echo "You can\'t approve your own session."'138 new_content = 'echo "You can\'t approve your own session."'
118 assert new_content in sudo_approve_content139 assert new_content in sudo_approve_content
119140
120141
121async def test_remove_relation(deploy_app, model, run_command):142async def test_remove_relation(app, model, run_command):
122 series = deploy_app.units[0].data['series']143 """Check that the relation is removed."""
144 series = app.units[0].data['series']
123 app_name = 'sudo-pair-{}'.format(series)145 app_name = 'sudo-pair-{}'.format(series)
124 principalname = 'ubuntu-{}'.format(series)146 principalname = 'ubuntu-{}'.format(series)
125 await deploy_app.remove_relation(147 await app.remove_relation(
126 '{}:juju-info'.format(app_name),148 '{}:juju-info'.format(app_name),
127 '{}:juju-info'.format(principalname))149 '{}:juju-info'.format(principalname))
128 await model.block_until(lambda: not deploy_app.relations)150 await model.block_until(lambda: not app.relations)
129 principal = model.applications[principalname].units[0]151 principal = model.applications[principalname].units[0]
130 res = await run_command('test -f /etc/sudo.conf || echo gone', target=principal)152 res = await run_command('test -f /etc/sudo.conf || echo gone', target=principal)
131 assert res['Stdout'].strip() == 'gone'153 assert res['Stdout'].strip() == 'gone'
132 await model.add_relation(154 await model.add_relation(
133 '{}:juju-info'.format(principalname),155 '{}:juju-info'.format(principalname),
134 '{}:juju-info'.format(app_name))156 '{}:juju-info'.format(app_name))
135 await model.block_until(lambda: deploy_app.relations)157 await model.block_until(lambda: app.relations)
136
137
138async def test_remove_unit(deploy_app, model, run_command):
139 series = deploy_app.units[0].data['series']
140 app_name = 'sudo-pair-{}'.format(series)
141 await deploy_app.destroy()
142 while app_name in model.applications:
143 await asyncio.sleep(2)
144 principal = model.applications['ubuntu-{}'.format(series)].units[0]
145 res = await run_command('test -f /etc/sudo.conf || echo gone', target=principal)
146 assert res['Stdout'].strip() == 'gone'
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
index 0e2c034..c0800ea 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -1,11 +1,12 @@
1#!/usr/bin/python31#!/usr/bin/python3
22
3import pytest
4import pwd
5import grp3import grp
6import os4import os
5import pwd
7from pathlib import Path6from pathlib import Path
87
8import pytest
9
910
10@pytest.fixture11@pytest.fixture
11def mock_hookenv_config(monkeypatch):12def mock_hookenv_config(monkeypatch):
diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
index 081ed97..04dbf13 100644
--- a/tests/unit/requirements.txt
+++ b/tests/unit/requirements.txt
@@ -2,3 +2,4 @@ charmhelpers
2charms.reactive2charms.reactive
3pytest3pytest
4mock4mock
5pytest-cov
5\ No newline at end of file6\ No newline at end of file
diff --git a/tests/unit/test_libsudopair.py b/tests/unit/test_libsudopair.py
index fea4985..3a598af 100644
--- a/tests/unit/test_libsudopair.py
+++ b/tests/unit/test_libsudopair.py
@@ -1,6 +1,6 @@
1import os
2import grp
3import filecmp1import filecmp
2import grp
3import os
44
5from libsudopair import (5from libsudopair import (
6 check_valid_group,6 check_valid_group,
@@ -18,14 +18,18 @@ def test_group_id():
1818
1919
20class TestSudoPairHelper():20class TestSudoPairHelper():
21 """Module to test SudoPairHelper lib."""
22
21 def test_pytest(self):23 def test_pytest(self):
24 """Assert testing is carryied using pytest."""
22 assert True25 assert True
2326
24 def test_sph(self, sph):27 def test_sph(self, sph):
25 ''' See if the ph fixture works to load charm configs '''28 """See if the sph fixture works to load charm configs."""
26 assert isinstance(sph.charm_config, dict)29 assert isinstance(sph.charm_config, dict)
2730
28 def test_get_config(self, sph):31 def test_get_config(self, sph):
32 """Check if config contains all the required entries."""
29 default_keywords = [33 default_keywords = [
30 'binary_path',34 'binary_path',
31 'user_prompt_path',35 'user_prompt_path',
@@ -39,6 +43,7 @@ class TestSudoPairHelper():
39 assert option in config43 assert option in config
4044
41 def test_set_charm_config(self, sph):45 def test_set_charm_config(self, sph):
46 """Set new config."""
42 charm_config = {47 charm_config = {
43 'groups_enforced': 'root',48 'groups_enforced': 'root',
44 'groups_exempted': '',49 'groups_exempted': '',
@@ -54,6 +59,7 @@ class TestSudoPairHelper():
54 assert sph.get_config()[option] == charm_config[option]59 assert sph.get_config()[option] == charm_config[option]
5560
56 def test_render_sudo_conf(self, sph, tmpdir):61 def test_render_sudo_conf(self, sph, tmpdir):
62 """Check that sudo.conf is rendered correctly."""
57 # Default config63 # Default config
58 content = sph.render_sudo_conf()64 content = sph.render_sudo_conf()
59 expected_content = 'Plugin sudo_pair sudo_pair.so binary_path={} ' \65 expected_content = 'Plugin sudo_pair sudo_pair.so binary_path={} ' \
@@ -107,7 +113,8 @@ class TestSudoPairHelper():
107 content = sph.render_sudo_conf()113 content = sph.render_sudo_conf()
108 assert expected_content in content114 assert expected_content in content
109115
110 def test_render_bypass_cmds(self, sph, tmpdir):116 def test_render_bypass_cmds(self, sph):
117 """Check that sudoers file is rendered correctly."""
111 # Root bypass /bin/ls118 # Root bypass /bin/ls
112 expected_content = '%root ALL = (ALL) NOLOG_OUTPUT: /bin/ls'119 expected_content = '%root ALL = (ALL) NOLOG_OUTPUT: /bin/ls'
113 charm_config = {120 charm_config = {
@@ -122,6 +129,7 @@ class TestSudoPairHelper():
122 assert expected_content in content129 assert expected_content in content
123130
124 def test_render_sudo_approve(self, sph, tmpdir):131 def test_render_sudo_approve(self, sph, tmpdir):
132 """Check that sudo_approve file is rendered correctly."""
125 # Auto Approve true133 # Auto Approve true
126 expected_content = 'echo ${log_line} >> /var/log/sudo_pair.log'134 expected_content = 'echo ${log_line} >> /var/log/sudo_pair.log'
127 socket_dir = tmpdir.join('/var/run/sudo_pair')135 socket_dir = tmpdir.join('/var/run/sudo_pair')
@@ -144,10 +152,12 @@ class TestSudoPairHelper():
144 assert expected_content in content152 assert expected_content in content
145153
146 def test_create_socket_dir(self, sph, tmpdir):154 def test_create_socket_dir(self, sph, tmpdir):
155 """Check that sudo_pair socket dir exists."""
147 sph.create_socket_dir()156 sph.create_socket_dir()
148 assert os.path.exists(tmpdir.join('/var/run/sudo_pair'))157 assert os.path.exists(tmpdir.join('/var/run/sudo_pair'))
149158
150 def test_create_tmpfiles_conf(self, sph, tmpdir):159 def test_create_tmpfiles_conf(self, sph, tmpdir):
160 """Check that sudo pair temporary conf is rendered correctly."""
151 sph.create_tmpfiles_conf()161 sph.create_tmpfiles_conf()
152 expected_content = 'd {} 0755 - - -\n'.format(sph.socket_dir)162 expected_content = 'd {} 0755 - - -\n'.format(sph.socket_dir)
153 with open(tmpdir.join('/usr/lib/tmpfiles.d/sudo_pair.conf')) as f:163 with open(tmpdir.join('/usr/lib/tmpfiles.d/sudo_pair.conf')) as f:
@@ -155,17 +165,21 @@ class TestSudoPairHelper():
155 assert expected_content in content165 assert expected_content in content
156166
157 def test_install_sudo_pair_so(self, sph, tmpdir):167 def test_install_sudo_pair_so(self, sph, tmpdir):
168 """Check that sudo system lib exists."""
158 sph.install_sudo_pair_so()169 sph.install_sudo_pair_so()
159 assert filecmp.cmp('./files/sudo_pair.so', tmpdir.join('/usr/lib/sudo/sudo_pair.so'))170 assert filecmp.cmp('./files/sudo_pair.so', tmpdir.join('/usr/lib/sudo/sudo_pair.so'))
160171
161 def test_copy_user_prompt(self, sph, tmpdir):172 def test_copy_user_prompt(self, sph, tmpdir):
173 """Check that user prompt exists."""
162 sph.copy_user_prompt()174 sph.copy_user_prompt()
163 assert filecmp.cmp('./files/sudo.prompt.user', tmpdir.join('/etc/sudo_pair.prompt.user'))175 assert filecmp.cmp('./files/sudo.prompt.user', tmpdir.join('/etc/sudo_pair.prompt.user'))
164176
165 def test_copy_pair_prompt(self, sph, tmpdir):177 def test_copy_pair_prompt(self, sph, tmpdir):
178 """Check that pair prompt exists."""
166 sph.copy_pair_prompt()179 sph.copy_pair_prompt()
167 assert filecmp.cmp('./files/sudo.prompt.pair', tmpdir.join('/etc/sudo_pair.prompt.pair'))180 assert filecmp.cmp('./files/sudo.prompt.pair', tmpdir.join('/etc/sudo_pair.prompt.pair'))
168181
169 def test_copy_sudoers(self, sph, tmpdir):182 def test_copy_sudoers(self, sph, tmpdir):
183 """Check that sudoers file exists."""
170 sph.copy_sudoers()184 sph.copy_sudoers()
171 assert filecmp.cmp('./files/sudoers', tmpdir.join('/etc/sudoers'))185 assert filecmp.cmp('./files/sudoers', tmpdir.join('/etc/sudoers'))
diff --git a/tox.ini b/tox.ini
index 562d0ba..fc364de 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
1[tox]1[tox]
2skipsdist=True2skipsdist=True
3envlist = unit, functional, func_xenial3envlist = unit, functional
4skip_missing_interpreters = True4skip_missing_interpreters = True
55
6[testenv]6[testenv]
@@ -9,38 +9,43 @@ setenv =
9 PYTHONPATH = .9 PYTHONPATH = .
1010
11[testenv:unit]11[testenv:unit]
12commands = pytest -v --ignore {toxinidir}/tests/amulet --ignore {toxinidir}/tests/functional12commands = pytest -v --ignore {toxinidir}/tests/functional \
13 --cov=lib \
14 --cov=reactive \
15 --cov=actions \
16 --cov-report=term \
17 --cov-report=annotate:report/annotated \
18 --cov-report=html:report/html
13deps = -r{toxinidir}/tests/unit/requirements.txt19deps = -r{toxinidir}/tests/unit/requirements.txt
20
14setenv = PYTHONPATH={toxinidir}/lib21setenv = PYTHONPATH={toxinidir}/lib
1522
16[testenv:functional]23[testenv:functional]
17passenv =24passenv =
18 HOME25 HOME
19 JUJU_REPOSITORY26 CHARM_BUILD_DIR
20 PATH27 PATH
21commands = pytest -v --ignore {toxinidir}/tests/unit --ignore {toxinidir}/tests/amulet28 PYTEST_KEEP_MODEL
29 PYTEST_CLOUD_NAME
30 PYTEST_CLOUD_REGION
31commands = pytest -v --ignore {toxinidir}/tests/unit
22deps = -r{toxinidir}/tests/functional/requirements.txt32deps = -r{toxinidir}/tests/functional/requirements.txt
2333
2434
25[testenv:func_xenial]
26passenv =
27 HOME
28 JUJU_REPOSITORY
29 PATH
30 test_preserve_model
31setenv = test_series=xenial
32commands = pytest -v --ignore {toxinidir}/tests/unit --ignore {toxinidir}/tests/amulet
33deps = -r{toxinidir}/tests/functional/requirements.txt
34
35[testenv:lint]35[testenv:lint]
36commands = flake836commands = flake8
37deps = flake837deps =
38 flake8
39 flake8-docstrings
40 flake8-import-order
41 pep8-naming
42 flake8-colors
3843
39[flake8]44[flake8]
40max-line-length = 12045ignore = D100,D103 # Missing docstring in public module/function
41max-complexity=10
42ignore = W503
43exclude =46exclude =
44 .git,47 .git,
45 __pycache__,48 __pycache__,
46 .tox,49 .tox,
50max-line-length = 120
51max-complexity = 10

Subscribers

People subscribed via source and target branches