Merge ~przemeklal/charm-local-users:add-tests into charm-local-users:main

Proposed by Przemyslaw Lal
Status: Merged
Approved by: James Troup
Approved revision: c03ad89ec2e0abd0290e3fd229f11e0fa7082a44
Merged at revision: 3fcfb4b15c5a94f0eb7dd29c899ae5e2dfb6a57f
Proposed branch: ~przemeklal/charm-local-users:add-tests
Merge into: charm-local-users:main
Diff against target: 316 lines (+242/-3)
6 files modified
.gitignore (+1/-0)
run_tests (+1/-0)
src/lib/local_users.py (+6/-3)
tests/__init__.py (+7/-0)
tests/test_charm.py (+73/-0)
tests/test_local_users.py (+154/-0)
Reviewer Review Type Date Requested Status
BootStack Reviewers Pending
BootStack Reviewers Pending
Review via email: mp+412759@code.launchpad.net

Commit message

Add tests and fix minor issues detected by tests

Signed-off-by: Przemysław Lal <email address hidden>

To post a comment you must log in.
Revision history for this message
Przemyslaw Lal (przemeklal) wrote (last edit ):

Tests are passing, total coverage is 74%:

Name Stmts Miss Branch BrPart Cover Missing
--------------------------------------------------------------------
src/__init__.py 0 0 0 0 100%
src/charm.py 77 21 34 7 69% 55, 68->72, 77->90, 79-84, 111, 113-120, 127-129, 139-145, 149
src/lib/__init__.py 0 0 0 0 100%
src/lib/local_users.py 113 22 32 2 78% 35-43, 52-61, 66-67, 82-84, 101-102, 141->exit
--------------------------------------------------------------------
TOTAL 190 43 66 9 74%

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
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 3fcfb4b15c5a94f0eb7dd29c899ae5e2dfb6a57f

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.gitignore b/.gitignore
index 2c3f0e5..f55930e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,6 @@ build/
3*.charm3*.charm
44
5.coverage5.coverage
6coverage.xml
6__pycache__/7__pycache__/
7*.py[cod]8*.py[cod]
diff --git a/run_tests b/run_tests
index 78c5a62..9f708b5 100755
--- a/run_tests
+++ b/run_tests
@@ -25,4 +25,5 @@ fi
2525
26flake826flake8
27coverage run --branch --source=src -m unittest -v "$@"27coverage run --branch --source=src -m unittest -v "$@"
28coverage xml -o coverage.xml
28coverage report -m29coverage report -m
diff --git a/src/lib/local_users.py b/src/lib/local_users.py
index 7a32124..7267967 100755
--- a/src/lib/local_users.py
+++ b/src/lib/local_users.py
@@ -27,6 +27,8 @@ log = logging.getLogger(__name__)
2727
28User = namedtuple("User", ["name", "gecos", "ssh_key"])28User = namedtuple("User", ["name", "gecos", "ssh_key"])
2929
30HOME_DIR_PATH = "/home"
31
3032
31def add_user(username, shell="/bin/bash", home_dir=None, gecos=None):33def add_user(username, shell="/bin/bash", home_dir=None, gecos=None):
32 """Add a user to the system."""34 """Add a user to the system."""
@@ -102,15 +104,16 @@ def remove_group(group_name):
102104
103def rename_group(old_name, new_name):105def rename_group(old_name, new_name):
104 """Rename the `old_name` group to `new_name` using `groupmod` command."""106 """Rename the `old_name` group to `new_name` using `groupmod` command."""
105 cmd = ["groupmod", "-n", new_name, old_name]107 if old_name != new_name:
106 subprocess.check_call(cmd)108 cmd = ["groupmod", "-n", new_name, old_name]
109 subprocess.check_call(cmd)
107110
108111
109def set_ssh_authorized_key(user):112def set_ssh_authorized_key(user):
110 """Idempotently set up the SSH public key in `authorized_keys`."""113 """Idempotently set up the SSH public key in `authorized_keys`."""
111 comment = "# charm-local-users"114 comment = "# charm-local-users"
112 authorized_key = " ".join([user.ssh_key, comment])115 authorized_key = " ".join([user.ssh_key, comment])
113 ssh_path = os.path.join("/home", user.name, ".ssh")116 ssh_path = os.path.join(HOME_DIR_PATH, user.name, ".ssh")
114 authorized_keys_path = os.path.join(ssh_path, "authorized_keys")117 authorized_keys_path = os.path.join(ssh_path, "authorized_keys")
115 if not os.path.exists(ssh_path):118 if not os.path.exists(ssh_path):
116 os.makedirs(ssh_path, mode=0o700)119 os.makedirs(ssh_path, mode=0o700)
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29..bb15096 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,7 @@
1import sys
2from unittest import mock
3
4# mock shutil for all tests
5shutil = mock.MagicMock()
6shutil.chown = mock.MagicMock()
7sys.modules['shutil'] = shutil
diff --git a/tests/test_charm.py b/tests/test_charm.py
index f0bbaa2..ce70df8 100644
--- a/tests/test_charm.py
+++ b/tests/test_charm.py
@@ -13,8 +13,10 @@
13# limitations under the License.13# limitations under the License.
1414
15import unittest15import unittest
16from unittest.mock import patch
1617
17from charm import CharmLocalUsersCharm18from charm import CharmLocalUsersCharm
19from ops.model import ActiveStatus, BlockedStatus
18from ops.testing import Harness20from ops.testing import Harness
1921
2022
@@ -23,3 +25,74 @@ class TestCharm(unittest.TestCase):
23 self.harness = Harness(CharmLocalUsersCharm)25 self.harness = Harness(CharmLocalUsersCharm)
24 self.addCleanup(self.harness.cleanup)26 self.addCleanup(self.harness.cleanup)
25 self.harness.begin()27 self.harness.begin()
28
29 @patch("os.makedirs")
30 @patch("charmhelpers.core.host.group_exists")
31 @patch("charm.rename_group")
32 @patch("charm.get_group_users")
33 @patch("charmhelpers.core.host.add_group")
34 @patch("charm.configure_user")
35 def test_config_changed(
36 self,
37 mock_conf_user,
38 mock_add_group,
39 mock_get_gr_users,
40 mock_rename,
41 mock_exists,
42 _,
43 ):
44 # group doesn't exist yet
45 mock_exists.return_value = False
46
47 # correct configuration
48 self.harness.update_config(
49 {"group": "testgroup", "users": "test1;Test 1;key1\ntest2;Test 2;key2"}
50 )
51 # a new group must be created
52 mock_add_group.assert_called_once_with("testgroup")
53 # first execution, no rename expected
54 mock_rename.assert_not_called()
55 mock_get_gr_users.assert_called_once()
56 # 2 users to configure
57 self.assertEqual(mock_conf_user.call_count, 2)
58 # everything went well
59 self.assertIsInstance(self.harness.model.unit.status, ActiveStatus)
60
61 @patch("os.makedirs")
62 @patch("charmhelpers.core.host.group_exists")
63 @patch("charm.rename_group")
64 @patch("charm.get_group_users")
65 @patch("charmhelpers.core.host.add_group")
66 @patch("charm.configure_user")
67 def test_config_changed_invalid_userlist(
68 self,
69 mock_conf_user,
70 mock_add_group,
71 mock_get_gr_users,
72 mock_rename,
73 mock_exists,
74 _,
75 ):
76 # group doesn't exist yet
77 mock_exists.return_value = False
78
79 # empty users list
80 self.harness.update_config(
81 {"group": "testgroup", "users": ";User with no name;\n"}
82 )
83
84 # a new group must be created
85 mock_add_group.assert_called_once_with("testgroup")
86 # first execution, no rename expected
87 mock_rename.assert_not_called()
88 # we shouldn't be comapring with existing users if the config is invalid
89 mock_get_gr_users.assert_not_called()
90 # no users to configure
91 mock_conf_user.assert_not_called()
92 # we should enter blocked state
93 self.assertIsInstance(self.harness.model.unit.status, BlockedStatus)
94
95 @patch("os.makedirs")
96 def test_empty_group_config(self, _):
97 self.harness.update_config({"group": "", "users": "test;;"})
98 self.assertIsInstance(self.harness.model.unit.status, BlockedStatus)
diff --git a/tests/test_local_users.py b/tests/test_local_users.py
26new file mode 10064499new file mode 100644
index 0000000..b53720f
--- /dev/null
+++ b/tests/test_local_users.py
@@ -0,0 +1,154 @@
1# Copyright 2021 Canonical
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import os
16import unittest
17from collections import namedtuple
18from tempfile import TemporaryDirectory
19from unittest.mock import patch
20
21from lib import local_users
22
23
24class TestLocalUsers(unittest.TestCase):
25 @patch("os.chmod")
26 def test_set_ssh_authorized_key_update(self, _):
27 testuser = local_users.User(
28 "testuser", ["Test User", "", "", "", ""], "ssh-rsa ABC testuser@testhost"
29 )
30
31 testuser2 = local_users.User(
32 "testuser", ["Test User", "", "", "", ""], "ssh-rsa XYZ testuser@testhost"
33 )
34
35 with TemporaryDirectory() as fake_home:
36 testfile_path = os.path.join(fake_home, "testuser", ".ssh", "authorized_keys")
37 with patch("lib.local_users.HOME_DIR_PATH", fake_home):
38 local_users.set_ssh_authorized_key(testuser)
39 with open(testfile_path, "r") as f:
40 keys = f.readlines()
41 self.assertIn("ssh-rsa ABC testuser@testhost # charm-local-users\n", keys)
42
43 # update the key
44 local_users.set_ssh_authorized_key(testuser2)
45 with open(testfile_path, "r") as f:
46 keys = f.readlines()
47 self.assertIn("ssh-rsa XYZ testuser@testhost # charm-local-users\n", keys)
48 self.assertNotIn("ssh-rsa ABC testuser@testhost # charm-local-users\n", keys)
49
50 def test_parse_gecos(self):
51 test_cases = [
52 ("", ["", "", "", "", ""]),
53 ("Test User", ["Test User", "", "", "", ""]),
54 ("Test,,,,", ["Test", "", "", "", ""]),
55 ("Test,,", ["Test", "", "", "", ""]),
56 ("Test,,+0123456789", ["Test", "", "+0123456789", "", ""]),
57 (",,,", ["", "", "", "", ""]),
58 (",,,,,,ignored", ["", "", "", "", ""]),
59 (",,,,'other' field", ["", "", "", "", "'other' field"]),
60 ]
61 for tc in test_cases:
62 result = local_users.parse_gecos(tc[0])
63 self.assertEqual(result, tc[1])
64
65 def test_get_gecos(self):
66 testcases = [
67 (
68 # standard account
69 "testuser",
70 b"testuser:x:1000:1000:Test User,,,:/home/testuser:/usr/bin/bash",
71 ["Test User", "", "", "", ""],
72 ),
73 (
74 # system account, empty GECOS
75 "svcaccount",
76 b"svcaccount:x:999:999::/var/lib/svcaccount:/bin/false",
77 ["", "", "", "", ""],
78 ),
79 (
80 # all fields
81 "testuser",
82 b"testuser:x:1000:1000:Test User,ACME,+123,+321,test:/home/testuser:/usr/bin/bash",
83 ["Test User", "ACME", "+123", "+321", "test"],
84 ),
85 (
86 # one field, no commas
87 "testuser",
88 b"testuser:x:1000:1000:Test:home/testuser:/usr/bin/bash",
89 ["Test", "", "", "", ""],
90 ),
91 ]
92 for tc in testcases:
93 with patch("subprocess.check_output") as mock_cmd:
94 mock_cmd.return_value = tc[1]
95 result = local_users.get_gecos(tc[0])
96 self.assertEqual(result, tc[2])
97
98 def test_update_gecos(self):
99 # ensure that only fields that changed are passed to chfn
100 testcase = namedtuple("testcase", ["prev", "updated", "expected", "should_call"])
101
102 # NOTE: order of chfn flags in GECOS fields order is: -f -r -w -h -o
103 testcases = [
104 testcase(["", "", "", "", ""], ["", "", "", "", ""], [], False),
105 testcase(["A", "B", "0", "1", "2"], ["A", "B", "0", "1", "2"], [], False),
106 testcase(
107 ["A", "B", "0", "1", "2"],
108 ["A", "X", "9", "1", "2"],
109 ["-r", "X", "-w", "9"],
110 True,
111 ),
112 testcase(["A", "", "", "", ""], ["B", "", "", "", ""], ["-f", "B"], True),
113 ]
114 for tc in testcases:
115 with patch("lib.local_users.get_gecos") as mock_prev, patch(
116 "subprocess.check_call"
117 ) as mock_call:
118 u = local_users.User("test", tc.updated, "")
119 mock_prev.return_value = tc.prev
120 local_users.update_gecos(u)
121 if tc.should_call:
122 expected_cmd = ["chfn"] + tc.expected + ["test"]
123 mock_call.assert_called_once_with(expected_cmd)
124 else:
125 mock_call.assert_not_called()
126
127 def test_rename_group(self):
128 with patch("subprocess.check_call") as mock_call:
129 # the old and the new name are identical - ensure groupmod not called
130 local_users.rename_group("test1", "test1")
131 mock_call.assert_not_called()
132
133 # ensure it is called when renaming test1 to test2
134 local_users.rename_group("test1", "test2")
135 mock_call.assert_called_once_with(["groupmod", "-n", "test2", "test1"])
136
137 def test_get_user_membership(self):
138 with patch("subprocess.check_output") as mock_call:
139 expected = ["test", "group1", "group2", "group3"]
140 mock_call.return_value = b"test : test group1 group2 group3"
141 result = local_users.get_user_membership("test")
142 self.assertListEqual(result, expected)
143
144 def test_get_group_users(self):
145 with patch("subprocess.check_output") as mock_call:
146 mock_call.return_value = b"acme:x:1001:test1,test2,test3"
147 result = local_users.get_group_users("acme")
148 self.assertListEqual(result, ["test1", "test2", "test3"])
149
150 def test_get_group_users_empty(self):
151 with patch("subprocess.check_output") as mock_call:
152 mock_call.return_value = b"acme:x:1001:"
153 result = local_users.get_group_users("acme")
154 self.assertListEqual(result, [])

Subscribers

People subscribed via source and target branches

to all changes: