diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml index 10173ed89..529f5c5b8 100644 --- a/.github/workflows/ansible-test.yml +++ b/.github/workflows/ansible-test.yml @@ -41,3 +41,40 @@ jobs: - name: Run sanity tests run: ansible-test sanity --docker -v --color + + unit: + name: Unit tests + strategy: + matrix: + ansible-version: + - stable-2.14 + python-version: + - '3.9' + runs-on: ubuntu-latest + steps: + + - name: Check out code + uses: actions/checkout@v1 + with: + path: ansible_collections/dellemc/enterprise_sonic + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install ansible-base (${{ matrix.ansible-version }}) + run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible-version }}.tar.gz --disable-pip-version-check + + - name: Install ansible and python prerequistes + run: | + ansible-galaxy collection install ansible.netcommon -p ../../ + pip install coverage==6.5.0 + + - name: Run unit tests using ansible-test + run: ansible-test units -v --color --python ${{ matrix.python-version }} --coverage --docker + + - name: Generate coverage report + run: | + ansible-test coverage combine --export tests/output/coverage/ + ansible-test coverage report diff --git a/tests/unit/compat/__init__.py b/tests/unit/compat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/compat/mock.py b/tests/unit/compat/mock.py new file mode 100644 index 000000000..58dc78e07 --- /dev/null +++ b/tests/unit/compat/mock.py @@ -0,0 +1,23 @@ +""" +Compatibility shim for mock imports in modules and module_utils. +This can be removed once support for Python 2.7 is dropped. +""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +try: + from unittest.mock import ( + call, + patch, + mock_open, + MagicMock, + Mock, + ) +except ImportError: + from mock import ( + call, + patch, + mock_open, + MagicMock, + Mock, + ) diff --git a/tests/unit/compat/unittest.py b/tests/unit/compat/unittest.py new file mode 100644 index 000000000..b41677417 --- /dev/null +++ b/tests/unit/compat/unittest.py @@ -0,0 +1,29 @@ +# (c) 2014, Toshio Kuratomi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# Allow wildcard import because we really do want to import all of +# unittests's symbols into this compat shim +# pylint: disable=wildcard-import,unused-wildcard-import +from unittest import * + +if not hasattr(TestCase, 'assertRaisesRegex'): + # added in Python 3.2 + TestCase.assertRaisesRegex = TestCase.assertRaisesRegexp diff --git a/tests/unit/modules/__init__.py b/tests/unit/modules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/modules/conftest.py b/tests/unit/modules/conftest.py new file mode 100644 index 000000000..a7d1e0475 --- /dev/null +++ b/tests/unit/modules/conftest.py @@ -0,0 +1,31 @@ +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + +import pytest + +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_bytes +from ansible.module_utils.common._collections_compat import MutableMapping + + +@pytest.fixture +def patch_ansible_module(request, mocker): + if isinstance(request.param, string_types): + args = request.param + elif isinstance(request.param, MutableMapping): + if 'ANSIBLE_MODULE_ARGS' not in request.param: + request.param = {'ANSIBLE_MODULE_ARGS': request.param} + if '_ansible_remote_tmp' not in request.param['ANSIBLE_MODULE_ARGS']: + request.param['ANSIBLE_MODULE_ARGS']['_ansible_remote_tmp'] = '/tmp' + if '_ansible_keep_remote_files' not in request.param['ANSIBLE_MODULE_ARGS']: + request.param['ANSIBLE_MODULE_ARGS']['_ansible_keep_remote_files'] = False + args = json.dumps(request.param) + else: + raise Exception('Malformed data to the patch_ansible_module pytest fixture') + + mocker.patch('ansible.module_utils.basic._ANSIBLE_ARGS', to_bytes(args)) diff --git a/tests/unit/modules/network/__init__.py b/tests/unit/modules/network/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/modules/network/sonic/__init__.py b/tests/unit/modules/network/sonic/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/modules/network/sonic/fixtures/sonic_dhcp_relay.yaml b/tests/unit/modules/network/sonic/fixtures/sonic_dhcp_relay.yaml new file mode 100644 index 000000000..f683649cc --- /dev/null +++ b/tests/unit/modules/network/sonic/fixtures/sonic_dhcp_relay.yaml @@ -0,0 +1,507 @@ +--- +merged_01: + module_args: + config: + - name: 'Eth1/5' + ipv4: + server_addresses: + - address: 100.1.1.2 + - address: 100.1.1.3 + source_interface: "Vlan 101" + vrf_name: "VrfReg1" + vrf_select: true + link_select: true + policy_action: "replace" + circuit_id: "%h:%p" + ipv6: + server_addresses: + - address: 100::2 + - address: 100::3 + source_interface: "Vlan 101" + vrf_name: "VrfReg2" + vrf_select: true + - name: 'Eth1/31' + ipv4: + max_hop_count: 8 + - name: 'Eth1/32' + ipv6: + max_hop_count: 8 + facts_get_requests: + - path: "data/openconfig-relay-agent:relay-agent/dhcp" + response: + code: 200 + value: + openconfig-relay-agent:dhcp: + interfaces: + interface: + - id: 'Eth1/31' + config: + id: 'Eth1/31' + helper-address: + - '131.1.1.2' + openconfig-relay-agent-ext:max-hop-count: 10 + openconfig-relay-agent-ext:policy-action: 'DISCARD' + agent-information-option: + config: + circuit-id: '%p' + openconfig-relay-agent-ext:vrf-select: 'DISABLE' + openconfig-relay-agent-ext:link-select: 'DISABLE' + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6" + response: + code: 200 + value: + openconfig-relay-agent:dhcpv6: + interfaces: + interface: + - id: 'Eth1/32' + config: + id: 'Eth1/32' + helper-address: + - '131::2' + openconfig-relay-agent-ext:max-hop-count: 10 + options: + config: + openconfig-relay-agent-ext:vrf-select: 'DISABLE' + config_requests: + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f5/config/helper-address" + method: "patch" + data: + openconfig-relay-agent:helper-address: + - '100.1.1.2' + - '100.1.1.3' + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f5/config/openconfig-relay-agent-ext:src-intf" + method: "patch" + data: + openconfig-relay-agent-ext:src-intf: "Vlan101" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f5/config/openconfig-relay-agent-ext:vrf" + method: "patch" + data: + openconfig-relay-agent-ext:vrf: "VrfReg1" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f5/agent-information-option/config/openconfig-relay-agent-ext:vrf-select" + method: "patch" + data: + openconfig-relay-agent-ext:vrf-select: "ENABLE" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f5/agent-information-option/config/openconfig-relay-agent-ext:link-select" + method: "patch" + data: + openconfig-relay-agent-ext:link-select: "ENABLE" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f5/config/openconfig-relay-agent-ext:policy-action" + method: "patch" + data: + openconfig-relay-agent-ext:policy-action: "REPLACE" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f5/agent-information-option/config/circuit-id" + method: "patch" + data: + openconfig-relay-agent:circuit-id: "%h:%p" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f5/config/helper-address" + method: "patch" + data: + openconfig-relay-agent:helper-address: + - '100::2' + - '100::3' + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f5/config/openconfig-relay-agent-ext:src-intf" + method: "patch" + data: + openconfig-relay-agent-ext:src-intf: "Vlan101" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f5/config/openconfig-relay-agent-ext:vrf" + method: "patch" + data: + openconfig-relay-agent-ext:vrf: "VrfReg2" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f5/options/config/openconfig-relay-agent-ext:vrf-select" + method: "patch" + data: + openconfig-relay-agent-ext:vrf-select: "ENABLE" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f31/config/openconfig-relay-agent-ext:max-hop-count" + method: "patch" + data: + openconfig-relay-agent-ext:max-hop-count: 8 + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f32/config/openconfig-relay-agent-ext:max-hop-count" + method: "patch" + data: + openconfig-relay-agent-ext:max-hop-count: 8 +merged_02: + module_args: + config: + - name: 'Eth1/32' + ipv4: + server_addresses: + - address: '132.1.1.2' + ipv6: + server_addresses: + - address: '132::2' + facts_get_requests: + - path: "data/openconfig-relay-agent:relay-agent/dhcp" + response: + code: 200 + value: + openconfig-relay-agent:dhcp: + interfaces: + interface: + - id: 'Eth1/32' + config: + id: 'Eth1/32' + helper-address: + - '132.1.1.2' + openconfig-relay-agent-ext:max-hop-count: 10 + openconfig-relay-agent-ext:policy-action: 'DISCARD' + agent-information-option: + config: + circuit-id: '%p' + openconfig-relay-agent-ext:vrf-select: 'DISABLE' + openconfig-relay-agent-ext:link-select: 'DISABLE' + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6" + response: + code: 200 + value: + openconfig-relay-agent:dhcpv6: + interfaces: + interface: + - id: 'Eth1/32' + config: + id: 'Eth1/32' + helper-address: + - '132::2' + openconfig-relay-agent-ext:max-hop-count: 10 + options: + config: + openconfig-relay-agent-ext:vrf-select: 'DISABLE' + config_requests: [] +deleted_01: + module_args: + config: + - name: 'Eth1/1' + ipv4: + server_addresses: + - address: '100.1.1.2' + vrf_select: true + source_interface: 'Vlan100' + link_select: true + policy_action: 'replace' + circuit_id: '%i' + - name: 'Eth1/2' + ipv6: + server_addresses: + - address: '101::2' + vrf_select: true + source_interface: 'Vlan100' + - name: 'Eth1/3' + ipv4: + max_hop_count: 12 + ipv6: + max_hop_count: 12 + state: deleted + facts_get_requests: + - path: "data/openconfig-relay-agent:relay-agent/dhcp" + response: + code: 200 + value: + openconfig-relay-agent:dhcp: + interfaces: + interface: + - id: 'Eth1/1' + config: + id: 'Eth1/1' + helper-address: + - '100.1.1.2' + - '100.1.1.3' + openconfig-relay-agent-ext:src-intf: 'Vlan100' + openconfig-relay-agent-ext:vrf: 'VrfReg1' + openconfig-relay-agent-ext:max-hop-count: 8 + openconfig-relay-agent-ext:policy-action: 'REPLACE' + agent-information-option: + config: + circuit-id: '%i' + openconfig-relay-agent-ext:vrf-select: 'ENABLE' + openconfig-relay-agent-ext:link-select: 'ENABLE' + - id: 'Eth1/2' + config: + id: 'Eth1/2' + helper-address: + - '101.1.1.2' + - '101.1.1.3' + openconfig-relay-agent-ext:src-intf: 'Vlan100' + openconfig-relay-agent-ext:vrf: 'VrfReg1' + openconfig-relay-agent-ext:max-hop-count: 8 + openconfig-relay-agent-ext:policy-action: 'REPLACE' + agent-information-option: + config: + circuit-id: '%i' + openconfig-relay-agent-ext:vrf-select: 'ENABLE' + openconfig-relay-agent-ext:link-select: 'ENABLE' + - id: 'Eth1/3' + config: + id: 'Eth1/3' + helper-address: + - '102.1.1.2' + - '102.1.1.3' + openconfig-relay-agent-ext:max-hop-count: 12 + openconfig-relay-agent-ext:policy-action: 'DISCARD' + agent-information-option: + config: + circuit-id: '%p' + openconfig-relay-agent-ext:vrf-select: 'DISABLE' + openconfig-relay-agent-ext:link-select: 'DISABLE' + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6" + response: + code: 200 + value: + openconfig-relay-agent:dhcpv6: + interfaces: + interface: + - id: 'Eth1/1' + config: + id: 'Eth1/1' + helper-address: + - '100::2' + - '100::3' + openconfig-relay-agent-ext:src-intf: 'Vlan100' + openconfig-relay-agent-ext:vrf: 'VrfReg1' + openconfig-relay-agent-ext:max-hop-count: 8 + options: + config: + openconfig-relay-agent-ext:vrf-select: 'ENABLE' + - id: 'Eth1/2' + config: + id: 'Eth1/2' + helper-address: + - '101::2' + - '101::3' + openconfig-relay-agent-ext:src-intf: 'Vlan100' + openconfig-relay-agent-ext:vrf: 'VrfReg1' + openconfig-relay-agent-ext:max-hop-count: 8 + options: + config: + openconfig-relay-agent-ext:vrf-select: 'ENABLE' + - id: 'Eth1/3' + config: + id: 'Eth1/3' + helper-address: + - '101.1.1.2' + - '101.1.1.3' + openconfig-relay-agent-ext:max-hop-count: 12 + options: + config: + openconfig-relay-agent-ext:vrf-select: 'DISABLE' + config_requests: + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f1/config/helper-address=100.1.1.2" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f1/agent-information-option/config/openconfig-relay-agent-ext:vrf-select" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f1/config/openconfig-relay-agent-ext:src-intf" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f1/agent-information-option/config/openconfig-relay-agent-ext:link-select" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f1/config/openconfig-relay-agent-ext:policy-action" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f1/agent-information-option/config/circuit-id" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f2/config/helper-address=101::2" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f2/options/config/openconfig-relay-agent-ext:vrf-select" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f2/config/openconfig-relay-agent-ext:src-intf" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f3/config/openconfig-relay-agent-ext:max-hop-count" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f3/config/openconfig-relay-agent-ext:max-hop-count" + method: "delete" +deleted_02: + module_args: + config: + - name: 'Eth1/1' + ipv4: + server_addresses: + - address: + ipv6: + server_addresses: + - address: + - name: 'Eth1/2' + ipv4: + server_addresses: + - address: '101.1.1.2' + - address: '101.1.1.3' + ipv6: + server_addresses: + - address: '101::2' + - address: '101::3' + - name: 'Eth1/3' + state: deleted + facts_get_requests: + - path: "data/openconfig-relay-agent:relay-agent/dhcp" + response: + code: 200 + value: + openconfig-relay-agent:dhcp: + interfaces: + interface: + - id: 'Eth1/1' + config: + id: 'Eth1/1' + helper-address: + - '100.1.1.2' + - '100.1.1.3' + openconfig-relay-agent-ext:src-intf: 'Vlan100' + openconfig-relay-agent-ext:vrf: 'VrfReg1' + openconfig-relay-agent-ext:max-hop-count: 8 + openconfig-relay-agent-ext:policy-action: 'REPLACE' + agent-information-option: + config: + circuit-id: '%i' + openconfig-relay-agent-ext:vrf-select: 'ENABLE' + openconfig-relay-agent-ext:link-select: 'ENABLE' + - id: 'Eth1/2' + config: + id: 'Eth1/2' + helper-address: + - '101.1.1.2' + - '101.1.1.3' + openconfig-relay-agent-ext:src-intf: 'Vlan100' + openconfig-relay-agent-ext:vrf: 'VrfReg1' + openconfig-relay-agent-ext:max-hop-count: 8 + openconfig-relay-agent-ext:policy-action: 'REPLACE' + agent-information-option: + config: + circuit-id: '%i' + openconfig-relay-agent-ext:vrf-select: 'ENABLE' + openconfig-relay-agent-ext:link-select: 'ENABLE' + - id: 'Eth1/3' + config: + id: 'Eth1/3' + helper-address: + - '102.1.1.2' + - '102.1.1.3' + openconfig-relay-agent-ext:max-hop-count: 10 + openconfig-relay-agent-ext:policy-action: 'DISCARD' + agent-information-option: + config: + circuit-id: '%p' + openconfig-relay-agent-ext:vrf-select: 'DISABLE' + openconfig-relay-agent-ext:link-select: 'DISABLE' + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6" + response: + code: 200 + value: + openconfig-relay-agent:dhcpv6: + interfaces: + interface: + - id: 'Eth1/1' + config: + id: 'Eth1/1' + helper-address: + - '100::2' + - '100::3' + openconfig-relay-agent-ext:src-intf: 'Vlan100' + openconfig-relay-agent-ext:vrf: 'VrfReg1' + openconfig-relay-agent-ext:max-hop-count: 8 + options: + config: + openconfig-relay-agent-ext:vrf-select: 'ENABLE' + - id: 'Eth1/2' + config: + id: 'Eth1/2' + helper-address: + - '101::2' + - '101::3' + openconfig-relay-agent-ext:src-intf: 'Vlan100' + openconfig-relay-agent-ext:vrf: 'VrfReg1' + openconfig-relay-agent-ext:max-hop-count: 8 + options: + config: + openconfig-relay-agent-ext:vrf-select: 'ENABLE' + - id: 'Eth1/3' + config: + id: 'Eth1/3' + helper-address: + - '101.1.1.2' + - '101.1.1.3' + openconfig-relay-agent-ext:max-hop-count: 10 + options: + config: + openconfig-relay-agent-ext:vrf-select: 'DISABLE' + config_requests: + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f1/config/helper-address" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f1/config/helper-address" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f2/config/helper-address" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f2/config/helper-address" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f3/config/helper-address" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f3/config/helper-address" + method: "delete" +deleted_03: + module_args: + config: + state: deleted + facts_get_requests: + - path: "data/openconfig-relay-agent:relay-agent/dhcp" + response: + code: 200 + value: + openconfig-relay-agent:dhcp: + interfaces: + interface: + - id: 'Eth1/32' + config: + id: 'Eth1/32' + helper-address: + - '132.1.1.2' + - '132.1.1.3' + openconfig-relay-agent-ext:max-hop-count: 10 + openconfig-relay-agent-ext:policy-action: 'DISCARD' + agent-information-option: + config: + circuit-id: '%p' + openconfig-relay-agent-ext:vrf-select: 'DISABLE' + openconfig-relay-agent-ext:link-select: 'DISABLE' + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6" + response: + code: 200 + value: + openconfig-relay-agent:dhcpv6: + interfaces: + interface: + - id: 'Eth1/32' + config: + id: 'Eth1/32' + helper-address: + - '132::2' + - '132::3' + openconfig-relay-agent-ext:max-hop-count: 10 + options: + config: + openconfig-relay-agent-ext:vrf-select: 'DISABLE' + config_requests: + - path: "data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface=Eth1%2f32/config/helper-address" + method: "delete" + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface=Eth1%2f32/config/helper-address" + method: "delete" +deleted_04: + module_args: + config: + - name: 'Eth1/1' + ipv4: + server_addresses: + - address: '100.1.1.2' + vrf_select: true + max_hop_count: 8 + source_interface: 'Vlan100' + link_select: true + policy_action: 'replace' + ipv6: + server_addresses: + - address: '100::2' + source_interface: 'Vlan100' + state: deleted + facts_get_requests: + - path: "data/openconfig-relay-agent:relay-agent/dhcp" + response: + code: 200 + value: {} + - path: "data/openconfig-relay-agent:relay-agent/dhcpv6" + response: + code: 200 + value: {} + config_requests: [] diff --git a/tests/unit/modules/network/sonic/sonic_module.py b/tests/unit/modules/network/sonic/sonic_module.py new file mode 100644 index 000000000..12faf76a2 --- /dev/null +++ b/tests/unit/modules/network/sonic/sonic_module.py @@ -0,0 +1,150 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import yaml + +from ansible_collections.dellemc.enterprise_sonic.tests.unit.modules.utils import ( + AnsibleExitJson, + AnsibleFailJson, + ModuleTestCase, +) + +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + update_url +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + get_diff +) + + +class TestSonicModule(ModuleTestCase): + """Enterprise SONiC ansible module base unit test class""" + + def setUp(self): + super(TestSonicModule, self).setUp() + + self.config_requests_valid = [] + self.config_requests_sent = [] + + self._config_requests_dict = {} + self._facts_requests_dict = {} + + @staticmethod + def load_fixtures(file_name, content="yaml"): + """Load data from specified fixture file and format""" + fixture_path = os.path.join(os.path.dirname(__file__), "fixtures") + file_path = os.path.join(fixture_path, file_name) + + file_stream = open(file_path, "r") + if content == "yaml": + data = yaml.full_load(file_stream) + else: + data = file_stream.read() + file_stream.close() + + return data + + def initialize_facts_get_requests(self, facts_get_requests): + for request in facts_get_requests: + self._facts_requests_dict[request['path']] = request['response'] + + def initialize_config_requests(self, config_requests): + for request in config_requests: + valid_request = request.copy() + path = valid_request['path'] + method = valid_request['method'].lower() + data = valid_request.get('data', {}) + if valid_request.get('response'): + response = valid_request.pop('response') + else: + response = {} + + self.config_requests_valid.append(valid_request) + if self._config_requests_dict.get(path) is None: + self._config_requests_dict[path] = {} + + config_request_dict = self._config_requests_dict[path] + if config_request_dict.get(method) is None: + config_request_dict[method] = [] + + config_request_dict[method].append([ + data, + {'code': response.get('code', 200), 'value': response.get('value', {})} + ]) + + def facts_side_effect(self, module, commands): + """Side effect function for 'facts' GET requests mock""" + responses = [] + for command in commands: + response = [] + path = update_url(command['path']) + method = command['method'].lower() + + if method == 'get': + if self._facts_requests_dict.get(path): + response.append(self._facts_requests_dict[path]['code']) + response.append(self._facts_requests_dict[path]['value']) + else: + self.module.fail_json(msg="Non GET REST API request made in get facts {0}".format(command)) + + responses.append(response) + + return responses + + def config_side_effect(self, module, commands): + """Side effect function for 'config' requests mock""" + responses = [] + for command in commands: + response = [] + path = update_url(command['path']) + method = command['method'].lower() + data = command['data'] + + self.config_requests_sent.append({'path': path, 'method': method, 'data': data}) + entries = self._config_requests_dict.get(path, {}).get(method, []) + for entry in entries: + if data == entry[0]: + response.append(entry[1]['code']) + response.append(entry[1]['value']) + break + + responses.append(response) + + return responses + + def execute_module(self, failed=False, changed=False): + if failed: + result = self.failed() + else: + result = self.changed(changed) + + return result + + def failed(self): + with self.assertRaises(AnsibleFailJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertTrue(result["failed"], result) + return result + + def changed(self, changed=False): + with self.assertRaises(AnsibleExitJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result["changed"], changed, result) + return result + + def validate_config_requests(self): + """Check if both list of requests sent and expected are same""" + # Sort by 'path' (primary) followed by 'method' (secondary) + self.config_requests_valid.sort(key=lambda request: request['method']) + self.config_requests_valid.sort(key=lambda request: request['path']) + self.config_requests_sent.sort(key=lambda request: request['method']) + self.config_requests_sent.sort(key=lambda request: request['path']) + + self.assertEqual(len(self.config_requests_valid), len(self.config_requests_sent)) + for valid_request, sent_request in zip(self.config_requests_valid, self.config_requests_sent): + self.assertEqual(get_diff(valid_request, sent_request, [{'path': "", 'method': "", 'data': {}}]), {}) diff --git a/tests/unit/modules/network/sonic/test_sonic_dhcp_relay.py b/tests/unit/modules/network/sonic/test_sonic_dhcp_relay.py new file mode 100644 index 000000000..433d4f5c2 --- /dev/null +++ b/tests/unit/modules/network/sonic/test_sonic_dhcp_relay.py @@ -0,0 +1,96 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible_collections.dellemc.enterprise_sonic.tests.unit.compat.mock import ( + patch, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.modules import ( + sonic_dhcp_relay, +) +from ansible_collections.dellemc.enterprise_sonic.tests.unit.modules.utils import ( + set_module_args, +) +from .sonic_module import TestSonicModule + + +class TestSonicDhcpRelayModule(TestSonicModule): + module = sonic_dhcp_relay + + @classmethod + def setUpClass(cls): + cls.mock_facts_edit_config = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.dhcp_relay.dhcp_relay.edit_config" + ) + cls.mock_config_edit_config = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.dhcp_relay.dhcp_relay.edit_config" + ) + cls.mock_get_interface_naming_mode = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils.get_device_interface_naming_mode" + ) + cls.fixture_data = cls.load_fixtures('sonic_dhcp_relay.yaml') + + def setUp(self): + super(TestSonicDhcpRelayModule, self).setUp() + self.facts_edit_config = self.mock_facts_edit_config.start() + self.config_edit_config = self.mock_config_edit_config.start() + + self.facts_edit_config.side_effect = self.facts_side_effect + self.config_edit_config.side_effect = self.config_side_effect + + self.get_interface_naming_mode = self.mock_get_interface_naming_mode.start() + self.get_interface_naming_mode.return_value = 'standard' + + def tearDown(self): + super(TestSonicDhcpRelayModule, self).tearDown() + self.mock_facts_edit_config.stop() + self.mock_config_edit_config.stop() + self.mock_get_interface_naming_mode.stop() + + def test_sonic_dhcp_relay_merged_01(self): + set_module_args(self.fixture_data['merged_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['merged_01']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['merged_01']['config_requests']) + + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_dhcp_relay_merged_02(self): + set_module_args(self.fixture_data['merged_02']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['merged_02']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['merged_02']['config_requests']) + + result = self.execute_module(changed=False) + self.validate_config_requests() + + def test_sonic_dhcp_relay_deleted_01(self): + set_module_args(self.fixture_data['deleted_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['deleted_01']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['deleted_01']['config_requests']) + + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_dhcp_relay_deleted_02(self): + set_module_args(self.fixture_data['deleted_02']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['deleted_02']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['deleted_02']['config_requests']) + + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_dhcp_relay_deleted_03(self): + set_module_args(self.fixture_data['deleted_03']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['deleted_03']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['deleted_03']['config_requests']) + + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_dhcp_relay_deleted_04(self): + set_module_args(self.fixture_data['deleted_04']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['deleted_04']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['deleted_04']['config_requests']) + + result = self.execute_module(changed=False) + self.validate_config_requests() diff --git a/tests/unit/modules/utils.py b/tests/unit/modules/utils.py new file mode 100644 index 000000000..0157649ce --- /dev/null +++ b/tests/unit/modules/utils.py @@ -0,0 +1,51 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes + +from ansible_collections.dellemc.enterprise_sonic.tests.unit.compat import unittest +from ansible_collections.dellemc.enterprise_sonic.tests.unit.compat.mock import patch + + +def set_module_args(args): + if '_ansible_remote_tmp' not in args: + args['_ansible_remote_tmp'] = '/tmp' + if '_ansible_keep_remote_files' not in args: + args['_ansible_keep_remote_files'] = False + + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + pass + + +class AnsibleFailJson(Exception): + pass + + +def exit_json(*args, **kwargs): + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +class ModuleTestCase(unittest.TestCase): + + def setUp(self): + self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) + self.mock_module.start() + self.mock_sleep = patch('time.sleep') + self.mock_sleep.start() + set_module_args({}) + self.addCleanup(self.mock_module.stop) + self.addCleanup(self.mock_sleep.stop)