summaryrefslogtreecommitdiff
path: root/tests/unit/modules/test_vyos_user.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit/modules/test_vyos_user.py')
-rw-r--r--tests/unit/modules/test_vyos_user.py278
1 files changed, 174 insertions, 104 deletions
diff --git a/tests/unit/modules/test_vyos_user.py b/tests/unit/modules/test_vyos_user.py
index 511ecef..32459ca 100644
--- a/tests/unit/modules/test_vyos_user.py
+++ b/tests/unit/modules/test_vyos_user.py
@@ -4,169 +4,239 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
-import json
-import os
import unittest
from unittest.mock import MagicMock
from ansible_collections.vyos.rest.plugins.modules.vyos_user import (
+ _device_to_argspec,
+ _public_keys_from_device,
+ _public_keys_to_device,
+ _user_from_device,
+ _user_to_device,
build_commands,
get_running_config,
)
-
-_BASE = ["system", "login", "user"]
+from .base import load_fixture
-def load_fixture(filename):
- fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures")
- with open(os.path.join(fixtures_dir, filename)) as f:
- return json.load(f)
+_BASE = ["system", "login", "user"]
class VyOSModuleTestCase(unittest.TestCase):
def setUp(self):
self.mock_vyos = MagicMock()
- self.fixture = load_fixture("user_running.json")
- self.mock_vyos.get_config = MagicMock(return_value=self.fixture)
-
+ fixture = load_fixture("user_running.json")
+ self.fixture = fixture.get("user", fixture)
+ self.mock_vyos.get_config = MagicMock(return_value={"user": self.fixture})
-class TestVyOSUserGetRunning(VyOSModuleTestCase):
- def test_parses_users(self):
+class TestGetRunningConfig(VyOSModuleTestCase):
+ def test_unwraps_user_key(self):
result = get_running_config(self.mock_vyos)
- names = [u["name"] for u in result]
- self.assertIn("vyos", names)
- self.assertIn("alice", names)
+ self.assertIn("alice", result)
+ self.assertIn("vyos", result)
- def test_parses_full_name(self):
- result = get_running_config(self.mock_vyos)
- alice = next(u for u in result if u["name"] == "alice")
- self.assertEqual(alice["full_name"], "Alice Smith")
+ def test_empty_config(self):
+ self.mock_vyos.get_config = MagicMock(return_value=None)
+ self.assertEqual(get_running_config(self.mock_vyos), {})
- def test_parses_encrypted_password(self):
- result = get_running_config(self.mock_vyos)
- alice = next(u for u in result if u["name"] == "alice")
- self.assertEqual(alice["encrypted_password"], "$6$def456")
- def test_parses_public_keys(self):
- result = get_running_config(self.mock_vyos)
- alice = next(u for u in result if u["name"] == "alice")
- self.assertEqual(len(alice["public_keys"]), 1)
- key = alice["public_keys"][0]
- self.assertEqual(key["name"], "alice-laptop")
- self.assertEqual(key["type"], "ssh-rsa")
- self.assertEqual(key["key"], "AAAAB3NzaC1yc2EAAAA")
+class TestPublicKeysToDeviceFromDevice(unittest.TestCase):
+ def test_to_device(self):
+ result = _public_keys_to_device([{"name": "laptop", "key": "AAAA", "type": "ssh-rsa"}])
+ self.assertEqual(result, {"laptop": {"key": "AAAA", "type": "ssh-rsa"}})
+
+ def test_from_device(self):
+ result = _public_keys_from_device({"laptop": {"key": "AAAA", "type": "ssh-rsa"}})
+ self.assertEqual(result, [{"name": "laptop", "key": "AAAA", "type": "ssh-rsa"}])
+
+ def test_empty(self):
+ self.assertEqual(_public_keys_to_device([]), {})
+ self.assertEqual(_public_keys_from_device({}), [])
- def test_empty_config(self):
- self.mock_vyos.get_config = MagicMock(return_value={})
- result = get_running_config(self.mock_vyos)
- self.assertEqual(result, [])
+class TestUserToDeviceFromDevice(unittest.TestCase):
+ """Password is the critical case here: it must NEVER appear in
+ _user_to_device's output (it's handled separately, outside dict_op,
+ since it can't be compared against have's encrypted-password)."""
-class TestVyOSUserBuildCommands(unittest.TestCase):
+ def test_password_never_enters_dict_op_path(self):
+ result = _user_to_device({"name": "alice", "password": "secret", "full_name": "Alice"})
+ self.assertNotIn("password", result)
+ self.assertNotIn("plaintext-password", str(result))
+ self.assertEqual(result, {"full_name": "Alice"})
- def _have(self):
- return [
- {"name": "vyos", "encrypted_password": "$6$abc123"},
+ def test_update_password_never_enters_dict_op_path(self):
+ result = _user_to_device({"name": "alice", "update_password": "on_create"})
+ self.assertEqual(result, {})
+
+ def test_public_keys_wrapped_under_authentication(self):
+ result = _user_to_device(
{
"name": "alice",
- "full_name": "Alice Smith",
- "encrypted_password": "$6$def456",
+ "public_keys": [{"name": "laptop", "key": "AAAA", "type": "ssh-rsa"}],
},
- ]
+ )
+ self.assertEqual(
+ result,
+ {"authentication": {"public_keys": {"laptop": {"key": "AAAA", "type": "ssh-rsa"}}}},
+ )
- def test_present_new_user_with_password(self):
- users = [
- {
- "name": "bob",
- "full_name": "Bob Jones",
- "password": "secret",
- "update_password": "always",
- },
- ]
- cmds = build_commands(users, self._have(), "present")
- self.assertIn(("set", _BASE + ["bob", "full-name", "Bob Jones"]), cmds)
- self.assertIn(
- ("set", _BASE + ["bob", "authentication", "plaintext-password", "secret"]),
- cmds,
+ def test_from_device_encrypted_password_surfaces_as_fact_only(self):
+ entry = _user_from_device("alice", {"authentication": {"encrypted-password": "hash1"}})
+ self.assertEqual(entry["encrypted_password"], "hash1")
+ self.assertNotIn("password", entry)
+
+ def test_from_device_plaintext_password_placeholder_ignored(self):
+ """VyOS's write-only placeholder (an empty plaintext-password
+ marker) must never surface in the argspec-facing output."""
+ entry = _user_from_device(
+ "vyos",
+ {"authentication": {"encrypted-password": "hash1", "plaintext-password": ""}},
)
+ self.assertNotIn("plaintext_password", entry)
+ self.assertNotIn("password", entry)
+
+ def test_from_device_with_public_keys(self):
+ entry = _user_from_device(
+ "alice",
+ {"authentication": {"public-keys": {"laptop": {"key": "AAAA", "type": "ssh-rsa"}}}},
+ )
+ self.assertEqual(
+ entry["public_keys"],
+ [{"name": "laptop", "key": "AAAA", "type": "ssh-rsa"}],
+ )
+
+
+class TestDeviceToArgspecFixture(VyOSModuleTestCase):
+ def test_alice_full_name_and_keys(self):
+ have = _device_to_argspec(self.fixture)
+ alice = next(u for u in have if u["name"] == "alice")
+ self.assertEqual(alice["full_name"], "Alice Smith")
+ self.assertEqual(alice["encrypted_password"], "$6$def456")
+ self.assertEqual(alice["public_keys"][0]["name"], "alice-laptop")
- def test_present_update_password_always(self):
- users = [{"name": "alice", "password": "newpass", "update_password": "always"}]
- cmds = build_commands(users, self._have(), "present")
+ def test_vyos_user_present_no_plaintext_leak(self):
+ have = _device_to_argspec(self.fixture)
+ vyos_user = next(u for u in have if u["name"] == "vyos")
+ self.assertNotIn("password", vyos_user)
+ self.assertEqual(vyos_user["encrypted_password"], "$6$abc123")
+
+ def test_empty_config(self):
+ self.assertEqual(_device_to_argspec({}), [])
+ self.assertEqual(_device_to_argspec(None), [])
+
+
+class TestBuildCommands(VyOSModuleTestCase):
+ """Password policy is the module's core correctness risk -- covered
+ heavily here since it can never be validated via idempotency
+ (there's no way to compare plaintext to a hash)."""
+
+ def test_present_idempotent_without_password(self):
+ have = _device_to_argspec(self.fixture)
+ # drop encrypted_password/keys not settable via argspec anyway;
+ # use only what a user would actually pass back in
+ users = [{"name": u["name"], "full_name": u.get("full_name")} for u in have]
+ cmds = build_commands(users, self.fixture, "present")
+ self.assertEqual(cmds, [])
+
+ def test_update_password_always_resets_existing_user(self):
+ cmds = build_commands(
+ [{"name": "alice", "password": "newpass", "update_password": "always"}],
+ self.fixture,
+ "present",
+ )
self.assertIn(
("set", _BASE + ["alice", "authentication", "plaintext-password", "newpass"]),
cmds,
)
- def test_present_update_password_on_create_existing(self):
- users = [{"name": "alice", "password": "newpass", "update_password": "on_create"}]
- cmds = build_commands(users, self._have(), "present")
- paths = [c[1] for c in cmds]
- self.assertNotIn(
- _BASE + ["alice", "authentication", "plaintext-password", "newpass"],
- paths,
+ def test_update_password_on_create_skips_existing_user(self):
+ cmds = build_commands(
+ [{"name": "alice", "password": "newpass", "update_password": "on_create"}],
+ self.fixture,
+ "present",
)
+ self.assertTrue(all("plaintext-password" not in c[1] for c in cmds))
- def test_present_update_password_on_create_new(self):
- users = [{"name": "bob", "password": "secret", "update_password": "on_create"}]
- cmds = build_commands(users, self._have(), "present")
+ def test_update_password_on_create_sets_for_new_user(self):
+ cmds = build_commands(
+ [{"name": "bob", "password": "newpass", "update_password": "on_create"}],
+ self.fixture,
+ "present",
+ )
self.assertIn(
- ("set", _BASE + ["bob", "authentication", "plaintext-password", "secret"]),
+ ("set", _BASE + ["bob", "authentication", "plaintext-password", "newpass"]),
cmds,
)
- def test_present_idempotent_full_name(self):
- users = [{"name": "alice", "full_name": "Alice Smith"}]
- cmds = build_commands(users, self._have(), "present")
- self.assertEqual(cmds, [])
-
- def test_present_update_full_name(self):
- users = [{"name": "alice", "full_name": "Alice Updated"}]
- cmds = build_commands(users, self._have(), "present")
+ def test_default_update_password_is_always(self):
+ """default of 'always' must re-set even without explicit
+ update_password, matching the argspec default."""
+ cmds = build_commands([{"name": "alice", "password": "newpass"}], self.fixture, "present")
self.assertIn(
- ("set", _BASE + ["alice", "full-name", "Alice Updated"]),
+ ("set", _BASE + ["alice", "authentication", "plaintext-password", "newpass"]),
cmds,
)
- def test_absent_existing_user(self):
- users = [{"name": "alice"}]
- cmds = build_commands(users, self._have(), "absent")
- self.assertIn(("delete", _BASE + ["alice"]), cmds)
+ def test_no_password_never_sets_plaintext(self):
+ cmds = build_commands(
+ [{"name": "alice", "full_name": "Alice Smith"}],
+ self.fixture,
+ "present",
+ )
+ self.assertTrue(all("plaintext-password" not in c[1] for c in cmds))
- def test_absent_nonexistent_user(self):
- users = [{"name": "bob"}]
- cmds = build_commands(users, self._have(), "absent")
+ def test_vyos_user_never_deleted(self):
+ cmds = build_commands([{"name": "vyos"}], self.fixture, "absent")
self.assertEqual(cmds, [])
- def test_present_public_key(self):
- users = [
- {
- "name": "alice",
- "public_keys": [
- {"name": "new-key", "key": "AAAAB3...", "type": "ssh-ed25519"},
- ],
- },
- ]
- cmds = build_commands(users, self._have(), "present")
- self.assertIn(
- (
- "set",
- _BASE + ["alice", "authentication", "public-keys", "new-key", "key", "AAAAB3..."],
- ),
- cmds,
+ def test_absent_deletes_named_existing_user(self):
+ cmds = build_commands([{"name": "alice"}], self.fixture, "absent")
+ self.assertEqual(cmds, [("delete", _BASE + ["alice"])])
+
+ def test_absent_skips_nonexistent_user(self):
+ cmds = build_commands([{"name": "nobody"}], self.fixture, "absent")
+ self.assertEqual(cmds, [])
+
+ def test_present_adds_new_public_key_without_removing_others(self):
+ """present is additive-only: adding a key for an existing user
+ must not touch other existing fields."""
+ cmds = build_commands(
+ [
+ {
+ "name": "alice",
+ "public_keys": [
+ {"name": "alice-desktop", "key": "BBBB", "type": "ssh-ed25519"},
+ ],
+ },
+ ],
+ self.fixture,
+ "present",
)
self.assertIn(
(
"set",
_BASE
- + ["alice", "authentication", "public-keys", "new-key", "type", "ssh-ed25519"],
+ + [
+ "alice",
+ "authentication",
+ "public-keys",
+ "alice-desktop",
+ "key",
+ "BBBB",
+ ],
),
cmds,
)
+ def test_collapsed_single_public_key_no_char_iteration_bug(self):
+ raw_have = {"alice": {"authentication": {"public-keys": "alice-laptop"}}}
+ users = [{"name": "alice", "public_keys": [{"name": "alice-laptop"}]}]
+ self.assertEqual(build_commands(users, raw_have, "present"), [])
+
if __name__ == "__main__":
unittest.main()