summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/dns/tests/unit
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/dns/tests/unit
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/dns/tests/unit')
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/inventory/test_hetzner_dns_records.py529
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/inventory/test_hosttech_dns_records.py465
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/conversion/test_converter.py260
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/conversion/test_txt.py277
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/helper.py47
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/hetzner/test_api.py150
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/hosttech/test_api.py46
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/hosttech/test_json_api.py381
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/module/test__utils.py52
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/resolver_helper.py73
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/test_argspec.py27
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/test_json_api_helper.py96
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/test_names.py104
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/test_provider.py33
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/test_record.py142
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/test_resolver.py905
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/test_wsdl.py283
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/module_utils/test_zone.py60
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/hetzner.py144
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/hosttech.py463
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record.py835
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_info.py798
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_set.py1901
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_set_info.py696
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_sets.py1236
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_zone_info.py192
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record.py1032
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_info.py766
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_set.py1899
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_set_info.py649
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_sets.py1429
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_zone_info.py351
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/modules/test_wait_for_txt.py1425
-rw-r--r--ansible_collections/community/dns/tests/unit/plugins/plugin_utils/test_public_suffix.py191
-rw-r--r--ansible_collections/community/dns/tests/unit/requirements.txt11
-rw-r--r--ansible_collections/community/dns/tests/unit/requirements.yml7
36 files changed, 17955 insertions, 0 deletions
diff --git a/ansible_collections/community/dns/tests/unit/plugins/inventory/test_hetzner_dns_records.py b/ansible_collections/community/dns/tests/unit/plugins/inventory/test_hetzner_dns_records.py
new file mode 100644
index 000000000..3b01de586
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/inventory/test_hetzner_dns_records.py
@@ -0,0 +1,529 @@
+# Copyright (c), Felix Fontein <felix@fontein.de>, 2021
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+import os
+import textwrap
+
+from ansible import constants as C
+from ansible.inventory.manager import InventoryManager
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.internal_test_tools.tests.unit.mock.path import mock_unfrackpath_noop
+from ansible_collections.community.internal_test_tools.tests.unit.mock.loader import DictDataLoader
+from ansible_collections.community.internal_test_tools.tests.unit.utils.open_url_framework import (
+ OpenUrlCall,
+ OpenUrlProxy,
+)
+
+
+HETZNER_DEFAULT_ZONE = {
+ 'id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ 'legacy_dns_host': 'string',
+ 'legacy_ns': ['string'],
+ 'name': 'example.com',
+ 'ns': ['string'],
+ 'owner': 'Example',
+ 'paused': True,
+ 'permission': 'string',
+ 'project': 'string',
+ 'registrar': 'string',
+ 'status': 'verified',
+ 'ttl': 10800,
+ 'verified': '2021-07-09T11:18:37Z',
+ 'records_count': 0,
+ 'is_secondary_dns': True,
+ 'txt_verification': {
+ 'name': 'string',
+ 'token': 'string',
+ },
+}
+
+HETZNER_JSON_DEFAULT_ENTRIES = [
+ {
+ 'id': '125',
+ 'type': 'A',
+ 'name': '@',
+ 'value': '1.2.3.4',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '126',
+ 'type': 'A',
+ 'name': '*',
+ 'value': '1.2.3.5',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '127',
+ 'type': 'AAAA',
+ 'name': '@',
+ 'value': '2001:1:2::3',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '128',
+ 'type': 'AAAA',
+ 'name': 'foo',
+ 'value': '2001:1:2::4',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '129',
+ 'type': 'MX',
+ 'name': '@',
+ 'value': '10 example.com',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '130',
+ 'type': 'CNAME',
+ 'name': 'bar',
+ 'value': 'example.org.',
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+]
+
+HETZNER_JSON_BAD_ENTRIES = [
+ {
+ 'id': '125',
+ 'type': 'TXT',
+ 'name': '@',
+ 'value': '"this is wrongly quoted',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+]
+
+HETZNER_JSON_ZONE_LIST_RESULT = {
+ 'zones': [
+ HETZNER_DEFAULT_ZONE,
+ ],
+}
+
+HETZNER_JSON_ZONE_GET_RESULT = {
+ 'zone': HETZNER_DEFAULT_ZONE,
+}
+
+HETZNER_JSON_ZONE_RECORDS_GET_RESULT = {
+ 'records': HETZNER_JSON_DEFAULT_ENTRIES,
+}
+
+HETZNER_JSON_ZONE_RECORDS_GET_RESULT_2 = {
+ 'records': HETZNER_JSON_BAD_ENTRIES,
+}
+
+
+original_exists = os.path.exists
+original_access = os.access
+
+
+def exists_mock(path, exists=True):
+ def exists(f):
+ if to_native(f) == path:
+ return exists
+ return original_exists(f)
+
+ return exists
+
+
+def access_mock(path, can_access=True):
+ def access(f, m, *args, **kwargs):
+ if to_native(f) == path:
+ return can_access
+ return original_access(f, m, *args, **kwargs)
+
+ return access
+
+
+def test_inventory_file_simple(mocker):
+ inventory_filename = "test.hetzner_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hetzner_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hetzner_dns_records
+ hetzner_token: foo
+ zone_name: example.com
+ filters:
+ type: A
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert im._inventory.hosts
+ assert 'example.com' in im._inventory.hosts
+ assert '*.example.com' in im._inventory.hosts
+ assert 'foo.example.com' not in im._inventory.hosts
+ assert 'bar.example.com' not in im._inventory.hosts
+ assert im._inventory.get_host('example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('*.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('example.com').get_vars()['ansible_host'] == '1.2.3.4'
+ assert im._inventory.get_host('*.example.com').get_vars()['ansible_host'] == '1.2.3.5'
+ assert len(im._inventory.groups['ungrouped'].hosts) == 2
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_collision(mocker):
+ inventory_filename = "test.hetzner_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hetzner_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hetzner_dns_records
+ hetzner_token: '{{ "foo" }}'
+ zone_name: '{{ "example." ~ "com" }}'
+ filters:
+ type:
+ - A
+ - AAAA
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert im._inventory.hosts
+ assert 'example.com' in im._inventory.hosts
+ assert '*.example.com' in im._inventory.hosts
+ assert 'foo.example.com' in im._inventory.hosts
+ assert 'bar.example.com' not in im._inventory.hosts
+ assert im._inventory.get_host('example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('*.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('foo.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('example.com').get_vars()['ansible_host'] == '2001:1:2::3'
+ assert im._inventory.get_host('*.example.com').get_vars()['ansible_host'] == '1.2.3.5'
+ assert im._inventory.get_host('foo.example.com').get_vars()['ansible_host'] == '2001:1:2::4'
+ assert len(im._inventory.groups['ungrouped'].hosts) == 3
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_no_filter(mocker):
+ inventory_filename = "test.hetzner_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hetzner_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hetzner_dns_records
+ hetzner_token: foo
+ zone_id: '42'
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert im._inventory.hosts
+ assert 'example.com' in im._inventory.hosts
+ assert '*.example.com' in im._inventory.hosts
+ assert 'foo.example.com' in im._inventory.hosts
+ assert 'bar.example.com' in im._inventory.hosts
+ assert im._inventory.get_host('example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('*.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('foo.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('bar.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('example.com').get_vars()['ansible_host'] == '2001:1:2::3'
+ assert im._inventory.get_host('*.example.com').get_vars()['ansible_host'] == '1.2.3.5'
+ assert im._inventory.get_host('foo.example.com').get_vars()['ansible_host'] == '2001:1:2::4'
+ assert im._inventory.get_host('bar.example.com').get_vars()['ansible_host'] == 'example.org.'
+ assert len(im._inventory.groups['ungrouped'].hosts) == 4
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_record_conversion_error(mocker):
+ inventory_filename = "test.hetzner_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hetzner_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hetzner_dns_records
+ hetzner_token: foo
+ zone_id: "{{ '42' }}"
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT_2),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ # TODO: make sure that the correct error was reported
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_missing_zone(mocker):
+ inventory_filename = "test.hetzner_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hetzner_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hetzner_dns_records
+ hetzner_token: foo
+ """)}
+
+ open_url = OpenUrlProxy([
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ # TODO: make sure that the correct error was reported
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_zone_not_found(mocker):
+ inventory_filename = "test.hetzner_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hetzner_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hetzner_dns_records
+ hetzner_token: foo
+ zone_id: '23'
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ # TODO: make sure that the correct error was reported
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_unauthorized(mocker):
+ inventory_filename = "test.hetzner_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hetzner_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hetzner_dns_records
+ hetzner_token: foo
+ zone_id: '23'
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ # TODO: make sure that the correct error was reported
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_error(mocker):
+ inventory_filename = "test.hetzner_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hetzner_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hetzner_dns_records
+ hetzner_token: foo
+ zone_id: '42'
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .result_json({}),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ # TODO: make sure that the correct error was reported
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_wrong_file(mocker):
+ inventory_filename = "test.hetznerdns.yml"
+ C.INVENTORY_ENABLED = ['community.dns.hetzner_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hetzner_dns_records
+ hetzner_token: foo
+ """)}
+
+ open_url = OpenUrlProxy([])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ # TODO: make sure that the correct error was reported
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_no_file(mocker):
+ inventory_filename = "test.hetzner_dns.yml"
+ C.INVENTORY_ENABLED = ['community.dns.hetzner_dns_records']
+
+ open_url = OpenUrlProxy([])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename, False))
+ mocker.patch('os.access', access_mock(inventory_filename, False))
+ im = InventoryManager(loader=DictDataLoader({}), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ # TODO: make sure that the correct error was reported
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
diff --git a/ansible_collections/community/dns/tests/unit/plugins/inventory/test_hosttech_dns_records.py b/ansible_collections/community/dns/tests/unit/plugins/inventory/test_hosttech_dns_records.py
new file mode 100644
index 000000000..a7adb2c09
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/inventory/test_hosttech_dns_records.py
@@ -0,0 +1,465 @@
+# Copyright (c), Felix Fontein <felix@fontein.de>, 2021
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+import os
+import textwrap
+
+from ansible import constants as C
+from ansible.inventory.manager import InventoryManager
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.internal_test_tools.tests.unit.mock.path import mock_unfrackpath_noop
+from ansible_collections.community.internal_test_tools.tests.unit.mock.loader import DictDataLoader
+from ansible_collections.community.internal_test_tools.tests.unit.utils.open_url_framework import (
+ OpenUrlCall,
+ OpenUrlProxy,
+)
+
+
+HOSTTECH_WSDL_DEFAULT_ENTRIES = [
+ (125, 42, 'A', '', '1.2.3.4', 3600, None, None),
+ (126, 42, 'A', '*', '1.2.3.5', 3600, None, None),
+ (127, 42, 'AAAA', '', '2001:1:2::3', 3600, None, None),
+ (128, 42, 'AAAA', 'foo', '2001:1:2::4', 3600, None, None),
+ (129, 42, 'MX', '', 'example.com', 3600, None, '10'),
+ (130, 42, 'CNAME', 'bar', 'example.org.', 10800, None, None),
+]
+
+HOSTTECH_JSON_DEFAULT_ENTRIES = [
+ # (125, 42, 'A', '', '1.2.3.4', 3600, None, None),
+ {
+ 'id': 125,
+ 'type': 'A',
+ 'name': '',
+ 'ipv4': '1.2.3.4',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ # (126, 42, 'A', '*', '1.2.3.5', 3600, None, None),
+ {
+ 'id': 126,
+ 'type': 'A',
+ 'name': '*',
+ 'ipv4': '1.2.3.5',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ # (127, 42, 'AAAA', '', '2001:1:2::3', 3600, None, None),
+ {
+ 'id': 127,
+ 'type': 'AAAA',
+ 'name': '',
+ 'ipv6': '2001:1:2::3',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ # (128, 42, 'AAAA', '*', '2001:1:2::4', 3600, None, None),
+ {
+ 'id': 128,
+ 'type': 'AAAA',
+ 'name': 'foo',
+ 'ipv6': '2001:1:2::4',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ # (129, 42, 'MX', '', 'example.com', 3600, None, '10'),
+ {
+ 'id': 129,
+ 'type': 'MX',
+ 'ownername': '',
+ 'name': 'example.com',
+ 'pref': 10,
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ # (130, 42, 'CNAME', 'bar', 'example.org.', 10800, None, None),
+ {
+ 'id': 130,
+ 'type': 'CNAME',
+ 'name': 'bar',
+ 'cname': 'example.org.',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+]
+
+
+HOSTTECH_JSON_ZONE_LIST_RESULT = {
+ "data": [
+ {
+ "id": 42,
+ "name": "example.com",
+ "email": "test@example.com",
+ "ttl": 10800,
+ "nameserver": "ns1.hosttech.ch",
+ "dnssec": False,
+ },
+ ],
+}
+
+HOSTTECH_JSON_ZONE_GET_RESULT = {
+ "data": {
+ "id": 42,
+ "name": "example.com",
+ "email": "test@example.com",
+ "ttl": 10800,
+ "nameserver": "ns1.hosttech.ch",
+ "dnssec": False,
+ "records": HOSTTECH_JSON_DEFAULT_ENTRIES,
+ }
+}
+
+HOSTTECH_JSON_ZONE_RECORDS_GET_RESULT = {
+ "data": HOSTTECH_JSON_DEFAULT_ENTRIES,
+}
+
+
+original_exists = os.path.exists
+original_access = os.access
+
+
+def exists_mock(path, exists=True):
+ def exists(f):
+ if to_native(f) == path:
+ return exists
+ return original_exists(f)
+
+ return exists
+
+
+def access_mock(path, can_access=True):
+ def access(f, m, *args, **kwargs):
+ if to_native(f) == path:
+ return can_access
+ return original_access(f, m, *args, **kwargs)
+
+ return access
+
+
+def test_inventory_file_simple(mocker):
+ inventory_filename = "test.hosttech_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hosttech_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hosttech_dns_records
+ hosttech_token: foo
+ zone_name: example.com
+ filters:
+ type: A
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert im._inventory.hosts
+ assert 'example.com' in im._inventory.hosts
+ assert '*.example.com' in im._inventory.hosts
+ assert 'foo.example.com' not in im._inventory.hosts
+ assert 'bar.example.com' not in im._inventory.hosts
+ assert im._inventory.get_host('example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('*.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('example.com').get_vars()['ansible_host'] == '1.2.3.4'
+ assert im._inventory.get_host('*.example.com').get_vars()['ansible_host'] == '1.2.3.5'
+ assert len(im._inventory.groups['ungrouped'].hosts) == 2
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_collision(mocker):
+ inventory_filename = "test.hosttech_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hosttech_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hosttech_dns_records
+ hosttech_token: "{{ 'foo' }}"
+ zone_name: "{{ 'example' ~ '.com' }}"
+ filters:
+ type:
+ - A
+ - AAAA
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert im._inventory.hosts
+ assert 'example.com' in im._inventory.hosts
+ assert '*.example.com' in im._inventory.hosts
+ assert 'foo.example.com' in im._inventory.hosts
+ assert 'bar.example.com' not in im._inventory.hosts
+ assert im._inventory.get_host('example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('*.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('foo.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('example.com').get_vars()['ansible_host'] == '2001:1:2::3'
+ assert im._inventory.get_host('*.example.com').get_vars()['ansible_host'] == '1.2.3.5'
+ assert im._inventory.get_host('foo.example.com').get_vars()['ansible_host'] == '2001:1:2::4'
+ assert len(im._inventory.groups['ungrouped'].hosts) == 3
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_no_filter(mocker):
+ inventory_filename = "test.hosttech_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hosttech_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hosttech_dns_records
+ hosttech_token: foo
+ zone_id: 42
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert im._inventory.hosts
+ assert 'example.com' in im._inventory.hosts
+ assert '*.example.com' in im._inventory.hosts
+ assert 'foo.example.com' in im._inventory.hosts
+ assert 'bar.example.com' in im._inventory.hosts
+ assert im._inventory.get_host('example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('*.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('foo.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('bar.example.com') in im._inventory.groups['ungrouped'].hosts
+ assert im._inventory.get_host('example.com').get_vars()['ansible_host'] == '2001:1:2::3'
+ assert im._inventory.get_host('*.example.com').get_vars()['ansible_host'] == '1.2.3.5'
+ assert im._inventory.get_host('foo.example.com').get_vars()['ansible_host'] == '2001:1:2::4'
+ assert im._inventory.get_host('bar.example.com').get_vars()['ansible_host'] == 'example.org.'
+ assert len(im._inventory.groups['ungrouped'].hosts) == 4
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_invalid_zone_id(mocker):
+ inventory_filename = "test.hosttech_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hosttech_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hosttech_dns_records
+ hosttech_token: foo
+ zone_id: invalid
+ """)}
+
+ open_url = OpenUrlProxy([
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_missing_zone(mocker):
+ inventory_filename = "test.hosttech_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hosttech_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hosttech_dns_records
+ hosttech_token: foo
+ """)}
+
+ open_url = OpenUrlProxy([
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_zone_not_found(mocker):
+ inventory_filename = "test.hosttech_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hosttech_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hosttech_dns_records
+ hosttech_token: foo
+ zone_id: "{{ 11 + 12 }}"
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_unauthorized(mocker):
+ inventory_filename = "test.hosttech_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hosttech_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hosttech_dns_records
+ hosttech_token: foo
+ zone_id: 23
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_file_error(mocker):
+ inventory_filename = "test.hosttech_dns.yaml"
+ C.INVENTORY_ENABLED = ['community.dns.hosttech_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hosttech_dns_records
+ hosttech_token: foo
+ zone_id: 42
+ """)}
+
+ open_url = OpenUrlProxy([
+ OpenUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .result_json({}),
+ ])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_wrong_file(mocker):
+ inventory_filename = "test.hetznerdns.yml"
+ C.INVENTORY_ENABLED = ['community.dns.hosttech_dns_records']
+ inventory_file = {inventory_filename: textwrap.dedent("""\
+ ---
+ plugin: community.dns.hosttech_dns_records
+ hosttech_token: foo
+ """)}
+
+ open_url = OpenUrlProxy([])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename))
+ mocker.patch('os.access', access_mock(inventory_filename))
+ im = InventoryManager(loader=DictDataLoader(inventory_file), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
+
+
+def test_inventory_no_file(mocker):
+ inventory_filename = "test.hosttech_dns.yml"
+ C.INVENTORY_ENABLED = ['community.dns.hosttech_dns_records']
+
+ open_url = OpenUrlProxy([])
+ mocker.patch('ansible_collections.community.dns.plugins.module_utils.http.open_url', open_url)
+ mocker.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ mocker.patch('os.path.exists', exists_mock(inventory_filename, False))
+ mocker.patch('os.access', access_mock(inventory_filename, False))
+ im = InventoryManager(loader=DictDataLoader({}), sources=inventory_filename)
+
+ open_url.assert_is_done()
+
+ assert not im._inventory.hosts
+ assert len(im._inventory.groups['ungrouped'].hosts) == 0
+ assert len(im._inventory.groups['all'].hosts) == 0
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/conversion/test_converter.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/conversion/test_converter.py
new file mode 100644
index 000000000..e9911ad73
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/conversion/test_converter.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.dns.plugins.module_utils.conversion.base import (
+ DNSConversionError,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.conversion.converter import (
+ RecordConverter,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.record import (
+ DNSRecord,
+)
+
+from ..helper import (
+ CustomProviderInformation,
+ CustomProvideOptions,
+)
+
+
+def test_user_api():
+ converter = RecordConverter(
+ CustomProviderInformation(txt_record_handling='decoded', txt_character_encoding='decimal'),
+ CustomProvideOptions({'txt_transformation': 'api', 'txt_character_encoding': 'decimal'}))
+ assert converter.process_value_from_user('TXT', u'"xyz \\') == u'"xyz \\'
+ assert converter.process_values_from_user('TXT', [u'"xyz \\']) == [u'"xyz \\']
+ assert converter.process_value_to_user('TXT', u'"xyz \\') == u'"xyz \\'
+ assert converter.process_values_to_user('TXT', [u'"xyz \\']) == [u'"xyz \\']
+
+ record = DNSRecord()
+ record.type = 'TXT'
+
+ record.target = u'"xyz \\'
+ converter.process_from_user(record)
+ assert record.target == u'"xyz \\'
+
+ record.target = u'"xyz \\'
+ converter.process_multiple_from_user([record])
+ assert record.target == u'"xyz \\'
+
+ record.target = u'"xyz \\'
+ converter.process_to_user(record)
+ assert record.target == u'"xyz \\'
+
+ record.target = u'"xyz \\'
+ converter.process_multiple_to_user([record])
+ assert record.target == u'"xyz \\'
+
+
+def test_user_quoted():
+ converter = RecordConverter(
+ CustomProviderInformation(txt_record_handling='decoded', txt_character_encoding='decimal'),
+ CustomProvideOptions({'txt_transformation': 'quoted', 'txt_character_encoding': 'decimal'}))
+ assert converter.process_value_from_user('TXT', u'hëllo " w\\195\\182rld"') == u'hëllo wörld'
+ assert converter.process_values_from_user('TXT', [u'hëllo " w\\195\\182rld"']) == [u'hëllo wörld']
+ assert converter.process_value_to_user('TXT', u'hello wörld') == u'"hello w\\195\\182rld"'
+ assert converter.process_values_to_user('TXT', [u'hello wörld']) == [u'"hello w\\195\\182rld"']
+
+ record = DNSRecord()
+ record.type = 'TXT'
+
+ record.target = u'hëllo " w\\195\\182rld"'
+ converter.process_from_user(record)
+ assert record.target == u'hëllo wörld'
+
+ record.target = u'hëllo " w\\195\\182rld"'
+ converter.process_multiple_from_user([record])
+ assert record.target == u'hëllo wörld'
+
+ record.target = u'hello wörld'
+ converter.process_to_user(record)
+ assert record.target == u'"hello w\\195\\182rld"'
+
+ record.target = u'hello wörld'
+ converter.process_multiple_to_user([record])
+ assert record.target == u'"hello w\\195\\182rld"'
+
+ record.target = u'"a\\o'
+ with pytest.raises(DNSConversionError) as exc:
+ converter.process_from_user(record)
+ print(exc.value.error_message)
+ assert exc.value.error_message == (
+ u'While processing record from the user: A backslash must not be followed by "o" (index 4)'
+ )
+
+
+def test_user_unquoted():
+ converter = RecordConverter(
+ CustomProviderInformation(txt_record_handling='decoded', txt_character_encoding='decimal'),
+ CustomProvideOptions({'txt_transformation': 'unquoted', 'txt_character_encoding': 'decimal'}))
+ assert converter.process_value_from_user('TXT', u'hello "wörl\\d"') == u'hello "wörl\\d"'
+ assert converter.process_values_from_user('TXT', [u'hello "wörl\\d"']) == [u'hello "wörl\\d"']
+ assert converter.process_value_to_user('TXT', u'hello "wörl\\d"') == u'hello "wörl\\d"'
+ assert converter.process_values_to_user('TXT', [u'hello "wörl\\d"']) == [u'hello "wörl\\d"']
+
+ record = DNSRecord()
+ record.type = 'TXT'
+
+ record.target = u'hello "wörl\\d"'
+ converter.process_from_user(record)
+ assert record.target == u'hello "wörl\\d"'
+
+ record.target = u'hello "wörl\\d"'
+ converter.process_multiple_from_user([record])
+ assert record.target == u'hello "wörl\\d"'
+
+ record.target = u'hello "wörl\\d"'
+ converter.process_to_user(record)
+ assert record.target == u'hello "wörl\\d"'
+
+ record.target = u'hello "wörl\\d"'
+ converter.process_multiple_to_user([record])
+ assert record.target == u'hello "wörl\\d"'
+
+
+def test_api_decoded():
+ converter = RecordConverter(
+ CustomProviderInformation(txt_record_handling='decoded', txt_character_encoding='decimal'),
+ CustomProvideOptions({'txt_transformation': 'unquoted', 'txt_character_encoding': 'decimal'}))
+ record = DNSRecord()
+ record.type = 'TXT'
+
+ record.target = u'"xyz \\'
+ record_2 = converter.clone_from_api(record)
+ assert record is not record_2
+ assert record.target == u'"xyz \\'
+ assert record_2.target == u'"xyz \\'
+ converter.process_from_api(record)
+ assert record.target == u'"xyz \\'
+
+ record.target = u'"xyz \\'
+ records = converter.clone_multiple_from_api([record])
+ assert len(records) == 1
+ assert record is not records[0]
+ assert record.target == u'"xyz \\'
+ assert records[0].target == u'"xyz \\'
+ converter.process_multiple_from_api([record])
+ assert record.target == u'"xyz \\'
+
+ record.target = u'"xyz \\'
+ record_2 = converter.clone_to_api(record)
+ assert record is not record_2
+ assert record.target == u'"xyz \\'
+ assert record_2.target == u'"xyz \\'
+ converter.process_to_api(record)
+ assert record.target == u'"xyz \\'
+
+ record.target = u'"xyz \\'
+ records = converter.clone_multiple_to_api([record])
+ assert len(records) == 1
+ assert record is not records[0]
+ assert record.target == u'"xyz \\'
+ assert records[0].target == u'"xyz \\'
+ converter.process_multiple_to_api([record])
+ assert record.target == u'"xyz \\'
+
+
+def test_api_encoded():
+ converter = RecordConverter(
+ CustomProviderInformation(txt_record_handling='encoded', txt_character_encoding='decimal'),
+ CustomProvideOptions({'txt_transformation': 'unquoted', 'txt_character_encoding': 'decimal'}))
+ record = DNSRecord()
+ record.type = 'TXT'
+
+ record.target = u'xyz " " \\\\\\195\\182'
+ record_2 = converter.clone_from_api(record)
+ assert record is not record_2
+ assert record.target == u'xyz " " \\\\\\195\\182'
+ print(record_2.target)
+ assert record_2.target == u'xyz \\ö'
+ converter.process_from_api(record)
+ assert record.target == u'xyz \\ö'
+
+ record.target = u'xyz " " \\\\\\195\\182'
+ records = converter.clone_multiple_from_api([record])
+ assert len(records) == 1
+ assert record is not records[0]
+ assert record.target == u'xyz " " \\\\\\195\\182'
+ assert records[0].target == u'xyz \\ö'
+ converter.process_multiple_from_api([record])
+ assert record.target == u'xyz \\ö'
+
+ record.target = u'xyz \\ö'
+ record_2 = converter.clone_to_api(record)
+ assert record is not record_2
+ assert record.target == u'xyz \\ö'
+ assert record_2.target == u'"xyz \\\\\\195\\182"'
+ converter.process_to_api(record)
+ assert record.target == u'"xyz \\\\\\195\\182"'
+
+ record.target = u'xyz \\ö'
+ records = converter.clone_multiple_to_api([record])
+ assert len(records) == 1
+ assert record is not records[0]
+ assert record.target == u'xyz \\ö'
+ assert records[0].target == u'"xyz \\\\\\195\\182"'
+ converter.process_multiple_to_api([record])
+ assert record.target == u'"xyz \\\\\\195\\182"'
+
+ record.target = u'"a'
+ with pytest.raises(DNSConversionError) as exc:
+ converter.process_from_api(record)
+ print(exc.value.error_message)
+ assert exc.value.error_message == (
+ u'While processing record from API: Missing double quotation mark at the end of value'
+ )
+
+
+def test_api_encoded_no_octal():
+ converter = RecordConverter(
+ CustomProviderInformation(txt_record_handling='encoded-no-octal', txt_character_encoding='decimal'),
+ CustomProvideOptions({'txt_transformation': 'unquoted', 'txt_character_encoding': 'decimal'}))
+ record = DNSRecord()
+ record.type = 'TXT'
+
+ record.target = u'xyz " " \\\\\\195\\182'
+ record_2 = converter.clone_from_api(record)
+ assert record is not record_2
+ assert record.target == u'xyz " " \\\\\\195\\182'
+ print(record_2.target)
+ assert record_2.target == u'xyz \\ö'
+ converter.process_from_api(record)
+ assert record.target == u'xyz \\ö'
+
+ record.target = u'xyz " " \\\\\\195\\182'
+ records = converter.clone_multiple_from_api([record])
+ assert len(records) == 1
+ assert record is not records[0]
+ assert record.target == u'xyz " " \\\\\\195\\182'
+ assert records[0].target == u'xyz \\ö'
+ converter.process_multiple_from_api([record])
+ assert record.target == u'xyz \\ö'
+
+ record.target = u'xyz \\ö"'
+ record_2 = converter.clone_to_api(record)
+ assert record is not record_2
+ assert record.target == u'xyz \\ö"'
+ assert record_2.target == u'"xyz \\\\ö\\""'
+ converter.process_to_api(record)
+ assert record.target == u'"xyz \\\\ö\\""'
+
+ record.target = u'xyz \\ö"'
+ records = converter.clone_multiple_to_api([record])
+ assert len(records) == 1
+ assert record is not records[0]
+ assert record.target == u'xyz \\ö"'
+ assert records[0].target == u'"xyz \\\\ö\\""'
+ converter.process_multiple_to_api([record])
+ assert record.target == u'"xyz \\\\ö\\""'
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/conversion/test_txt.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/conversion/test_txt.py
new file mode 100644
index 000000000..e5984444b
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/conversion/test_txt.py
@@ -0,0 +1,277 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import warnings
+
+import pytest
+
+from ansible_collections.community.dns.plugins.module_utils.conversion.base import (
+ DNSConversionError,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.conversion.txt import (
+ _get_utf8_length,
+ decode_txt_value,
+ encode_txt_value,
+)
+
+
+TEST_DECODE = [
+ (r'', 'decimal', u''),
+ (r'"" "" ""', 'decimal', u''),
+ (r' "" "" ', 'decimal', u''),
+ (r'\032\033', 'decimal', u' !'),
+ (r'"\032" \033 ""', 'decimal', u' !'),
+ (r'\040\041', 'octal', u' !'),
+ (r'"\040" \041 ""', 'octal', u' !'),
+]
+
+
+@pytest.mark.parametrize("encoded, character_encoding, decoded", TEST_DECODE)
+def test_decode(encoded, character_encoding, decoded):
+ decoded_ = decode_txt_value(encoded, character_encoding=character_encoding)
+ print(repr(decoded_), repr(decoded))
+ assert decoded_ == decoded
+
+
+TEST_GET_UTF8_LENGTH = [
+ # See https://en.wikipedia.org/wiki/UTF-8#Examples
+ (0xC2, 2), # first byte of UTF-8 encoding of U+0024
+ (0xC3, 2), # first byte of UTF-8 encoding of ä
+ (0xE0, 3), # first byte of UTF-8 encoding of U+0939
+ (0xE2, 3), # first byte of UTF-8 encoding of U+20AC
+ (0xED, 3), # first byte of UTF-8 encoding of U+D55C
+ (0xF0, 4), # first byte of UTF-8 encoding of U+10348
+ (0x00, 1),
+ (0xFF, 1),
+]
+
+
+@pytest.mark.parametrize("letter_code, length", TEST_GET_UTF8_LENGTH)
+def test_get_utf8_length(letter_code, length):
+ length_ = _get_utf8_length(letter_code)
+ print(length_, length)
+ assert length_ == length
+
+
+TEST_ENCODE_DECODE = [
+ (u'', u'""', False, True, 'decimal'),
+ (u'', u'""', True, True, 'decimal'),
+ (u'Hi', u'Hi', False, True, 'decimal'),
+ (u'Hi', u'"Hi"', True, True, 'decimal'),
+ (u'"\\', u'\\\"\\\\', False, True, 'decimal'),
+ (u'"\\', u'"\\"\\\\"', True, True, 'decimal'),
+ (u'ä', u'ä', False, False, 'decimal'),
+ (u'ä', u'"ä"', True, False, 'decimal'),
+ (u'ä', u'\\195\\164', False, True, 'decimal'),
+ (u'ä', u'"\\195\\164"', True, True, 'decimal'),
+ (u'a b', u'"a b"', False, True, 'decimal'),
+ (u'a b', u'"a b"', True, True, 'decimal'),
+ (
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuv'
+ u'wxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefg hijklmnopqrstu'
+ u'vwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
+ False, True, 'decimal'
+ ),
+ (
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuv'
+ u'wxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
+ u'"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzA'
+ u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
+ u'3456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefg" "hijklmnopqr'
+ u'stuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"',
+ True, True, 'decimal'
+ ),
+ (
+ u'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzA'
+ u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
+ u'3456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstu'
+ u'vwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
+ u'"abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'
+ u'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01'
+ u'23456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdef" ghijklmnopqr'
+ u'stuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
+ False, True, 'decimal'
+ ),
+ (
+ u'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzA'
+ u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
+ u'3456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstu'
+ u'vwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
+ u'"abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'
+ u'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01'
+ u'23456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdef" "ghijklmnopq'
+ u'rstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"',
+ True, True, 'decimal'
+ ),
+ (
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefg',
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefg',
+ False, True, 'decimal'
+ ),
+ (
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefg',
+ u'"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzA'
+ u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
+ u'3456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefg"',
+ True, True, 'decimal'
+ ),
+ (
+ # Avoid splitting up an decimal sequence into multiple TXT strings
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789aä',
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789a\\195 \\164',
+ False, True, 'decimal'
+ ),
+ (
+ # Avoid splitting up a UTF-8 character into multiple TXT strings
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefä',
+ u'"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzA'
+ u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
+ u'3456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdef" "ä"',
+ True, False, 'decimal'
+ ),
+ (
+ # Avoid splitting up an octal sequence into multiple TXT strings
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789aä',
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789a\\303 \\244',
+ False, True, 'octal'
+ ),
+ (
+ # Avoid splitting up a UTF-8 character into multiple TXT strings
+ u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzAB'
+ u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
+ u'456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefä',
+ u'"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzA'
+ u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
+ u'3456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdef" "ä"',
+ True, False, 'octal'
+ ),
+]
+
+
+@pytest.mark.parametrize("decoded, encoded, always_quote, use_character_encoding, character_encoding", TEST_ENCODE_DECODE)
+def test_encode_decode(decoded, encoded, always_quote, use_character_encoding, character_encoding):
+ decoded_ = decode_txt_value(encoded, character_encoding=character_encoding)
+ print(repr(decoded_), repr(decoded))
+ assert decoded_ == decoded
+ encoded_ = encode_txt_value(decoded, always_quote=always_quote, use_character_encoding=use_character_encoding, character_encoding=character_encoding)
+ print(repr(encoded_), repr(encoded))
+ assert encoded_ == encoded
+
+
+TEST_DECODE_ERROR = [
+ (u'\\', 'decimal', 'Unexpected backslash at end of string'),
+ (u'\\a', 'decimal', 'A backslash must not be followed by "a" (index 2)'),
+ (u'\\0', 'decimal', 'The decimal sequence at the end requires 2 more digit(s)'),
+ (u'\\00', 'decimal', 'The decimal sequence at the end requires 1 more digit(s)'),
+ (u'\\0a', 'decimal', 'The decimal sequence at the end requires 1 more digit(s)'),
+ (u'\\0a0', 'decimal', 'The second letter of the decimal sequence at index 3 is not a decimal digit, but "a"'),
+ (u'\\00a', 'decimal', 'The third letter of the decimal sequence at index 4 is not a decimal digit, but "a"'),
+ (u'\\0', 'octal', 'The octal sequence at the end requires 2 more digit(s)'),
+ (u'\\00', 'octal', 'The octal sequence at the end requires 1 more digit(s)'),
+ (u'\\0a', 'octal', 'The octal sequence at the end requires 1 more digit(s)'),
+ (u'\\0a0', 'octal', 'The second letter of the octal sequence at index 3 is not a octal digit, but "a"'),
+ (u'\\00a', 'octal', 'The third letter of the octal sequence at index 4 is not a octal digit, but "a"'),
+ (u'a"b', 'decimal', 'Unexpected double quotation mark inside an unquoted block at position 2'),
+ (u'"', 'decimal', 'Missing double quotation mark at the end of value'),
+]
+
+
+@pytest.mark.parametrize("encoded, character_encoding, error", TEST_DECODE_ERROR)
+def test_decode_error(encoded, character_encoding, error):
+ with pytest.raises(DNSConversionError) as exc:
+ decode_txt_value(encoded, character_encoding=character_encoding)
+ print(exc.value.error_message)
+ assert exc.value.error_message == error
+
+
+def test_validation():
+ with pytest.raises(ValueError) as exc:
+ decode_txt_value('foo', character_encoding='foo')
+ print(exc.value.args)
+ assert exc.value.args == ('character_encoding must be set to "octal" or "decimal"', )
+
+ with pytest.raises(ValueError) as exc:
+ encode_txt_value('foo', character_encoding='foo')
+ print(exc.value.args)
+ assert exc.value.args == ('character_encoding must be set to "octal" or "decimal"', )
+
+
+def test_deprecation():
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter('always')
+ encode_txt_value('foo')
+
+ print(len(w), w)
+ assert len(w) >= 1
+ warning = w[0]
+ assert issubclass(warning.category, DeprecationWarning)
+ msg = (
+ 'The default value of the encode_txt_value parameter character_encoding is deprecated.'
+ ' Set explicitly to "octal" for the old behavior, or set to "decimal" for the new and correct behavior.'
+ )
+ print(str(warning.message))
+ assert msg == str(warning.message)
+
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter('always')
+ encode_txt_value('foo', use_octal=True, character_encoding='octal')
+
+ print(len(w), w)
+ assert len(w) >= 1
+ warning = w[0]
+ assert issubclass(warning.category, DeprecationWarning)
+ msg = 'The encode_txt_value parameter use_octal is deprecated. Use use_character_encoding instead.'
+ print(str(warning.message))
+ assert msg == str(warning.message)
+
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter('always')
+ decode_txt_value('foo')
+
+ print(len(w), w)
+ assert len(w) >= 1
+ warning = w[0]
+ assert issubclass(warning.category, DeprecationWarning)
+ msg = (
+ 'The default value of the decode_txt_value parameter character_encoding is deprecated.'
+ ' Set explicitly to "octal" for the old behavior, or set to "decimal" for the new and correct behavior.'
+ )
+ print(str(warning.message))
+ assert msg == str(warning.message)
+
+ with pytest.raises(ValueError) as exc:
+ encode_txt_value('foo', use_octal=True, use_character_encoding=True)
+ print(exc.value.args)
+ assert exc.value.args == ('Cannot use both use_character_encoding and use_octal. Use only use_character_encoding!', )
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/helper.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/helper.py
new file mode 100644
index 000000000..aa0f8d6e1
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/helper.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+from ansible_collections.community.dns.plugins.module_utils.provider import (
+ ProviderInformation,
+)
+
+
+class CustomProviderInformation(ProviderInformation):
+ def __init__(self, txt_record_handling='decoded', txt_character_encoding='decimal'):
+ super(CustomProviderInformation, self).__init__()
+ self._txt_record_handling = txt_record_handling
+ self._txt_character_encoding = txt_character_encoding
+
+ def get_supported_record_types(self):
+ return ['A']
+
+ def get_zone_id_type(self):
+ return 'str'
+
+ def get_record_id_type(self):
+ return 'str'
+
+ def get_record_default_ttl(self):
+ return 300
+
+ def txt_record_handling(self):
+ return self._txt_record_handling
+
+ def txt_character_encoding(self):
+ return self._txt_character_encoding
+
+
+class CustomProvideOptions(object):
+ def __init__(self, option_dict):
+ self._option_dict = option_dict
+
+ def get_option(self, name):
+ return self._option_dict.get(name)
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/hetzner/test_api.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/hetzner/test_api.py
new file mode 100644
index 000000000..c7d440857
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/hetzner/test_api.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock
+
+from ansible_collections.community.dns.plugins.module_utils.record import (
+ DNSRecord,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.zone_record_api import (
+ DNSAPIError,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.hetzner.api import (
+ HetznerAPI,
+)
+
+
+def test_list_pagination():
+ def get_1(url, query=None, must_have_content=True, expected=None):
+ assert url == 'https://example.com'
+ assert must_have_content == [200]
+ assert expected == [200]
+ assert query is not None
+ assert len(query) == 2
+ assert query['per_page'] == 1
+ assert query['page'] in [1, 2, 3]
+ if query['page'] < 3:
+ return {
+ 'data': [query['page']],
+ 'meta': {
+ 'pagination': {
+ 'page': query['page'],
+ 'per_page': 1,
+ 'last_page': 3,
+ 'total_entries': 2,
+ },
+ },
+ }, {'status': 200}
+ else:
+ return {
+ 'data': [],
+ 'meta': {
+ 'pagination': {
+ 'page': query['page'],
+ 'per_page': 1,
+ 'last_page': 3,
+ 'total_entries': 2,
+ },
+ },
+ }, {'status': 200}
+
+ def get_2(url, query=None, must_have_content=True, expected=None):
+ assert url == 'https://example.com'
+ assert must_have_content == [200]
+ assert expected == ([200, 404] if query['page'] == 1 else [200])
+ assert query is not None
+ assert len(query) == 3
+ assert query['foo'] == 'bar'
+ assert query['per_page'] == 2
+ assert query['page'] in [1, 2]
+ if query['page'] < 2:
+ return {
+ 'foobar': ['bar', 'baz'],
+ 'meta': {
+ 'pagination': {
+ 'page': query['page'],
+ 'per_page': 2,
+ 'last_page': 2,
+ 'total_entries': 3,
+ },
+ },
+ }, {'status': 200}
+ else:
+ return {
+ 'foobar': ['foo'],
+ 'meta': {
+ 'pagination': {
+ 'page': query['page'],
+ 'per_page': 2,
+ 'last_page': 2,
+ 'total_entries': 3,
+ },
+ },
+ }, {'status': 200}
+
+ def get_3(url, query=None, must_have_content=True, expected=None):
+ assert url == 'https://example.com'
+ assert must_have_content == [200]
+ assert expected == [200, 404]
+ assert query is not None
+ assert len(query) == 2
+ assert query['per_page'] == 100
+ assert query['page'] == 1
+ return None, {'status': 404}
+
+ api = HetznerAPI(MagicMock(), '123')
+
+ api._get = MagicMock(side_effect=get_1)
+ result = api._list_pagination('https://example.com', 'data', block_size=1, accept_404=False)
+ assert result == [1, 2]
+
+ api._get = MagicMock(side_effect=get_2)
+ result = api._list_pagination('https://example.com', 'foobar', query=dict(foo='bar'), block_size=2, accept_404=True)
+ assert result == ['bar', 'baz', 'foo']
+
+ api._get = MagicMock(side_effect=get_3)
+ result = api._list_pagination('https://example.com', 'baz', accept_404=True)
+ assert result is None
+
+
+def test_update_id_missing():
+ api = HetznerAPI(MagicMock(), '123')
+ with pytest.raises(DNSAPIError) as exc:
+ api.update_record(1, DNSRecord())
+ assert exc.value.args[0] == 'Need record ID to update record!'
+
+
+def test_update_id_delete():
+ api = HetznerAPI(MagicMock(), '123')
+ with pytest.raises(DNSAPIError) as exc:
+ api.delete_record(1, DNSRecord())
+ assert exc.value.args[0] == 'Need record ID to delete record!'
+
+
+def test_extract_error_message():
+ api = HetznerAPI(MagicMock(), '123')
+ assert api._extract_error_message(None) == ''
+ assert api._extract_error_message('foo') == ' with data: foo'
+ assert api._extract_error_message(dict()) == ' with data: {}'
+ assert api._extract_error_message(dict(message='')) == " with data: {'message': ''}"
+ assert api._extract_error_message(dict(message='foo')) == ' with message "foo"'
+ assert api._extract_error_message(dict(message='foo', error='')) == ' with message "foo"'
+ assert api._extract_error_message(dict(message='foo', error=dict())) == ' with message "foo"'
+ assert api._extract_error_message(dict(message='foo', error=dict(code=123))) == ' (error code 123) with message "foo"'
+ assert api._extract_error_message(dict(message='foo', error=dict(message='baz'))) == ' with error message "baz" with message "foo"'
+ assert api._extract_error_message(dict(message='foo', error=dict(message='baz', code=123))) == (
+ ' with error message "baz" (error code 123) with message "foo"'
+ )
+ assert api._extract_error_message(dict(error=dict(message='baz', code=123))) == ' with error message "baz" (error code 123)'
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/hosttech/test_api.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/hosttech/test_api.py
new file mode 100644
index 000000000..cd2ef3402
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/hosttech/test_api.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock
+
+from ansible_collections.community.dns.plugins.module_utils.zone_record_api import (
+ DNSAPIError,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.hosttech import api
+
+from ..helper import (
+ CustomProvideOptions,
+)
+
+
+def test_internal_error():
+ option_provider = CustomProvideOptions({})
+ with pytest.raises(DNSAPIError) as exc:
+ api.create_hosttech_api(option_provider, MagicMock())
+ assert exc.value.args[0] == 'One of hosttech_token or both hosttech_username and hosttech_password must be provided!'
+
+
+def test_wsdl_missing():
+ option_provider = CustomProvideOptions({
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'foo',
+ })
+ old_value = api.HAS_LXML_ETREE
+ try:
+ api.HAS_LXML_ETREE = False
+ with pytest.raises(DNSAPIError) as exc:
+ api.create_hosttech_api(option_provider, MagicMock())
+ assert exc.value.args[0] == 'Needs lxml Python module (pip install lxml)'
+ finally:
+ api.HAS_LXML_ETREE = old_value
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/hosttech/test_json_api.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/hosttech/test_json_api.py
new file mode 100644
index 000000000..b1c6dad20
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/hosttech/test_json_api.py
@@ -0,0 +1,381 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock
+
+from ansible_collections.community.dns.plugins.module_utils.record import (
+ DNSRecord,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.zone_record_api import (
+ DNSAPIError,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.hosttech.json_api import (
+ _create_record_from_json,
+ _record_to_json,
+ HostTechJSONAPI,
+)
+
+
+# The example JSONs for all record types are taken from https://api.ns1.hosttech.eu/api/documentation/
+
+def test_AAAA():
+ data = {
+ "id": 11,
+ "type": "AAAA",
+ "name": "www",
+ "ipv6": "2001:db8:1234::1",
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ record = _create_record_from_json(data)
+ assert record.id == 11
+ assert record.type == 'AAAA'
+ assert record.prefix == 'www'
+ assert record.target == '2001:db8:1234::1'
+ assert record.ttl == 3600
+ assert record.extra == {
+ 'comment': 'my first record',
+ }
+ assert _record_to_json(record, include_id=True) == data
+
+
+def test_A():
+ data = {
+ "id": 10,
+ "type": "A",
+ "name": "www",
+ "ipv4": "1.2.3.4",
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ record = _create_record_from_json(data)
+ assert record.id == 10
+ assert record.type == 'A'
+ assert record.prefix == 'www'
+ assert record.target == '1.2.3.4'
+ assert record.ttl == 3600
+ assert record.extra == {
+ 'comment': 'my first record',
+ }
+ assert _record_to_json(record, include_id=True) == data
+
+
+def test_CAA():
+ data = {
+ "id": 12,
+ "type": "CAA",
+ "name": "",
+ "flag": "0",
+ "tag": "issue",
+ "value": "letsencrypt.org",
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ record = _create_record_from_json(data)
+ assert record.id == 12
+ assert record.type == 'CAA'
+ assert record.prefix is None
+ assert record.target == '0 issue "letsencrypt.org"'
+ assert record.ttl == 3600
+ assert record.extra == {
+ 'comment': 'my first record',
+ }
+ assert _record_to_json(record, include_id=True) == data
+
+ # We also accept versions without quotes:
+ record.target = '0 issue letsencrypt.org'
+ assert _record_to_json(record, include_id=True) == data
+
+ record.target = '0\tissue "letsencrypt.org"'
+ with pytest.raises(DNSAPIError) as exc:
+ _record_to_json(record)
+ assert exc.value.args[0].startswith('Cannot split CAA record "0\tissue "letsencrypt.org"" into flag, tag and value: ')
+
+
+def test_CNAME():
+ data = {
+ "id": 13,
+ "type": "CNAME",
+ "name": "www",
+ "cname": "site.example.com",
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ record = _create_record_from_json(data)
+ assert record.id == 13
+ assert record.type == 'CNAME'
+ assert record.prefix == 'www'
+ assert record.target == 'site.example.com'
+ assert record.ttl == 3600
+ assert record.extra == {
+ 'comment': 'my first record',
+ }
+ assert _record_to_json(record, include_id=True) == data
+
+
+def test_MX():
+ data = {
+ "id": 14,
+ "type": "MX",
+ "ownername": "",
+ "name": "mail.example.com",
+ "pref": 10,
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ record = _create_record_from_json(data)
+ assert record.id == 14
+ assert record.type == 'MX'
+ assert record.prefix is None
+ assert record.target == '10 mail.example.com'
+ assert record.ttl == 3600
+ assert record.extra == {
+ 'comment': 'my first record',
+ }
+ assert _record_to_json(record, include_id=True) == data
+
+ record.target = 'mail.example.com'
+ with pytest.raises(DNSAPIError) as exc:
+ _record_to_json(record)
+ assert exc.value.args[0].startswith('Cannot split MX record "mail.example.com" into integer preference and name: ')
+
+ record.target = 'x mail.example.com'
+ with pytest.raises(DNSAPIError) as exc:
+ _record_to_json(record)
+ assert exc.value.args[0].startswith('Cannot split MX record "x mail.example.com" into integer preference and name: ')
+
+
+def test_NS():
+ # WARNING: as opposed to documented on https://api.ns1.hosttech.eu/api/documentation/,
+ # NS records use 'targetname' and not 'name'!
+ data = {
+ "id": 14,
+ "type": "NS",
+ "ownername": "sub",
+ "targetname": "ns1.example.com",
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ record = _create_record_from_json(data)
+ assert record.id == 14
+ assert record.type == 'NS'
+ assert record.prefix == 'sub'
+ assert record.target == 'ns1.example.com'
+ assert record.ttl == 3600
+ assert record.extra == {
+ 'comment': 'my first record',
+ }
+ assert _record_to_json(record, include_id=True) == data
+
+
+def test_PTR():
+ data = {
+ "id": 15,
+ "type": "PTR",
+ "origin": "4.3.2.1",
+ "name": "smtp.example.com",
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ record = _create_record_from_json(data)
+ assert record.id == 15
+ assert record.type == 'PTR'
+ assert record.prefix is None
+ assert record.target == '4.3.2.1 smtp.example.com'
+ assert record.ttl == 3600
+ assert record.extra == {
+ 'comment': 'my first record',
+ }
+ assert _record_to_json(record, include_id=True) == data
+
+ record.target = 'smtp.example.com'
+ with pytest.raises(DNSAPIError) as exc:
+ _record_to_json(record)
+ assert exc.value.args[0].startswith('Cannot split PTR record "smtp.example.com" into origin and name: ')
+
+
+def test_SRV():
+ data = {
+ "id": 16,
+ "type": "SRV",
+ "service": "_autodiscover._tcp",
+ "priority": 0,
+ "weight": 1,
+ "port": 443,
+ "target": "exchange.example.com",
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ record = _create_record_from_json(data)
+ assert record.id == 16
+ assert record.type == 'SRV'
+ assert record.prefix == '_autodiscover._tcp'
+ assert record.target == '0 1 443 exchange.example.com'
+ assert record.ttl == 3600
+ assert record.extra == {
+ 'comment': 'my first record',
+ }
+ assert _record_to_json(record, include_id=True) == data
+
+ record.target = '1 443 exchange.example.com'
+ with pytest.raises(DNSAPIError) as exc:
+ _record_to_json(record)
+ assert exc.value.args[0].startswith(
+ 'Cannot split SRV record "1 443 exchange.example.com" into integer priority, integer weight, integer port and target: ')
+
+ record.target = 'x 1 443 exchange.example.com'
+ with pytest.raises(DNSAPIError) as exc:
+ _record_to_json(record)
+ assert exc.value.args[0].startswith(
+ 'Cannot split SRV record "x 1 443 exchange.example.com" into integer priority, integer weight, integer port and target: ')
+
+ record.target = '0 x 443 exchange.example.com'
+ with pytest.raises(DNSAPIError) as exc:
+ _record_to_json(record)
+ assert exc.value.args[0].startswith(
+ 'Cannot split SRV record "0 x 443 exchange.example.com" into integer priority, integer weight, integer port and target: ')
+
+ record.target = '0 1 x exchange.example.com'
+ with pytest.raises(DNSAPIError) as exc:
+ _record_to_json(record)
+ assert exc.value.args[0].startswith(
+ 'Cannot split SRV record "0 1 x exchange.example.com" into integer priority, integer weight, integer port and target: ')
+
+
+def test_TXT():
+ data = {
+ "id": 17,
+ "type": "TXT",
+ "name": "",
+ "text": "v=spf1 ip4:1.2.3.4/32 -all",
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ record = _create_record_from_json(data)
+ assert record.id == 17
+ assert record.type == 'TXT'
+ assert record.prefix is None
+ assert record.target == 'v=spf1 ip4:1.2.3.4/32 -all'
+ assert record.ttl == 3600
+ assert record.extra == {
+ 'comment': 'my first record',
+ }
+ assert _record_to_json(record, include_id=True) == data
+
+
+def test_TLSA():
+ data = {
+ "id": 17,
+ "type": "TLSA",
+ "name": "",
+ "text": "0 0 1 d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971",
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ record = _create_record_from_json(data)
+ assert record.id == 17
+ assert record.type == 'TLSA'
+ assert record.prefix is None
+ assert record.target == '0 0 1 d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971'
+ assert record.ttl == 3600
+ assert record.extra == {
+ 'comment': 'my first record',
+ }
+ assert _record_to_json(record, include_id=True) == data
+
+
+def test_unknown_records():
+ data = {
+ "id": 17,
+ "type": "unknown",
+ "name": "",
+ "ttl": 3600,
+ "comment": "my first record",
+ }
+ with pytest.raises(DNSAPIError) as exc:
+ _create_record_from_json(data)
+ assert exc.value.args[0] == 'Cannot parse unknown record type: unknown'
+
+ record = DNSRecord()
+ record.type = 'unknown'
+ with pytest.raises(DNSAPIError) as exc:
+ _record_to_json(record)
+ assert exc.value.args[0] == 'Cannot serialize unknown record type: unknown'
+
+
+def test_list_pagination():
+ def get_1(url, query=None, must_have_content=True, expected=None):
+ assert url == 'https://example.com'
+ assert must_have_content is True
+ assert expected == [200]
+ assert query is not None
+ assert len(query) == 2
+ assert query['limit'] == 1
+ assert query['offset'] in [0, 1, 2]
+ if query['offset'] < 2:
+ return {'data': [query['offset']]}, {}
+ else:
+ return {'data': []}, {}
+
+ def get_2(url, query=None, must_have_content=True, expected=None):
+ assert url == 'https://example.com'
+ assert must_have_content is True
+ assert expected == [200]
+ assert query is not None
+ assert len(query) == 3
+ assert query['foo'] == 'bar'
+ assert query['limit'] == 2
+ assert query['offset'] in [0, 2]
+ if query['offset'] < 2:
+ return {'data': ['bar', 'baz']}, {}
+ else:
+ return {'data': ['foo']}, {}
+
+ api = HostTechJSONAPI(MagicMock(), '123')
+
+ api._get = MagicMock(side_effect=get_1)
+ result = api._list_pagination('https://example.com', block_size=1)
+ assert result == [0, 1]
+
+ api._get = MagicMock(side_effect=get_2)
+ result = api._list_pagination('https://example.com', query=dict(foo='bar'), block_size=2)
+ assert result == ['bar', 'baz', 'foo']
+
+
+def test_update_id_missing():
+ api = HostTechJSONAPI(MagicMock(), '123')
+ with pytest.raises(DNSAPIError) as exc:
+ api.update_record(1, DNSRecord())
+ assert exc.value.args[0] == 'Need record ID to update record!'
+
+
+def test_update_id_delete():
+ api = HostTechJSONAPI(MagicMock(), '123')
+ with pytest.raises(DNSAPIError) as exc:
+ api.delete_record(1, DNSRecord())
+ assert exc.value.args[0] == 'Need record ID to delete record!'
+
+
+def test_extract_error_message():
+ api = HostTechJSONAPI(MagicMock(), '123')
+ assert api._extract_error_message(None) == ''
+ assert api._extract_error_message('foo') == ' with data: foo'
+ assert api._extract_error_message(dict()) == ' with data: {}'
+ assert api._extract_error_message(dict(message='')) == " with data: {'message': ''}"
+ assert api._extract_error_message(dict(message='foo')) == ' with message "foo"'
+ assert api._extract_error_message(dict(message='foo', errors='')) == ' with message "foo"'
+ assert api._extract_error_message(dict(message='foo', errors=dict())) == ' with message "foo"'
+ assert api._extract_error_message(dict(message='foo', errors=dict(bar='baz'))) == ' with message "foo" (field "bar": baz)'
+ assert api._extract_error_message(dict(errors=dict(bar=['baz', 'bam'], arf='fra'))) == ' (field "arf": fra) (field "bar": baz; bam)'
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/module/test__utils.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/module/test__utils.py
new file mode 100644
index 000000000..972b433e1
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/module/test__utils.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.dns.plugins.module_utils.zone_record_api import (
+ DNSAPIError,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.module._utils import (
+ normalize_dns_name,
+ get_prefix,
+)
+
+from ..helper import (
+ CustomProviderInformation,
+)
+
+
+def test_normalize_dns_name():
+ assert normalize_dns_name('ExAMPLE.CoM.') == 'example.com'
+ assert normalize_dns_name('EXAMpLE.CoM') == 'example.com'
+ assert normalize_dns_name('Example.com') == 'example.com'
+ assert normalize_dns_name('.') == ''
+ assert normalize_dns_name(None) is None
+
+
+def test_get_prefix():
+ provider_information = CustomProviderInformation()
+ provider_information.get_supported_record_types()
+ assert get_prefix(
+ normalized_zone='example.com', normalized_record='example.com', provider_information=provider_information) == ('example.com', None)
+ assert get_prefix(
+ normalized_zone='example.com', normalized_record='www.example.com', provider_information=provider_information) == ('www.example.com', 'www')
+ assert get_prefix(normalized_zone='example.com', provider_information=provider_information) == ('example.com', None)
+ assert get_prefix(normalized_zone='example.com', prefix='', provider_information=provider_information) == ('example.com', None)
+ assert get_prefix(normalized_zone='example.com', prefix='.', provider_information=provider_information) == ('example.com', None)
+ assert get_prefix(normalized_zone='example.com', prefix='www', provider_information=provider_information) == ('www.example.com', 'www')
+ assert get_prefix(normalized_zone='example.com', prefix='www.', provider_information=provider_information) == ('www.example.com', 'www')
+ assert get_prefix(normalized_zone='example.com', prefix='wWw.', provider_information=provider_information) == ('www.example.com', 'www')
+ with pytest.raises(DNSAPIError):
+ get_prefix(normalized_zone='example.com', normalized_record='example.org', provider_information=provider_information)
+ with pytest.raises(DNSAPIError):
+ get_prefix(normalized_zone='example.com', normalized_record='wwwexample.com', provider_information=provider_information)
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/resolver_helper.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/resolver_helper.py
new file mode 100644
index 000000000..e410d2076
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/resolver_helper.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock
+
+
+def mock_resolver(default_nameservers, nameserver_resolve_sequence):
+ def create_resolver(configure=True):
+ resolver = MagicMock()
+ resolver.nameservers = default_nameservers if configure else []
+
+ def mock_resolver_resolve(target, rdtype=None, lifetime=None):
+ resolver_index = tuple(sorted(resolver.nameservers))
+ assert resolver_index in nameserver_resolve_sequence, 'No resolver sequence for {0}'.format(resolver_index)
+ resolve_sequence = nameserver_resolve_sequence[resolver_index]
+ assert len(resolve_sequence) > 0, 'Resolver sequence for {0} is empty'.format(resolver_index)
+ resolve_data = resolve_sequence[0]
+ del resolve_sequence[0]
+
+ assert target == resolve_data['target'], 'target: {0!r} vs {1!r}'.format(target, resolve_data['target'])
+ assert rdtype == resolve_data.get('rdtype'), 'rdtype: {0!r} vs {1!r}'.format(rdtype, resolve_data.get('rdtype'))
+ assert lifetime == resolve_data['lifetime'], 'lifetime: {0!r} vs {1!r}'.format(lifetime, resolve_data['lifetime'])
+
+ if 'raise' in resolve_data:
+ raise resolve_data['raise']
+
+ return resolve_data['result']
+
+ resolver.resolve = MagicMock(side_effect=mock_resolver_resolve)
+ return resolver
+
+ return create_resolver
+
+
+def mock_query_udp(call_sequence):
+ def udp(query, nameserver, **kwargs):
+ assert len(call_sequence) > 0, 'UDP query call sequence is empty'
+ call = call_sequence[0]
+ del call_sequence[0]
+
+ assert query.question[0].name == call['query_target'], 'query_target: {0!r} vs {1!r}'.format(query.question[0].name, call['query_target'])
+ assert query.question[0].rdtype == call['query_type'], 'query_type: {0!r} vs {1!r}'.format(query.question[0].rdtype, call['query_type'])
+ assert nameserver == call['nameserver'], 'nameserver: {0!r} vs {1!r}'.format(nameserver, call['nameserver'])
+ assert kwargs == call['kwargs'], 'kwargs: {0!r} vs {1!r}'.format(kwargs, call['kwargs'])
+
+ if 'raise' in call:
+ raise call['raise']
+
+ return call['result']
+
+ return udp
+
+
+def create_mock_answer(rrset=None):
+ answer = MagicMock()
+ answer.rrset = rrset
+ return answer
+
+
+def create_mock_response(rcode, authority=None, answer=None):
+ response = MagicMock()
+ response.rcode = MagicMock(return_value=rcode)
+ response.authority = authority or []
+ response.answer = answer or []
+ return response
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_argspec.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_argspec.py
new file mode 100644
index 000000000..296e1647d
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_argspec.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+from ansible_collections.community.dns.plugins.module_utils.argspec import (
+ ArgumentSpec,
+)
+
+
+def test_argspec():
+ empty = ArgumentSpec()
+ non_empty = ArgumentSpec(
+ argument_spec=dict(test=dict(type='str'), foo=dict()),
+ required_together=[('test', 'foo')],
+ required_if=[('test', 'bar', ['foo'])],
+ required_one_of=[('test', 'foo')],
+ mutually_exclusive=[('test', 'foo')]
+ )
+ empty.merge(non_empty)
+ assert empty.to_kwargs() == non_empty.to_kwargs()
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_json_api_helper.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_json_api_helper.py
new file mode 100644
index 000000000..0d5d7bd06
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_json_api_helper.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock
+
+from ansible_collections.community.dns.plugins.module_utils.zone_record_api import (
+ DNSAPIError,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.json_api_helper import (
+ _get_header_value,
+ JSONAPIHelper,
+)
+
+
+def test_get_header_value():
+ assert _get_header_value({'Return-Type': 1}, 'return-type') == 1
+ assert _get_header_value({'Return-Type': 1}, 'Return-Type') == 1
+ assert _get_header_value({'return-Type': 1}, 'Return-type') == 1
+ assert _get_header_value({'return_type': 1}, 'Return-type') is None
+
+
+def test_extract_error_message():
+ api = JSONAPIHelper(MagicMock(), '123', 'https://example.com')
+ assert api._extract_error_message(None) == ''
+ assert api._extract_error_message('foo') == ' with data: foo'
+ assert api._extract_error_message(dict()) == ' with data: {}'
+ assert api._extract_error_message(dict(message='')) == " with data: {'message': ''}"
+ assert api._extract_error_message(dict(message='foo')) == " with data: {'message': 'foo'}"
+
+
+def test_validate():
+ module = MagicMock()
+ api = JSONAPIHelper(module, '123', 'https://example.com')
+ with pytest.raises(DNSAPIError) as exc:
+ api._validate()
+ assert exc.value.args[0] == 'Internal error: info needs to be provided'
+
+
+def test_process_json_result():
+ http_helper = MagicMock()
+ api = JSONAPIHelper(http_helper, '123', 'https://example.com')
+ with pytest.raises(DNSAPIError) as exc:
+ api._process_json_result(content=None, info=dict(status=401, url='https://example.com'))
+ assert exc.value.args[0] == 'Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+ with pytest.raises(DNSAPIError) as exc:
+ api._process_json_result(content='{"message": ""}'.encode('utf-8'), info=dict(status=401, url='https://example.com'))
+ assert exc.value.args[0] == 'Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+ with pytest.raises(DNSAPIError) as exc:
+ api._process_json_result(content='{"message": "foo"}'.encode('utf-8'), info=dict(status=401, url='https://example.com'))
+ assert exc.value.args[0] == 'Unauthorized: the authentication parameters are incorrect (HTTP status 401): foo'
+ with pytest.raises(DNSAPIError) as exc:
+ api._process_json_result(content=None, info=dict(status=403, url='https://example.com'))
+ assert exc.value.args[0] == 'Forbidden: you do not have access to this resource (HTTP status 403)'
+ with pytest.raises(DNSAPIError) as exc:
+ api._process_json_result(content='{"message": ""}'.encode('utf-8'), info=dict(status=403, url='https://example.com'))
+ assert exc.value.args[0] == 'Forbidden: you do not have access to this resource (HTTP status 403)'
+ with pytest.raises(DNSAPIError) as exc:
+ api._process_json_result(content='{"message": "foo"}'.encode('utf-8'), info=dict(status=403, url='https://example.com'))
+ assert exc.value.args[0] == 'Forbidden: you do not have access to this resource (HTTP status 403): foo'
+
+ info = dict(status=200, url='https://example.com')
+ info['content-TYPE'] = 'application/json'
+ with pytest.raises(DNSAPIError) as exc:
+ api._process_json_result(content='not JSON'.encode('utf-8'), info=info)
+ assert exc.value.args[0] == 'GET https://example.com did not yield JSON data, but HTTP status code 200 with data: not JSON'
+
+ info = dict(status=200, url='https://example.com')
+ info['Content-type'] = 'application/json'
+ r, i = api._process_json_result(content='not JSON'.encode('utf-8'), info=info, must_have_content=False)
+ assert r is None
+ info = dict(status=200, url='https://example.com')
+ info['Content-type'] = 'application/json'
+ assert i == info
+
+ info = dict(status=404, url='https://example.com')
+ info['content-type'] = 'application/json'
+ with pytest.raises(DNSAPIError) as exc:
+ api._process_json_result(content='{}'.encode('utf-8'), info=info)
+ assert exc.value.args[0] == 'Expected successful HTTP status for GET https://example.com, but got HTTP status 404 (Not found) with data: {}'
+
+ info = dict(status=404, url='https://example.com')
+ info['content-type'] = 'application/json'
+ with pytest.raises(DNSAPIError) as exc:
+ api._process_json_result(content='{}'.encode('utf-8'), info=info, expected=[200, 201])
+ assert exc.value.args[0] == 'Expected HTTP status 200, 201 for GET https://example.com, but got HTTP status 404 (Not found) with data: {}'
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_names.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_names.py
new file mode 100644
index 000000000..74dd83cc1
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_names.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.dns.plugins.module_utils.names import (
+ join_labels,
+ is_ascii_label,
+ normalize_label,
+ split_into_labels,
+ InvalidDomainName,
+)
+
+
+TEST_IS_ASCII_LABEL = [
+ ('asdf', True),
+ ('', True),
+ ('ä', False),
+ ('☹', False),
+ ('_dmarc', True),
+]
+
+
+@pytest.mark.parametrize("domain, result", TEST_IS_ASCII_LABEL)
+def test_is_ascii_label(domain, result):
+ assert is_ascii_label(domain) == result
+
+
+TEST_LABEL_SPLIT = [
+ ('', [], ''),
+ ('.', [], '.'),
+ ('com', ['com'], ''),
+ ('com.', ['com'], '.'),
+ ('foo.bar', ['bar', 'foo'], ''),
+ ('foo.bar.', ['bar', 'foo'], '.'),
+ ('*.bar.', ['bar', '*'], '.'),
+ (u'☺.A', ['A', u'☺'], ''),
+]
+
+
+@pytest.mark.parametrize("domain, labels, tail", TEST_LABEL_SPLIT)
+def test_split_into_labels(domain, labels, tail):
+ _labels, _tail = split_into_labels(domain)
+ assert _labels == labels
+ assert _tail == tail
+ assert join_labels(_labels, _tail) == domain
+
+
+TEST_LABEL_SPLIT_ERRORS = [
+ '.bar.',
+ '..bar',
+ '-bar',
+ 'bar-',
+]
+
+
+@pytest.mark.parametrize("domain", TEST_LABEL_SPLIT_ERRORS)
+def test_split_into_labels_errors(domain):
+ with pytest.raises(InvalidDomainName):
+ split_into_labels(domain)
+
+
+TEST_LABEL_JOIN = [
+ ([], '', ''),
+ ([], '.', '.'),
+ (['a', 'b', 'c'], '', 'c.b.a'),
+ (['a', 'b', 'c'], '.', 'c.b.a.'),
+]
+
+
+@pytest.mark.parametrize("labels, tail, result", TEST_LABEL_JOIN)
+def test_join_labels(labels, tail, result):
+ domain = join_labels(labels, tail)
+ assert domain == result
+ _labels, _tail = split_into_labels(domain)
+ assert _labels == labels
+ assert _tail == tail
+
+
+TEST_LABEL_NORMALIZE = [
+ ('', ''),
+ ('*', '*'),
+ ('foo', 'foo'),
+ ('Foo', 'foo'),
+ ('_dmarc', '_dmarc'),
+ (u'hëllö', 'xn--hll-jma1d'),
+ (u'食狮', 'xn--85x722f'),
+ (u'☺', 'xn--74h'),
+ (u'😉', 'xn--n28h'),
+]
+
+
+@pytest.mark.parametrize("label, normalized_label", TEST_LABEL_NORMALIZE)
+def test_normalize_label(label, normalized_label):
+ print(normalize_label(label))
+ assert normalize_label(label) == normalized_label
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_provider.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_provider.py
new file mode 100644
index 000000000..79f688f60
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_provider.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.dns.plugins.module_utils.provider import (
+ ensure_type,
+)
+
+
+CHECK_TYPE_DATA = [
+ ('asdf', 'str', 'asdf'),
+ (1, 'str', '1'),
+ ([], 'list', []),
+ ({}, 'dict', {}),
+ ('yes', 'bool', True),
+ ('5', 'int', 5),
+ ('5.10', 'float', 5.10),
+ ('foobar', 'raw', 'foobar'),
+]
+
+
+@pytest.mark.parametrize("input, type_name, output", CHECK_TYPE_DATA)
+def test_is_ascii_label(input, type_name, output):
+ assert ensure_type(input, type_name) == output
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_record.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_record.py
new file mode 100644
index 000000000..d3a8ff385
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_record.py
@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+from ansible_collections.community.dns.plugins.module_utils.record import (
+ format_ttl,
+ DNSRecord,
+ format_records_for_output,
+)
+
+
+def test_format_ttl():
+ assert format_ttl(None) == 'default'
+ assert format_ttl(1) == '1s'
+ assert format_ttl(59) == '59s'
+ assert format_ttl(60) == '1m'
+ assert format_ttl(61) == '1m 1s'
+ assert format_ttl(3539) == '58m 59s'
+ assert format_ttl(3540) == '59m'
+ assert format_ttl(3541) == '59m 1s'
+ assert format_ttl(3599) == '59m 59s'
+ assert format_ttl(3600) == '1h'
+ assert format_ttl(3601) == '1h 1s'
+ assert format_ttl(3661) == '1h 1m 1s'
+
+
+def test_format_records_for_output():
+ A1 = DNSRecord()
+ A1.type = 'A'
+ A1.ttl = 300
+ A1.target = '1.2.3.4'
+ A1.extra['foo'] = 'bar'
+ A2 = DNSRecord()
+ A2.type = 'A'
+ A2.ttl = 300
+ A2.target = '1.2.3.5'
+ A3 = DNSRecord()
+ A3.type = 'A'
+ A3.ttl = 3600
+ A3.target = '1.2.3.6'
+ AAAA = DNSRecord()
+ AAAA.type = 'AAAA'
+ AAAA.ttl = 600
+ AAAA.target = '::1'
+ AAAA2 = DNSRecord()
+ AAAA2.type = 'AAAA'
+ AAAA2.ttl = None
+ AAAA2.target = '::2'
+ assert format_records_for_output([], 'foo', '') == {
+ 'record': 'foo',
+ 'prefix': '',
+ 'type': None,
+ 'ttl': None,
+ 'value': [],
+ }
+ assert format_records_for_output([A1, A2], 'foo', 'bar') == {
+ 'record': 'foo',
+ 'prefix': 'bar',
+ 'type': 'A',
+ 'ttl': 300,
+ 'value': ['1.2.3.4', '1.2.3.5'],
+ }
+ assert format_records_for_output([A3, A1], 'foo', None) == {
+ 'record': 'foo',
+ 'prefix': '',
+ 'type': 'A',
+ 'ttl': 300,
+ 'ttls': [300, 3600],
+ 'value': ['1.2.3.6', '1.2.3.4'],
+ }
+ assert format_records_for_output([A3], 'foo', None) == {
+ 'record': 'foo',
+ 'prefix': '',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': ['1.2.3.6'],
+ }
+ assert format_records_for_output([AAAA], 'foo', None) == {
+ 'record': 'foo',
+ 'prefix': '',
+ 'type': 'AAAA',
+ 'ttl': 600,
+ 'value': ['::1'],
+ }
+ assert format_records_for_output([A3, AAAA], 'foo', None) == {
+ 'record': 'foo',
+ 'prefix': '',
+ 'type': 'A',
+ 'ttl': 600,
+ 'ttls': [600, 3600],
+ 'value': ['1.2.3.6', '::1'],
+ }
+ assert format_records_for_output([AAAA, A3], 'foo', None) == {
+ 'record': 'foo',
+ 'prefix': '',
+ 'type': 'A',
+ 'ttl': 600,
+ 'ttls': [600, 3600],
+ 'value': ['::1', '1.2.3.6'],
+ }
+ assert format_records_for_output([AAAA2], 'foo', None) == {
+ 'record': 'foo',
+ 'prefix': '',
+ 'type': 'AAAA',
+ 'ttl': None,
+ 'value': ['::2'],
+ }
+ print(format_records_for_output([AAAA2, AAAA], 'foo', None))
+ assert format_records_for_output([AAAA2, AAAA], 'foo', None) == {
+ 'record': 'foo',
+ 'prefix': '',
+ 'type': 'AAAA',
+ 'ttl': None,
+ 'ttls': [None, 600],
+ 'value': ['::2', '::1'],
+ }
+
+
+def test_record_str_repr():
+ A1 = DNSRecord()
+ A1.prefix = None
+ A1.type = 'A'
+ A1.ttl = 300
+ A1.target = '1.2.3.4'
+ assert str(A1) == 'DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m)'
+ assert repr(A1) == 'DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m)'
+ A2 = DNSRecord()
+ A2.id = 23
+ A2.prefix = 'bar'
+ A2.type = 'A'
+ A2.ttl = 1
+ A2.target = ''
+ A2.extra['foo'] = 'bar'
+ assert str(A2) == 'DNSRecord(id: 23, type: A, prefix: "bar", target: "", ttl: 1s, extra: {\'foo\': \'bar\'})'
+ assert repr(A2) == 'DNSRecord(id: 23, type: A, prefix: "bar", target: "", ttl: 1s, extra: {\'foo\': \'bar\'})'
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_resolver.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_resolver.py
new file mode 100644
index 000000000..1f51601e0
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_resolver.py
@@ -0,0 +1,905 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock, patch
+
+from ansible_collections.community.dns.plugins.module_utils import resolver
+
+from ansible_collections.community.dns.plugins.module_utils.resolver import (
+ ResolveDirectlyFromNameServers,
+ ResolverError,
+ assert_requirements_present,
+)
+
+from .resolver_helper import (
+ mock_resolver,
+ mock_query_udp,
+ create_mock_answer,
+ create_mock_response,
+)
+
+# We need dnspython
+dns = pytest.importorskip('dns')
+
+
+def test_assert_requirements_present():
+ class ModuleFailException(Exception):
+ pass
+
+ def fail_json(**kwargs):
+ raise ModuleFailException(kwargs)
+
+ module = MagicMock()
+ module.fail_json = MagicMock(side_effect=fail_json)
+
+ orig_importerror = resolver.DNSPYTHON_IMPORTERROR
+ resolver.DNSPYTHON_IMPORTERROR = None
+ assert_requirements_present(module)
+
+ resolver.DNSPYTHON_IMPORTERROR = 'asdf'
+ with pytest.raises(ModuleFailException) as exc:
+ assert_requirements_present(module)
+
+ assert 'dnspython' in exc.value.args[0]['msg']
+ assert 'asdf' == exc.value.args[0]['exception']
+
+ resolver.DNSPYTHON_IMPORTERROR = orig_importerror
+
+
+def test_lookup_ns_names():
+ resolver = mock_resolver(['1.1.1.1'], {})
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.org.'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com.'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '3.3.3.3',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 60,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'foo.bar.'),
+ )], authority=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com.'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ resolver = ResolveDirectlyFromNameServers(always_ask_default_resolver=False)
+ # Use default resolver
+ ns, cname = resolver._lookup_ns_names(dns.name.from_unicode(u'example.com'))
+ assert ns == ['ns.example.com.', 'ns.example.org.']
+ assert cname is None
+ # Provide nameserver IPs
+ ns, cname = resolver._lookup_ns_names(dns.name.from_unicode(u'example.com'), nameserver_ips=['3.3.3.3', '1.1.1.1'])
+ assert ns == ['ns.example.com.']
+ assert cname == dns.name.from_unicode(u'foo.bar.')
+ # Provide empty nameserver list
+ with pytest.raises(ResolverError) as exc:
+ resolver._lookup_ns_names(dns.name.from_unicode(u'example.com'), nameservers=[])
+ assert exc.value.args[0] == 'Have neither nameservers nor nameserver IPs'
+
+
+def test_resolver():
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.AAAA, '1:2::3'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.AAAA, '2:3::4'),
+ )),
+ },
+ {
+ 'target': 'ns.example.org',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '4.4.4.4'),
+ )),
+ },
+ {
+ 'target': 'ns.example.org',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ {
+ 'target': 'ns.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '2.2.2.2'),
+ )),
+ },
+ {
+ 'target': 'ns.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('1:2::3', '2:3::4', '3.3.3.3'): [
+ {
+ 'target': dns.name.from_unicode(u'example.org'),
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '1.2.3.4'),
+ )),
+ },
+ ],
+ ('4.4.4.4', ): [
+ {
+ 'target': dns.name.from_unicode(u'example.org'),
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '1.2.3.5'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'www.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ ), dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'example.org')
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'org'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.org'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.org'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.org'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ resolver = ResolveDirectlyFromNameServers()
+ assert resolver.resolve_nameservers('example.com', resolve_addresses=True) == ['1:2::3', '2:3::4', '3.3.3.3']
+ # www.example.com is a CNAME for example.org
+ rrset_dict = resolver.resolve('www.example.com')
+ assert sorted(rrset_dict.keys()) == ['ns.example.com', 'ns.example.org']
+ rrset = rrset_dict['ns.example.com']
+ assert len(rrset) == 1
+ assert rrset.name == dns.name.from_unicode(u'example.org', origin=None)
+ assert rrset.rdtype == dns.rdatatype.A
+ assert rrset[0].to_text() == u'1.2.3.4'
+ rrset = rrset_dict['ns.example.org']
+ assert len(rrset) == 1
+ assert rrset.name == dns.name.from_unicode(u'example.org', origin=None)
+ assert rrset.rdtype == dns.rdatatype.A
+ assert rrset[0].to_text() == u'1.2.3.5'
+ # The following results should be cached:
+ assert resolver.resolve_nameservers('com', resolve_addresses=True) == ['2.2.2.2']
+ assert resolver.resolve_nameservers('org') == ['ns.org']
+ assert resolver.resolve_nameservers('example.com') == ['ns.example.com']
+ assert resolver.resolve_nameservers('example.org') == ['ns.example.com', 'ns.example.org']
+
+
+def test_timeout_handling():
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'raise': dns.exception.Timeout(timeout=10),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.exception.Timeout(timeout=10),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ {
+ 'target': 'ns.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'raise': dns.exception.Timeout(timeout=10),
+ },
+ {
+ 'target': 'ns.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'raise': dns.exception.Timeout(timeout=10),
+ },
+ {
+ 'target': 'ns.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '2.2.2.2'),
+ )),
+ },
+ {
+ 'target': 'ns.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'raise': dns.exception.Timeout(timeout=10),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, authority=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ resolver = ResolveDirectlyFromNameServers()
+ assert resolver.resolve_nameservers('example.com', resolve_addresses=True) == ['3.3.3.3']
+ # The following results should be cached:
+ assert resolver.resolve_nameservers('com') == ['ns.com']
+ assert resolver.resolve_nameservers('com', resolve_addresses=True) == ['2.2.2.2']
+ assert resolver.resolve_nameservers('example.com') == ['ns.example.com']
+ assert resolver.resolve_nameservers('example.com', resolve_addresses=True) == ['3.3.3.3']
+
+
+def test_timeout_failure():
+ resolver = mock_resolver(['1.1.1.1'], {})
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'raise': dns.exception.Timeout(timeout=1),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'raise': dns.exception.Timeout(timeout=2),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'raise': dns.exception.Timeout(timeout=3),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'raise': dns.exception.Timeout(timeout=4),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with pytest.raises(dns.exception.Timeout) as exc:
+ resolver = ResolveDirectlyFromNameServers()
+ resolver.resolve_nameservers('example.com')
+ assert exc.value.kwargs['timeout'] == 4
+
+
+def test_error_nxdomain():
+ resolver = mock_resolver(['1.1.1.1'], {})
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NXDOMAIN),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with pytest.raises(dns.resolver.NXDOMAIN) as exc:
+ resolver = ResolveDirectlyFromNameServers()
+ resolver.resolve_nameservers('example.com')
+ assert exc.value.kwargs['qnames'] == [dns.name.from_unicode(u'com')]
+
+
+def test_error_servfail():
+ resolver = mock_resolver(['1.1.1.1'], {})
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.SERVFAIL),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with pytest.raises(ResolverError) as exc:
+ resolver = ResolveDirectlyFromNameServers()
+ resolver.resolve_nameservers('example.com')
+ assert exc.value.args[0] == 'Error SERVFAIL while querying 1.1.1.1 with query get NS for "com."'
+
+
+def test_no_response():
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '5.5.5.5'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ {
+ 'target': 'ns2.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '4.4.4.4'),
+ )),
+ },
+ {
+ 'target': 'ns2.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('3.3.3.3', '5.5.5.5'): [
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'lifetime': 10,
+ 'result': create_mock_answer(),
+ },
+ ],
+ ('4.4.4.4', ): [
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns2.example.com'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ resolver = ResolveDirectlyFromNameServers()
+ rrset_dict = resolver.resolve('example.com')
+ assert sorted(rrset_dict.keys()) == ['ns.example.com', 'ns2.example.com']
+ assert rrset_dict['ns.example.com'] is None
+ assert rrset_dict['ns2.example.com'] is None
+ # Verify nameserver IPs
+ assert resolver.resolve_nameservers('example.com') == ['ns.example.com', 'ns2.example.com']
+ assert resolver.resolve_nameservers('example.com', resolve_addresses=True) == ['3.3.3.3', '4.4.4.4', '5.5.5.5']
+
+
+def test_cname_loop():
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.com',
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '2.2.2.2'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.org',
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '2.2.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.org',
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '4.4.4.4'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'www.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ ), dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'example.org')
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'org'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.org'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.org'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.org'),
+ ), dns.rrset.from_rdata(
+ 'example.org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'www.example.com')
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ resolver = ResolveDirectlyFromNameServers()
+ with pytest.raises(ResolverError) as exc:
+ resolver.resolve('www.example.com')
+ assert exc.value.args[0] == 'Found CNAME loop starting at www.example.com'
+
+
+def test_resolver_non_default():
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '2.2.2.2'),
+ )),
+ },
+ {
+ 'target': 'ns.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ {
+ 'target': 'ns.org',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '2.2.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.org',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ {
+ 'target': 'ns.example.org',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '4.4.4.4'),
+ )),
+ },
+ {
+ 'target': 'ns.example.org',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('3.3.3.3', ): [
+ {
+ 'target': dns.name.from_unicode(u'example.org'),
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '1.2.3.4'),
+ )),
+ },
+ ],
+ ('4.4.4.4', ): [
+ {
+ 'target': dns.name.from_unicode(u'example.org'),
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '1.2.3.4'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '2.2.2.2',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'www.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '3.3.3.3',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ ), dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'example.org')
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'org'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.org'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.org'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '2.2.3.3',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.org'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ resolver = ResolveDirectlyFromNameServers(always_ask_default_resolver=False)
+ assert resolver.resolve_nameservers('example.com') == ['ns.example.com']
+ # www.example.com is a CNAME for example.org
+ rrset_dict = resolver.resolve('www.example.com')
+ assert sorted(rrset_dict.keys()) == ['ns.example.com', 'ns.example.org']
+ rrset = rrset_dict['ns.example.com']
+ assert len(rrset) == 1
+ assert rrset.name == dns.name.from_unicode(u'example.org', origin=None)
+ assert rrset.rdtype == dns.rdatatype.A
+ assert rrset[0].to_text() == u'1.2.3.4'
+ rrset = rrset_dict['ns.example.org']
+ assert len(rrset) == 1
+ assert rrset.name == dns.name.from_unicode(u'example.org', origin=None)
+ assert rrset.rdtype == dns.rdatatype.A
+ assert rrset[0].to_text() == u'1.2.3.4'
+ # The following results should be cached:
+ assert resolver.resolve_nameservers('com') == ['ns.com']
+ print(resolver.resolve_nameservers('example.com', resolve_addresses=True))
+ assert resolver.resolve_nameservers('example.com', resolve_addresses=True) == ['3.3.3.3']
+ print(resolver.resolve_nameservers('example.org', resolve_addresses=True))
+ assert resolver.resolve_nameservers('example.org', resolve_addresses=True) == ['3.3.3.3', '4.4.4.4']
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_wsdl.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_wsdl.py
new file mode 100644
index 000000000..14dd66428
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_wsdl.py
@@ -0,0 +1,283 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017-2021 Felix Fontein
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+import sys
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock
+
+lxmletree = pytest.importorskip("lxml.etree")
+
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.dns.plugins.module_utils.wsdl import (
+ Parser,
+ Composer,
+)
+
+
+def test_composer_generation():
+ composer = Composer(MagicMock(), api='https://example.com/api')
+ composer.add_simple_command(
+ 'test',
+ int_value=42,
+ str_value='bar',
+ list_value=[1, 2, 3],
+ dict_value={
+ 'hello': 'world',
+ 'list': [2, 3, 5, 7],
+ }
+ )
+ command = to_native(lxmletree.tostring(composer._root, pretty_print=True)).splitlines()
+
+ print(command)
+
+ expected_lines = [
+ ' <SOAP-ENV:Header/>',
+ ' <SOAP-ENV:Body>',
+ ' <ns0:test xmlns:ns0="https://example.com/api">',
+ ' <int_value xsi:type="xsd:int">42</int_value>',
+ ' <str_value xsi:type="xsd:string">bar</str_value>',
+ ' <list_value xsi:type="SOAP-ENC:Array">',
+ ' <item xsi:type="xsd:int">1</item>',
+ ' <item xsi:type="xsd:int">2</item>',
+ ' <item xsi:type="xsd:int">3</item>',
+ ' </list_value>',
+ ' <dict_value xmlns:ns0="http://xml.apache.org/xml-soap" xsi:type="ns0:Map">',
+ ' <item>',
+ ' <key xsi:type="xsd:string">hello</key>',
+ ' <value xsi:type="xsd:string">world</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">list</key>',
+ ' <value xsi:type="SOAP-ENC:Array">',
+ ' <item xsi:type="xsd:int">2</item>',
+ ' <item xsi:type="xsd:int">3</item>',
+ ' <item xsi:type="xsd:int">5</item>',
+ ' <item xsi:type="xsd:int">7</item>',
+ ' </value>',
+ ' </item>',
+ ' </dict_value>',
+ ' </ns0:test>',
+ ' </SOAP-ENV:Body>',
+ '</SOAP-ENV:Envelope>',
+ ]
+
+ if sys.version_info < (3, 7):
+ assert sorted(command[1:]) == sorted(expected_lines)
+ else:
+ assert command[1:] == expected_lines
+
+ for part in [
+ '<SOAP-ENV:Envelope',
+ ' xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"',
+ ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"',
+ ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
+ ' xmlns:ns2="auth"',
+ ' xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"',
+ ' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"',
+ ]:
+ assert part in command[0]
+
+
+def test_parsing():
+ input = '\n'.join([
+ '<?xml version="1.0" encoding="UTF-8"?>',
+ ''.join([
+ '<SOAP-ENV:Envelope',
+ ' xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"',
+ ' xmlns:ns1="https://example.com/api"',
+ ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"',
+ ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
+ ' xmlns:ns2="http://xml.apache.org/xml-soap"',
+ ' xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"',
+ ' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"',
+ '>',
+ ]),
+ ' <SOAP-ENV:Header>',
+ ' <ns1:authenticateResponse>',
+ ' <return xsi:type="xsd:boolean">true</return>',
+ ' </ns1:authenticateResponse>',
+ ' </SOAP-ENV:Header>',
+ ' <SOAP-ENV:Body>',
+ ' <ns1:getZoneResponse>',
+ ' <return xsi:type="ns2:Map">',
+ ' <item>',
+ ' <key xsi:type="xsd:string">id</key>',
+ ' <value xsi:type="xsd:int">1</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">user</key>',
+ ' <value xsi:type="xsd:int">2</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">name</key>',
+ ' <value xsi:type="xsd:string">example.com</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">email</key>',
+ ' <value xsi:type="xsd:string">info@example.com</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">ttl</key>',
+ ' <value xsi:type="xsd:int">10800</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">nameserver</key>',
+ ' <value xsi:type="xsd:string">ns1.hostserv.eu</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">serial</key>',
+ ' <value xsi:type="xsd:string">1234567890</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">serialLastUpdate</key>',
+ ' <value xsi:type="xsd:int">0</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">refresh</key>',
+ ' <value xsi:type="xsd:int">7200</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">retry</key>',
+ ' <value xsi:type="xsd:int">120</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">expire</key>',
+ ' <value xsi:type="xsd:int">1234567</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">template</key>',
+ ' <value xsi:nil="true"/>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">ns3</key>',
+ ' <value xsi:type="xsd:int">1</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">records</key>',
+ ' <value SOAP-ENC:arrayType="ns2:Map[2]" xsi:type="SOAP-ENC:Array">',
+ ' <item xsi:type="ns2:Map">',
+ ' <item>',
+ ' <key xsi:type="xsd:string">id</key>',
+ ' <value xsi:type="xsd:int">3</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">zone</key>',
+ ' <value xsi:type="xsd:int">4</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">type</key>',
+ ' <value xsi:type="xsd:string">A</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">prefix</key>',
+ ' <value xsi:type="xsd:string"></value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">target</key>',
+ ' <value xsi:type="xsd:string">1.2.3.4</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">ttl</key>',
+ ' <value xsi:type="xsd:int">3600</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">comment</key>',
+ ' <value xsi:nil="true"/>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">priority</key>',
+ ' <value xsi:nil="true"/>',
+ ' </item>',
+ ' </item>',
+ ' <item xsi:type="ns2:Map">',
+ ' <item>',
+ ' <key xsi:type="xsd:string">id</key>',
+ ' <value xsi:type="xsd:int">5</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">zone</key>',
+ ' <value xsi:type="xsd:int">4</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">type</key>',
+ ' <value xsi:type="xsd:string">A</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">prefix</key>',
+ ' <value xsi:type="xsd:string">*</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">target</key>',
+ ' <value xsi:type="xsd:string">1.2.3.5</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">ttl</key>',
+ ' <value xsi:type="xsd:int">3600</value>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">comment</key>',
+ ' <value xsi:nil="true"/>',
+ ' </item>',
+ ' <item>',
+ ' <key xsi:type="xsd:string">priority</key>',
+ ' <value xsi:nil="true"/>',
+ ' </item>',
+ ' </item>',
+ ' </value>',
+ ' </item>',
+ ' </return>',
+ ' </ns1:getZoneResponse>',
+ ' </SOAP-ENV:Body>',
+ '</SOAP-ENV:Envelope>',
+ ]).encode('utf-8')
+
+ parser = Parser('https://example.com/api', lxmletree.fromstring(input))
+ assert parser.get_header('authenticateResponse') is True
+ assert len(parser._header) == 1
+ assert parser.get_result('getZoneResponse') == {
+ 'id': 1,
+ 'user': 2,
+ 'name': 'example.com',
+ 'email': 'info@example.com',
+ 'ttl': 10800,
+ 'nameserver': 'ns1.hostserv.eu',
+ 'serial': '1234567890',
+ 'serialLastUpdate': 0,
+ 'refresh': 7200,
+ 'retry': 120,
+ 'expire': 1234567,
+ 'template': None,
+ 'ns3': 1,
+ 'records': [
+ {
+ 'id': 3,
+ 'zone': 4,
+ 'type': 'A',
+ 'prefix': None,
+ 'target': '1.2.3.4',
+ 'ttl': 3600,
+ 'comment': None,
+ 'priority': None,
+ },
+ {
+ 'id': 5,
+ 'zone': 4,
+ 'type': 'A',
+ 'prefix': '*',
+ 'target': '1.2.3.5',
+ 'ttl': 3600,
+ 'comment': None,
+ 'priority': None,
+ },
+ ],
+ }
+ assert len(parser._body) == 1
diff --git a/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_zone.py b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_zone.py
new file mode 100644
index 000000000..df16adf2a
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/module_utils/test_zone.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+from ansible_collections.community.dns.plugins.module_utils.record import (
+ DNSRecord,
+)
+
+from ansible_collections.community.dns.plugins.module_utils.zone import (
+ DNSZone,
+ DNSZoneWithRecords,
+)
+
+
+def test_zone_str_repr():
+ Z1 = DNSZone('foo')
+ assert str(Z1) == 'DNSZone(name: foo, info: {})'
+ assert repr(Z1) == 'DNSZone(name: foo, info: {})'
+ Z2 = DNSZone('foo')
+ Z2.id = 42
+ Z2.info['foo'] = 'bar'
+ assert str(Z2) == "DNSZone(id: 42, name: foo, info: {'foo': 'bar'})"
+ assert repr(Z2) == "DNSZone(id: 42, name: foo, info: {'foo': 'bar'})"
+
+
+def test_zone_with_records_str_repr():
+ Z1 = DNSZone('foo')
+ Z2 = DNSZone('foo')
+ Z2.id = 42
+ A1 = DNSRecord()
+ A1.prefix = None
+ A1.type = 'A'
+ A1.ttl = 300
+ A1.target = '1.2.3.4'
+ A2 = DNSRecord()
+ A2.id = 23
+ A2.prefix = 'bar'
+ A2.type = 'A'
+ A2.ttl = 1
+ A2.target = ''
+ A2.extra['foo'] = 23
+ ZZ1 = DNSZoneWithRecords(Z1, [A1])
+ ZZ2 = DNSZoneWithRecords(Z2, [A1, A2])
+ assert str(ZZ1) == '(DNSZone(name: foo, info: {}), [DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m)])'
+ assert repr(ZZ1) == 'DNSZoneWithRecords(DNSZone(name: foo, info: {}), [DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m)])'
+ assert str(ZZ2) == (
+ '(DNSZone(id: 42, name: foo, info: {}), [DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m),'
+ ' DNSRecord(id: 23, type: A, prefix: "bar", target: "", ttl: 1s, extra: {\'foo\': 23})])'
+ )
+ assert repr(ZZ2) == (
+ 'DNSZoneWithRecords(DNSZone(id: 42, name: foo, info: {}), [DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m),'
+ ' DNSRecord(id: 23, type: A, prefix: "bar", target: "", ttl: 1s, extra: {\'foo\': 23})])'
+ )
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/hetzner.py b/ansible_collections/community/dns/tests/unit/plugins/modules/hetzner.py
new file mode 100644
index 000000000..e28edf736
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/hetzner.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+HETZNER_DEFAULT_ZONE = {
+ 'id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ 'legacy_dns_host': 'string',
+ 'legacy_ns': ['foo', 'bar'],
+ 'name': 'example.com',
+ 'ns': ['string'],
+ 'owner': 'Example',
+ 'paused': True,
+ 'permission': 'string',
+ 'project': 'string',
+ 'registrar': 'string',
+ 'status': 'verified',
+ 'ttl': 10800,
+ 'verified': '2021-07-09T11:18:37Z',
+ 'records_count': 0,
+ 'is_secondary_dns': True,
+ 'txt_verification': {
+ 'name': 'string',
+ 'token': 'string',
+ },
+}
+
+HETZNER_JSON_DEFAULT_ENTRIES = [
+ {
+ 'id': '125',
+ 'type': 'A',
+ 'name': '@',
+ 'value': '1.2.3.4',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '126',
+ 'type': 'A',
+ 'name': '*',
+ 'value': '1.2.3.5',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '127',
+ 'type': 'AAAA',
+ 'name': '@',
+ 'value': '2001:1:2::3',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '128',
+ 'type': 'AAAA',
+ 'name': '*',
+ 'value': '2001:1:2::4',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '129',
+ 'type': 'MX',
+ 'name': '@',
+ 'value': '10 example.com',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '130',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'helium.ns.hetzner.de.',
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '131',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'hydrogen.ns.hetzner.com.',
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '132',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'oxygen.ns.hetzner.com.',
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '200',
+ 'type': 'SOA',
+ 'name': '@',
+ 'value': 'hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600',
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ {
+ 'id': '201',
+ 'type': 'TXT',
+ 'name': 'foo',
+ 'value': u'bär " \\"with quotes\\"" " " "(use \\\\ to escape)"',
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+]
+
+HETZNER_JSON_ZONE_LIST_RESULT = {
+ 'zones': [
+ HETZNER_DEFAULT_ZONE,
+ ],
+}
+
+HETZNER_JSON_ZONE_GET_RESULT = {
+ 'zone': HETZNER_DEFAULT_ZONE,
+}
+
+HETZNER_JSON_ZONE_RECORDS_GET_RESULT = {
+ 'records': HETZNER_JSON_DEFAULT_ENTRIES,
+}
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/hosttech.py b/ansible_collections/community/dns/tests/unit/plugins/modules/hosttech.py
new file mode 100644
index 000000000..696cf7d01
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/hosttech.py
@@ -0,0 +1,463 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+try:
+ import lxml.etree
+except ImportError:
+ # should be handled in module importing this one
+ pass
+
+
+HOSTTECH_WSDL_DEFAULT_ENTRIES = [
+ (125, 42, 'A', '', '1.2.3.4', 3600, None, None),
+ (126, 42, 'A', '*', '1.2.3.5', 3600, None, None),
+ (127, 42, 'AAAA', '', '2001:1:2::3', 3600, None, None),
+ (128, 42, 'AAAA', '*', '2001:1:2::4', 3600, None, None),
+ (129, 42, 'MX', '', 'example.com', 3600, None, '10'),
+ (130, 42, 'NS', '', 'ns3.hostserv.eu', 10800, None, None),
+ (131, 42, 'NS', '', 'ns2.hostserv.eu', 10800, None, None),
+ (132, 42, 'NS', '', 'ns1.hostserv.eu', 10800, None, None),
+]
+
+HOSTTECH_JSON_DEFAULT_ENTRIES = [
+ # (125, 42, 'A', '', '1.2.3.4', 3600, None, None),
+ {
+ 'id': 125,
+ 'type': 'A',
+ 'name': '',
+ 'ipv4': '1.2.3.4',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ # (126, 42, 'A', '*', '1.2.3.5', 3600, None, None),
+ {
+ 'id': 126,
+ 'type': 'A',
+ 'name': '*',
+ 'ipv4': '1.2.3.5',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ # (127, 42, 'AAAA', '', '2001:1:2::3', 3600, None, None),
+ {
+ 'id': 127,
+ 'type': 'AAAA',
+ 'name': '',
+ 'ipv6': '2001:1:2::3',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ # (128, 42, 'AAAA', '*', '2001:1:2::4', 3600, None, None),
+ {
+ 'id': 128,
+ 'type': 'AAAA',
+ 'name': '*',
+ 'ipv6': '2001:1:2::4',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ # (129, 42, 'MX', '', 'example.com', 3600, None, '10'),
+ {
+ 'id': 129,
+ 'type': 'MX',
+ 'ownername': '',
+ 'name': 'example.com',
+ 'pref': 10,
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ # (130, 42, 'NS', '', 'ns3.hostserv.eu', 10800, None, None),
+ {
+ 'id': 130,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'ns3.hostserv.eu',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ # (131, 42, 'NS', '', 'ns2.hostserv.eu', 10800, None, None),
+ {
+ 'id': 131,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'ns2.hostserv.eu',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ # (132, 42, 'NS', '', 'ns1.hostserv.eu', 10800, None, None),
+ {
+ 'id': 132,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'ns1.hostserv.eu',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+]
+
+
+def validate_wsdl_call(conditions):
+ def predicate(content):
+ assert content.startswith(b"<?xml version='1.0' encoding='utf-8'?>\n")
+
+ root = lxml.etree.fromstring(content)
+ header = None
+ body = None
+
+ for header_ in root.iter(lxml.etree.QName('http://schemas.xmlsoap.org/soap/envelope/', 'Header').text):
+ header = header_
+ for body_ in root.iter(lxml.etree.QName('http://schemas.xmlsoap.org/soap/envelope/', 'Body').text):
+ body = body_
+
+ for condition in conditions:
+ if not condition(content, header, body):
+ return False
+ return True
+
+ return predicate
+
+
+def get_wsdl_value(root, name):
+ for auth in root.iter(name):
+ return auth
+ raise Exception('Cannot find child "{0}" in node {1}: {2}'.format(name, root, lxml.etree.tostring(root)))
+
+
+def expect_wsdl_authentication(username, password):
+ def predicate(content, header, body):
+ auth = get_wsdl_value(header, lxml.etree.QName('auth', 'authenticate').text)
+ assert get_wsdl_value(auth, 'UserName').text == username
+ assert get_wsdl_value(auth, 'Password').text == password
+ return True
+
+ return predicate
+
+
+def check_wsdl_nil(node):
+ nil_flag = node.get(lxml.etree.QName('http://www.w3.org/2001/XMLSchema-instance', 'nil'))
+ if nil_flag != 'true':
+ print(nil_flag)
+ assert nil_flag == 'true'
+
+
+def check_wsdl_value(node, value, type=None):
+ if type is not None:
+ type_text = node.get(lxml.etree.QName('http://www.w3.org/2001/XMLSchema-instance', 'type'))
+ assert type_text is not None, 'Cannot find type in {0}: {1}'.format(node, lxml.etree.tostring(node))
+ i = type_text.find(':')
+ if i < 0:
+ ns = None
+ else:
+ ns = node.nsmap.get(type_text[:i])
+ type_text = type_text[i + 1:]
+ if ns != type[0] or type_text != type[1]:
+ print(ns, type[0], type_text, type[1])
+ assert ns == type[0] and type_text == type[1]
+ if node.text != value:
+ print(node.text, value)
+ assert node.text == value
+
+
+def find_xml_map_entry(map_root, key_name, allow_non_existing=False):
+ for map_entry in map_root.iter('item'):
+ key = get_wsdl_value(map_entry, 'key')
+ value = get_wsdl_value(map_entry, 'value')
+ if key.text == key_name:
+ check_wsdl_value(key, key_name, type=('http://www.w3.org/2001/XMLSchema', 'string'))
+ return value
+ if allow_non_existing:
+ return None
+ raise Exception('Cannot find map entry with key "{0}" in node {1}: {2}'.format(key_name, map_root, lxml.etree.tostring(map_root)))
+
+
+def expect_wsdl_value(path, value, type=None):
+ def predicate(content, header, body):
+ node = body
+ for entry in path:
+ node = get_wsdl_value(node, entry)
+ check_wsdl_value(node, value, type=type)
+ return True
+
+ return predicate
+
+
+def add_wsdl_answer_start_lines(lines):
+ lines.extend([
+ '<?xml version="1.0" encoding="UTF-8"?>\n',
+ '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'
+ ' xmlns:ns1="https://ns1.hosttech.eu/public/api"'
+ ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
+ ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
+ ' xmlns:ns2="http://xml.apache.org/xml-soap"'
+ ' xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"'
+ ' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">',
+ '<SOAP-ENV:Header>',
+ '<ns1:authenticateResponse>',
+ '<return xsi:type="xsd:boolean">true</return>',
+ '</ns1:authenticateResponse>',
+ '</SOAP-ENV:Header>',
+ '<SOAP-ENV:Body>',
+ ])
+
+
+def add_wsdl_answer_end_lines(lines):
+ lines.extend([
+ '</SOAP-ENV:Body>',
+ '</SOAP-ENV:Envelope>'
+ ])
+
+
+def add_wsdl_dns_record_lines(lines, entry, tag_name):
+ lines.extend([
+ '<{tag_name} xsi:type="ns2:Map">'.format(tag_name=tag_name),
+ '<item><key xsi:type="xsd:string">id</key><value xsi:type="xsd:int">{value}</value></item>'.format(value=entry[0]),
+ '<item><key xsi:type="xsd:string">zone</key><value xsi:type="xsd:int">{value}</value></item>'.format(value=entry[1]),
+ '<item><key xsi:type="xsd:string">type</key><value xsi:type="xsd:string">{value}</value></item>'.format(value=entry[2]),
+ '<item><key xsi:type="xsd:string">prefix</key><value xsi:type="xsd:string">{value}</value></item>'.format(value=entry[3]),
+ '<item><key xsi:type="xsd:string">target</key><value xsi:type="xsd:string">{value}</value></item>'.format(value=entry[4]),
+ '<item><key xsi:type="xsd:string">ttl</key><value xsi:type="xsd:int">{value}</value></item>'.format(value=entry[5]),
+ ])
+ if entry[6] is None:
+ lines.append('<item><key xsi:type="xsd:string">comment</key><value xsi:nil="true"/></item>')
+ else:
+ lines.append('<item><key xsi:type="xsd:string">comment</key><value xsi:type="xsd:string">{value}</value></item>'.format(value=entry[6]))
+ if entry[7] is None:
+ lines.append('<item><key xsi:type="xsd:string">priority</key><value xsi:nil="true"/></item>')
+ else:
+ lines.append('<item><key xsi:type="xsd:string">priority</key><value xsi:type="xsd:int">{value}</value></item>'.format(value=entry[7]))
+ lines.append('</{tag_name}>'.format(tag_name=tag_name))
+
+
+def create_wsdl_zones_answer(zone_id, zone_name, entries):
+ lines = []
+ add_wsdl_answer_start_lines(lines)
+ lines.extend([
+ '<ns1:getZoneResponse>',
+ '<return xsi:type="ns2:Map">',
+ '<item><key xsi:type="xsd:string">id</key><value xsi:type="xsd:int">{zone_id}</value></item>'.format(zone_id=zone_id),
+ '<item><key xsi:type="xsd:string">user</key><value xsi:type="xsd:int">23</value></item>',
+ '<item><key xsi:type="xsd:string">name</key><value xsi:type="xsd:string">{zone_name}</value></item>'.format(zone_name=zone_name),
+ '<item><key xsi:type="xsd:string">email</key><value xsi:type="xsd:string">dns@hosttech.eu</value></item>',
+ '<item><key xsi:type="xsd:string">ttl</key><value xsi:type="xsd:int">10800</value></item>',
+ '<item><key xsi:type="xsd:string">nameserver</key><value xsi:type="xsd:string">ns1.hostserv.eu</value></item>',
+ '<item><key xsi:type="xsd:string">serial</key><value xsi:type="xsd:string">12345</value></item>',
+ '<item><key xsi:type="xsd:string">serialLastUpdate</key><value xsi:type="xsd:int">0</value></item>',
+ '<item><key xsi:type="xsd:string">refresh</key><value xsi:type="xsd:int">7200</value></item>',
+ '<item><key xsi:type="xsd:string">retry</key><value xsi:type="xsd:int">120</value></item>',
+ '<item><key xsi:type="xsd:string">expire</key><value xsi:type="xsd:int">1234567</value></item>',
+ '<item><key xsi:type="xsd:string">template</key><value xsi:nil="true"/></item>',
+ '<item><key xsi:type="xsd:string">ns3</key><value xsi:type="xsd:int">1</value></item>',
+ ])
+ lines.append(
+ '<item><key xsi:type="xsd:string">records</key><value SOAP-ENC:arrayType="ns2:Map[{count}]" xsi:type="SOAP-ENC:Array">'.format(
+ count=len(entries)))
+ for entry in entries:
+ add_wsdl_dns_record_lines(lines, entry, 'item')
+ lines.extend([
+ '</value>',
+ '</item>',
+ '</return>',
+ '</ns1:getZoneResponse>',
+ ])
+ add_wsdl_answer_end_lines(lines)
+ return ''.join(lines)
+
+
+def create_wsdl_zone_not_found_answer():
+ lines = [
+ '<?xml version="1.0" encoding="UTF-8"?>\n',
+ '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'
+ ' xmlns:ns1="https://ns1.hosttech.eu/public/api"'
+ ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
+ ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
+ ' xmlns:ns2="http://xml.apache.org/xml-soap"'
+ ' xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"'
+ ' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">',
+ '<SOAP-ENV:Header>',
+ '<ns1:authenticateResponse>',
+ '<return xsi:type="xsd:boolean">true</return>',
+ '</ns1:authenticateResponse>',
+ '</SOAP-ENV:Header>',
+ '<SOAP-ENV:Fault>',
+ '<faultstring>zone not found</faultstring>'
+ '</SOAP-ENV:Fault>',
+ '</SOAP-ENV:Envelope>'
+ ]
+ return ''.join(lines)
+
+
+def check_wsdl_record(record_data, entry):
+ check_wsdl_value(find_xml_map_entry(record_data, 'type'), entry[2], type=('http://www.w3.org/2001/XMLSchema', 'string'))
+ prefix = find_xml_map_entry(record_data, 'prefix')
+ if entry[3]:
+ check_wsdl_value(prefix, entry[3], type=('http://www.w3.org/2001/XMLSchema', 'string'))
+ elif prefix is not None:
+ check_wsdl_nil(prefix)
+ check_wsdl_value(find_xml_map_entry(record_data, 'target'), entry[4], type=('http://www.w3.org/2001/XMLSchema', 'string'))
+ check_wsdl_value(find_xml_map_entry(record_data, 'ttl'), str(entry[5]), type=('http://www.w3.org/2001/XMLSchema', 'int'))
+ if entry[6] is None:
+ comment = find_xml_map_entry(record_data, 'comment', allow_non_existing=True)
+ if comment is not None:
+ check_wsdl_nil(comment)
+ else:
+ check_wsdl_value(find_xml_map_entry(record_data, 'comment'), entry[6], type=('http://www.w3.org/2001/XMLSchema', 'string'))
+ if entry[7] is None:
+ check_wsdl_nil(find_xml_map_entry(record_data, 'priority'))
+ else:
+ check_wsdl_value(find_xml_map_entry(record_data, 'priority'), entry[7], type=('http://www.w3.org/2001/XMLSchema', 'string'))
+
+
+def validate_wsdl_add_request(zone, entry):
+ def predicate(content, header, body):
+ fn_data = get_wsdl_value(body, lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'addRecord').text)
+ check_wsdl_value(get_wsdl_value(fn_data, 'search'), zone, type=('http://www.w3.org/2001/XMLSchema', 'string'))
+ check_wsdl_record(get_wsdl_value(fn_data, 'recorddata'), entry)
+ return True
+
+ return predicate
+
+
+def validate_wsdl_update_request(entry):
+ def predicate(content, header, body):
+ fn_data = get_wsdl_value(body, lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'updateRecord').text)
+ check_wsdl_value(get_wsdl_value(fn_data, 'recordId'), str(entry[0]), type=('http://www.w3.org/2001/XMLSchema', 'int'))
+ check_wsdl_record(get_wsdl_value(fn_data, 'recorddata'), entry)
+ return True
+
+ return predicate
+
+
+def validate_wsdl_del_request(entry):
+ def predicate(content, header, body):
+ fn_data = get_wsdl_value(body, lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'deleteRecord').text)
+ check_wsdl_value(get_wsdl_value(fn_data, 'recordId'), str(entry[0]), type=('http://www.w3.org/2001/XMLSchema', 'int'))
+ return True
+
+ return predicate
+
+
+def create_wsdl_add_result(entry):
+ lines = []
+ add_wsdl_answer_start_lines(lines)
+ lines.append('<ns1:addRecordResponse>')
+ add_wsdl_dns_record_lines(lines, entry, 'return')
+ lines.append('</ns1:addRecordResponse>')
+ add_wsdl_answer_end_lines(lines)
+ return ''.join(lines)
+
+
+def create_wsdl_update_result(entry):
+ lines = []
+ add_wsdl_answer_start_lines(lines)
+ lines.append('<ns1:updateRecordResponse>')
+ add_wsdl_dns_record_lines(lines, entry, 'return')
+ lines.append('</ns1:updateRecordResponse>')
+ add_wsdl_answer_end_lines(lines)
+ return ''.join(lines)
+
+
+def create_wsdl_del_result(success):
+ lines = []
+ add_wsdl_answer_start_lines(lines)
+ lines.extend([
+ '<ns1:deleteRecordResponse>',
+ '<return xsi:type="xsd:boolean">{success}</return>'.format(success='true' if success else 'false'),
+ '</ns1:deleteRecordResponse>',
+ ])
+ add_wsdl_answer_end_lines(lines)
+ return ''.join(lines)
+
+
+HOSTTECH_WSDL_DEFAULT_ZONE_RESULT = create_wsdl_zones_answer(42, 'example.com', HOSTTECH_WSDL_DEFAULT_ENTRIES)
+
+HOSTTECH_WSDL_ZONE_NOT_FOUND = create_wsdl_zone_not_found_answer()
+
+HOSTTECH_JSON_ZONE_LIST_RESULT = {
+ "data": [
+ {
+ "id": 42,
+ "name": "example.com",
+ "email": "test@example.com",
+ "ttl": 10800,
+ "nameserver": "ns1.hosttech.ch",
+ "dnssec": False,
+ },
+ {
+ "id": 43,
+ "name": "foo.com",
+ "email": "test@foo.com",
+ "ttl": 10800,
+ "nameserver": "ns1.hosttech.ch",
+ 'dnssec': True,
+ 'dnssec_email': 'test@foo.com',
+ },
+ ],
+}
+
+HOSTTECH_JSON_ZONE_GET_RESULT = {
+ "data": {
+ "id": 42,
+ "name": "example.com",
+ "email": "test@example.com",
+ "ttl": 10800,
+ "nameserver": "ns1.hosttech.ch",
+ "dnssec": False,
+ "records": HOSTTECH_JSON_DEFAULT_ENTRIES,
+ }
+}
+
+HOSTTECH_JSON_ZONE_2_GET_RESULT = {
+ "data": {
+ "id": 43,
+ "name": "foo.com",
+ "email": "test@foo.com",
+ "ttl": 10800,
+ "nameserver": "ns1.hosttech.ch",
+ 'dnssec': True,
+ 'dnssec_email': 'test@foo.com',
+ 'ds_records': [
+ {
+ 'key_tag': 12345,
+ 'algorithm': 8,
+ 'digest_type': 1,
+ 'digest': '012356789ABCDEF0123456789ABCDEF012345678',
+ 'flags': 257,
+ 'protocol': 3,
+ 'public_key':
+ 'MuhdzsQdqEGShwjtJDKZZjdKqUSGluFzTTinpuEeIRzLLcgkwgAPKWFa '
+ 'eQntNlmcNDeCziGwpdvhJnvKXEMbFcZwsaDIJuWqERxAQNGABWfPlCLh '
+ 'HQPnbpRPNKipSdBaUhuOubvFvjBpFAwiwSAapRDVsAgKvjXucfXpFfYb '
+ 'pCundbAXBWhbpHVbqgmGoixXzFSwUsGVYLPpBCiDlLJwzjRKYYaoVYge '
+ 'kMtKFYUVnWIKbectWkDFdVqXwkKigCUDiuTTJxOBRJRNzGiDNMWBjYSm '
+ 'bBCAHMaMYaghLbYTwyKXltdHTHwBwtswGNfpnEdSpKFzZJonBZArQfHD '
+ 'lfceKgmKwEF=',
+ },
+ {
+ 'key_tag': 12345,
+ 'algorithm': 8,
+ 'digest_type': 2,
+ 'digest': '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF',
+ 'flags': 257,
+ 'protocol': 3,
+ 'public_key':
+ 'MuhdzsQdqEGShwjtJDKZZjdKqUSGluFzTTinpuEeIRzLLcgkwgAPKWFa '
+ 'eQntNlmcNDeCziGwpdvhJnvKXEMbFcZwsaDIJuWqERxAQNGABWfPlCLh '
+ 'HQPnbpRPNKipSdBaUhuOubvFvjBpFAwiwSAapRDVsAgKvjXucfXpFfYb '
+ 'pCundbAXBWhbpHVbqgmGoixXzFSwUsGVYLPpBCiDlLJwzjRKYYaoVYge '
+ 'kMtKFYUVnWIKbectWkDFdVqXwkKigCUDiuTTJxOBRJRNzGiDNMWBjYSm '
+ 'bBCAHMaMYaghLbYTwyKXltdHTHwBwtswGNfpnEdSpKFzZJonBZArQfHD '
+ 'lfceKgmKwEF=',
+ }
+ ],
+ "records": [],
+ }
+}
+
+HOSTTECH_JSON_ZONE_RECORDS_GET_RESULT = {
+ "data": HOSTTECH_JSON_DEFAULT_ENTRIES,
+}
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record.py
new file mode 100644
index 000000000..f5bb05c7e
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record.py
@@ -0,0 +1,835 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hetzner_dns_record
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hetzner import (
+ HETZNER_JSON_DEFAULT_ENTRIES,
+ HETZNER_JSON_ZONE_GET_RESULT,
+ HETZNER_JSON_ZONE_LIST_RESULT,
+ HETZNER_JSON_ZONE_RECORDS_GET_RESULT,
+)
+
+
+class TestHetznerDNSRecordJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hetzner_dns_record.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_id': '23',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'error': {'message': 'zone not found', 'code': 404}}),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id_prefix(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_id': '23',
+ 'prefix': '',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '23')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'records': [], 'error': {'message': 'zone not found', 'code': 404}}),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_json({'message': 'Invalid authentication credentials'}),
+ ])
+
+ assert result['msg'] == (
+ 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401): Invalid authentication credentials'
+ )
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://dns.hetzner.com/api/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_conversion_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'TXT',
+ 'ttl': 3600,
+ 'value': u'"hellö',
+ 'txt_transformation': 'quoted',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['msg'] == (
+ 'Error while converting DNS values: While processing record from the user: Missing double quotation mark at the end of value'
+ )
+
+ def test_idempotency_present(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['diff']['before'] == result['diff']['after']
+
+ def test_idempotency_absent_value(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': '1.2.3.6',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert result['diff']['before'] == {}
+ assert result['diff']['before'] == {}
+
+ def test_idempotency_absent_value_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'prefix': '*',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': '1.2.3.6',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+
+ def test_idempotency_absent_type(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+
+ def test_idempotency_absent_record(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com.',
+ 'record': 'somewhere.example.com.',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': '1.2.3.6',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+
+ def test_absent_check(self, mocker):
+ record = HETZNER_JSON_DEFAULT_ENTRIES[0]
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': ((record['name'] + '.') if record['name'] != '@' else '') + 'example.com',
+ 'type': record['type'],
+ 'value': record['value'],
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_absent(self, mocker):
+ record = HETZNER_JSON_DEFAULT_ENTRIES[0]
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': ((record['name'] + '.') if record['name'] != '@' else '') + 'example.com',
+ 'type': record['type'],
+ 'value': record['value'],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/{0}'.format(record['id']))
+ .result_str(''),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_check_mode(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_id': '42',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_check_mode_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_id': '42',
+ 'prefix': '@',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ '_ansible_diff': True,
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {}
+ assert result['diff']['after'] == {
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ 'extra': {},
+ }
+
+ def test_change_add_one(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '128 issue "letsencrypt.org xxx"',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], '128 issue "letsencrypt.org xxx"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '133',
+ 'type': 'CAA',
+ 'name': '@',
+ 'value': '128 issue "letsencrypt.org xxx"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {}
+ assert result['diff']['after'] == {
+ 'prefix': '',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '128 issue "letsencrypt.org xxx"',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+
+ def test_change_add_one_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '128 issue "letsencrypt.org"',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], '128 issue "letsencrypt.org"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '133',
+ 'type': 'CAA',
+ 'name': '@',
+ 'value': '128 issue "letsencrypt.org"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_idn_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'prefix': '☺',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '128 issue "letsencrypt.org"',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], 'xn--74h')
+ .expect_json_value(['value'], '128 issue "letsencrypt.org"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '133',
+ 'type': 'CAA',
+ 'name': 'xn--74h',
+ 'value': '128 issue "letsencrypt.org"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_modify_check(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 300,
+ 'value': '1.2.3.5',
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_modify(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 300,
+ 'value': '1.2.3.5',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/126')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'A')
+ .expect_json_value(['ttl'], 300)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '*')
+ .expect_json_value(['value'], '1.2.3.5')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '126',
+ 'type': 'A',
+ 'name': '*',
+ 'value': '1.2.3.5',
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_create_bad(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 300,
+ 'value': '1.2.3.5.6',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 422)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'A')
+ .expect_json_value(['ttl'], 300)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '*')
+ .expect_json_value(['value'], '1.2.3.5.6')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '',
+ 'type': '',
+ 'name': '',
+ 'value': '',
+ 'zone_id': '',
+ 'created': '',
+ 'modified': '',
+ },
+ 'error': {
+ 'message': 'invalid A record',
+ 'code': 422,
+ }
+ }),
+ ])
+
+ assert result['msg'] == (
+ 'Error: The new A record with value "1.2.3.5.6" and TTL 300 has not been accepted'
+ ' by the server with error message "invalid A record" (error code 422)'
+ )
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_info.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_info.py
new file mode 100644
index 000000000..8f0a79b02
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_info.py
@@ -0,0 +1,798 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hetzner_dns_record_info
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hetzner import (
+ HETZNER_JSON_ZONE_GET_RESULT,
+ HETZNER_JSON_ZONE_LIST_RESULT,
+ HETZNER_JSON_ZONE_RECORDS_GET_RESULT,
+)
+
+
+def mock_sleep(delay):
+ pass
+
+
+class TestHetznerDNSRecordInfoJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hetzner_dns_record_info.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message='')),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+
+ def test_auth_error_forbidden(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .result_json(dict(message='')),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Forbidden: you do not have access to this resource (HTTP status 403)'
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://dns.hetzner.com/api/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_too_many_retries(self, mocker):
+ sleep_values = [5, 10, 1, 1, 1, 60, 10, 1, 10, 3.1415]
+
+ def sleep_check(delay):
+ expected = sleep_values.pop(0)
+ assert delay == expected
+
+ with patch('time.sleep', sleep_check):
+ result = self.run_module_failed(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '1')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '0')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '-1')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '61')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', 'foo')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '0.9')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '3.1415')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '42')
+ .result_str(''),
+ ])
+ print(sleep_values)
+ assert result['msg'] == 'Error: Stopping after 10 failed retries with 429 Too Many Attempts'
+ assert len(sleep_values) == 0
+
+ def test_conversion_error(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_failed(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'TXT',
+ 'txt_transformation': 'quoted',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'records': [
+ {
+ 'id': '201',
+ 'type': 'TXT',
+ 'name': '@',
+ 'value': u'"hellö',
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ ]}),
+ ])
+
+ assert result['msg'] == (
+ 'Error while converting DNS values: While processing record from API: Missing double quotation mark at the end of value'
+ )
+
+ def test_get_single(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_success(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert len(result['records']) == 1
+ assert result['records'][0] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.4',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+
+ def test_get_single_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prefix': '*',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert len(result['records']) == 1
+ assert result['records'][0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.5',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+
+ def test_get_all_for_one_record(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'what': 'all_types_for_record',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert len(result['records']) == 2
+ assert result['records'][0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.5',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::4',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+
+ def test_get_all_for_one_record_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'what': 'all_types_for_record',
+ 'zone_name': 'example.com.',
+ 'prefix': '@',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert len(result['records']) == 7
+ assert result['records'][0] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.4',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][1] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::3',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][2] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': '10 example.com',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][3] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': 'helium.ns.hetzner.de.',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][4] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': 'hydrogen.ns.hetzner.com.',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][5] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': 'oxygen.ns.hetzner.com.',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][6] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'SOA',
+ 'value': 'hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+
+ def test_get_all(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foobar',
+ 'what': 'all_records',
+ 'zone_id': '42',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foobar')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foobar')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert len(result['records']) == 10
+ assert result['records'][0] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.4',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.5',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][2] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::3',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][3] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::4',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][4] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': '10 example.com',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][5] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': 'helium.ns.hetzner.de.',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][6] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': 'hydrogen.ns.hetzner.com.',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][7] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': 'oxygen.ns.hetzner.com.',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][8] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'SOA',
+ 'value': 'hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+ assert result['records'][9] == {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': u'bär "with quotes" (use \\ to escape)',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }
+
+ def test_get_single_txt_api(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_success(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'txt_transformation': 'api',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert result['records'] == [{
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'ttl': None,
+ 'type': 'TXT',
+ 'value': u'bär " \\"with quotes\\"" " " "(use \\\\ to escape)"',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }]
+
+ def test_get_single_txt_quoted(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_success(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'txt_transformation': 'quoted',
+ 'txt_character_encoding': 'decimal',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert result['records'] == [{
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'ttl': None,
+ 'type': 'TXT',
+ 'value': u'"b\\195\\164r \\"with quotes\\" (use \\\\ to escape)"',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }]
+
+ def test_get_single_txt_quoted_octal(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_success(mocker, hetzner_dns_record_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'txt_transformation': 'quoted',
+ 'txt_character_encoding': 'octal',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert result['records'] == [{
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'ttl': None,
+ 'type': 'TXT',
+ 'value': u'"b\\303\\244r \\"with quotes\\" (use \\\\ to escape)"',
+ 'extra': {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ }]
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_set.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_set.py
new file mode 100644
index 000000000..4bf2d1050
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_set.py
@@ -0,0 +1,1901 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hetzner_dns_record_set
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hetzner import (
+ HETZNER_JSON_DEFAULT_ENTRIES,
+ HETZNER_JSON_ZONE_GET_RESULT,
+ HETZNER_JSON_ZONE_LIST_RESULT,
+ HETZNER_JSON_ZONE_RECORDS_GET_RESULT,
+)
+
+
+class TestHetznerDNSRecordJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hetzner_dns_record_set.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_id': '23',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'error': {'message': 'zone not found', 'code': 404}}),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id_prefix(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_id': '23',
+ 'prefix': '',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '23')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'records': [], 'error': {'message': 'zone not found', 'code': 404}}),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_json({'message': 'Invalid authentication credentials'}),
+ ])
+
+ assert result['msg'] == (
+ 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401): Invalid authentication credentials'
+ )
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://dns.hetzner.com/api/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_conversion_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'TXT',
+ 'ttl': 3600,
+ 'value': [
+ u'"hellö',
+ ],
+ 'txt_transformation': 'quoted',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['msg'] == (
+ 'Error while converting DNS values: While processing record from the user: Missing double quotation mark at the end of value'
+ )
+
+ def test_idempotency_present(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': ['10 example.com'],
+ }
+ assert result['diff']['before'] == result['diff']['after']
+
+ def test_idempotency_absent_value(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert result['diff']['before'] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': ['1.2.3.5'],
+ }
+ assert result['diff']['before'] == result['diff']['after']
+
+ def test_idempotency_absent_value_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'prefix': '*',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+
+ def test_idempotency_absent_ttl(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 1800,
+ 'value': [
+ '1.2.3.5',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+
+ def test_idempotency_absent_type(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+
+ def test_idempotency_absent_record(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com.',
+ 'record': 'somewhere.example.com.',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'warnings' not in result
+
+ def test_idempotency_absent_record_warn(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com.',
+ 'record': 'somewhere.example.com.',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep_and_warn',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert list(result['warnings']) == ["Record already exists with different value. Set on_existing=replace to remove it"]
+
+ def test_idempotency_absent_record_fail(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com.',
+ 'record': 'somewhere.example.com.',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep_and_fail',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['msg'] == "Record already exists with different value. Set on_existing=replace to remove it"
+
+ def test_absent(self, mocker):
+ record = HETZNER_JSON_DEFAULT_ENTRIES[0]
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': ((record['name'] + '.') if record['name'] != '@' else '') + 'example.com',
+ 'type': record['type'],
+ 'ttl': record['ttl'],
+ 'value': [
+ record['value'],
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/{0}'.format(record['id']))
+ .result_str(''),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_absent_error(self, mocker):
+ record = HETZNER_JSON_DEFAULT_ENTRIES[0]
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': ((record['name'] + '.') if record['name'] != '@' else '') + 'example.com',
+ 'type': record['type'],
+ 'ttl': record['ttl'],
+ 'value': [
+ record['value'],
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('DELETE', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/{0}'.format(record['id']))
+ .return_header('Content-Type', 'application/json')
+ .result_json({'error': {'message': 'Internal Server Error', 'code': 500}}),
+ ])
+
+ print(result['msg'])
+ assert result['msg'] == (
+ 'Error: Expected HTTP status 200, 404 for DELETE https://dns.hetzner.com/api/v1/records/125,'
+ ' but got HTTP status 500 (Internal Server Error) with error message "Internal Server Error" (error code 500)'
+ )
+
+ def test_absent_bulk(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'value': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/130')
+ .result_str(''),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/131')
+ .result_str(''),
+ # Record 132 has been deleted between querying and we trying to delete it
+ FetchUrlCall('DELETE', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/132')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'message': 'record does not exist'}),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_absent_bulk_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'value': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/130')
+ .result_str(''),
+ FetchUrlCall('DELETE', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/131')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'error': {'message': 'Internal Server Error', 'code': 500}}),
+ ])
+
+ assert result['msg'] == (
+ 'Error: Expected HTTP status 200, 404 for DELETE https://dns.hetzner.com/api/v1/records/131,'
+ ' but got HTTP status 500 (Internal Server Error) with error message "Internal Server Error" (error code 500)'
+ )
+
+ def test_absent_other_value(self, mocker):
+ record = HETZNER_JSON_DEFAULT_ENTRIES[0]
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': ((record['name'] + '.') if record['name'] != '@' else '') + 'example.com',
+ 'type': record['type'],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/{0}'.format(record['id']))
+ .result_str(''),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_check_mode(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_id': '42',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_check_mode_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_id': '42',
+ 'prefix': '@',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ '_ansible_diff': True,
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {}
+ assert result['diff']['after'] == {
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': ['0 issue "letsencrypt.org"'],
+ }
+
+ def test_change_add_one(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org xxx"',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], '128 issue "letsencrypt.org xxx"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '133',
+ 'type': 'CAA',
+ 'name': '@',
+ 'value': '128 issue "letsencrypt.org xxx"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org"',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], '128 issue "letsencrypt.org"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '133',
+ 'type': 'CAA',
+ 'name': '@',
+ 'value': '128 issue "letsencrypt.org"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_idn_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'prefix': '☺',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org"',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], 'xn--74h')
+ .expect_json_value(['value'], '128 issue "letsencrypt.org"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '133',
+ 'type': 'CAA',
+ 'name': 'xn--74h',
+ 'value': '128 issue "letsencrypt.org"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_modify_list_fail(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': None,
+ 'value': [
+ 'helium.ns.hetzner.de.',
+ 'ytterbium.ns.hetzner.com.',
+ ],
+ 'on_existing': 'keep_and_fail',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['msg'] == "Record already exists with different value. Set on_existing=replace to replace it"
+
+ def test_change_modify_list_warn(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'helium.ns.hetzner.de.',
+ 'ytterbium.ns.hetzner.com.',
+ ],
+ 'on_existing': 'keep_and_warn',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': None,
+ 'value': ['helium.ns.hetzner.de.', 'hydrogen.ns.hetzner.com.', 'oxygen.ns.hetzner.com.'],
+ }
+ assert result['diff']['after'] == result['diff']['before']
+ assert list(result['warnings']) == ["Record already exists with different value. Set on_existing=replace to replace it"]
+
+ def test_change_modify_list_keep(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': None,
+ 'value': [
+ 'helium.ns.hetzner.de.',
+ 'ytterbium.ns.hetzner.com.',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert 'warnings' not in result
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': None,
+ 'value': ['helium.ns.hetzner.de.', 'hydrogen.ns.hetzner.com.', 'oxygen.ns.hetzner.com.'],
+ }
+ assert result['diff']['after'] == result['diff']['before']
+
+ def test_change_modify_list(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': None,
+ 'value': [
+ 'helium.ns.hetzner.de.',
+ 'ytterbium.ns.hetzner.com.',
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/131')
+ .result_str(''),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/132')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value_absent(['ttl'])
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'ytterbium.ns.hetzner.com.')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '132',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'ytterbium.ns.hetzner.com.',
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': None,
+ 'value': ['helium.ns.hetzner.de.', 'hydrogen.ns.hetzner.com.', 'oxygen.ns.hetzner.com.'],
+ }
+ assert result['diff']['after'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': None,
+ 'value': ['helium.ns.hetzner.de.', 'ytterbium.ns.hetzner.com.'],
+ }
+
+ def test_change_modify_txt_unquoted(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'foo.example.com',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [u'bär "with quotes" (use \\ to escape)!'],
+ 'txt_transformation': 'unquoted',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/201')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'TXT')
+ .expect_json_value_absent(['ttl'])
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], 'foo')
+ .expect_json_value(['value'], u'"bär \\"with quotes\\" (use \\\\ to escape)!"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '201',
+ 'type': 'TXT',
+ 'name': 'foo',
+ 'value': u'"bär \\"with quotes\\" (use \\\\ to escape)!"',
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [u'bär "with quotes" (use \\ to escape)'],
+ }
+ assert result['diff']['after'] == {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [u'bär "with quotes" (use \\ to escape)!'],
+ }
+
+ def test_change_modify_txt_quoted(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'foo.example.com',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [r'"b\195\164r \"with quotes\" (use \\ to escape)!"'],
+ 'txt_transformation': 'quoted',
+ 'txt_character_encoding': 'decimal',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/201')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'TXT')
+ .expect_json_value_absent(['ttl'])
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], 'foo')
+ .expect_json_value(['value'], u'"bär \\"with quotes\\" (use \\\\ to escape)!"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '201',
+ 'type': 'TXT',
+ 'name': 'foo',
+ 'value': u'"bär \\"with quotes\\" (use \\\\ to escape)!"',
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [r'"b\195\164r \"with quotes\" (use \\ to escape)"'],
+ }
+ assert result['diff']['after'] == {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [r'"b\195\164r \"with quotes\" (use \\ to escape)!"'],
+ }
+
+ def test_change_modify_txt_quoted_octal(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'foo.example.com',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [r'"b\303\244r \"with quotes\" (use \\ to escape)!"'],
+ 'txt_transformation': 'quoted',
+ 'txt_character_encoding': 'octal',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/201')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'TXT')
+ .expect_json_value_absent(['ttl'])
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], 'foo')
+ .expect_json_value(['value'], u'"bär \\"with quotes\\" (use \\\\ to escape)!"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '201',
+ 'type': 'TXT',
+ 'name': 'foo',
+ 'value': u'"bär \\"with quotes\\" (use \\\\ to escape)!"',
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [r'"b\303\244r \"with quotes\" (use \\ to escape)"'],
+ }
+ assert result['diff']['after'] == {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [r'"b\303\244r \"with quotes\" (use \\ to escape)!"'],
+ }
+
+ def test_change_modify_txt_api(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'foo.example.com',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [u'bär " " \\"with " " quotes\\" " (use \\\\ to escape)!"'],
+ 'txt_transformation': 'api',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/201')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'TXT')
+ .expect_json_value_absent(['ttl'])
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], 'foo')
+ .expect_json_value(['value'], u'bär " " \\"with " " quotes\\" " (use \\\\ to escape)!"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '201',
+ 'type': 'TXT',
+ 'name': 'foo',
+ 'value': u'bär " " \\"with " " quotes\\" " (use \\\\ to escape)!"',
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [u'bär " \\"with quotes\\"" " " "(use \\\\ to escape)"'],
+ }
+ assert result['diff']['after'] == {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [u'bär " " \\"with " " quotes\\" " (use \\\\ to escape)!"'],
+ }
+
+ def test_change_modify_bulk(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'a1',
+ 'a2',
+ 'a3',
+ 'a4',
+ 'a5',
+ 'a6',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/132')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'a1')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '132',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a1',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ }),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/131')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'a2')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '131',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a2',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ }),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/130')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'a3')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '130',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a3',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ }),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/bulk')
+ .expect_json_value_absent(['records', 0, 'id'])
+ .expect_json_value(['records', 0, 'type'], 'NS')
+ .expect_json_value(['records', 0, 'ttl'], 10800)
+ .expect_json_value(['records', 0, 'zone_id'], '42')
+ .expect_json_value(['records', 0, 'name'], '@')
+ .expect_json_value(['records', 0, 'value'], 'a4')
+ .expect_json_value_absent(['records', 1, 'id'])
+ .expect_json_value(['records', 1, 'type'], 'NS')
+ .expect_json_value(['records', 1, 'ttl'], 10800)
+ .expect_json_value(['records', 1, 'zone_id'], '42')
+ .expect_json_value(['records', 1, 'name'], '@')
+ .expect_json_value(['records', 1, 'value'], 'a5')
+ .expect_json_value_absent(['records', 2, 'id'])
+ .expect_json_value(['records', 2, 'type'], 'NS')
+ .expect_json_value(['records', 2, 'ttl'], 10800)
+ .expect_json_value(['records', 2, 'zone_id'], '42')
+ .expect_json_value(['records', 2, 'name'], '@')
+ .expect_json_value(['records', 2, 'value'], 'a6')
+ .expect_json_value_absent(['records', 3])
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'invalid_records': [],
+ 'valid_records': [],
+ 'records': [
+ {
+ 'id': '300',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a4',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ {
+ 'id': '301',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a5',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ {
+ 'id': '302',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a6',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ ],
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' not in result
+
+ def test_change_modify_bulk_errors(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'a1',
+ 'a2',
+ 'a3',
+ 'a4',
+ 'a5',
+ 'a6',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('PUT', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/132')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'a1')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'message': 'Internal Server Error'}),
+ ])
+
+ assert result['msg'] == (
+ 'Error: Expected HTTP status 200, 422 for PUT https://dns.hetzner.com/api/v1/records/132,'
+ ' but got HTTP status 500 (Internal Server Error) with message "Internal Server Error"'
+ )
+
+ def test_change_modify_bulk_errors_2(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'a1',
+ 'a2',
+ 'a3',
+ 'a4',
+ 'a5',
+ 'a6',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/132')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'a1')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '132',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a1',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ }),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/131')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'a2')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '131',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a2',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ }),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/130')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'a3')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '130',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a3',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ }),
+ FetchUrlCall('POST', 422)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/bulk')
+ .expect_json_value_absent(['records', 0, 'id'])
+ .expect_json_value(['records', 0, 'type'], 'NS')
+ .expect_json_value(['records', 0, 'ttl'], 10800)
+ .expect_json_value(['records', 0, 'zone_id'], '42')
+ .expect_json_value(['records', 0, 'name'], '@')
+ .expect_json_value(['records', 0, 'value'], 'a4')
+ .expect_json_value_absent(['records', 1, 'id'])
+ .expect_json_value(['records', 1, 'type'], 'NS')
+ .expect_json_value(['records', 1, 'ttl'], 10800)
+ .expect_json_value(['records', 1, 'zone_id'], '42')
+ .expect_json_value(['records', 1, 'name'], '@')
+ .expect_json_value(['records', 1, 'value'], 'a5')
+ .expect_json_value_absent(['records', 2, 'id'])
+ .expect_json_value(['records', 2, 'type'], 'NS')
+ .expect_json_value(['records', 2, 'ttl'], 10800)
+ .expect_json_value(['records', 2, 'zone_id'], '42')
+ .expect_json_value(['records', 2, 'name'], '@')
+ .expect_json_value(['records', 2, 'value'], 'a6')
+ .expect_json_value_absent(['records', 3])
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'invalid_records': [
+ {
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a4',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ {
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a5',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ ],
+ 'valid_records': [
+ {
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'a6',
+ 'ttl': 10800,
+ 'zone_id': '42',
+ },
+ ],
+ 'records': [],
+ 'error': {
+ 'message': 'invalid NS record, invalid NS record, ',
+ 'code': 422,
+ },
+ }),
+ ])
+
+ assert result['msg'] == (
+ 'Errors: Creating NS record "a4" with TTL 10800 for zone 42 failed with unknown reason;'
+ ' Creating NS record "a5" with TTL 10800 for zone 42 failed with unknown reason'
+ )
+
+ def test_change_change_bad(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set, {
+ 'hetzner_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.4.5',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('PUT', 422)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/125')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'A')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], '1.2.3.4.5')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '',
+ 'type': '',
+ 'name': '',
+ 'value': '',
+ 'zone_id': '',
+ 'created': '',
+ 'modified': '',
+ },
+ 'error': {
+ 'message': 'invalid A record',
+ 'code': 422,
+ }
+ }),
+ ])
+
+ assert result['msg'] == (
+ 'Error: The updated A record with value "1.2.3.4.5" and TTL 3600 has not been accepted'
+ ' by the server with error message "invalid A record" (error code 422)'
+ )
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_set_info.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_set_info.py
new file mode 100644
index 000000000..a42add09d
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_set_info.py
@@ -0,0 +1,696 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hetzner_dns_record_set_info
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hetzner import (
+ HETZNER_JSON_ZONE_GET_RESULT,
+ HETZNER_JSON_ZONE_LIST_RESULT,
+ HETZNER_JSON_ZONE_RECORDS_GET_RESULT,
+)
+
+
+def mock_sleep(delay):
+ pass
+
+
+class TestHetznerDNSRecordSetInfoJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hetzner_dns_record_set_info.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+
+ def test_auth_error_forbidden(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Forbidden: you do not have access to this resource (HTTP status 403)'
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://dns.hetzner.com/api/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_too_many_retries(self, mocker):
+ sleep_values = [5, 10, 1, 1, 1, 60, 10, 1, 10, 3.1415]
+
+ def sleep_check(delay):
+ expected = sleep_values.pop(0)
+ assert delay == expected
+
+ with patch('time.sleep', sleep_check):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '1')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '0')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '-1')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '61')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', 'foo')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '0.9')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '3.1415')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '42')
+ .result_str(''),
+ ])
+ print(sleep_values)
+ assert result['msg'] == 'Error: Stopping after 10 failed retries with 429 Too Many Attempts'
+ assert len(sleep_values) == 0
+
+ def test_conversion_error(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_failed(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'TXT',
+ 'txt_transformation': 'quoted',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'records': [
+ {
+ 'id': '201',
+ 'type': 'TXT',
+ 'name': '@',
+ 'value': u'"hellö',
+ 'zone_id': '42',
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ },
+ ]}),
+ ])
+
+ assert result['msg'] == (
+ 'Error while converting DNS values: While processing record from API: Missing double quotation mark at the end of value'
+ )
+
+ def test_get_single(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_success(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'set' in result
+ assert result['set']['record'] == 'example.com'
+ assert result['set']['prefix'] == ''
+ assert result['set']['ttl'] == 3600
+ assert result['set']['type'] == 'A'
+ assert result['set']['value'] == ['1.2.3.4']
+ assert 'sets' not in result
+
+ def test_get_single_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prefix': '*',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'set' in result
+ assert result['set']['record'] == '*.example.com'
+ assert result['set']['prefix'] == '*'
+ assert result['set']['ttl'] == 3600
+ assert result['set']['type'] == 'A'
+ assert result['set']['value'] == ['1.2.3.5']
+ assert 'sets' not in result
+
+ def test_get_all_for_one_record(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'what': 'all_types_for_record',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'set' not in result
+ assert 'sets' in result
+ sets = result['sets']
+ assert len(sets) == 2
+ assert sets[0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ }
+ assert sets[1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ }
+
+ def test_get_all_for_one_record_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'what': 'all_types_for_record',
+ 'zone_name': 'example.com.',
+ 'prefix': '@',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'set' not in result
+ assert 'sets' in result
+ sets = result['sets']
+ assert len(sets) == 5
+ assert sets[0] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ }
+ assert sets[1] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ }
+ assert sets[2] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ }
+ assert sets[3] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': ['helium.ns.hetzner.de.', 'hydrogen.ns.hetzner.com.', 'oxygen.ns.hetzner.com.'],
+ }
+ assert sets[4] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'SOA',
+ 'value': ['hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600'],
+ }
+
+ def test_get_all(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'what': 'all_records',
+ 'zone_id': '42',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'set' not in result
+ assert 'sets' in result
+ sets = result['sets']
+ assert len(sets) == 8
+ assert sets[0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ }
+ assert sets[1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ }
+ assert sets[2] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ }
+ assert sets[3] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ }
+ assert sets[4] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ }
+ assert sets[5] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': ['helium.ns.hetzner.de.', 'hydrogen.ns.hetzner.com.', 'oxygen.ns.hetzner.com.'],
+ }
+ assert sets[6] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'SOA',
+ 'value': ['hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600'],
+ }
+ assert sets[7] == {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'ttl': None,
+ 'type': 'TXT',
+ 'value': [u'bär "with quotes" (use \\ to escape)'],
+ }
+
+ def test_get_single_txt_api(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_success(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'txt_transformation': 'api',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'set' in result
+ assert result['set']['record'] == 'foo.example.com'
+ assert result['set']['prefix'] == 'foo'
+ assert result['set']['ttl'] is None
+ assert result['set']['type'] == 'TXT'
+ assert result['set']['value'] == [u'bär " \\"with quotes\\"" " " "(use \\\\ to escape)"']
+ assert 'sets' not in result
+
+ def test_get_single_txt_quoted(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_success(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'txt_transformation': 'quoted',
+ 'txt_character_encoding': 'decimal',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'set' in result
+ assert result['set']['record'] == 'foo.example.com'
+ assert result['set']['prefix'] == 'foo'
+ assert result['set']['ttl'] is None
+ assert result['set']['type'] == 'TXT'
+ assert result['set']['value'] == [u'"b\\195\\164r \\"with quotes\\" (use \\\\ to escape)"']
+ assert 'sets' not in result
+
+ def test_get_single_txt_quoted_deprecation(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_success(mocker, hetzner_dns_record_set_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prefix': 'foo',
+ 'type': 'TXT',
+ 'txt_transformation': 'quoted',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert 'set' in result
+ assert result['set']['record'] == 'foo.example.com'
+ assert result['set']['prefix'] == 'foo'
+ assert result['set']['ttl'] is None
+ assert result['set']['type'] == 'TXT'
+ assert result['set']['value'] == [u'"b\\303\\244r \\"with quotes\\" (use \\\\ to escape)"']
+ assert 'sets' not in result
+ assert 'deprecations' in result
+ found = False
+ for deprecation in result['deprecations']:
+ if 'collection_name' in deprecation and deprecation['collection_name'] != 'community.dns':
+ continue
+ found = True
+ assert deprecation['msg'] == (
+ 'The default of the txt_character_encoding option will change from "octal" to "decimal" in community.dns 3.0.0.'
+ ' This potentially affects you since you use txt_transformation=quoted.'
+ ' You can explicitly set txt_character_encoding to "octal" to keep the current behavior,'
+ ' or "decimal" to already now switch to the new behavior.'
+ ' We recommend switching to the new behavior, and using check/diff mode to figure out potential changes'
+ )
+ assert deprecation['version'] == '3.0.0'
+ assert deprecation.get('date') is None
+ assert found
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_sets.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_sets.py
new file mode 100644
index 000000000..339fb49c5
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_record_sets.py
@@ -0,0 +1,1236 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hetzner_dns_record_sets
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hetzner import (
+ HETZNER_JSON_ZONE_GET_RESULT,
+ HETZNER_JSON_ZONE_LIST_RESULT,
+ HETZNER_JSON_ZONE_RECORDS_GET_RESULT,
+)
+
+
+class TestHetznerDNSRecordJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hetzner_dns_record_sets.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_id': 23,
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+
+ def test_auth_error_forbidden(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_id': 23,
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Forbidden: you do not have access to this resource (HTTP status 403)'
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://dns.hetzner.com/api/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_key_collision_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_id': '42',
+ 'record_sets': [
+ {
+ 'record': 'test.example.com',
+ 'type': 'A',
+ 'ignore': True,
+ },
+ {
+ 'prefix': 'test',
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['msg'] == 'Found multiple sets for record test.example.com and type A: index #0 and #1'
+
+ def test_conversion_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'TXT',
+ 'ttl': 3600,
+ 'value': [
+ '"hellö',
+ ],
+ },
+ ],
+ 'txt_transformation': 'quoted',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['msg'] == (
+ 'Error while converting DNS values: While processing record from the user: Missing double quotation mark at the end of value'
+ )
+
+ def test_idempotency_empty(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_id': '42',
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+
+ def test_idempotency_present(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+
+ def test_removal_prune(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prune': 'true',
+ 'record_sets': [
+ {
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'prefix': '@',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': [],
+ },
+ {
+ 'record': 'example.com',
+ 'type': 'MX',
+ 'ignore': True,
+ },
+ {
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ignore': True,
+ },
+ {
+ 'record': 'example.com',
+ 'type': 'SOA',
+ 'ignore': True,
+ },
+ {
+ 'record': 'foo.example.com',
+ 'type': 'TXT',
+ 'ttl': None,
+ 'value': [u'bär "with quotes" (use \\ to escape)'],
+ },
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/{0}'.format(127))
+ .result_str(''),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/{0}'.format(128))
+ .result_str(''),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert result['diff']['before'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': ['helium.ns.hetzner.de.', 'hydrogen.ns.hetzner.com.', 'oxygen.ns.hetzner.com.'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'SOA',
+ 'value': ['hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600'],
+ },
+ {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'ttl': None,
+ 'type': 'TXT',
+ 'value': [u'bär "with quotes" (use \\ to escape)'],
+ },
+ ],
+ }
+ assert result['diff']['after'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': None,
+ 'value': ['helium.ns.hetzner.de.', 'hydrogen.ns.hetzner.com.', 'oxygen.ns.hetzner.com.'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'SOA',
+ 'value': ['hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600'],
+ },
+ {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'ttl': None,
+ 'type': 'TXT',
+ 'value': [u'bär "with quotes" (use \\ to escape)'],
+ },
+ ],
+ }
+
+ def test_change_add_one_check_mode(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_id': '42',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ },
+ ],
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_check_mode_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_id': '42',
+ 'record_sets': [
+ {
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ },
+ ],
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org xxx"',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], '128 issue "letsencrypt.org xxx"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '133',
+ 'type': 'CAA',
+ 'name': '@',
+ 'value': '128 issue "letsencrypt.org xxx"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org"',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], '128 issue "letsencrypt.org"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '133',
+ 'type': 'CAA',
+ 'name': '@',
+ 'value': '128 issue "letsencrypt.org"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_idn_prefix(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'prefix': '☺',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org"',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], 'xn--74h')
+ .expect_json_value(['value'], '128 issue "letsencrypt.org"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '133',
+ 'type': 'CAA',
+ 'name': 'xn--74h',
+ 'value': '128 issue "letsencrypt.org"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+
+ def test_change_add_one_failed(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org xxx"',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], '128 issue "letsencrypt.org xxx"')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'record': {}, 'error': {'code': 500, 'message': 'Internal Server Error'}}),
+ ])
+
+ assert result['msg'] == (
+ 'Error: POST https://dns.hetzner.com/api/v1/records resulted in API error 500 (Internal Server Error)'
+ ' with error message "Internal Server Error" (error code 500)'
+ )
+
+ def test_change_add_two_failed(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org xxx"',
+ '128 issuewild "letsencrypt.org"',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('POST', 422)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/bulk')
+ .expect_json_value_absent(['records', 0, 'id'])
+ .expect_json_value(['records', 0, 'type'], 'CAA')
+ .expect_json_value(['records', 0, 'ttl'], 3600)
+ .expect_json_value(['records', 0, 'zone_id'], '42')
+ .expect_json_value(['records', 0, 'name'], '@')
+ .expect_json_value(['records', 0, 'value'], '128 issue "letsencrypt.org xxx"')
+ .expect_json_value_absent(['records', 1, 'id'])
+ .expect_json_value(['records', 1, 'type'], 'CAA')
+ .expect_json_value(['records', 1, 'ttl'], 3600)
+ .expect_json_value(['records', 1, 'zone_id'], '42')
+ .expect_json_value(['records', 1, 'name'], '@')
+ .expect_json_value(['records', 1, 'value'], '128 issuewild "letsencrypt.org"')
+ .expect_json_value_absent(['records', 2])
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'invalid_records': [
+ {
+ 'type': 'CAA',
+ 'name': '@',
+ 'value': '128 issue "letsencrypt.org xxx"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ {
+ 'type': 'CAA',
+ 'name': '@',
+ 'value': '128 issuewild "letsencrypt.org"',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ ],
+ 'valid_records': [],
+ 'records': [],
+ 'error': {
+ 'message': 'invalid CAA record, invalid CAA record, ',
+ 'code': 422,
+ },
+ }),
+ ])
+
+ assert result['msg'] == (
+ 'Errors: Creating CAA record "128 issue "letsencrypt.org xxx"" with TTL 3600 for zone 42 failed with unknown reason;'
+ ' Creating CAA record "128 issuewild "letsencrypt.org"" with TTL 3600 for zone 42 failed with unknown reason'
+ )
+
+ def test_change_modify_list(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': None,
+ 'value': [
+ 'helium.ns.hetzner.de.',
+ 'ytterbium.ns.hetzner.com.',
+ ],
+ },
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/131')
+ .result_str(''),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/132')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value_absent(['ttl'])
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'ytterbium.ns.hetzner.com.')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '131',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'ytterbium.ns.hetzner.com.',
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': ['helium.ns.hetzner.de.', 'hydrogen.ns.hetzner.com.', 'oxygen.ns.hetzner.com.'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'SOA',
+ 'value': ['hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600'],
+ },
+ {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'ttl': None,
+ 'type': 'TXT',
+ 'value': [u'bär "with quotes" (use \\ to escape)'],
+ },
+ ],
+ }
+ assert result['diff']['after'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': None,
+ 'value': ['helium.ns.hetzner.de.', 'ytterbium.ns.hetzner.com.'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'SOA',
+ 'value': ['hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600'],
+ },
+ {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'ttl': None,
+ 'type': 'TXT',
+ 'value': [u'bär "with quotes" (use \\ to escape)'],
+ },
+ ],
+ }
+
+ def test_change_modify_list_ttl(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_record_sets, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 3600,
+ 'value': [
+ 'helium.ns.hetzner.de.',
+ 'ytterbium.ns.hetzner.com.',
+ ],
+ },
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records', without_query=True)
+ .expect_query_values('zone_id', '42')
+ .expect_query_values('page', '1')
+ .expect_query_values('per_page', '100')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HETZNER_JSON_ZONE_RECORDS_GET_RESULT),
+ FetchUrlCall('DELETE', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/130')
+ .result_str(''),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/132')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'helium.ns.hetzner.de.')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '130',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'ytterbium.ns.hetzner.com.',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ }),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/records/131')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['zone_id'], '42')
+ .expect_json_value(['name'], '@')
+ .expect_json_value(['value'], 'ytterbium.ns.hetzner.com.')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'record': {
+ 'id': '131',
+ 'type': 'NS',
+ 'name': '@',
+ 'value': 'ytterbium.ns.hetzner.com.',
+ 'ttl': 3600,
+ 'zone_id': '42',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == '42'
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'NS',
+ 'value': ['helium.ns.hetzner.de.', 'hydrogen.ns.hetzner.com.', 'oxygen.ns.hetzner.com.'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'SOA',
+ 'value': ['hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600'],
+ },
+ {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'ttl': None,
+ 'type': 'TXT',
+ 'value': [u'bär "with quotes" (use \\ to escape)'],
+ },
+ ],
+ }
+ assert result['diff']['after'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 3600,
+ 'value': ['helium.ns.hetzner.de.', 'ytterbium.ns.hetzner.com.'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': None,
+ 'type': 'SOA',
+ 'value': ['hydrogen.ns.hetzner.com. dns.hetzner.com. 2021070900 86400 10800 3600000 3600'],
+ },
+ {
+ 'record': 'foo.example.com',
+ 'prefix': 'foo',
+ 'ttl': None,
+ 'type': 'TXT',
+ 'value': [u'bär "with quotes" (use \\ to escape)'],
+ },
+ ],
+ }
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_zone_info.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_zone_info.py
new file mode 100644
index 000000000..473d4d1bc
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hetzner_dns_zone_info.py
@@ -0,0 +1,192 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hetzner_dns_zone_info
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hetzner import (
+ HETZNER_JSON_ZONE_GET_RESULT,
+ HETZNER_JSON_ZONE_LIST_RESULT,
+)
+
+
+class TestHetznerDNSZoneInfoJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hetzner_dns_zone_info.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_zone_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .return_header('Content-Type', 'application/json; charset=utf-8')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_zone_info, {
+ 'hetzner_token': 'foo',
+ 'zone_id': '23',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .return_header('Content-Type', 'application/json; charset=utf-8')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_zone_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+
+ def test_auth_error_forbidden(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_zone_info, {
+ 'hetzner_token': 'foo',
+ 'zone_id': '23',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Forbidden: you do not have access to this resource (HTTP status 403)'
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hetzner_dns_zone_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.org',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://dns.hetzner.com/api/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_get(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_zone_info, {
+ 'hetzner_token': 'foo',
+ 'zone_name': 'example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones', without_query=True)
+ .expect_query_values('name', 'example.com')
+ .return_header('Content-Type', 'application/json; charset=utf-8')
+ .result_json(HETZNER_JSON_ZONE_LIST_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert result['zone_name'] == 'example.com'
+ assert result['zone_info'] == {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ 'legacy_dns_host': 'string',
+ 'legacy_ns': ['bar', 'foo'],
+ 'ns': ['string'],
+ 'owner': 'Example',
+ 'paused': True,
+ 'permission': 'string',
+ 'project': 'string',
+ 'registrar': 'string',
+ 'status': 'verified',
+ 'ttl': 10800,
+ 'verified': '2021-07-09T11:18:37Z',
+ 'records_count': 0,
+ 'is_secondary_dns': True,
+ 'txt_verification': {
+ 'name': 'string',
+ 'token': 'string',
+ },
+ }
+
+ def test_get_id(self, mocker):
+ result = self.run_module_success(mocker, hetzner_dns_zone_info, {
+ 'hetzner_token': 'foo',
+ 'zone_id': '42',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('auth-api-token', 'foo')
+ .expect_url('https://dns.hetzner.com/api/v1/zones/42')
+ .return_header('Content-Type', 'application/json; charset=utf-8')
+ .result_json(HETZNER_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == '42'
+ assert result['zone_name'] == 'example.com'
+ assert result['zone_info'] == {
+ 'created': '2021-07-09T11:18:37Z',
+ 'modified': '2021-07-09T11:18:37Z',
+ 'legacy_dns_host': 'string',
+ 'legacy_ns': ['bar', 'foo'],
+ 'ns': ['string'],
+ 'owner': 'Example',
+ 'paused': True,
+ 'permission': 'string',
+ 'project': 'string',
+ 'registrar': 'string',
+ 'status': 'verified',
+ 'ttl': 10800,
+ 'verified': '2021-07-09T11:18:37Z',
+ 'records_count': 0,
+ 'is_secondary_dns': True,
+ 'txt_verification': {
+ 'name': 'string',
+ 'token': 'string',
+ },
+ }
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record.py
new file mode 100644
index 000000000..c72566a2b
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record.py
@@ -0,0 +1,1032 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hosttech_dns_record
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hosttech import (
+ expect_wsdl_authentication,
+ expect_wsdl_value,
+ validate_wsdl_call,
+ validate_wsdl_add_request,
+ validate_wsdl_del_request,
+ create_wsdl_add_result,
+ create_wsdl_del_result,
+ HOSTTECH_WSDL_DEFAULT_ENTRIES,
+ HOSTTECH_WSDL_DEFAULT_ZONE_RESULT,
+ HOSTTECH_WSDL_ZONE_NOT_FOUND,
+ HOSTTECH_JSON_DEFAULT_ENTRIES,
+ HOSTTECH_JSON_ZONE_GET_RESULT,
+ HOSTTECH_JSON_ZONE_LIST_RESULT,
+ HOSTTECH_JSON_ZONE_RECORDS_GET_RESULT,
+)
+
+try:
+ import lxml.etree
+ HAS_LXML_ETREE = True
+except ImportError:
+ HAS_LXML_ETREE = False
+
+
+@pytest.mark.skipif(not HAS_LXML_ETREE, reason="Need lxml.etree for WSDL tests")
+class TestHosttechDNSRecordWSDL(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_record.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.org',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '23',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id_prefix(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_id': 23,
+ 'prefix': '',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '23',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_idempotency_present(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_value(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': '1.2.3.6',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_type(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'absent',
+ 'zone_id': 42,
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '42',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_record(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'absent',
+ 'zone_name': 'example.com.',
+ 'record': 'somewhere.example.com.',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': '1.2.3.6',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_absent(self, mocker):
+ record = HOSTTECH_WSDL_DEFAULT_ENTRIES[0]
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': record[3] + 'example.com',
+ 'type': record[2],
+ 'value': record[4],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ validate_wsdl_del_request(record),
+ ]))
+ .result_str(create_wsdl_del_result(True)),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_check_mode(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one(self, mocker):
+ new_entry = (131, 42, 'CAA', 'foo', '0 issue "letsencrypt.org"', 3600, None, None)
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'foo.example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ validate_wsdl_add_request('42', new_entry),
+ ]))
+ .result_str(create_wsdl_add_result(new_entry)),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+
+class TestHosttechDNSRecordJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_record.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id_prefix(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_id': 23,
+ 'prefix': '',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23/records', without_query=True)
+ .expect_query_values('type', 'MX')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+
+ def test_auth_error_forbidden(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Forbidden: you do not have access to this resource (HTTP status 403)'
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://api.ns1.hosttech.eu/api/user/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_idempotency_present(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': '10 example.com',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['diff']['before'] == result['diff']['after']
+
+ def test_idempotency_absent_value(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': '1.2.3.6',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert result['diff']['before'] == {}
+ assert result['diff']['before'] == {}
+
+ def test_idempotency_absent_value_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'prefix': '*',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': '1.2.3.6',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_type(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_record(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com.',
+ 'record': 'somewhere.example.com.',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': '1.2.3.6',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'warnings' not in result
+
+ def test_absent_check(self, mocker):
+ record = HOSTTECH_JSON_DEFAULT_ENTRIES[0]
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': record['name'] + 'example.com',
+ 'type': record['type'],
+ 'value': record['ipv4'],
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_absent(self, mocker):
+ record = HOSTTECH_JSON_DEFAULT_ENTRIES[0]
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': record['name'] + 'example.com',
+ 'type': record['type'],
+ 'value': record['ipv4'],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/{0}'.format(record['id']))
+ .result_str(''),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_check_mode(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_id': 42,
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_check_mode_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_id': 42,
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ '_ansible_diff': True,
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records', without_query=True)
+ .expect_query_values('type', 'CAA')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {}
+ assert result['diff']['after'] == {
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '0 issue "letsencrypt.org"',
+ 'extra': {},
+ }
+
+ def test_change_add_one(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '128 issue "letsencrypt.org xxx"',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], '')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org xxx')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 133,
+ 'type': 'CAA',
+ 'name': '',
+ 'flag': '128',
+ 'tag': 'issue',
+ 'value': 'letsencrypt.org xxx',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {}
+ assert result['diff']['after'] == {
+ 'prefix': '',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '128 issue "letsencrypt.org xxx"',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+
+ def test_change_add_one_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '128 issue "letsencrypt.org"',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], '')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 133,
+ 'type': 'CAA',
+ 'name': '',
+ 'flag': '128',
+ 'tag': 'issue',
+ 'value': 'letsencrypt.org',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_idn_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'prefix': '☺',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': '128 issue "letsencrypt.org"',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], 'xn--74h')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 133,
+ 'type': 'CAA',
+ 'name': 'xn--74h',
+ 'flag': '128',
+ 'tag': 'issue',
+ 'value': 'letsencrypt.org',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_modify_check(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 300,
+ 'value': '1.2.3.5',
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_modify(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 300,
+ 'value': '1.2.3.5',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/126')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 300)
+ .expect_json_value(['name'], '*')
+ .expect_json_value(['ipv4'], '1.2.3.5')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': '126',
+ 'type': 'A',
+ 'name': '*',
+ 'ipv4': '1.2.3.5',
+ 'ttl': 300,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_info.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_info.py
new file mode 100644
index 000000000..73c9d16a1
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_info.py
@@ -0,0 +1,766 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hosttech_dns_record_info
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hosttech import (
+ expect_wsdl_authentication,
+ expect_wsdl_value,
+ validate_wsdl_call,
+ HOSTTECH_WSDL_DEFAULT_ZONE_RESULT,
+ HOSTTECH_WSDL_ZONE_NOT_FOUND,
+ HOSTTECH_JSON_ZONE_GET_RESULT,
+ HOSTTECH_JSON_ZONE_LIST_RESULT,
+)
+
+try:
+ import lxml.etree
+ HAS_LXML_ETREE = True
+except ImportError:
+ HAS_LXML_ETREE = False
+
+
+def mock_sleep(delay):
+ pass
+
+
+@pytest.mark.skipif(not HAS_LXML_ETREE, reason="Need lxml.etree for WSDL tests")
+class TestHosttechDNSRecordInfoWSDL(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_record_info.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.org',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '23',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_get_single(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert len(result['records']) == 1
+ assert result['records'][0] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.4',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+
+ def test_get_all_for_one_record(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'what': 'all_types_for_record',
+ 'zone_id': 42,
+ 'record': '*.example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '42',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert len(result['records']) == 2
+ assert result['records'][0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.5',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::4',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+
+ def test_get_all(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'what': 'all_records',
+ 'zone_name': 'example.com.',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert len(result['records']) == 8
+ assert result['records'][0] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.4',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.5',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][2] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::3',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][3] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::4',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][4] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': '10 example.com',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][5] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': 'ns3.hostserv.eu',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][6] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': 'ns2.hostserv.eu',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][7] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': 'ns1.hostserv.eu',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+
+
+class TestHosttechDNSRecordInfoJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_record_info.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+
+ def test_auth_error_forbidden(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Forbidden: you do not have access to this resource (HTTP status 403)'
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://api.ns1.hosttech.eu/api/user/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_too_many_retries(self, mocker):
+ sleep_values = [5, 10, 1, 1, 1, 60, 10, 1, 10, 3.1415]
+
+ def sleep_check(delay):
+ expected = sleep_values.pop(0)
+ assert delay == expected
+
+ with patch('time.sleep', sleep_check):
+ result = self.run_module_failed(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '1')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '0')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '-1')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '61')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', 'foo')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '0.9')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '3.1415')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '42')
+ .result_str(''),
+ ])
+ print(sleep_values)
+ assert result['msg'] == 'Error: Stopping after 10 failed retries with 429 Too Many Attempts'
+ assert len(sleep_values) == 0
+
+ def test_get_single(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_success(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert len(result['records']) == 1
+ assert result['records'][0] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.4',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+
+ def test_get_single_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prefix': '*',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert len(result['records']) == 1
+ assert result['records'][0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.5',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+
+ def test_get_all_for_one_record(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'what': 'all_types_for_record',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert len(result['records']) == 2
+ assert result['records'][0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.5',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::4',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+
+ def test_get_all_for_one_record_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'what': 'all_types_for_record',
+ 'zone_name': 'example.com.',
+ 'prefix': '',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert len(result['records']) == 6
+ assert result['records'][0] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.4',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][1] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::3',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][2] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': '10 example.com',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][3] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': 'ns3.hostserv.eu',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][4] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': 'ns2.hostserv.eu',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][5] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': 'ns1.hostserv.eu',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+
+ def test_get_all(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_info, {
+ 'hosttech_token': 'foo',
+ 'what': 'all_records',
+ 'zone_id': 42,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert len(result['records']) == 8
+ assert result['records'][0] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.4',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': '1.2.3.5',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][2] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::3',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][3] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': '2001:1:2::4',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][4] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': '10 example.com',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][5] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': 'ns3.hostserv.eu',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][6] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': 'ns2.hostserv.eu',
+ 'extra': {
+ 'comment': '',
+ },
+ }
+ assert result['records'][7] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': 'ns1.hostserv.eu',
+ 'extra': {
+ 'comment': '',
+ },
+ }
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_set.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_set.py
new file mode 100644
index 000000000..579d58091
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_set.py
@@ -0,0 +1,1899 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hosttech_dns_record_set
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hosttech import (
+ expect_wsdl_authentication,
+ expect_wsdl_value,
+ validate_wsdl_call,
+ validate_wsdl_add_request,
+ validate_wsdl_update_request,
+ validate_wsdl_del_request,
+ create_wsdl_add_result,
+ create_wsdl_update_result,
+ create_wsdl_del_result,
+ HOSTTECH_WSDL_DEFAULT_ENTRIES,
+ HOSTTECH_WSDL_DEFAULT_ZONE_RESULT,
+ HOSTTECH_WSDL_ZONE_NOT_FOUND,
+ HOSTTECH_JSON_DEFAULT_ENTRIES,
+ HOSTTECH_JSON_ZONE_GET_RESULT,
+ HOSTTECH_JSON_ZONE_LIST_RESULT,
+ HOSTTECH_JSON_ZONE_RECORDS_GET_RESULT,
+)
+
+try:
+ import lxml.etree
+ HAS_LXML_ETREE = True
+except ImportError:
+ HAS_LXML_ETREE = False
+
+
+@pytest.mark.skipif(not HAS_LXML_ETREE, reason="Need lxml.etree for WSDL tests")
+class TestHosttechDNSRecordWSDL(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_record_set.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.org',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '23',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id_prefix(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_id': 23,
+ 'prefix': '',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '23',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_idempotency_present(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_value(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_ttl(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 1800,
+ 'value': [
+ '1.2.3.5',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_type(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'absent',
+ 'zone_id': 42,
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '42',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_record(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'absent',
+ 'zone_name': 'example.com.',
+ 'record': 'somewhere.example.com.',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_absent(self, mocker):
+ record = HOSTTECH_WSDL_DEFAULT_ENTRIES[0]
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': record[3] + 'example.com',
+ 'type': record[2],
+ 'ttl': record[5],
+ 'value': [
+ record[4],
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ validate_wsdl_del_request(record),
+ ]))
+ .result_str(create_wsdl_del_result(True)),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_check_mode(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one(self, mocker):
+ new_entry = (131, 42, 'CAA', 'foo', '0 issue "letsencrypt.org"', 3600, None, None)
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'foo.example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ validate_wsdl_add_request('42', new_entry),
+ ]))
+ .result_str(create_wsdl_add_result(new_entry)),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_modify_list_fail(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'ns1.hostserv.eu',
+ 'ns4.hostserv.eu',
+ ],
+ 'on_existing': 'keep_and_fail',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['msg'] == "Record already exists with different value. Set on_existing=replace to replace it"
+
+ def test_change_modify_list(self, mocker):
+ del_entry = (130, 42, 'NS', '', 'ns3.hostserv.eu', 10800, None, None)
+ update_entry = (131, 42, 'NS', '', 'ns4.hostserv.eu', 10800, None, None)
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'ns1.hostserv.eu',
+ 'ns4.hostserv.eu',
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ validate_wsdl_del_request(del_entry),
+ ]))
+ .result_str(create_wsdl_del_result(True)),
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ validate_wsdl_update_request(update_entry),
+ ]))
+ .result_str(create_wsdl_update_result(update_entry)),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': ['ns1.hostserv.eu', 'ns2.hostserv.eu', 'ns3.hostserv.eu'],
+ }
+ assert result['diff']['after'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': ['ns1.hostserv.eu', 'ns4.hostserv.eu'],
+ }
+
+
+class TestHosttechDNSRecordJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_record_set.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id_prefix(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_id': 23,
+ 'prefix': '',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23/records', without_query=True)
+ .expect_query_values('type', 'MX')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+
+ def test_auth_error_forbidden(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Forbidden: you do not have access to this resource (HTTP status 403)'
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://api.ns1.hosttech.eu/api/user/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_idempotency_present(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': ['10 example.com'],
+ }
+ assert result['diff']['before'] == result['diff']['after']
+
+ def test_idempotency_absent_value(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert result['diff']['before'] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': ['1.2.3.5'],
+ }
+ assert result['diff']['before'] == result['diff']['after']
+
+ def test_idempotency_absent_value_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'prefix': '*',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_ttl(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ 'type': 'A',
+ 'ttl': 1800,
+ 'value': [
+ '1.2.3.5',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_type(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_absent_record(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com.',
+ 'record': 'somewhere.example.com.',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'warnings' not in result
+
+ def test_idempotency_absent_record_warn(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com.',
+ 'record': 'somewhere.example.com.',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep_and_warn',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert list(result['warnings']) == ["Record already exists with different value. Set on_existing=replace to remove it"]
+
+ def test_idempotency_absent_record_fail(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com.',
+ 'record': 'somewhere.example.com.',
+ 'type': 'A',
+ 'ttl': 3600,
+ 'value': [
+ '1.2.3.6',
+ ],
+ 'on_existing': 'keep_and_fail',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['msg'] == "Record already exists with different value. Set on_existing=replace to remove it"
+
+ def test_absent(self, mocker):
+ record = HOSTTECH_JSON_DEFAULT_ENTRIES[0]
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': record['name'] + 'example.com',
+ 'type': record['type'],
+ 'ttl': record['ttl'],
+ 'value': [
+ record['ipv4'],
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/{0}'.format(record['id']))
+ .result_str(''),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_absent_bulk(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'value': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/130')
+ .result_str(''),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/131')
+ .result_str(''),
+ # Record 132 has been deleted between querying and we trying to delete it
+ FetchUrlCall('DELETE', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/132')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'message': 'record does not exist'}),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_absent_bulk_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'value': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/130')
+ .result_str(''),
+ FetchUrlCall('DELETE', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/131')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'message': 'Internal Server Error'}),
+ ])
+
+ assert result['msg'] == (
+ 'Error: Expected HTTP status 204, 404 for DELETE https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/131,'
+ ' but got HTTP status 500 (Internal Server Error) with message "Internal Server Error"'
+ )
+
+ def test_absent_other_value(self, mocker):
+ record = HOSTTECH_JSON_DEFAULT_ENTRIES[0]
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'absent',
+ 'zone_name': 'example.com',
+ 'record': record['name'] + 'example.com',
+ 'type': record['type'],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/{0}'.format(record['id']))
+ .result_str(''),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_check_mode(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_id': 42,
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_check_mode_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_id': 42,
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ '_ansible_diff': True,
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records', without_query=True)
+ .expect_query_values('type', 'CAA')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_RECORDS_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {}
+ assert result['diff']['after'] == {
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': ['0 issue "letsencrypt.org"'],
+ }
+
+ def test_change_add_one(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org xxx"',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], '')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org xxx')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 133,
+ 'type': 'CAA',
+ 'name': '',
+ 'flag': '128',
+ 'tag': 'issue',
+ 'value': 'letsencrypt.org xxx',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org"',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], '')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 133,
+ 'type': 'CAA',
+ 'name': '',
+ 'flag': '128',
+ 'tag': 'issue',
+ 'value': 'letsencrypt.org',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_idn_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'prefix': '☺',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org"',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], 'xn--74h')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 133,
+ 'type': 'CAA',
+ 'name': 'xn--74h',
+ 'flag': '128',
+ 'tag': 'issue',
+ 'value': 'letsencrypt.org',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_fail(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'prefix': '☺',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org"',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], 'xn--74h')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'message': 'Internal Server Error'}),
+ ])
+
+ assert result['msg'] == (
+ 'Error: Expected HTTP status 201 for POST https://api.ns1.hosttech.eu/api/user/v1/zones/42/records,'
+ ' but got HTTP status 500 (Internal Server Error) with message "Internal Server Error"'
+ )
+
+ def test_change_modify_list_fail(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'ns1.hostserv.eu',
+ 'ns4.hostserv.eu',
+ ],
+ 'on_existing': 'keep_and_fail',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['msg'] == "Record already exists with different value. Set on_existing=replace to replace it"
+
+ def test_change_modify_list_warn(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'ns1.hostserv.eu',
+ 'ns4.hostserv.eu',
+ ],
+ 'on_existing': 'keep_and_warn',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': ['ns1.hostserv.eu', 'ns2.hostserv.eu', 'ns3.hostserv.eu'],
+ }
+ assert result['diff']['after'] == result['diff']['before']
+ assert list(result['warnings']) == ["Record already exists with different value. Set on_existing=replace to replace it"]
+
+ def test_change_modify_list_keep(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'ns1.hostserv.eu',
+ 'ns4.hostserv.eu',
+ ],
+ 'on_existing': 'keep',
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert 'warnings' not in result
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': ['ns1.hostserv.eu', 'ns2.hostserv.eu', 'ns3.hostserv.eu'],
+ }
+ assert result['diff']['after'] == result['diff']['before']
+
+ def test_change_modify_list(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'ns1.hostserv.eu',
+ 'ns4.hostserv.eu',
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/130')
+ .result_str(''),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/131')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'ns4.hostserv.eu')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 131,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'ns4.hostserv.eu',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': ['ns1.hostserv.eu', 'ns2.hostserv.eu', 'ns3.hostserv.eu'],
+ }
+ assert result['diff']['after'] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': ['ns1.hostserv.eu', 'ns4.hostserv.eu'],
+ }
+
+ def test_change_modify_bulk(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'a1',
+ 'a2',
+ 'a3',
+ 'a4',
+ 'a5',
+ 'a6',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/132')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a1')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 132,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'a1',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/131')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a2')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 131,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'a2',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/130')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a3')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 130,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'a3',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a4')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 300,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'a4',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a5')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 301,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'a5',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a6')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 302,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'a6',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert 'diff' not in result
+
+ def test_change_modify_bulk_errors_update(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'a1',
+ 'a2',
+ 'a3',
+ 'a4',
+ 'a5',
+ 'a6',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('PUT', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/132')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a1')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'message': 'Internal Server Error'}),
+ ])
+
+ assert result['msg'] == (
+ 'Error: Expected HTTP status 200 for PUT https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/132,'
+ ' but got HTTP status 500 (Internal Server Error) with message "Internal Server Error"'
+ )
+
+ def test_change_modify_bulk_errors_create(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set, {
+ 'hosttech_token': 'foo',
+ 'state': 'present',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'a1',
+ 'a2',
+ 'a3',
+ 'a4',
+ 'a5',
+ 'a6',
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/132')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a1')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 132,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'a1',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/131')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a2')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 131,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'a2',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/130')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a3')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 130,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'a3',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ FetchUrlCall('POST', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'NS')
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'a4')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'message': 'Internal Server Error'}),
+ ])
+
+ assert result['msg'] == (
+ 'Error: Expected HTTP status 201 for POST https://api.ns1.hosttech.eu/api/user/v1/zones/42/records,'
+ ' but got HTTP status 500 (Internal Server Error) with message "Internal Server Error"'
+ )
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_set_info.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_set_info.py
new file mode 100644
index 000000000..35fd53730
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_set_info.py
@@ -0,0 +1,649 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hosttech_dns_record_set_info
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hosttech import (
+ expect_wsdl_authentication,
+ expect_wsdl_value,
+ validate_wsdl_call,
+ HOSTTECH_WSDL_DEFAULT_ZONE_RESULT,
+ HOSTTECH_WSDL_ZONE_NOT_FOUND,
+ HOSTTECH_JSON_ZONE_GET_RESULT,
+ HOSTTECH_JSON_ZONE_LIST_RESULT,
+)
+
+try:
+ import lxml.etree
+ HAS_LXML_ETREE = True
+except ImportError:
+ HAS_LXML_ETREE = False
+
+
+def mock_sleep(delay):
+ pass
+
+
+@pytest.mark.skipif(not HAS_LXML_ETREE, reason="Need lxml.etree for WSDL tests")
+class TestHosttechDNSRecordSetInfoWSDL(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_record_set_info.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.org',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '23',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_get_single(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'set' in result
+ assert result['set']['record'] == 'example.com'
+ assert result['set']['prefix'] == ''
+ assert result['set']['ttl'] == 3600
+ assert result['set']['type'] == 'A'
+ assert result['set']['value'] == ['1.2.3.4']
+ assert 'sets' not in result
+
+ def test_get_all_for_one_record(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'what': 'all_types_for_record',
+ 'zone_id': 42,
+ 'record': '*.example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '42',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'set' not in result
+ assert 'sets' in result
+ sets = result['sets']
+ assert len(sets) == 2
+ assert sets[0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ }
+ assert sets[1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ }
+
+ def test_get_all(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'what': 'all_records',
+ 'zone_name': 'example.com.',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'set' not in result
+ assert 'sets' in result
+ sets = result['sets']
+ assert len(sets) == 6
+ assert sets[0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ }
+ assert sets[1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ }
+ assert sets[2] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ }
+ assert sets[3] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ }
+ assert sets[4] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ }
+ assert sets[5] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': ['ns3.hostserv.eu', 'ns2.hostserv.eu', 'ns1.hostserv.eu'],
+ }
+
+
+class TestHosttechDNSRecordSetInfoJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_record_set_info.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+
+ def test_auth_error_forbidden(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 23,
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Forbidden: you do not have access to this resource (HTTP status 403)'
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record': 'example.org',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://api.ns1.hosttech.eu/api/user/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_too_many_retries(self, mocker):
+ sleep_values = [5, 10, 1, 1, 1, 60, 10, 1, 10, 3.1415]
+
+ def sleep_check(delay):
+ expected = sleep_values.pop(0)
+ assert delay == expected
+
+ with patch('time.sleep', sleep_check):
+ result = self.run_module_failed(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '1')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '0')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '-1')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '61')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', 'foo')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '0.9')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '3.1415')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .return_header('Retry-After', '42')
+ .result_str(''),
+ ])
+ print(sleep_values)
+ assert result['msg'] == 'Error: Stopping after 10 failed retries with 429 Too Many Attempts'
+ assert len(sleep_values) == 0
+
+ def test_get_single(self, mocker):
+ with patch('time.sleep', mock_sleep):
+ result = self.run_module_success(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record': 'example.com',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Retry-After', '5')
+ .result_str(''),
+ FetchUrlCall('GET', 429)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Retry-After', '10')
+ .result_str(''),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'set' in result
+ assert result['set']['record'] == 'example.com'
+ assert result['set']['prefix'] == ''
+ assert result['set']['ttl'] == 3600
+ assert result['set']['type'] == 'A'
+ assert result['set']['value'] == ['1.2.3.4']
+ assert 'sets' not in result
+
+ def test_get_single_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prefix': '*',
+ 'type': 'A',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'set' in result
+ assert result['set']['record'] == '*.example.com'
+ assert result['set']['prefix'] == '*'
+ assert result['set']['ttl'] == 3600
+ assert result['set']['type'] == 'A'
+ assert result['set']['value'] == ['1.2.3.5']
+ assert 'sets' not in result
+
+ def test_get_all_for_one_record(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'what': 'all_types_for_record',
+ 'zone_name': 'example.com',
+ 'record': '*.example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'set' not in result
+ assert 'sets' in result
+ sets = result['sets']
+ assert len(sets) == 2
+ assert sets[0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ }
+ assert sets[1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ }
+
+ def test_get_all_for_one_record_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'what': 'all_types_for_record',
+ 'zone_name': 'example.com.',
+ 'prefix': '',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'set' not in result
+ assert 'sets' in result
+ sets = result['sets']
+ assert len(sets) == 4
+ assert sets[0] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ }
+ assert sets[1] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ }
+ assert sets[2] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ }
+ assert sets[3] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': ['ns3.hostserv.eu', 'ns2.hostserv.eu', 'ns1.hostserv.eu'],
+ }
+
+ def test_get_all(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_set_info, {
+ 'hosttech_token': 'foo',
+ 'what': 'all_records',
+ 'zone_id': 42,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert 'set' not in result
+ assert 'sets' in result
+ sets = result['sets']
+ assert len(sets) == 6
+ assert sets[0] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ }
+ assert sets[1] == {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ }
+ assert sets[2] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ }
+ assert sets[3] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ }
+ assert sets[4] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ }
+ assert sets[5] == {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': ['ns3.hostserv.eu', 'ns2.hostserv.eu', 'ns1.hostserv.eu'],
+ }
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_sets.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_sets.py
new file mode 100644
index 000000000..8c609b3af
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_record_sets.py
@@ -0,0 +1,1429 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hosttech_dns_record_sets
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hosttech import (
+ expect_wsdl_authentication,
+ expect_wsdl_value,
+ validate_wsdl_call,
+ validate_wsdl_add_request,
+ validate_wsdl_update_request,
+ validate_wsdl_del_request,
+ create_wsdl_add_result,
+ create_wsdl_update_result,
+ create_wsdl_del_result,
+ HOSTTECH_WSDL_DEFAULT_ZONE_RESULT,
+ HOSTTECH_WSDL_ZONE_NOT_FOUND,
+ HOSTTECH_JSON_ZONE_GET_RESULT,
+ HOSTTECH_JSON_ZONE_LIST_RESULT,
+)
+
+try:
+ import lxml.etree
+ HAS_LXML_ETREE = True
+except ImportError:
+ HAS_LXML_ETREE = False
+
+
+@pytest.mark.skipif(not HAS_LXML_ETREE, reason="Need lxml.etree for WSDL tests")
+class TestHosttechDNSRecordWSDL(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_record_sets.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_sets, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.org',
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.org',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_sets, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_id': 23,
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '23',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_idempotency_present(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'prefix': '',
+ 'type': 'A',
+ 'value': '1.2.3.4',
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_check_mode(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ }
+ ],
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one(self, mocker):
+ new_entry = (131, 42, 'CAA', 'foo', '0 issue "letsencrypt.org"', 3600, None, None)
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'foo.example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ }
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ validate_wsdl_add_request('42', new_entry),
+ ]))
+ .result_str(create_wsdl_add_result(new_entry)),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_modify_list(self, mocker):
+ del_entry = (130, 42, 'NS', '', 'ns3.hostserv.eu', 10800, None, None)
+ update_entry = (131, 42, 'NS', '', 'ns4.hostserv.eu', 10800, None, None)
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'ns1.hostserv.eu',
+ 'ns4.hostserv.eu',
+ ],
+ },
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ validate_wsdl_del_request(del_entry),
+ ]))
+ .result_str(create_wsdl_del_result(True)),
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ validate_wsdl_update_request(update_entry),
+ ]))
+ .result_str(create_wsdl_update_result(update_entry)),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': ['ns3.hostserv.eu', 'ns2.hostserv.eu', 'ns1.hostserv.eu'],
+ },
+ ],
+ }
+ assert result['diff']['after'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': ['ns1.hostserv.eu', 'ns4.hostserv.eu'],
+ },
+ ],
+ }
+
+
+class TestHosttechDNSRecordJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_record_sets.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 23,
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+
+ def test_auth_error_forbidden(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 23,
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Forbidden: you do not have access to this resource (HTTP status 403)'
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://api.ns1.hosttech.eu/api/user/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_key_collision_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 42,
+ 'record_sets': [
+ {
+ 'record': 'test.example.com',
+ 'type': 'A',
+ 'ignore': True,
+ },
+ {
+ 'prefix': 'test',
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['msg'] == 'Found multiple sets for record test.example.com and type A: index #0 and #1'
+
+ def test_idempotency_empty(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 42,
+ 'record_sets': [],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_idempotency_present(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'MX',
+ 'ttl': 3600,
+ 'value': [
+ '10 example.com',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+
+ def test_removal_prune(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'prune': 'true',
+ 'record_sets': [
+ {
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': [],
+ },
+ {
+ 'record': 'example.com',
+ 'type': 'MX',
+ 'ignore': True,
+ },
+ {
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ignore': True,
+ },
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/{0}'.format(127))
+ .result_str(''),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/{0}'.format(128))
+ .result_str(''),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert result['diff']['before'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': ['ns3.hostserv.eu', 'ns2.hostserv.eu', 'ns1.hostserv.eu'],
+ },
+ ],
+ }
+ assert result['diff']['after'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': ['ns3.hostserv.eu', 'ns2.hostserv.eu', 'ns1.hostserv.eu'],
+ },
+ ],
+ }
+
+ def test_change_add_one_check_mode(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 42,
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ },
+ ],
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_check_mode_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 42,
+ 'record_sets': [
+ {
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '0 issue "letsencrypt.org"',
+ ],
+ },
+ ],
+ '_ansible_check_mode': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_failed(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org xxx"',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], '')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org xxx')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'message': 'Internal Server Error'}),
+ ])
+
+ assert result['msg'] == (
+ 'Error: Expected HTTP status 201 for POST https://api.ns1.hosttech.eu/api/user/v1/zones/42/records,'
+ ' but got HTTP status 500 (Internal Server Error) with message "Internal Server Error"'
+ )
+
+ def test_change_add_one(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org xxx"',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], '')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org xxx')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 133,
+ 'type': 'CAA',
+ 'name': '',
+ 'flag': '128',
+ 'tag': 'issue',
+ 'value': 'letsencrypt.org xxx',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'prefix': '',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org"',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], '')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 133,
+ 'type': 'CAA',
+ 'name': '',
+ 'flag': '128',
+ 'tag': 'issue',
+ 'value': 'letsencrypt.org',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_add_one_idn_prefix(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'prefix': '☺',
+ 'type': 'CAA',
+ 'ttl': 3600,
+ 'value': [
+ '128 issue "letsencrypt.org"',
+ ],
+ },
+ ],
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('POST', 201)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records')
+ .expect_json_value_absent(['id'])
+ .expect_json_value(['type'], 'CAA')
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['name'], 'xn--74h')
+ .expect_json_value(['flag'], '128')
+ .expect_json_value(['tag'], 'issue')
+ .expect_json_value(['value'], 'letsencrypt.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 133,
+ 'type': 'CAA',
+ 'name': 'xn--74h',
+ 'flag': '128',
+ 'tag': 'issue',
+ 'value': 'letsencrypt.org',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+
+ def test_change_modify_list(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'ns1.hostserv.eu',
+ 'ns4.hostserv.eu',
+ ],
+ },
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/130')
+ .result_str(''),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/131')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 10800)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'ns4.hostserv.eu')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 131,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'ns4.hostserv.eu',
+ 'ttl': 10800,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': ['ns3.hostserv.eu', 'ns2.hostserv.eu', 'ns1.hostserv.eu'],
+ },
+ ],
+ }
+ assert result['diff']['after'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': ['ns1.hostserv.eu', 'ns4.hostserv.eu'],
+ },
+ ],
+ }
+
+ def test_change_modify_list_nodelete(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': [
+ 'ns1.hostserv.eu',
+ 'ns2.hostserv.eu',
+ ],
+ },
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('DELETE', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/130')
+ .return_header('Content-Type', 'application/json')
+ .result_json({'message': 'record does not exist'}),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': ['ns3.hostserv.eu', 'ns2.hostserv.eu', 'ns1.hostserv.eu'],
+ },
+ ],
+ }
+ assert result['diff']['after'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 10800,
+ 'value': ['ns2.hostserv.eu', 'ns1.hostserv.eu'],
+ },
+ ],
+ }
+
+ def test_change_modify_list_ttl(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_record_sets, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ 'record_sets': [
+ {
+ 'record': 'example.com',
+ 'type': 'NS',
+ 'ttl': 3600,
+ 'value': [
+ 'ns1.hostserv.eu',
+ 'ns4.hostserv.eu',
+ ],
+ },
+ ],
+ '_ansible_diff': True,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ FetchUrlCall('DELETE', 204)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/130')
+ .result_str(''),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/132')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'ns1.hostserv.eu')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 130,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'ns4.hostserv.eu',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ FetchUrlCall('PUT', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42/records/131')
+ .expect_json_value_absent(['id'])
+ .expect_json_value_absent(['type'])
+ .expect_json_value(['ttl'], 3600)
+ .expect_json_value(['comment'], '')
+ .expect_json_value(['ownername'], '')
+ .expect_json_value(['targetname'], 'ns4.hostserv.eu')
+ .return_header('Content-Type', 'application/json')
+ .result_json({
+ 'data': {
+ 'id': 131,
+ 'type': 'NS',
+ 'ownername': '',
+ 'targetname': 'ns4.hostserv.eu',
+ 'ttl': 3600,
+ 'comment': '',
+ },
+ }),
+ ])
+
+ assert result['changed'] is True
+ assert result['zone_id'] == 42
+ assert 'diff' in result
+ assert 'before' in result['diff']
+ assert 'after' in result['diff']
+ assert result['diff']['before'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 10800,
+ 'type': 'NS',
+ 'value': ['ns3.hostserv.eu', 'ns2.hostserv.eu', 'ns1.hostserv.eu'],
+ },
+ ],
+ }
+ assert result['diff']['after'] == {
+ 'record_sets': [
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.5'],
+ },
+ {
+ 'record': '*.example.com',
+ 'prefix': '*',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'A',
+ 'value': ['1.2.3.4'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'AAAA',
+ 'value': ['2001:1:2::3'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'ttl': 3600,
+ 'type': 'MX',
+ 'value': ['10 example.com'],
+ },
+ {
+ 'record': 'example.com',
+ 'prefix': '',
+ 'type': 'NS',
+ 'ttl': 3600,
+ 'value': ['ns1.hostserv.eu', 'ns4.hostserv.eu'],
+ },
+ ],
+ }
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_zone_info.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_zone_info.py
new file mode 100644
index 000000000..e95aebe3a
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_hosttech_dns_zone_info.py
@@ -0,0 +1,351 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.utils.fetch_url_module_framework import (
+ BaseTestModule,
+ FetchUrlCall,
+)
+
+from ansible_collections.community.dns.plugins.modules import hosttech_dns_zone_info
+
+# These imports are needed so patching below works
+import ansible_collections.community.dns.plugins.module_utils.http # noqa: F401, pylint: disable=unused-import
+
+from .hosttech import (
+ expect_wsdl_authentication,
+ expect_wsdl_value,
+ validate_wsdl_call,
+ HOSTTECH_WSDL_DEFAULT_ZONE_RESULT,
+ HOSTTECH_WSDL_ZONE_NOT_FOUND,
+ HOSTTECH_JSON_ZONE_GET_RESULT,
+ HOSTTECH_JSON_ZONE_2_GET_RESULT,
+ HOSTTECH_JSON_ZONE_LIST_RESULT,
+)
+
+try:
+ import lxml.etree
+ HAS_LXML_ETREE = True
+except ImportError:
+ HAS_LXML_ETREE = False
+
+
+@pytest.mark.skipif(not HAS_LXML_ETREE, reason="Need lxml.etree for WSDL tests")
+class TestHosttechDNSZoneInfoWSDL(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_zone_info.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_zone_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.org',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.org',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_zone_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_id': 23,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '23',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_ZONE_NOT_FOUND),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_get(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_zone_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_name': 'example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ 'example.com',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert result['zone_name'] == 'example.com'
+
+ def test_get_id(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_zone_info, {
+ 'hosttech_username': 'foo',
+ 'hosttech_password': 'bar',
+ 'zone_id': '42',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('POST', 200)
+ .expect_content_predicate(validate_wsdl_call([
+ expect_wsdl_authentication('foo', 'bar'),
+ expect_wsdl_value(
+ [lxml.etree.QName('https://ns1.hosttech.eu/public/api', 'getZone').text, 'sZoneName'],
+ '42',
+ ('http://www.w3.org/2001/XMLSchema', 'string')
+ ),
+ ]))
+ .result_str(HOSTTECH_WSDL_DEFAULT_ZONE_RESULT),
+ ])
+
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert result['zone_name'] == 'example.com'
+ assert result['zone_info'] == {
+ 'email': 'dns@hosttech.eu',
+ 'ttl': 10800,
+ }
+
+
+class TestHosttechDNSZoneInfoJSON(BaseTestModule):
+ MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.dns.plugins.modules.hosttech_dns_zone_info.AnsibleModule'
+ MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.dns.plugins.module_utils.http.fetch_url'
+
+ def test_unknown_zone(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_zone_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_unknown_zone_id(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_zone_info, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 23,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 404)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .return_header('Content-Type', 'application/json')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Zone not found'
+
+ def test_auth_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_zone_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 401)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
+
+ def test_auth_error_forbidden(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_zone_info, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 23,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 403)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/23')
+ .result_json(dict(message="")),
+ ])
+
+ assert result['msg'] == 'Cannot authenticate: Forbidden: you do not have access to this resource (HTTP status 403)'
+
+ def test_other_error(self, mocker):
+ result = self.run_module_failed(mocker, hosttech_dns_zone_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.org',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 500)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.org')
+ .result_str(''),
+ ])
+
+ assert result['msg'].startswith('Error: GET https://api.ns1.hosttech.eu/api/user/v1/zones?')
+ assert 'did not yield JSON data, but HTTP status code 500 with Content-Type' in result['msg']
+
+ def test_get(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_zone_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'example.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'example.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert result['zone_name'] == 'example.com'
+ assert result['zone_info'] == {
+ 'email': 'test@example.com',
+ 'ttl': 10800,
+ 'dnssec': False,
+ 'dnssec_email': None,
+ 'ds_records': None,
+ }
+
+ def test_get_id(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_zone_info, {
+ 'hosttech_token': 'foo',
+ 'zone_id': 42,
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/42')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 42
+ assert result['zone_name'] == 'example.com'
+ assert result['zone_info'] == {
+ 'email': 'test@example.com',
+ 'ttl': 10800,
+ 'dnssec': False,
+ 'dnssec_email': None,
+ 'ds_records': None,
+ }
+
+ def test_get_dnssec(self, mocker):
+ result = self.run_module_success(mocker, hosttech_dns_zone_info, {
+ 'hosttech_token': 'foo',
+ 'zone_name': 'foo.com',
+ '_ansible_remote_tmp': '/tmp/tmp',
+ '_ansible_keep_remote_files': True,
+ }, [
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones', without_query=True)
+ .expect_query_values('query', 'foo.com')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_LIST_RESULT),
+ FetchUrlCall('GET', 200)
+ .expect_header('accept', 'application/json')
+ .expect_header('authorization', 'Bearer foo')
+ .expect_url('https://api.ns1.hosttech.eu/api/user/v1/zones/43')
+ .return_header('Content-Type', 'application/json')
+ .result_json(HOSTTECH_JSON_ZONE_2_GET_RESULT),
+ ])
+ assert result['changed'] is False
+ assert result['zone_id'] == 43
+ assert result['zone_name'] == 'foo.com'
+ assert result['zone_info'] == {
+ 'email': 'test@foo.com',
+ 'ttl': 10800,
+ 'dnssec': True,
+ 'dnssec_email': 'test@foo.com',
+ 'ds_records': [
+ {
+ 'key_tag': 12345,
+ 'algorithm': 8,
+ 'digest_type': 1,
+ 'digest': '012356789ABCDEF0123456789ABCDEF012345678',
+ 'flags': 257,
+ 'protocol': 3,
+ 'public_key':
+ 'MuhdzsQdqEGShwjtJDKZZjdKqUSGluFzTTinpuEeIRzLLcgkwgAPKWFa '
+ 'eQntNlmcNDeCziGwpdvhJnvKXEMbFcZwsaDIJuWqERxAQNGABWfPlCLh '
+ 'HQPnbpRPNKipSdBaUhuOubvFvjBpFAwiwSAapRDVsAgKvjXucfXpFfYb '
+ 'pCundbAXBWhbpHVbqgmGoixXzFSwUsGVYLPpBCiDlLJwzjRKYYaoVYge '
+ 'kMtKFYUVnWIKbectWkDFdVqXwkKigCUDiuTTJxOBRJRNzGiDNMWBjYSm '
+ 'bBCAHMaMYaghLbYTwyKXltdHTHwBwtswGNfpnEdSpKFzZJonBZArQfHD '
+ 'lfceKgmKwEF=',
+ },
+ {
+ 'key_tag': 12345,
+ 'algorithm': 8,
+ 'digest_type': 2,
+ 'digest': '0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF',
+ 'flags': 257,
+ 'protocol': 3,
+ 'public_key':
+ 'MuhdzsQdqEGShwjtJDKZZjdKqUSGluFzTTinpuEeIRzLLcgkwgAPKWFa '
+ 'eQntNlmcNDeCziGwpdvhJnvKXEMbFcZwsaDIJuWqERxAQNGABWfPlCLh '
+ 'HQPnbpRPNKipSdBaUhuOubvFvjBpFAwiwSAapRDVsAgKvjXucfXpFfYb '
+ 'pCundbAXBWhbpHVbqgmGoixXzFSwUsGVYLPpBCiDlLJwzjRKYYaoVYge '
+ 'kMtKFYUVnWIKbectWkDFdVqXwkKigCUDiuTTJxOBRJRNzGiDNMWBjYSm '
+ 'bBCAHMaMYaghLbYTwyKXltdHTHwBwtswGNfpnEdSpKFzZJonBZArQfHD '
+ 'lfceKgmKwEF=',
+ }
+ ],
+ }
diff --git a/ansible_collections/community/dns/tests/unit/plugins/modules/test_wait_for_txt.py b/ansible_collections/community/dns/tests/unit/plugins/modules/test_wait_for_txt.py
new file mode 100644
index 000000000..829d6465e
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/modules/test_wait_for_txt.py
@@ -0,0 +1,1425 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock, patch
+
+from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
+ set_module_args,
+ ModuleTestCase,
+ AnsibleExitJson,
+ AnsibleFailJson,
+)
+
+from ansible_collections.community.dns.plugins.modules import wait_for_txt
+
+from ..module_utils.resolver_helper import (
+ mock_resolver,
+ mock_query_udp,
+ create_mock_answer,
+ create_mock_response,
+)
+
+# We need dnspython
+dns = pytest.importorskip('dns')
+
+
+def mock_sleep(delay):
+ pass
+
+
+def mock_monotonic(call_sequence):
+ def f():
+ assert len(call_sequence) > 0, 'monotonic() was called more often than expected'
+ value = call_sequence[0]
+ del call_sequence[0]
+ return value
+
+ return f
+
+
+class TestWaitForTXT(ModuleTestCase):
+ def test_single(self):
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.AAAA, '1:2::3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.org',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '4.4.4.4'),
+ )),
+ },
+ {
+ 'target': 'ns.example.org',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('1:2::3', '3.3.3.3'): [
+ {
+ 'target': dns.name.from_unicode(u'example.org'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'asdf'),
+ )),
+ },
+ ],
+ ('4.4.4.4', ): [
+ {
+ 'target': dns.name.from_unicode(u'example.org'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'asdf'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'www.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ ), dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'example.org')
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'org'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.org'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.org'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.org'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'www.example.com',
+ 'values': [
+ 'asdf',
+ ]
+ },
+ ],
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['changed'] is False
+ assert exc.value.args[0]['completed'] == 1
+ assert len(exc.value.args[0]['records']) == 1
+ assert exc.value.args[0]['records'][0]['name'] == 'www.example.com'
+ assert exc.value.args[0]['records'][0]['done'] is True
+ assert exc.value.args[0]['records'][0]['values'] == {
+ 'ns.example.com': ['asdf'],
+ 'ns.example.org': ['asdf'],
+ }
+ assert exc.value.args[0]['records'][0]['check_count'] == 1
+
+ def test_double(self):
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('3.3.3.3', ): [
+ {
+ 'target': dns.name.from_unicode(u'www.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'www.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'fdsa'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'mail.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'mail.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"any bar"'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'www.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'www.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'fdsa'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'asdf'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'www.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'www.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'asdf'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'www.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, authority=[dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'mail.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, authority=[dns.rrset.from_rdata(
+ 'mail.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'www.example.com',
+ 'values': [
+ 'asdf',
+ ],
+ 'mode': 'equals',
+ },
+ {
+ 'name': 'mail.example.com',
+ 'values': [
+ 'foo bar',
+ 'any bar',
+ ],
+ 'mode': 'superset',
+ },
+ ],
+ 'timeout': 10,
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['changed'] is False
+ assert exc.value.args[0]['completed'] == 2
+ assert len(exc.value.args[0]['records']) == 2
+ assert exc.value.args[0]['records'][0]['name'] == 'www.example.com'
+ assert exc.value.args[0]['records'][0]['done'] is True
+ assert exc.value.args[0]['records'][0]['values'] == {
+ 'ns.example.com': ['asdf'],
+ }
+ assert exc.value.args[0]['records'][0]['check_count'] == 3
+ assert exc.value.args[0]['records'][1]['name'] == 'mail.example.com'
+ assert exc.value.args[0]['records'][1]['done'] is True
+ assert exc.value.args[0]['records'][1]['values'] == {
+ 'ns.example.com': ['any bar'],
+ }
+ assert exc.value.args[0]['records'][1]['check_count'] == 1
+
+ def test_subset(self):
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('3.3.3.3', ): [
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'as df'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"another one"'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"foo bar"'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"another one"'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"foo bar"'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"another one"'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'as df'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'example.com',
+ 'values': [
+ 'asdf',
+ 'asdf',
+ 'foo bar',
+ ],
+ 'mode': 'subset',
+ },
+ ],
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['changed'] is False
+ assert exc.value.args[0]['completed'] == 1
+ assert len(exc.value.args[0]['records']) == 1
+ assert exc.value.args[0]['records'][0]['name'] == 'example.com'
+ assert exc.value.args[0]['records'][0]['done'] is True
+ assert exc.value.args[0]['records'][0]['values'] == {
+ 'ns.example.com': ['foo bar', 'another one', 'asdf'],
+ }
+ assert exc.value.args[0]['records'][0]['check_count'] == 3
+
+ def test_superset(self):
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('3.3.3.3', ): [
+ {
+ 'target': dns.name.from_unicode(u'www.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'www.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"bumble bee"'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'mail.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(),
+ },
+ {
+ 'target': dns.name.from_unicode(u'www.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'www.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'fdsa'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'asdf'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'www.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'www.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'asdf ""'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'bee'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'www.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, authority=[dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'mail.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, authority=[dns.rrset.from_rdata(
+ 'mail.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'www.example.com',
+ 'values': [
+ 'asdf',
+ 'bee',
+ ],
+ 'mode': 'superset',
+ },
+ {
+ 'name': 'mail.example.com',
+ 'values': [
+ 'foo bar',
+ 'any bar',
+ ],
+ 'mode': 'superset',
+ },
+ ],
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['changed'] is False
+ assert exc.value.args[0]['completed'] == 2
+ assert len(exc.value.args[0]['records']) == 2
+ assert exc.value.args[0]['records'][0]['name'] == 'www.example.com'
+ assert exc.value.args[0]['records'][0]['done'] is True
+ assert exc.value.args[0]['records'][0]['values'] == {
+ 'ns.example.com': ['asdf', 'bee'],
+ }
+ assert exc.value.args[0]['records'][0]['check_count'] == 3
+ assert exc.value.args[0]['records'][1]['name'] == 'mail.example.com'
+ assert exc.value.args[0]['records'][1]['done'] is True
+ assert exc.value.args[0]['records'][1]['values'] == {
+ 'ns.example.com': [],
+ }
+ assert exc.value.args[0]['records'][1]['check_count'] == 1
+
+ def test_superset_not_empty(self):
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('3.3.3.3', ): [
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"bumble bee"'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'bumble'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'bee'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'wizard'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'bumble'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'bee'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'example.com',
+ 'values': [
+ 'bumble',
+ 'bee',
+ ],
+ 'mode': 'superset_not_empty',
+ },
+ ],
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['changed'] is False
+ assert exc.value.args[0]['completed'] == 1
+ assert len(exc.value.args[0]['records']) == 1
+ assert exc.value.args[0]['records'][0]['name'] == 'example.com'
+ assert exc.value.args[0]['records'][0]['done'] is True
+ assert exc.value.args[0]['records'][0]['values'] == {
+ 'ns.example.com': ['bumble', 'bee'],
+ }
+ assert exc.value.args[0]['records'][0]['check_count'] == 4
+
+ def test_equals(self):
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('3.3.3.3', ): [
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"bumble bee"'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'bumble bee'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'wizard'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"bumble bee"'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'wizard'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'foo'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'example.com',
+ 'values': [
+ 'foo',
+ 'bumble bee',
+ 'wizard',
+ ],
+ 'mode': 'equals',
+ },
+ ],
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['changed'] is False
+ assert exc.value.args[0]['completed'] == 1
+ assert len(exc.value.args[0]['records']) == 1
+ assert exc.value.args[0]['records'][0]['name'] == 'example.com'
+ assert exc.value.args[0]['records'][0]['done'] is True
+ assert exc.value.args[0]['records'][0]['values'] == {
+ 'ns.example.com': ['bumble bee', 'wizard', 'foo'],
+ }
+ assert exc.value.args[0]['records'][0]['check_count'] == 4
+
+ def test_equals_ordered(self):
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('3.3.3.3', ): [
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"bumble bee"'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"bumble bee"'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'wizard'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'foo'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'foo'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"bumble bee"'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'wizard'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'example.com',
+ 'values': [
+ 'foo',
+ 'bumble bee',
+ 'wizard',
+ ],
+ 'mode': 'equals_ordered',
+ },
+ ],
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['changed'] is False
+ assert exc.value.args[0]['completed'] == 1
+ assert len(exc.value.args[0]['records']) == 1
+ assert exc.value.args[0]['records'][0]['name'] == 'example.com'
+ assert exc.value.args[0]['records'][0]['done'] is True
+ assert exc.value.args[0]['records'][0]['values'] == {
+ 'ns.example.com': ['foo', 'bumble bee', 'wizard'],
+ }
+ assert exc.value.args[0]['records'][0]['check_count'] == 4
+
+ def test_timeout(self):
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ ('3.3.3.3', ): [
+ {
+ 'target': dns.name.from_unicode(u'www.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'www.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'fdsa'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'mail.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'mail.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"any bar"'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'www.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'www.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'fdsa'),
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'asdf'),
+ )),
+ },
+ {
+ 'target': dns.name.from_unicode(u'www.example.com'),
+ 'rdtype': dns.rdatatype.TXT,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'www.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'asdfasdf'),
+ )),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'www.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, authority=[dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'mail.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, authority=[dns.rrset.from_rdata(
+ 'mail.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with patch('ansible_collections.community.dns.plugins.modules.wait_for_txt.monotonic',
+ mock_monotonic([0, 0.01, 1.2, 6.013, 7.41, 12.021])):
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'www.example.com',
+ 'values': [
+ 'asdf',
+ ],
+ 'mode': 'equals',
+ },
+ {
+ 'name': 'mail.example.com',
+ 'values': [
+ 'foo bar',
+ 'any bar',
+ ],
+ 'mode': 'superset',
+ },
+ ],
+ 'timeout': 12,
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['msg'] == 'Timeout (1 out of 2 check(s) passed).'
+ assert exc.value.args[0]['completed'] == 1
+ assert len(exc.value.args[0]['records']) == 2
+ assert exc.value.args[0]['records'][0]['name'] == 'www.example.com'
+ assert exc.value.args[0]['records'][0]['done'] is False
+ assert exc.value.args[0]['records'][0]['values'] == {
+ 'ns.example.com': ['asdfasdf'],
+ }
+ assert exc.value.args[0]['records'][0]['check_count'] == 3
+ assert exc.value.args[0]['records'][1]['name'] == 'mail.example.com'
+ assert exc.value.args[0]['records'][1]['done'] is True
+ assert exc.value.args[0]['records'][1]['values'] == {
+ 'ns.example.com': ['any bar'],
+ }
+ assert exc.value.args[0]['records'][1]['check_count'] == 1
+
+ def test_nxdomain(self):
+ resolver = mock_resolver(['1.1.1.1'], {})
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NXDOMAIN),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NXDOMAIN),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with patch('ansible_collections.community.dns.plugins.modules.wait_for_txt.monotonic',
+ mock_monotonic([0, 0.01, 1.2, 6.013])):
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'www.example.com',
+ 'values': [
+ 'asdf',
+ ],
+ },
+ ],
+ 'timeout': 2,
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['msg'] == 'Timeout (0 out of 1 check(s) passed).'
+ assert exc.value.args[0]['completed'] == 0
+ assert len(exc.value.args[0]['records']) == 1
+ assert exc.value.args[0]['records'][0]['name'] == 'www.example.com'
+ assert exc.value.args[0]['records'][0]['done'] is False
+ assert exc.value.args[0]['records'][0]['values'] == {}
+ assert exc.value.args[0]['records'][0]['check_count'] == 2
+
+ def test_servfail(self):
+ resolver = mock_resolver(['1.1.1.1'], {})
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.SERVFAIL),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'www.example.com',
+ 'values': [
+ 'asdf',
+ ],
+ },
+ ],
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['msg'] == 'Unexpected resolving error: Error SERVFAIL while querying 1.1.1.1 with query get NS for "com."'
+ assert exc.value.args[0]['completed'] == 0
+ assert len(exc.value.args[0]['records']) == 1
+ assert exc.value.args[0]['records'][0]['name'] == 'www.example.com'
+ assert exc.value.args[0]['records'][0]['done'] is False
+ assert 'values' not in exc.value.args[0]['records'][0]
+ assert exc.value.args[0]['records'][0]['check_count'] == 0
+
+ def test_cname_loop(self):
+ fake_query = MagicMock()
+ fake_query.question = 'Doctor Who?'
+ resolver = mock_resolver(['1.1.1.1'], {
+ ('1.1.1.1', ): [
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.com',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'),
+ )),
+ },
+ {
+ 'target': 'ns.example.com',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ {
+ 'target': 'ns.example.org',
+ 'rdtype': dns.rdatatype.A,
+ 'lifetime': 10,
+ 'result': create_mock_answer(dns.rrset.from_rdata(
+ 'ns.example.org',
+ 300,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '4.4.4.4'),
+ )),
+ },
+ {
+ 'target': 'ns.example.org',
+ 'rdtype': dns.rdatatype.AAAA,
+ 'lifetime': 10,
+ 'raise': dns.resolver.NoAnswer(response=fake_query),
+ },
+ ],
+ })
+ udp_sequence = [
+ {
+ 'query_target': dns.name.from_unicode(u'com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'www.example.com'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'),
+ ), dns.rrset.from_rdata(
+ 'www.example.com',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'example.org')
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'org'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.org'),
+ )]),
+ },
+ {
+ 'query_target': dns.name.from_unicode(u'example.org'),
+ 'query_type': dns.rdatatype.NS,
+ 'nameserver': '1.1.1.1',
+ 'kwargs': {
+ 'timeout': 10,
+ },
+ 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata(
+ 'example.org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.org'),
+ ), dns.rrset.from_rdata(
+ 'example.org',
+ 3600,
+ dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'www.example.com')
+ )]),
+ },
+ ]
+ with patch('dns.resolver.get_default_resolver', resolver):
+ with patch('dns.resolver.Resolver', resolver):
+ with patch('dns.query.udp', mock_query_udp(udp_sequence)):
+ with patch('time.sleep', mock_sleep):
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args({
+ 'records': [
+ {
+ 'name': 'www.example.com',
+ 'values': [
+ 'asdf',
+ ],
+ },
+ ],
+ })
+ wait_for_txt.main()
+
+ print(exc.value.args[0])
+ assert exc.value.args[0]['msg'] == 'Unexpected resolving error: Found CNAME loop starting at www.example.com'
+ assert exc.value.args[0]['completed'] == 0
+ assert len(exc.value.args[0]['records']) == 1
+ assert exc.value.args[0]['records'][0]['name'] == 'www.example.com'
+ assert exc.value.args[0]['records'][0]['done'] is False
+ assert 'values' not in exc.value.args[0]['records'][0]
+ assert exc.value.args[0]['records'][0]['check_count'] == 0
diff --git a/ansible_collections/community/dns/tests/unit/plugins/plugin_utils/test_public_suffix.py b/ansible_collections/community/dns/tests/unit/plugins/plugin_utils/test_public_suffix.py
new file mode 100644
index 000000000..984fd7600
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/plugins/plugin_utils/test_public_suffix.py
@@ -0,0 +1,191 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Note that this file contains some public domain test data from
+# https://raw.githubusercontent.com/publicsuffix/list/master/tests/test_psl.txt
+# The data is marked and documented as public domain appropriately.
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible_collections.community.dns.plugins.plugin_utils.public_suffix import (
+ PublicSuffixList,
+ PUBLIC_SUFFIX_LIST,
+)
+
+
+TEST_GET_SUFFIX = [
+ ('', {}, {}, '', ''),
+ ('.', {}, {}, '', ''),
+ ('foo.com', {}, {}, 'com', 'foo.com'),
+ ('bar.foo.com.', {}, {}, 'com.', 'foo.com.'),
+ ('BaR.fOo.CoM.', {'normalize_result': True}, {}, 'com.', 'foo.com.'),
+ ('BaR.fOo.CoM.', {}, {}, 'CoM.', 'fOo.CoM.'),
+ ('com', {}, {}, 'com', ''),
+ ('com', {}, {'only_if_registerable': False}, 'com', 'com'),
+ ('com', {'keep_unknown_suffix': False}, {}, 'com', ''),
+ ('foo.com', {}, {}, 'com', 'foo.com'),
+ ('foo.com', {'keep_unknown_suffix': False}, {}, 'com', 'foo.com'),
+ ('foobarbaz', {}, {}, 'foobarbaz', ''),
+ ('foobarbaz', {}, {'only_if_registerable': False}, 'foobarbaz', 'foobarbaz'),
+ ('foobarbaz', {'keep_unknown_suffix': False}, {}, '', ''),
+ ('foo.foobarbaz', {}, {}, 'foobarbaz', 'foo.foobarbaz'),
+ ('foo.foobarbaz', {'keep_unknown_suffix': False}, {}, '', ''),
+ ('-a.com', {}, {}, '', ''), # invalid domain name (leading dash in label)
+ ('a-.com', {}, {}, '', ''), # invalid domain name (trailing dash in label)
+ ('-.com', {}, {}, '', ''), # invalid domain name (leading and trailing dash in label)
+ ('.com', {}, {}, '', ''), # invalid domain name (empty label)
+ ('test.cloudfront.net', {}, {}, 'cloudfront.net', 'test.cloudfront.net'), # private rule
+ ('test.cloudfront.net', {'icann_only': True}, {}, 'net', 'cloudfront.net'),
+]
+
+
+@pytest.mark.parametrize("domain, kwargs, reg_extra_kwargs, suffix, reg_domain", TEST_GET_SUFFIX)
+def test_get_suffix(domain, kwargs, reg_extra_kwargs, suffix, reg_domain):
+ assert PUBLIC_SUFFIX_LIST.get_suffix(domain, **kwargs) == suffix
+ kwargs.update(reg_extra_kwargs)
+ assert PUBLIC_SUFFIX_LIST.get_registrable_domain(domain, **kwargs) == reg_domain
+
+
+# -------------------------------------------------------------------------------------------------
+# The following list is taken from https://raw.githubusercontent.com/publicsuffix/list/master/tests/test_psl.txt
+# Any copyright for this list is dedicated to the Public Domain. (https://creativecommons.org/publicdomain/zero/1.0/)
+# This list has been provided by Rob Stradling of Comodo (see last section on https://publicsuffix.org/list/).
+TEST_SUFFIX_OFFICIAL_TESTS = [
+ # '' input.
+ ('', '', {}),
+ # Mixed case.
+ ('COM', '', {}),
+ ('example.COM', 'example.com', {'normalize_result': True}),
+ ('WwW.example.COM', 'example.com', {'normalize_result': True}),
+ ('example.COM', 'example.COM', {}),
+ ('WwW.example.COM', 'example.COM', {}),
+ # Leading dot.
+ ('.com', '', {}),
+ ('.example', '', {}),
+ ('.example.com', '', {}),
+ ('.example.example', '', {}),
+ # Unlisted TLD.
+ ('example', '', {}),
+ ('example.example', 'example.example', {}),
+ ('b.example.example', 'example.example', {}),
+ ('a.b.example.example', 'example.example', {}),
+ # Listed, but non-Internet, TLD.
+ # ('local', '', {}),
+ # ('example.local', '', {}),
+ # ('b.example.local', '', {}),
+ # ('a.b.example.local', '', {}),
+ # TLD with only 1 rule.
+ ('biz', '', {}),
+ ('domain.biz', 'domain.biz', {}),
+ ('b.domain.biz', 'domain.biz', {}),
+ ('a.b.domain.biz', 'domain.biz', {}),
+ # TLD with some 2-level rules.
+ ('com', '', {}),
+ ('example.com', 'example.com', {}),
+ ('b.example.com', 'example.com', {}),
+ ('a.b.example.com', 'example.com', {}),
+ ('uk.com', '', {}),
+ ('example.uk.com', 'example.uk.com', {}),
+ ('b.example.uk.com', 'example.uk.com', {}),
+ ('a.b.example.uk.com', 'example.uk.com', {}),
+ ('test.ac', 'test.ac', {}),
+ # TLD with only 1 (wildcard) rule.
+ ('mm', '', {}),
+ ('c.mm', '', {}),
+ ('b.c.mm', 'b.c.mm', {}),
+ ('a.b.c.mm', 'b.c.mm', {}),
+ # More complex TLD.
+ ('jp', '', {}),
+ ('test.jp', 'test.jp', {}),
+ ('www.test.jp', 'test.jp', {}),
+ ('ac.jp', '', {}),
+ ('test.ac.jp', 'test.ac.jp', {}),
+ ('www.test.ac.jp', 'test.ac.jp', {}),
+ ('kyoto.jp', '', {}),
+ ('test.kyoto.jp', 'test.kyoto.jp', {}),
+ ('ide.kyoto.jp', '', {}),
+ ('b.ide.kyoto.jp', 'b.ide.kyoto.jp', {}),
+ ('a.b.ide.kyoto.jp', 'b.ide.kyoto.jp', {}),
+ ('c.kobe.jp', '', {}),
+ ('b.c.kobe.jp', 'b.c.kobe.jp', {}),
+ ('a.b.c.kobe.jp', 'b.c.kobe.jp', {}),
+ ('city.kobe.jp', 'city.kobe.jp', {}),
+ ('www.city.kobe.jp', 'city.kobe.jp', {}),
+ # TLD with a wildcard rule and exceptions.
+ ('ck', '', {}),
+ ('test.ck', '', {}),
+ ('b.test.ck', 'b.test.ck', {}),
+ ('a.b.test.ck', 'b.test.ck', {}),
+ ('www.ck', 'www.ck', {}),
+ ('www.www.ck', 'www.ck', {}),
+ # US K12.
+ ('us', '', {}),
+ ('test.us', 'test.us', {}),
+ ('www.test.us', 'test.us', {}),
+ ('ak.us', '', {}),
+ ('test.ak.us', 'test.ak.us', {}),
+ ('www.test.ak.us', 'test.ak.us', {}),
+ ('k12.ak.us', '', {}),
+ ('test.k12.ak.us', 'test.k12.ak.us', {}),
+ ('www.test.k12.ak.us', 'test.k12.ak.us', {}),
+ # IDN labels.
+ (u'食狮.com.cn', u'食狮.com.cn', {}),
+ (u'食狮.公司.cn', u'食狮.公司.cn', {}),
+ (u'www.食狮.公司.cn', u'食狮.公司.cn', {}),
+ (u'shishi.公司.cn', u'shishi.公司.cn', {}),
+ (u'公司.cn', u'', {}),
+ (u'食狮.中国', u'食狮.中国', {}),
+ (u'www.食狮.中国', u'食狮.中国', {}),
+ (u'shishi.中国', u'shishi.中国', {}),
+ (u'中国', u'', {}),
+ # Same as above, but punycoded. (TODO: punycode not supported yet!)
+ ('xn--85x722f.com.cn', 'xn--85x722f.com.cn', {}),
+ ('xn--85x722f.xn--55qx5d.cn', 'xn--85x722f.xn--55qx5d.cn', {}),
+ ('www.xn--85x722f.xn--55qx5d.cn', 'xn--85x722f.xn--55qx5d.cn', {}),
+ ('shishi.xn--55qx5d.cn', 'shishi.xn--55qx5d.cn', {}),
+ ('xn--55qx5d.cn', '', {}),
+ ('xn--85x722f.xn--fiqs8s', 'xn--85x722f.xn--fiqs8s', {}),
+ ('www.xn--85x722f.xn--fiqs8s', 'xn--85x722f.xn--fiqs8s', {}),
+ ('shishi.xn--fiqs8s', 'shishi.xn--fiqs8s', {}),
+ ('xn--fiqs8s', '', {}),
+]
+# End of public domain test data
+# -------------------------------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize("domain, registrable_domain, kwargs", TEST_SUFFIX_OFFICIAL_TESTS)
+def test_get_suffix_official(domain, registrable_domain, kwargs):
+ reg_domain = PUBLIC_SUFFIX_LIST.get_registrable_domain(domain, **kwargs)
+ assert reg_domain == registrable_domain
+
+
+def test_load_psl_dot(tmpdir):
+ fn = tmpdir / 'psl.dat'
+ fn.write('''// ===BEGIN BLA BLA DOMAINS===
+.com.
+// ===END BLA BLA DOMAINS==='''.encode('utf-8'))
+ psl = PublicSuffixList.load(str(fn))
+ assert len(psl._rules) == 1
+ rule = psl._rules[0]
+ assert rule.labels == ('com', )
+ assert rule.exception_rule is False
+ assert rule.part == 'bla bla'
+
+
+def test_load_psl_no_part(tmpdir):
+ fn = tmpdir / 'psl.dat'
+ fn.write('''// ===BEGIN BLA BLA DOMAINS===
+com
+// ===END BLA BLA DOMAINS===
+net'''.encode('utf-8'))
+ with pytest.raises(Exception) as excinfo:
+ PublicSuffixList.load(str(fn))
+ assert str(excinfo.value) == 'Internal error: found PSL entry with no part!'
diff --git a/ansible_collections/community/dns/tests/unit/requirements.txt b/ansible_collections/community/dns/tests/unit/requirements.txt
new file mode 100644
index 000000000..8b7c7cd61
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/requirements.txt
@@ -0,0 +1,11 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+unittest2 ; python_version < '2.7'
+importlib ; python_version < '2.7'
+
+dnspython
+
+lxml < 4.3.0 ; python_version < '2.7' # lxml 4.3.0 and later require python 2.7 or later
+lxml ; python_version >= '2.7'
diff --git a/ansible_collections/community/dns/tests/unit/requirements.yml b/ansible_collections/community/dns/tests/unit/requirements.yml
new file mode 100644
index 000000000..586a6a1b3
--- /dev/null
+++ b/ansible_collections/community/dns/tests/unit/requirements.yml
@@ -0,0 +1,7 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+collections:
+- community.internal_test_tools