Skip to content

Commit

Permalink
salt/tests: Add unit tests for metalk8s_etcd salt module
Browse files Browse the repository at this point in the history
Add unit tests for `metalk8s_etcd` salt custom module, install etcd3 in
pytest environment and mock all etcd3 call needed

Refs: #2266
  • Loading branch information
TeddyAndrieux committed Jul 10, 2020
1 parent a61c13c commit 8d87fae
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 2 deletions.
1 change: 1 addition & 0 deletions salt/tests/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ salt == 3000.3
salttesting == 2018.9.21
mock == 3.0.5
parameterized == 0.7.4
etcd3 != 0.11.0
77 changes: 75 additions & 2 deletions salt/tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,54 @@ contextlib2==0.6.0.post1 \
--hash=sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e \
--hash=sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b \
# via importlib-metadata, zipp
enum34==1.1.10 \
--hash=sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53 \
--hash=sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328 \
--hash=sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248 \
# via grpcio
etcd3==0.12.0 \
--hash=sha256:89a704cb389bf0a010a1fa050ce19342d23bf6371ebda1c21cfe8ff3ed488726
funcsigs==1.0.2 \
--hash=sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca \
--hash=sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50 \
# via mock, pytest
futures==3.3.0 \
--hash=sha256:49b3f5b064b6e3afc3316421a3f25f66c137ae88f068abbf72830170033c5e16 \
--hash=sha256:7e033af76a5e35f58e56da7a91e687706faf4e7bdfb2cbc3f2cca6b9bcda9794 \
# via salt
# via grpcio, salt, tenacity
grpcio==1.30.0 \
--hash=sha256:08362b8b09562179b14db6ffce4b88e1a6a6edac8bccb85dd35f7b214fa5a0f5 \
--hash=sha256:09bea7902adc33620d68462671942e163ab12214073ffb613d2fef3df94254f6 \
--hash=sha256:0c334d6cbe27ebaa9e7211236dc99f3a9ca2ea4b3bf89b0d2544df2924343cc5 \
--hash=sha256:0c4e316e02fc227c6fba858707baee46f30d890754fc4acdf2cfec2ea0bf0aa1 \
--hash=sha256:14743e8fdfdabbab1a2075ffafd25e0a8b1a864505e3cccdf19793766cdc4624 \
--hash=sha256:1f45ec5003101f16673436b150bac73c2355cd9ae78cb14f3707be01a39b5450 \
--hash=sha256:2121afee4e3ebea7df1137bfb4dc396b1856aff4c517780108d9ce82f57bf2f8 \
--hash=sha256:2522f1808fe41bd8807feb5330025378553745b727eacb07562319205d1fd405 \
--hash=sha256:31e9891ac742e6866aec0cf67f1892618982cfbaf08bdcf3bb2e0f0828530c38 \
--hash=sha256:32fe6369143c262d096995ebdd55eeb77f0e1dbe8343a956462ef0607527c7bc \
--hash=sha256:37da010e209289085d3362f371d9feefc152790859470f5e413d84a95a8d3998 \
--hash=sha256:38ab75168a9024d393bf43343960da425736038d249920955f223bc762587697 \
--hash=sha256:3cb78f8078ae583810c2eb47e536b0803a039656685144db43897e8beca4e203 \
--hash=sha256:474bb992355b4a3cb8d7cb783b2d81f628c16ea921cec54ff492420e11c896f5 \
--hash=sha256:74e8b6bd0f7ae64a7eecfe9bf10bc7a905d3b3eb2775cd3a9fdcdafd277469dd \
--hash=sha256:795f351ef70a931f8f7be6a10a509714ec0a6e36c674a071abe5da8eb6b8bb35 \
--hash=sha256:7b47ec90cab0827679b511f7f9ef4fb0077cb5d7bb3d7b917154e718bb4d983b \
--hash=sha256:7f264d740906655a147448d57e4422723639d2d3f891734b8d5eb1675cb47192 \
--hash=sha256:872d45a2e01f47db095bec032470a8c5c0a5ebd00fc930b5ae35c756b20d2cff \
--hash=sha256:8d3249566b2d8b97925fbb2ae6c5b63c5ebdb919828230eae06a25e9614e051b \
--hash=sha256:9ae898c15d122a046f04ea99327e3e0bd10593eb413c4810b931103da6311a21 \
--hash=sha256:ac97beab4a749c7faf6f267f7b149f6dff4f3ad64f6f6ac1d94d04019785d6a4 \
--hash=sha256:afe1f9173b51945e66c72002995eb6d4217384aaaee53215ae85d8543251fec2 \
--hash=sha256:b022cedea66b7d6774bbd7d32d5a8a374947fb572da1a6915210b09a6f51cbdf \
--hash=sha256:b0f7bfba0ae7a97b802348aba4e08b1e84988103cc1eb887241e7b069010058a \
--hash=sha256:b8e5194fb20f4365eacfc3c33d61662651e12e166978186faf378ee972eb0bab \
--hash=sha256:b934542dd61746651f7907d2d7878f62ef42fdb46935088fc6a1d8266a406ba5 \
--hash=sha256:c8ad75925e87ed68d5f7d5e3ec4b9f2ed209fae67c0abbcbd17481cc474421ba \
--hash=sha256:d18e7fb5c5c336cc349d06cde24582e0bfa5e067fdd6268bf1519c4eb4af0199 \
--hash=sha256:d5eee9d205518ee4feb9c424475ddad18a44fea97ff405780e7cd1d6df8ee96a \
--hash=sha256:e8f2f5d16e0164c415f1b31a8d9a81f2e4645a43d1b261375d6bab7b0adf511f \
# via etcd3
idna==2.10 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
Expand Down Expand Up @@ -90,6 +130,10 @@ markupsafe==1.1.1 \
mock==3.0.5 \
--hash=sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3 \
--hash=sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8
monotonic==1.5 \
--hash=sha256:23953d55076df038541e648a53676fb24980f7a1be290cdda21300b3bc21dfb0 \
--hash=sha256:552a91f381532e33cbd07c6a2655a21908088962bb8fa7239ecbcc6ad1140cc7 \
# via tenacity
more-itertools==5.0.0 \
--hash=sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4 \
--hash=sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc \
Expand Down Expand Up @@ -129,6 +173,26 @@ pluggy==0.13.1 \
--hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \
--hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d \
# via pytest
protobuf==3.12.2 \
--hash=sha256:304e08440c4a41a0f3592d2a38934aad6919d692bb0edfb355548786728f9a5e \
--hash=sha256:49ef8ab4c27812a89a76fa894fe7a08f42f2147078392c0dee51d4a444ef6df5 \
--hash=sha256:50b5fee674878b14baea73b4568dc478c46a31dd50157a5b5d2f71138243b1a9 \
--hash=sha256:5524c7020eb1fb7319472cb75c4c3206ef18b34d6034d2ee420a60f99cddeb07 \
--hash=sha256:612bc97e42b22af10ba25e4140963fbaa4c5181487d163f4eb55b0b15b3dfcd2 \
--hash=sha256:6f349adabf1c004aba53f7b4633459f8ca8a09654bf7e69b509c95a454755776 \
--hash=sha256:85b94d2653b0fdf6d879e39d51018bf5ccd86c81c04e18a98e9888694b98226f \
--hash=sha256:87535dc2d2ef007b9d44e309d2b8ea27a03d2fa09556a72364d706fcb7090828 \
--hash=sha256:a7ab28a8f1f043c58d157bceb64f80e4d2f7f1b934bc7ff5e7f7a55a337ea8b0 \
--hash=sha256:a96f8fc625e9ff568838e556f6f6ae8eca8b4837cdfb3f90efcb7c00e342a2eb \
--hash=sha256:b5a114ea9b7fc90c2cc4867a866512672a47f66b154c6d7ee7e48ddb68b68122 \
--hash=sha256:be04fe14ceed7f8641e30f36077c1a654ff6f17d0c7a5283b699d057d150d82a \
--hash=sha256:bff02030bab8b969f4de597543e55bd05e968567acb25c0a87495a31eb09e925 \
--hash=sha256:c9ca9f76805e5a637605f171f6c4772fc4a81eced4e2f708f79c75166a2c99ea \
--hash=sha256:e1464a4a2cf12f58f662c8e6421772c07947266293fb701cb39cd9c1e183f63c \
--hash=sha256:e72736dd822748b0721f41f9aaaf6a5b6d5cfc78f6c8690263aef8bba4457f0e \
--hash=sha256:eafe9fa19fcefef424ee089fb01ac7177ff3691af7cc2ae8791ae523eb6ca907 \
--hash=sha256:f4b73736108a416c76c17a8a09bc73af3d91edaa26c682aaa460ef91a47168d3 \
# via etcd3
psutil==5.7.0 \
--hash=sha256:1413f4158eb50e110777c4f15d7c759521703bd6beb58926f1d562da40180058 \
--hash=sha256:298af2f14b635c3c7118fd9183843f4e73e681bb6f01e12284d4d70d48a60953 \
Expand Down Expand Up @@ -223,7 +287,16 @@ singledispatch==3.4.0.3 \
six==1.15.0 \
--hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \
--hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \
# via mock, more-itertools, pathlib2, pytest, salttesting, singledispatch
# via etcd3, grpcio, mock, more-itertools, pathlib2, protobuf, pytest, salttesting, singledispatch, tenacity
tenacity==6.2.0 \
--hash=sha256:29ae90e7faf488a8628432154bb34ace1cca58244c6ea399fd33f066ac71339a \
--hash=sha256:5a5d3dcd46381abe8b4f82b5736b8726fd3160c6c7161f53f8af7f1eb9b82173 \
# via etcd3
typing==3.7.4.1 \
--hash=sha256:91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23 \
--hash=sha256:c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36 \
--hash=sha256:f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714 \
# via tenacity
urllib3==1.25.9 \
--hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \
--hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 \
Expand Down
258 changes: 258 additions & 0 deletions salt/tests/unit/modules/test_metalk8s_etcd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
from parameterized import parameterized

from salt.exceptions import CommandExecutionError

from salttesting.mixins import LoaderModuleMockMixin
from salttesting.unit import TestCase
from salttesting.mock import MagicMock, patch
from salttesting.helpers import ForceImportErrorOn

import metalk8s_etcd

MEMBERS_LIST_DICT = [{
"client_urls": [
"https://10.11.12.13:2379"
],
"id": "17971792102091431977L",
"name": "bootstrap",
"peer_urls": [
"https://10.11.12.13:2380"
]
}, {
"client_urls": [
"https://10.11.12.14:2379"
],
"id": "17971792102091431978L",
"name": "node1",
"peer_urls": [
"https://10.11.12.14:2380",
"https://11.11.12.14:2380"
]
}]
MEMBERS_LIST = [MagicMock(**member) for member in MEMBERS_LIST_DICT]
# Add special case for "name" as the "name" field is used by default
# by `MagicMock`
for i, member in enumerate(MEMBERS_LIST):
member.name = MEMBERS_LIST_DICT[i]["name"]


class Metalk8sEtcdTestCase(TestCase, LoaderModuleMockMixin):
"""
TestCase for `metalk8s_etcd` module
"""
loader_module = metalk8s_etcd

def test_virtual_success(self):
"""
Tests the return of `__virtual__` function, success
"""
reload(metalk8s_etcd)
self.assertEqual(metalk8s_etcd.__virtual__(), 'metalk8s_etcd')

def test_virtual_fail_import(self):
"""
Tests the return of `__virtual__` function, unable to import etcd3
"""
with ForceImportErrorOn("etcd3"):
reload(metalk8s_etcd)
self.assertTupleEqual(
metalk8s_etcd.__virtual__(),
(False, "python-etcd3 not available")
)

@parameterized.expand([
(['minion1', 'minion2'], {'minion1': '10.11.12.13'}, True, '10.11.12.13'),
(['minion2'], {'minion1': '10.11.12.13', 'minion2': '10.11.12.14'}, True, '10.11.12.14'),
(['minion1', 'minion2'], {'minion1': '10.11.12.13', 'minion2': '10.11.12.14'}, True, '10.11.12.13'),
(['minion1', 'minion2'], {'minion1': '10.11.12.13', 'minion2': '10.11.12.14'}, [False, True], '10.11.12.14'),
(['minion1', 'minion2'], {'minion1': '10.11.12.13', 'minion2': '10.11.12.14'}, False, 'Unable to find an available etcd member in the cluster', True),
(['minion2'], {'minion1': '10.11.12.13'}, False, 'Unable to find an available etcd member in the cluster', True),
([], {'minion1': '10.11.12.13'}, False, 'Unable to find an available etcd member in the cluster', True),
(['minion1'], {}, False, 'Unable to find an available etcd member in the cluster', True)
])
def test_get_endpoint_up(self, etcd_minions, cp_ips, status,
result, raises=False):
"""
Tests the return of `_get_endpoint_up` function
"""
def _saltutil_runner_mock(name, **kwargs):
if name == "mine.get":
if kwargs.get('fun') == "control_plane_ip":
return cp_ips
return None

def _etcd_status():
result = status
if isinstance(status, list):
result = status.pop(0)
if result:
return True
else:
raise Exception("Unhealthy member")

patch_dict = {
"saltutil.runner": MagicMock(side_effect=_saltutil_runner_mock),
"metalk8s.minions_by_role": MagicMock(return_value=etcd_minions)
}
etcd3_mock = MagicMock()
status_mock = etcd3_mock.return_value.__enter__.return_value.status
status_mock.side_effect = _etcd_status
with patch.dict(metalk8s_etcd.__salt__, patch_dict), \
patch("etcd3.client", etcd3_mock), \
patch("etcd3.exceptions.ConnectionFailedError", Exception):
if raises:
self.assertRaisesRegexp(
Exception,
result,
metalk8s_etcd._get_endpoint_up,
"ca", "key", "cert"
)
else:
self.assertEqual(
metalk8s_etcd._get_endpoint_up("ca", "key", "cert"),
result
)
status_mock.assert_called()

@parameterized.expand([
(),
("10.11.12.13")
])
def test_add_etcd_node(self, endpoint=None):
"""
Tests the return of `add_etcd_node` function
"""
etcd3_mock = MagicMock()
add_member = etcd3_mock.return_value.__enter__.return_value.add_member
add_member.return_value = "my new node"
with patch("etcd3.client", etcd3_mock), \
patch("metalk8s_etcd._get_endpoint_up",
MagicMock(return_value=endpoint)):
self.assertEqual(
metalk8s_etcd.add_etcd_node(
"10.11.12.14",
None if endpoint else "my_endpoint"
),
"my new node"
)
add_member.assert_called_once_with("10.11.12.14")

@parameterized.expand([
(["https://10.11.12.13:2380"], MEMBERS_LIST, True),
(["https://10.11.12.13:2380"], MEMBERS_LIST, True, "10.11.12.13"),
(["https://10.11.12.13:2381"], MEMBERS_LIST, False),
([], MEMBERS_LIST, True),
([], [], True),
(["https://10.11.12.13:2380"], [], False),
(["https://10.11.12.14:2380", "https://11.11.12.14:2380"], MEMBERS_LIST, True),
(["https://10.11.12.14:2380", "https://11.11.12.14:2380"], MEMBERS_LIST, True),
(["https://10.11.12.13:2380", "https://10.11.12.13:2380"], MEMBERS_LIST, True),
(["https://10.11.12.14:2380", "https://10.11.12.14:2381"], MEMBERS_LIST, False)
])
def test_urls_exist_in_cluster(self, peer_urls, members, result,
endpoint=None):
"""
Tests the return of `urls_exist_in_cluster` function
"""
etcd3_mock = MagicMock()
etcd3_mock.return_value.__enter__.return_value.members = members
with patch("etcd3.client", etcd3_mock), \
patch("metalk8s_etcd._get_endpoint_up",
MagicMock(return_value=endpoint)):
self.assertEqual(
metalk8s_etcd.urls_exist_in_cluster(
peer_urls,
None if endpoint else "my_endpoint"
),
result
)

@parameterized.expand([
({'minion1': 'https://10.11.12.13:2380'}, None, MEMBERS_LIST, True, "cluster is healthy", False),
(None, 'https://10.11.12.13:2380', MEMBERS_LIST, True, "cluster is healthy", False),
(None, 'https://10.11.12.13:2380', MEMBERS_LIST, [True, False], "cluster is degraded", True),
(None, 'https://10.11.12.13:2380', MEMBERS_LIST, False, "cluster is unavailable", True),
(None, 'https://10.11.12.13:2380', [], False, "cluster is unavailable", True)
])
def test_check_etcd_health(self, cp_ips, endpoint, members, status,
result, raises):
"""
Tests the return of `check_etcd_health` function
"""
def _saltutil_runner_mock(name, **kwargs):
if name == "mine.get":
if kwargs.get('fun') == "control_plane_ip":
return cp_ips
return None

def _etcd_status():
result = status
if isinstance(status, list):
result = status.pop(0)
if result:
return True
else:
raise Exception("Unhealthy member")

etcd3_mock = MagicMock()
etcd3_mock.return_value.__enter__.return_value.members = members
etcd3_mock.return_value.__enter__.return_value.status.side_effect = \
_etcd_status

patch_dict = {
"saltutil.runner": MagicMock(side_effect=_saltutil_runner_mock)
}
with patch.dict(metalk8s_etcd.__salt__, patch_dict), \
patch("etcd3.client", etcd3_mock), \
patch("metalk8s_etcd._get_endpoint_up",
MagicMock(return_value=endpoint)):
minion_id = "minion1" if cp_ips else None
if raises:
self.assertRaisesRegexp(
CommandExecutionError,
result,
metalk8s_etcd.check_etcd_health,
minion_id,
ca_cert="ca", cert_key="key", cert_cert="cert"
)
else:
self.assertEqual(
metalk8s_etcd.check_etcd_health(
minion_id,
ca_cert="ca", cert_key="key", cert_cert="cert"
),
result
)
etcd3_mock.assert_any_call(
host=cp_ips[minion_id] if minion_id else endpoint,
ca_cert="ca", cert_key="key", cert_cert="cert", timeout=30
)

@parameterized.expand([
(MEMBERS_LIST, MEMBERS_LIST_DICT),
(MEMBERS_LIST, MEMBERS_LIST_DICT, "10.11.12.13"),
(MEMBERS_LIST, [], Exception("No endpoint up")),
([], []),
([], [], "10.11.12.13")
])
def test_get_etcd_member_list(self, members, result, endpoint=None):
"""
Tests the return of `get_etcd_member_list` function
"""
def _get_endpoint(*args, **kwargs):
if isinstance(endpoint, Exception):
raise endpoint
return endpoint

etcd3_mock = MagicMock()
etcd3_mock.return_value.__enter__.return_value.members = members

with patch("etcd3.client", etcd3_mock), \
patch("metalk8s_etcd._get_endpoint_up",
MagicMock(side_effect=_get_endpoint)):
self.assertEqual(
metalk8s_etcd.get_etcd_member_list(
None if endpoint else "my_endpoint"
),
result
)

0 comments on commit 8d87fae

Please sign in to comment.