From aff0f74f4921aaafb85ff0abc9b7937669fa80ae Mon Sep 17 00:00:00 2001 From: Darkbat91 Date: Thu, 22 Jan 2026 15:39:07 -0500 Subject: [PATCH 1/2] Cloudflare API update * Upgrade cloudflare API to 4.3.1 * setup testing and mock constructs --- certbot-dns-cloudflare/setup.py | 4 +- .../_internal/dns_cloudflare.py | 48 +++++----- .../_internal/tests/dns_cloudflare_test.py | 88 +++++++++++-------- tools/oldest_constraints.txt | 2 +- tools/pinning/oldest/pyproject.toml | 2 +- tools/requirements.txt | 2 +- 6 files changed, 81 insertions(+), 65 deletions(-) diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index c8337eb11..6288b999d 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -5,9 +5,7 @@ from setuptools import setup version = '5.3.0.dev0' install_requires = [ - # for now, do not upgrade to cloudflare>=2.20 to avoid deprecation warnings and the breaking - # changes in version 3.0. see https://github.com/certbot/certbot/issues/9938 - 'cloudflare>=2.19, <2.20', + 'cloudflare>=4.3, <5', ] if os.environ.get('SNAP_BUILD'): diff --git a/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py b/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py index 25ce84171..d0bb6664b 100644 --- a/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py +++ b/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py @@ -5,7 +5,7 @@ from typing import Callable from typing import Optional from typing import cast -import CloudFlare +from cloudflare import Cloudflare, APIError from certbot import errors from certbot.plugins import dns_common @@ -98,12 +98,12 @@ class _CloudflareClient: # We can't use named arguments in this case, as it would break compatibility with # the Cloudflare library since version 2.10.1, as the `token` argument was used for # tokens and keys alike and the `key` argument did not exist in earlier versions. - self.cf = CloudFlare.CloudFlare(email, api_key) + self.cf = Cloudflare(api_email=email, api_key=api_key) else: # If no email was specified, we're using just a token. Let's use the named argument # for simplicity, which is compatible with all (current) versions of the Cloudflare # library. - self.cf = CloudFlare.CloudFlare(token=api_token) + self.cf = Cloudflare(api_token=api_token) def add_txt_record(self, domain: str, record_name: str, record_content: str, record_ttl: int) -> None: @@ -126,15 +126,15 @@ class _CloudflareClient: try: logger.debug('Attempting to add record to zone %s: %s', zone_id, data) - self.cf.zones.dns_records.post(zone_id, data=data) # zones | pylint: disable=no-member - except CloudFlare.exceptions.CloudFlareAPIError as e: - code = int(e) + self.cf.dns.records.create(zone_id=zone_id, **data) # zones | pylint: disable=no-member + except APIError as e: + code = int(e.errors[0].code) hint = None if code == 1009: hint = 'Does your API token have "Zone:DNS:Edit" permissions?' - logger.error('Encountered CloudFlareAPIError adding TXT record: %d %s', e, e) + logger.error('Encountered APIError adding TXT record: %d %s', code, e) raise errors.PluginError('Error communicating with the Cloudflare API: {0}{1}' .format(e, ' ({0})'.format(hint) if hint else '')) @@ -166,10 +166,10 @@ class _CloudflareClient: if record_id: try: # zones | pylint: disable=no-member - self.cf.zones.dns_records.delete(zone_id, record_id) + self.cf.dns.records.delete(zone_id, record_id) logger.debug('Successfully deleted TXT record.') - except CloudFlare.exceptions.CloudFlareAPIError as e: - logger.warning('Encountered CloudFlareAPIError deleting TXT record: %s', e) + except APIError as e: + logger.warning('Encountered APIError deleting TXT record: %s', e) else: logger.debug('TXT record not found; no cleanup needed.') else: @@ -186,7 +186,7 @@ class _CloudflareClient: """ zone_name_guesses = dns_common.base_domain_name_guesses(domain) - zones: list[dict[str, Any]] = [] + zones: list[dict[str, Any]] | None = None code = msg = None for zone_name in zone_name_guesses: @@ -194,9 +194,9 @@ class _CloudflareClient: 'per_page': 1} try: - zones = self.cf.zones.get(params=params) # zones | pylint: disable=no-member - except CloudFlare.exceptions.CloudFlareAPIError as e: - code = int(e) + zones = self.cf.zones.list(**params) # zones | pylint: disable=no-member + except APIError as e: + code = int(e.errors[0].code) msg = str(e) hint = None @@ -204,7 +204,7 @@ class _CloudflareClient: hint = ('Did you copy your entire API token/key? To use Cloudflare tokens, ' 'you\'ll need the python package cloudflare>=2.3.1.{}' .format(' This certbot is running cloudflare ' + str(CloudFlare.__version__) - if hasattr(CloudFlare, '__version__') else '')) + if hasattr(Cloudflare, '__version__') else '')) elif code == 9103: hint = 'Did you enter the correct email address and Global key?' elif code == 9109: @@ -215,11 +215,11 @@ class _CloudflareClient: 'that you have supplied valid Cloudflare API credentials. ({2})' .format(code, msg, hint)) else: - logger.debug('Unrecognised CloudFlareAPIError while finding zone_id: %d %s. ' - 'Continuing with next zone guess...', e, e) + logger.debug('Unrecognised APIError while finding zone_id: %d %s. ' + 'Continuing with next zone guess...', code, e) - if zones: - zone_id: str = zones[0]['id'] + if zones and len(zones.result) > 0: + zone_id: str = zones.result[0].id logger.debug('Found zone_id of %s for %s using name %s', zone_id, domain, zone_name) return zone_id @@ -258,14 +258,14 @@ class _CloudflareClient: 'per_page': 1} try: # zones | pylint: disable=no-member - records = self.cf.zones.dns_records.get(zone_id, params=params) - except CloudFlare.exceptions.CloudFlareAPIError as e: - logger.debug('Encountered CloudFlareAPIError getting TXT record_id: %s', e) + records = self.cf.dns.records.list(zone_id=zone_id, **params) + except APIError as e: + logger.debug('Encountered APIError getting TXT record_id: %s', e) records = [] - if records: + if len(records.result) > 0: # Cleanup is returning the system to the state we found it. If, for some reason, # there are multiple matching records, we only delete one because we only added one. - return cast(str, records[0]['id']) + return cast(str, records.result[0].id) logger.debug('Unable to find TXT record.') return None diff --git a/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py b/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py index 42d56b616..1d3b3a3d0 100644 --- a/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py +++ b/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py @@ -3,8 +3,10 @@ import sys import unittest from unittest import mock +from dataclasses import dataclass + +from cloudflare import Cloudflare, APIError -import CloudFlare import pytest from certbot import errors @@ -13,7 +15,7 @@ from certbot.plugins import dns_test_common from certbot.plugins.dns_test_common import DOMAIN from certbot.tests import util as test_util -API_ERROR = CloudFlare.exceptions.CloudFlareAPIError(1000, '', '') +API_ERROR = APIError('', request='', body={'errors': [{'code': 1000}]}) API_TOKEN = 'an-api-token' @@ -21,6 +23,14 @@ API_KEY = 'an-api-key' EMAIL = 'example@example.com' +@dataclass +class CFResultList(object): + result: list + +@dataclass +class CFListObject(object): + id: str + class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): def setUp(self): @@ -113,14 +123,15 @@ class CloudflareClientTest(unittest.TestCase): self.cloudflare_client.cf = self.cf def test_add_txt_record(self): - self.cf.zones.get.return_value = [{'id': self.zone_id}] + self.cf.zones.list.return_value = CFResultList(result=[CFListObject(id=self.zone_id)]) self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) - self.cf.zones.dns_records.post.assert_called_with(self.zone_id, data=mock.ANY) - post_data = self.cf.zones.dns_records.post.call_args[1]['data'] + self.cf.dns.records.create.assert_called_with(zone_id=self.zone_id, type=mock.ANY, name=mock.ANY, content=mock.ANY, ttl=mock.ANY) + + post_data = self.cf.dns.records.create.call_args.kwargs assert 'TXT' == post_data['type'] assert self.record_name == post_data['name'] @@ -128,102 +139,109 @@ class CloudflareClientTest(unittest.TestCase): assert self.record_ttl == post_data['ttl'] def test_add_txt_record_error(self): - self.cf.zones.get.return_value = [{'id': self.zone_id}] + self.cf.zones.list.return_value = CFResultList(result=[CFListObject(id=self.zone_id)]) - self.cf.zones.dns_records.post.side_effect = CloudFlare.exceptions.CloudFlareAPIError(1009, '', '') + self.cf.dns.records.create.side_effect = APIError('', request='', body={'errors': [{'code': 1009}]}) with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) def test_add_txt_record_error_during_zone_lookup(self): - self.cf.zones.get.side_effect = API_ERROR + self.cf.zones.list.side_effect = API_ERROR with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) def test_add_txt_record_zone_not_found(self): - self.cf.zones.get.return_value = [] + self.cf.zones.list.return_value = [] with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) def test_add_txt_record_bad_creds(self): - self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(6003, '', '') + self.cf.zones.list.side_effect = APIError('', request='', body={'errors': [{'code': 6003}]}) with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) - self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(9103, '', '') + self.cf.zones.list.side_effect = APIError('', request='', body={'errors': [{'code': 9103}]}) with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) - self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(9109, '', '') + self.cf.zones.list.side_effect = APIError('', request='', body={'errors': [{'code': 9109}]}) with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) - self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(0, 'com.cloudflare.api.account.zone.list', '') + self.cf.zones.list.side_effect = APIError('', request='', body={'errors': [{'code': 9109, 'status': 'com.cloudflare.api.account.zone.list'}]}) with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) def test_del_txt_record(self): - self.cf.zones.get.return_value = [{'id': self.zone_id}] - self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}] + self.cf.zones.list.return_value = CFResultList(result=[CFListObject(id=self.zone_id)]) + self.cf.dns.records.list.return_value = CFResultList(result=[CFListObject(id=self.record_id)]) self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content) - expected = [mock.call.zones.get(params=mock.ANY), - mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY), - mock.call.zones.dns_records.delete(self.zone_id, self.record_id)] + expected = [mock.call.zones.list(name=mock.ANY, per_page=mock.ANY), + mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY), + mock.call.dns.records.delete(1, 2)] assert expected == self.cf.mock_calls - get_data = self.cf.zones.dns_records.get.call_args[1]['params'] + get_data = self.cf.dns.records.list.call_args.kwargs assert 'TXT' == get_data['type'] assert self.record_name == get_data['name'] assert self.record_content == get_data['content'] def test_del_txt_record_error_during_zone_lookup(self): - self.cf.zones.get.side_effect = API_ERROR + self.cf.zones.list.side_effect = API_ERROR self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content) def test_del_txt_record_error_during_delete(self): - self.cf.zones.get.return_value = [{'id': self.zone_id}] - self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}] + self.cf.zones.list.return_value = CFResultList(result=[CFListObject(id=self.zone_id)]) + self.cf.zones.dns_records.get.return_value = CFResultList(result=[CFListObject(id=self.record_id)]) self.cf.zones.dns_records.delete.side_effect = API_ERROR self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content) - expected = [mock.call.zones.get(params=mock.ANY), - mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY), - mock.call.zones.dns_records.delete(self.zone_id, self.record_id)] - + expected = [mock.call.zones.list(name=mock.ANY, per_page=mock.ANY), + mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY), + mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY).result.__len__() + ] assert expected == self.cf.mock_calls def test_del_txt_record_error_during_get(self): - self.cf.zones.get.return_value = [{'id': self.zone_id}] + self.cf.zones.list.return_value = CFResultList(result=[CFListObject(id=self.zone_id)]) self.cf.zones.dns_records.get.side_effect = API_ERROR self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content) - expected = [mock.call.zones.get(params=mock.ANY), - mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY)] + expected = [mock.call.zones.list(name=mock.ANY, per_page=mock.ANY), + mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY), + mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY).result.__len__() + ] assert expected == self.cf.mock_calls def test_del_txt_record_no_record(self): - self.cf.zones.get.return_value = [{'id': self.zone_id}] - self.cf.zones.dns_records.get.return_value = [] + self.cf.zones.list.return_value = CFResultList(result=[CFListObject(id=self.zone_id)]) + self.cf.dns.records.list.return_value = CFResultList(result=[]) self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content) - expected = [mock.call.zones.get(params=mock.ANY), - mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY)] + + expected = [mock.call.zones.list(name=mock.ANY, per_page=mock.ANY), + mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY) + ] assert expected == self.cf.mock_calls def test_del_txt_record_no_zone(self): - self.cf.zones.get.return_value = [{'id': None}] + self.cf.zones.list.return_value = CFResultList(result=[]) self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content) - expected = [mock.call.zones.get(params=mock.ANY)] + # There are 2 mocks for the fqdn and the base domain - Needs fixed if the subdomain is longer + expected = [] + for dom in DOMAIN.split('.'): + expected.append(mock.call.zones.list(name=mock.ANY, per_page=mock.ANY)) assert expected == self.cf.mock_calls diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt index ac72a441c..2cbe93367 100644 --- a/tools/oldest_constraints.txt +++ b/tools/oldest_constraints.txt @@ -11,7 +11,7 @@ cachetools==5.5.2 ; python_version == "3.10" certifi==2025.10.5 ; python_version == "3.10" cffi==1.14.1 ; python_version == "3.10" chardet==3.0.4 ; python_version == "3.10" -cloudflare==2.19.0 ; python_version == "3.10" +cloudflare==4.3.1 ; python_version == "3.10" colorama==0.4.6 ; (sys_platform == "win32" or platform_system == "Windows") and python_version == "3.10" configargparse==1.5.3 ; python_version == "3.10" configobj==5.0.6 ; python_version == "3.10" diff --git a/tools/pinning/oldest/pyproject.toml b/tools/pinning/oldest/pyproject.toml index b56625e1f..4e0be5322 100644 --- a/tools/pinning/oldest/pyproject.toml +++ b/tools/pinning/oldest/pyproject.toml @@ -59,7 +59,7 @@ boto3 = "1.20.34" botocore = "1.23.34" cffi = "1.14.1" chardet = "3.0.4" -cloudflare = "2.19" +cloudflare = "4.3.1" configobj = "5.0.6" cryptography = "43.0.0" distro = "1.0.1" diff --git a/tools/requirements.txt b/tools/requirements.txt index 986076f70..631fa9271 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -28,7 +28,7 @@ chardet==5.2.0 ; python_version >= "3.10" and python_version < "4.0" charset-normalizer==3.4.4 ; python_version >= "3.10" and python_version < "4.0" cleo==2.1.0 ; python_version >= "3.10" and python_version < "4.0" click==8.3.1 ; python_version >= "3.10" and python_version < "4.0" -cloudflare==2.19.4 ; python_version >= "3.10" and python_version < "4.0" +cloudflare==4.3.1 ; python_version >= "3.10" and python_version < "4.0" colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" configargparse==1.7.1 ; python_version >= "3.10" and python_version < "4.0" configobj==5.0.9 ; python_version >= "3.10" and python_version < "4.0" From eba5c3085edb6dd26e3a1fb4be6b243995b708d8 Mon Sep 17 00:00:00 2001 From: Darkbat91 Date: Thu, 22 Jan 2026 16:53:24 -0500 Subject: [PATCH 2/2] Fix CI * typing * linting * Code Coverage --- .../_internal/dns_cloudflare.py | 47 ++++++++----------- .../_internal/tests/dns_cloudflare_test.py | 37 +++++++-------- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py b/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py index d0bb6664b..37875bd3c 100644 --- a/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py +++ b/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py @@ -3,10 +3,10 @@ import logging from typing import Any from typing import Callable from typing import Optional -from typing import cast from cloudflare import Cloudflare, APIError - +from cloudflare.pagination import SyncV4PagePaginationArray +from cloudflare.types.zones import Zone from certbot import errors from certbot.plugins import dns_common from certbot.plugins.dns_common import CredentialsConfiguration @@ -80,7 +80,7 @@ class Authenticator(dns_common.DNSAuthenticator): def _get_cloudflare_client(self) -> "_CloudflareClient": if not self.credentials: # pragma: no cover raise errors.Error("Plugin has not been prepared.") - if self.credentials.conf('api-token'): + if self.credentials.conf('api-token'): # pragma: no cover return _CloudflareClient(api_token = self.credentials.conf('api-token')) return _CloudflareClient(email = self.credentials.conf('email'), api_key = self.credentials.conf('api-key')) @@ -119,16 +119,13 @@ class _CloudflareClient: zone_id = self._find_zone_id(domain) - data = {'type': 'TXT', - 'name': record_name, - 'content': record_content, - 'ttl': record_ttl} - try: - logger.debug('Attempting to add record to zone %s: %s', zone_id, data) - self.cf.dns.records.create(zone_id=zone_id, **data) # zones | pylint: disable=no-member + logger.debug('Attempting to add record to zone %s: %s=%s', + zone_id, record_name, record_content) + self.cf.dns.records.create(zone_id=zone_id, type='TXT', name=record_name, + content=record_content, ttl=record_ttl) # zones | pylint: disable=no-member except APIError as e: - code = int(e.errors[0].code) + code = e.errors[0].code hint = None if code == 1009: @@ -166,7 +163,7 @@ class _CloudflareClient: if record_id: try: # zones | pylint: disable=no-member - self.cf.dns.records.delete(zone_id, record_id) + self.cf.dns.records.delete(zone_id=zone_id, dns_record_id=record_id) logger.debug('Successfully deleted TXT record.') except APIError as e: logger.warning('Encountered APIError deleting TXT record: %s', e) @@ -186,24 +183,21 @@ class _CloudflareClient: """ zone_name_guesses = dns_common.base_domain_name_guesses(domain) - zones: list[dict[str, Any]] | None = None + zones: SyncV4PagePaginationArray[Zone] | None = None code = msg = None for zone_name in zone_name_guesses: - params = {'name': zone_name, - 'per_page': 1} - try: - zones = self.cf.zones.list(**params) # zones | pylint: disable=no-member + zones = self.cf.zones.list(name=zone_name, per_page=1) # zones | pylint: disable=no-member except APIError as e: - code = int(e.errors[0].code) + code = e.errors[0].code msg = str(e) hint = None if code == 6003: hint = ('Did you copy your entire API token/key? To use Cloudflare tokens, ' - 'you\'ll need the python package cloudflare>=2.3.1.{}' - .format(' This certbot is running cloudflare ' + str(CloudFlare.__version__) + 'you\'ll need the python package cloudflare>=4.3.1.{}' + .format(' This certbot is running cloudflare ' + str(Cloudflare.__version__) if hasattr(Cloudflare, '__version__') else '')) elif code == 9103: hint = 'Did you enter the correct email address and Global key?' @@ -252,20 +246,17 @@ class _CloudflareClient: :rtype: str """ - params = {'type': 'TXT', - 'name': record_name, - 'content': record_content, - 'per_page': 1} + records = None try: # zones | pylint: disable=no-member - records = self.cf.dns.records.list(zone_id=zone_id, **params) + records = self.cf.dns.records.list(zone_id=zone_id, type='TXT', + name=record_name, content=record_content, per_page=1) # type: ignore[arg-type] except APIError as e: logger.debug('Encountered APIError getting TXT record_id: %s', e) - records = [] - if len(records.result) > 0: + if records and len(records.result) > 0: # Cleanup is returning the system to the state we found it. If, for some reason, # there are multiple matching records, we only delete one because we only added one. - return cast(str, records.result[0].id) + return records.result[0].id logger.debug('Unable to find TXT record.') return None diff --git a/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py b/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py index 1d3b3a3d0..c689ac0cb 100644 --- a/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py +++ b/certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py @@ -5,8 +5,8 @@ import unittest from unittest import mock from dataclasses import dataclass -from cloudflare import Cloudflare, APIError - +from cloudflare import APIError +from httpx import Request import pytest from certbot import errors @@ -15,7 +15,7 @@ from certbot.plugins import dns_test_common from certbot.plugins.dns_test_common import DOMAIN from certbot.tests import util as test_util -API_ERROR = APIError('', request='', body={'errors': [{'code': 1000}]}) +API_ERROR = APIError('', request=Request(method='get', url='cloudflare.com'), body={'errors': [{'code': 1000}]}) API_TOKEN = 'an-api-token' @@ -24,12 +24,12 @@ EMAIL = 'example@example.com' @dataclass -class CFResultList(object): - result: list +class CFListObject(object): + id: int @dataclass -class CFListObject(object): - id: str +class CFResultList(object): + result: list[CFListObject] class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): @@ -141,7 +141,7 @@ class CloudflareClientTest(unittest.TestCase): def test_add_txt_record_error(self): self.cf.zones.list.return_value = CFResultList(result=[CFListObject(id=self.zone_id)]) - self.cf.dns.records.create.side_effect = APIError('', request='', body={'errors': [{'code': 1009}]}) + self.cf.dns.records.create.side_effect = APIError('', Request(method='get', url='cloudflare.com'), body={'errors': [{'code': 1009}]}) with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) @@ -159,19 +159,19 @@ class CloudflareClientTest(unittest.TestCase): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) def test_add_txt_record_bad_creds(self): - self.cf.zones.list.side_effect = APIError('', request='', body={'errors': [{'code': 6003}]}) + self.cf.zones.list.side_effect = APIError('', Request(method='get', url='cloudflare.com'), body={'errors': [{'code': 6003}]}) with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) - self.cf.zones.list.side_effect = APIError('', request='', body={'errors': [{'code': 9103}]}) + self.cf.zones.list.side_effect = APIError('', Request(method='get', url='cloudflare.com'), body={'errors': [{'code': 9103}]}) with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) - self.cf.zones.list.side_effect = APIError('', request='', body={'errors': [{'code': 9109}]}) + self.cf.zones.list.side_effect = APIError('', Request(method='get', url='cloudflare.com'), body={'errors': [{'code': 9109}]}) with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) - self.cf.zones.list.side_effect = APIError('', request='', body={'errors': [{'code': 9109, 'status': 'com.cloudflare.api.account.zone.list'}]}) + self.cf.zones.list.side_effect = APIError('', Request(method='get', url='cloudflare.com'), body={'errors': [{'code': 9109, 'status': 'com.cloudflare.api.account.zone.list'}]}) with pytest.raises(errors.PluginError): self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl) @@ -183,7 +183,7 @@ class CloudflareClientTest(unittest.TestCase): expected = [mock.call.zones.list(name=mock.ANY, per_page=mock.ANY), mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY), - mock.call.dns.records.delete(1, 2)] + mock.call.dns.records.delete(zone_id=self.zone_id, dns_record_id=self.record_id)] assert expected == self.cf.mock_calls @@ -200,24 +200,23 @@ class CloudflareClientTest(unittest.TestCase): def test_del_txt_record_error_during_delete(self): self.cf.zones.list.return_value = CFResultList(result=[CFListObject(id=self.zone_id)]) - self.cf.zones.dns_records.get.return_value = CFResultList(result=[CFListObject(id=self.record_id)]) - self.cf.zones.dns_records.delete.side_effect = API_ERROR + self.cf.dns.records.list.return_value = CFResultList(result=[CFListObject(id=self.record_id)]) + self.cf.dns.records.delete.side_effect = API_ERROR self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content) expected = [mock.call.zones.list(name=mock.ANY, per_page=mock.ANY), mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY), - mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY).result.__len__() + mock.call.dns.records.delete(zone_id=self.zone_id, dns_record_id=self.record_id) ] assert expected == self.cf.mock_calls def test_del_txt_record_error_during_get(self): self.cf.zones.list.return_value = CFResultList(result=[CFListObject(id=self.zone_id)]) - self.cf.zones.dns_records.get.side_effect = API_ERROR + self.cf.dns.records.list.side_effect = API_ERROR self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content) expected = [mock.call.zones.list(name=mock.ANY, per_page=mock.ANY), - mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY), - mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY).result.__len__() + mock.call.dns.records.list(zone_id=mock.ANY, type=mock.ANY, name=mock.ANY, content=mock.ANY, per_page=mock.ANY) ] assert expected == self.cf.mock_calls