diff -Nru python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py --- python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py 2022-11-21 12:58:20.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py 2025-04-07 18:03:33.000000000 -0400 @@ -5,6 +5,7 @@ from typing import Dict from typing import List from typing import Optional +from typing import cast import CloudFlare @@ -220,7 +221,7 @@ 'Continuing with next zone guess...', e, e) if zones: - zone_id = zones[0]['id'] + zone_id: str = zones[0]['id'] logger.debug('Found zone_id of %s for %s using name %s', zone_id, domain, zone_name) return zone_id @@ -267,6 +268,6 @@ if records: # 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 records[0]['id'] + return cast(str, records[0]['id']) logger.debug('Unable to find TXT record.') return None diff -Nru python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py --- python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py 1969-12-31 19:00:00.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py 2025-04-07 18:03:33.000000000 -0400 @@ -0,0 +1,232 @@ +"""Tests for certbot_dns_cloudflare._internal.dns_cloudflare.""" + +import sys +import unittest +from unittest import mock + +import CloudFlare +import pytest + +from certbot import errors +from certbot.compat import os +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_TOKEN = 'an-api-token' + +API_KEY = 'an-api-key' +EMAIL = 'example@example.com' + + +class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): + + def setUp(self): + from certbot_dns_cloudflare._internal.dns_cloudflare import Authenticator + + super().setUp() + + path = os.path.join(self.tempdir, 'file.ini') + dns_test_common.write({"cloudflare_email": EMAIL, "cloudflare_api_key": API_KEY}, path) + + self.config = mock.MagicMock(cloudflare_credentials=path, + cloudflare_propagation_seconds=0) # don't wait during tests + + self.auth = Authenticator(self.config, "cloudflare") + + self.mock_client = mock.MagicMock() + # _get_cloudflare_client | pylint: disable=protected-access + # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with + # both strict and non-strict mypy + setattr(self.auth, '_get_cloudflare_client', mock.MagicMock(return_value=self.mock_client)) + + @test_util.patch_display_util() + def test_perform(self, unused_mock_get_utility): + self.auth.perform([self.achall]) + + expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)] + assert expected == self.mock_client.mock_calls + + def test_cleanup(self): + # _attempt_cleanup | pylint: disable=protected-access + self.auth._attempt_cleanup = True + self.auth.cleanup([self.achall]) + + expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] + assert expected == self.mock_client.mock_calls + + @test_util.patch_display_util() + def test_api_token(self, unused_mock_get_utility): + dns_test_common.write({"cloudflare_api_token": API_TOKEN}, + self.config.cloudflare_credentials) + self.auth.perform([self.achall]) + + expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)] + assert expected == self.mock_client.mock_calls + + def test_no_creds(self): + dns_test_common.write({}, self.config.cloudflare_credentials) + with pytest.raises(errors.PluginError): + self.auth.perform([self.achall]) + + def test_missing_email_or_key(self): + dns_test_common.write({"cloudflare_api_key": API_KEY}, self.config.cloudflare_credentials) + with pytest.raises(errors.PluginError): + self.auth.perform([self.achall]) + + dns_test_common.write({"cloudflare_email": EMAIL}, self.config.cloudflare_credentials) + with pytest.raises(errors.PluginError): + self.auth.perform([self.achall]) + + def test_email_or_key_with_token(self): + dns_test_common.write({"cloudflare_api_token": API_TOKEN, "cloudflare_email": EMAIL}, + self.config.cloudflare_credentials) + with pytest.raises(errors.PluginError): + self.auth.perform([self.achall]) + + dns_test_common.write({"cloudflare_api_token": API_TOKEN, "cloudflare_api_key": API_KEY}, + self.config.cloudflare_credentials) + with pytest.raises(errors.PluginError): + self.auth.perform([self.achall]) + + dns_test_common.write({"cloudflare_api_token": API_TOKEN, "cloudflare_email": EMAIL, + "cloudflare_api_key": API_KEY}, self.config.cloudflare_credentials) + with pytest.raises(errors.PluginError): + self.auth.perform([self.achall]) + + +class CloudflareClientTest(unittest.TestCase): + record_name = "foo" + record_content = "bar" + record_ttl = 42 + zone_id = 1 + record_id = 2 + + def setUp(self): + from certbot_dns_cloudflare._internal.dns_cloudflare import _CloudflareClient + + self.cloudflare_client = _CloudflareClient(EMAIL, API_KEY) + + self.cf = mock.MagicMock() + self.cloudflare_client.cf = self.cf + + def test_add_txt_record(self): + self.cf.zones.get.return_value = [{'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'] + + assert 'TXT' == post_data['type'] + assert self.record_name == post_data['name'] + assert self.record_content == post_data['content'] + 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.dns_records.post.side_effect = CloudFlare.exceptions.CloudFlareAPIError(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 + + 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 = [] + + 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, '', '') + 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, '', '') + 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, '', '') + 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', '') + 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.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)] + + assert expected == self.cf.mock_calls + + get_data = self.cf.zones.dns_records.get.call_args[1]['params'] + + 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.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.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)] + + 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.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)] + + 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.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)] + + assert expected == self.cf.mock_calls + + def test_del_txt_record_no_zone(self): + self.cf.zones.get.return_value = [{'id': None}] + + self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content) + expected = [mock.call.zones.get(params=mock.ANY)] + + assert expected == self.cf.mock_calls + + +if __name__ == "__main__": + sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover diff -Nru python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare/_internal/tests/__init__.py python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/__init__.py --- python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare/_internal/tests/__init__.py 1969-12-31 19:00:00.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/__init__.py 2025-04-07 18:03:33.000000000 -0400 @@ -0,0 +1 @@ +"""certbot-dns-cloudflare tests""" diff -Nru python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare.egg-info/PKG-INFO python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/PKG-INFO --- python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare.egg-info/PKG-INFO 2022-11-21 12:58:28.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/PKG-INFO 2025-04-07 18:03:36.000000000 -0400 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: certbot-dns-cloudflare -Version: 2.0.0 +Version: 4.0.0 Summary: Cloudflare DNS Authenticator plugin for Certbot Home-page: https://github.com/certbot/certbot Author: Certbot Project @@ -13,17 +13,34 @@ Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities -Requires-Python: >=3.7 -Provides-Extra: docs +Requires-Python: >=3.9 License-File: LICENSE.txt +Requires-Dist: cloudflare<2.20,>=2.19 +Requires-Dist: acme>=4.0.0 +Requires-Dist: certbot>=4.0.0 +Provides-Extra: docs +Requires-Dist: Sphinx>=1.0; extra == "docs" +Requires-Dist: sphinx_rtd_theme; extra == "docs" +Provides-Extra: test +Requires-Dist: pytest; extra == "test" +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: provides-extra +Dynamic: requires-dist +Dynamic: requires-python +Dynamic: summary diff -Nru python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare.egg-info/requires.txt python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/requires.txt --- python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare.egg-info/requires.txt 2022-11-21 12:58:28.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/requires.txt 2025-04-07 18:03:36.000000000 -0400 @@ -1,8 +1,10 @@ -cloudflare>=1.5.1 -setuptools>=41.6.0 -acme>=2.0.0 -certbot>=2.0.0 +cloudflare<2.20,>=2.19 +acme>=4.0.0 +certbot>=4.0.0 [docs] Sphinx>=1.0 sphinx_rtd_theme + +[test] +pytest diff -Nru python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare.egg-info/SOURCES.txt python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/SOURCES.txt --- python-certbot-dns-cloudflare-2.0.0/certbot_dns_cloudflare.egg-info/SOURCES.txt 2022-11-21 12:58:28.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/SOURCES.txt 2025-04-07 18:03:36.000000000 -0400 @@ -12,10 +12,11 @@ certbot_dns_cloudflare.egg-info/top_level.txt certbot_dns_cloudflare/_internal/__init__.py certbot_dns_cloudflare/_internal/dns_cloudflare.py +certbot_dns_cloudflare/_internal/tests/__init__.py +certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py docs/.gitignore docs/Makefile docs/api.rst docs/conf.py docs/index.rst -docs/make.bat -tests/dns_cloudflare_test.py \ No newline at end of file +docs/make.bat \ No newline at end of file diff -Nru python-certbot-dns-cloudflare-2.0.0/debian/changelog python-certbot-dns-cloudflare-4.0.0/debian/changelog --- python-certbot-dns-cloudflare-2.0.0/debian/changelog 2022-11-24 00:49:18.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/debian/changelog 2025-05-24 18:14:31.000000000 -0400 @@ -1,3 +1,12 @@ +python-certbot-dns-cloudflare (4.0.0-1) unstable; urgency=medium + + * d/watch: newer packages use an underscore + * New upstream version 4.0.0 (Closes: #1106467) + * Bump dependency versions + * Override python-cloudflare to be <3.0, not <2.20 + + -- Harlan Lieberman-Berg Sat, 24 May 2025 18:14:31 -0400 + python-certbot-dns-cloudflare (2.0.0-1) unstable; urgency=medium * Bump certbot, acme dependencies diff -Nru python-certbot-dns-cloudflare-2.0.0/debian/control python-certbot-dns-cloudflare-4.0.0/debian/control --- python-certbot-dns-cloudflare-2.0.0/debian/control 2022-11-24 00:47:26.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/debian/control 2025-05-24 17:58:01.000000000 -0400 @@ -7,9 +7,10 @@ Build-Depends: debhelper-compat (= 13), dh-python, python3, - python3-acme-abi-2 (>= 2.0), - python3-certbot-abi-2 (>= 2.0), - python3-cloudflare, + python3-acme-abi-4 (>= 4.0~), + python3-certbot-abi-4 (>= 4.0~), + python3-cloudflare (>= 2.19~), + python3-cloudflare (<< 3.0), python3-setuptools, python3-sphinx, python3-sphinx-rtd-theme @@ -22,7 +23,7 @@ Package: python3-certbot-dns-cloudflare Architecture: all -Depends: certbot, python3-certbot-abi-2 (>= ${Abi-major-minor-version}), ${misc:Depends}, ${python3:Depends} +Depends: certbot, python3-certbot-abi-4 (>= ${Abi-major-minor-version}), ${misc:Depends}, ${python3:Depends} Enhances: certbot Description: Cloudflare DNS plugin for Certbot The objective of Certbot, Let's Encrypt, and the ACME (Automated diff -Nru python-certbot-dns-cloudflare-2.0.0/debian/patches/0001-test-2.20.patch python-certbot-dns-cloudflare-4.0.0/debian/patches/0001-test-2.20.patch --- python-certbot-dns-cloudflare-2.0.0/debian/patches/0001-test-2.20.patch 1969-12-31 19:00:00.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/debian/patches/0001-test-2.20.patch 2025-05-24 18:14:06.000000000 -0400 @@ -0,0 +1,16 @@ +Description: Override python-cloudflare dep to <3.0 +Author: Harlan Lieberman-Berg +Forwarded: not-needed +Index: dns-cloudflare/setup.py +=================================================================== +--- dns-cloudflare.orig/setup.py ++++ dns-cloudflare/setup.py +@@ -9,7 +9,7 @@ version = '4.0.0' + 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>=2.19, <3.0', + ] + + if os.environ.get('SNAP_BUILD'): diff -Nru python-certbot-dns-cloudflare-2.0.0/debian/patches/series python-certbot-dns-cloudflare-4.0.0/debian/patches/series --- python-certbot-dns-cloudflare-2.0.0/debian/patches/series 1969-12-31 19:00:00.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/debian/patches/series 2025-05-24 17:57:32.000000000 -0400 @@ -0,0 +1 @@ +0001-test-2.20.patch diff -Nru python-certbot-dns-cloudflare-2.0.0/debian/watch python-certbot-dns-cloudflare-4.0.0/debian/watch --- python-certbot-dns-cloudflare-2.0.0/debian/watch 2021-08-23 18:37:56.000000000 -0400 +++ python-certbot-dns-cloudflare-4.0.0/debian/watch 2025-05-24 17:47:50.000000000 -0400 @@ -1,3 +1,3 @@ version=4 opts=uversionmangle=s/(rc|a|b|c)/~$1/,pgpsigurlmangle=s/$/.asc/ \ -https://pypi.debian.net/certbot-dns-cloudflare/certbot-dns-cloudflare-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) +https://pypi.debian.net/certbot-dns-cloudflare/certbot_dns_cloudflare-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) diff -Nru python-certbot-dns-cloudflare-2.0.0/docs/conf.py python-certbot-dns-cloudflare-4.0.0/docs/conf.py --- python-certbot-dns-cloudflare-2.0.0/docs/conf.py 2022-11-21 12:58:20.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/docs/conf.py 2025-04-07 18:03:33.000000000 -0400 @@ -16,7 +16,7 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import os +# import os # import sys # sys.path.insert(0, os.path.abspath('.')) @@ -35,7 +35,8 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', - 'sphinx.ext.viewcode'] + 'sphinx.ext.viewcode', + 'sphinx_rtd_theme'] autodoc_member_order = 'bysource' autodoc_default_flags = ['show-inheritance'] @@ -93,14 +94,7 @@ # a list of builtin themes. # -# https://docs.readthedocs.io/en/stable/faq.html#i-want-to-use-the-read-the-docs-theme-locally -# on_rtd is whether we are on readthedocs.org -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' -if not on_rtd: # only import and set the theme if we're building docs locally - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -# otherwise, readthedocs.org uses their theme by default, so no need to specify it +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -176,6 +170,6 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'python': ('https://docs.python.org/', None), - 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'acme': ('https://acme-python.readthedocs.io/en/latest/', None), 'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None), } diff -Nru python-certbot-dns-cloudflare-2.0.0/MANIFEST.in python-certbot-dns-cloudflare-4.0.0/MANIFEST.in --- python-certbot-dns-cloudflare-2.0.0/MANIFEST.in 2022-11-21 12:58:20.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/MANIFEST.in 2025-04-07 18:03:33.000000000 -0400 @@ -1,7 +1,6 @@ include LICENSE.txt include README.rst recursive-include docs * -recursive-include tests * include certbot_dns_cloudflare/py.typed global-exclude __pycache__ global-exclude *.py[cod] diff -Nru python-certbot-dns-cloudflare-2.0.0/PKG-INFO python-certbot-dns-cloudflare-4.0.0/PKG-INFO --- python-certbot-dns-cloudflare-2.0.0/PKG-INFO 2022-11-21 12:58:28.347641200 -0500 +++ python-certbot-dns-cloudflare-4.0.0/PKG-INFO 2025-04-07 18:03:36.605529000 -0400 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: certbot-dns-cloudflare -Version: 2.0.0 +Version: 4.0.0 Summary: Cloudflare DNS Authenticator plugin for Certbot Home-page: https://github.com/certbot/certbot Author: Certbot Project @@ -13,17 +13,34 @@ Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities -Requires-Python: >=3.7 -Provides-Extra: docs +Requires-Python: >=3.9 License-File: LICENSE.txt +Requires-Dist: cloudflare<2.20,>=2.19 +Requires-Dist: acme>=4.0.0 +Requires-Dist: certbot>=4.0.0 +Provides-Extra: docs +Requires-Dist: Sphinx>=1.0; extra == "docs" +Requires-Dist: sphinx_rtd_theme; extra == "docs" +Provides-Extra: test +Requires-Dist: pytest; extra == "test" +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: provides-extra +Dynamic: requires-dist +Dynamic: requires-python +Dynamic: summary diff -Nru python-certbot-dns-cloudflare-2.0.0/setup.py python-certbot-dns-cloudflare-4.0.0/setup.py --- python-certbot-dns-cloudflare-2.0.0/setup.py 2022-11-21 12:58:20.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/setup.py 2025-04-07 18:03:33.000000000 -0400 @@ -4,14 +4,17 @@ from setuptools import find_packages from setuptools import setup -version = '2.0.0' +version = '4.0.0' install_requires = [ - 'cloudflare>=1.5.1', - 'setuptools>=41.6.0', + # 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', ] -if not os.environ.get('SNAP_BUILD'): +if os.environ.get('SNAP_BUILD'): + install_requires.append('packaging') +else: install_requires.extend([ # We specify the minimum acme and certbot version as the current plugin # version for simplicity. See @@ -19,17 +22,16 @@ f'acme>={version}', f'certbot>={version}', ]) -elif 'bdist_wheel' in sys.argv[1:]: - raise RuntimeError('Unset SNAP_BUILD when building wheels ' - 'to include certbot dependencies.') -if os.environ.get('SNAP_BUILD'): - install_requires.append('packaging') docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', ] +test_extras = [ + 'pytest', +] + setup( name='certbot-dns-cloudflare', version=version, @@ -38,7 +40,7 @@ author="Certbot Project", author_email='certbot-dev@eff.org', license='Apache License 2.0', - python_requires='>=3.7', + python_requires='>=3.9', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Plugins', @@ -47,11 +49,11 @@ 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', @@ -65,6 +67,7 @@ install_requires=install_requires, extras_require={ 'docs': docs_extras, + 'test': test_extras, }, entry_points={ 'certbot.plugins': [ diff -Nru python-certbot-dns-cloudflare-2.0.0/tests/dns_cloudflare_test.py python-certbot-dns-cloudflare-4.0.0/tests/dns_cloudflare_test.py --- python-certbot-dns-cloudflare-2.0.0/tests/dns_cloudflare_test.py 2022-11-21 12:58:20.000000000 -0500 +++ python-certbot-dns-cloudflare-4.0.0/tests/dns_cloudflare_test.py 1969-12-31 19:00:00.000000000 -0500 @@ -1,248 +0,0 @@ -"""Tests for certbot_dns_cloudflare._internal.dns_cloudflare.""" - -import unittest -from unittest import mock - -import CloudFlare - -from certbot import errors -from certbot.compat import os -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_TOKEN = 'an-api-token' - -API_KEY = 'an-api-key' -EMAIL = 'example@example.com' - - -class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): - - def setUp(self): - from certbot_dns_cloudflare._internal.dns_cloudflare import Authenticator - - super().setUp() - - path = os.path.join(self.tempdir, 'file.ini') - dns_test_common.write({"cloudflare_email": EMAIL, "cloudflare_api_key": API_KEY}, path) - - self.config = mock.MagicMock(cloudflare_credentials=path, - cloudflare_propagation_seconds=0) # don't wait during tests - - self.auth = Authenticator(self.config, "cloudflare") - - self.mock_client = mock.MagicMock() - # _get_cloudflare_client | pylint: disable=protected-access - self.auth._get_cloudflare_client = mock.MagicMock(return_value=self.mock_client) - - @test_util.patch_display_util() - def test_perform(self, unused_mock_get_utility): - self.auth.perform([self.achall]) - - expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)] - self.assertEqual(expected, self.mock_client.mock_calls) - - def test_cleanup(self): - # _attempt_cleanup | pylint: disable=protected-access - self.auth._attempt_cleanup = True - self.auth.cleanup([self.achall]) - - expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] - self.assertEqual(expected, self.mock_client.mock_calls) - - @test_util.patch_display_util() - def test_api_token(self, unused_mock_get_utility): - dns_test_common.write({"cloudflare_api_token": API_TOKEN}, - self.config.cloudflare_credentials) - self.auth.perform([self.achall]) - - expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)] - self.assertEqual(expected, self.mock_client.mock_calls) - - def test_no_creds(self): - dns_test_common.write({}, self.config.cloudflare_credentials) - self.assertRaises(errors.PluginError, - self.auth.perform, - [self.achall]) - - def test_missing_email_or_key(self): - dns_test_common.write({"cloudflare_api_key": API_KEY}, self.config.cloudflare_credentials) - self.assertRaises(errors.PluginError, - self.auth.perform, - [self.achall]) - - dns_test_common.write({"cloudflare_email": EMAIL}, self.config.cloudflare_credentials) - self.assertRaises(errors.PluginError, - self.auth.perform, - [self.achall]) - - def test_email_or_key_with_token(self): - dns_test_common.write({"cloudflare_api_token": API_TOKEN, "cloudflare_email": EMAIL}, - self.config.cloudflare_credentials) - self.assertRaises(errors.PluginError, - self.auth.perform, - [self.achall]) - - dns_test_common.write({"cloudflare_api_token": API_TOKEN, "cloudflare_api_key": API_KEY}, - self.config.cloudflare_credentials) - self.assertRaises(errors.PluginError, - self.auth.perform, - [self.achall]) - - dns_test_common.write({"cloudflare_api_token": API_TOKEN, "cloudflare_email": EMAIL, - "cloudflare_api_key": API_KEY}, self.config.cloudflare_credentials) - self.assertRaises(errors.PluginError, - self.auth.perform, - [self.achall]) - - -class CloudflareClientTest(unittest.TestCase): - record_name = "foo" - record_content = "bar" - record_ttl = 42 - zone_id = 1 - record_id = 2 - - def setUp(self): - from certbot_dns_cloudflare._internal.dns_cloudflare import _CloudflareClient - - self.cloudflare_client = _CloudflareClient(EMAIL, API_KEY) - - self.cf = mock.MagicMock() - self.cloudflare_client.cf = self.cf - - def test_add_txt_record(self): - self.cf.zones.get.return_value = [{'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.assertEqual('TXT', post_data['type']) - self.assertEqual(self.record_name, post_data['name']) - self.assertEqual(self.record_content, post_data['content']) - self.assertEqual(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.dns_records.post.side_effect = CloudFlare.exceptions.CloudFlareAPIError(1009, '', '') - - self.assertRaises( - 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.assertRaises( - 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.assertRaises( - 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.assertRaises( - 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.assertRaises( - 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.assertRaises( - 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.assertRaises( - 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.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)] - - self.assertEqual(expected, self.cf.mock_calls) - - get_data = self.cf.zones.dns_records.get.call_args[1]['params'] - - self.assertEqual('TXT', get_data['type']) - self.assertEqual(self.record_name, get_data['name']) - self.assertEqual(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.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.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)] - - self.assertEqual(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.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)] - - self.assertEqual(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.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)] - - self.assertEqual(expected, self.cf.mock_calls) - - def test_del_txt_record_no_zone(self): - self.cf.zones.get.return_value = [{'id': None}] - - self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content) - expected = [mock.call.zones.get(params=mock.ANY)] - - self.assertEqual(expected, self.cf.mock_calls) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover