ACME: add dns-persist-01 support (#997)

* Add dns-persist-01 DNS TXT record filters.

* Refactor parsing and joining CAA issue-values out.

* Add basic tests.

* Fix bug and add integration tests for filters.

* Add dns-persist-01 support to ACME modules.

* Add changelog fragment.
This commit is contained in:
Felix Fontein
2026-04-01 19:46:59 +02:00
committed by GitHub
parent 4c5962788d
commit f3b43185bf
17 changed files with 1148 additions and 22 deletions

View File

@@ -387,6 +387,66 @@
ansible.builtin.set_fact:
cert_9_obtain_results: "{{ certificate_obtain_result }}"
cert_9_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
- when: ansible_version.full is version('2.21', '>=')
vars:
validation_record_domains:
- name: example.com
wildcard: true
- name: example.org
persist_until: "+5m"
- name: t1.example.com
issuer_domain_name: pebble.letsencrypt.org
block:
- name: Obtain ACME account
community.crypto.acme_account_info:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
register: account_info
- name: Create dns-persist-01 DNS entries
ansible.builtin.uri:
url: "http://{{ acme_host }}:5000/dns/_validation-persist.{{ item.name }}"
method: PUT
body_format: json
body:
- >-
{{
issuer_domain_name
| community.crypto.acme_dns_persist_record(
account_uri=account_info.account_uri,
policy='wildcard' if item.wildcard | default(false) else none,
persist_until=item.persist_until | default(none),
)
}}
loop: "{{ validation_record_domains }}"
- name: Obtain cert 10
ansible.builtin.include_tasks: obtain-cert.yml
vars:
certgen_title: Certificate 10
certificate_name: cert-10
key_type: ec256
subject_alt_name: "DNS:*.example.com,DNS:example.org,DNS:t1.example.com"
subject_alt_name_critical: false
account_key: account-ec256
challenge: dns-persist-01
modify_account: true
deactivate_authzs: false
force: false
remaining_days: 1
terms_agreed: true
account_email: "example@example.org"
use_csr_content: true
- name: Store obtain results for cert 10
ansible.builtin.set_fact:
cert_10_obtain_results: "{{ certificate_obtain_result }}"
cert_10_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
- name: Remove dns-persist-01 DNS entries
ansible.builtin.uri:
url: "http://{{ acme_host }}:5000/dns/_validation-persist.{{ item.name }}"
method: DELETE
loop: "{{ validation_record_domains }}"
## DISSECT CERTIFICATES #######################################################################
# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate.
@@ -429,6 +489,11 @@
ignore_errors: true
register: cert_9_valid
when: ansible_version.full is version('2.21', '>=')
- name: Verifying cert 10
ansible.builtin.command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-10-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-10-chain.pem" "{{ remote_tmp_dir }}/cert-10.pem"'
ignore_errors: true
register: cert_10_valid
when: ansible_version.full is version('2.21', '>=')
# Dump certificate info
- name: Dumping cert 1
@@ -461,6 +526,10 @@
ansible.builtin.command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-9.pem" -noout -text'
register: cert_9_text
when: ansible_version.full is version('2.21', '>=')
- name: Dumping cert 10
ansible.builtin.command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-10.pem" -noout -text'
register: cert_10_text
when: ansible_version.full is version('2.21', '>=')
# Dump certificate info
- name: Dumping cert 1
@@ -502,6 +571,11 @@
path: "{{ remote_tmp_dir }}/cert-9.pem"
register: cert_9_info
when: ansible_version.full is version('2.21', '>=')
- name: Dumping cert 10
community.crypto.x509_certificate_info:
path: "{{ remote_tmp_dir }}/cert-10.pem"
register: cert_10_info
when: ansible_version.full is version('2.21', '>=')
## GET ACCOUNT ORDERS #########################################################################
- name: Don't retrieve orders

View File

@@ -173,6 +173,19 @@
- "'DNS:example.org' in cert_9_text.stdout"
- "'DNS:t1.example.com' in cert_9_text.stdout"
- when: ansible_version.full is version('2.21', '>=')
block:
- name: Check that certificate 10 is valid
ansible.builtin.assert:
that:
- cert_10_valid is not failed
- name: Check that certificate 10 contains correct SANs
ansible.builtin.assert:
that:
- "'DNS:*.example.com' in cert_10_text.stdout"
- "'DNS:example.org' in cert_10_text.stdout"
- "'DNS:t1.example.com' in cert_10_text.stdout"
- name: Validate that orders were not retrieved
ansible.builtin.assert:
that:

View File

@@ -60,7 +60,7 @@
ansible.builtin.debug:
var: order_1
- name: "({{ select_crypto_backend }}) Check order"
- name: "({{ select_crypto_backend }}) Check order (1/2)"
ansible.builtin.assert:
that:
- order_1 is changed
@@ -80,6 +80,24 @@
- order_1.challenge_data_dns['_acme-challenge.' ~ domain_name] | length == 1
- order_1.account_uri == account.account_uri
- name: "({{ select_crypto_backend }}) Check order (2/2)"
when: ansible_version.full is version('2.21', '>=')
ansible.builtin.assert:
that:
- "'dns-account-01' in order_1.challenge_data[0].challenges"
- "'dns-persist-01' in order_1.challenge_data[0].challenges"
- order_1.challenge_data[0].challenges['dns-account-01'].record.endswith('._acme-challenge.' ~ domain_name)
- order_1.challenge_data[0].challenges['dns-account-01'].resource.endswith('._acme-challenge')
- order_1.challenge_data[0].challenges['dns-account-01'].resource_value is string
- "'record' not in order_1.challenge_data[0].challenges['dns-persist-01']"
- "'resource' not in order_1.challenge_data[0].challenges['dns-persist-01']"
- "'resource_value' not in order_1.challenge_data[0].challenges['dns-persist-01']"
- order_1.challenge_data[0].challenges['dns-persist-01'].account_uri == order_1.account_uri
- order_1.challenge_data[0].challenges['dns-persist-01'].issuer_domain_names | length == 2
- order_1.challenge_data[0].challenges['dns-persist-01'].issuer_domain_names == ["pebble.letsencrypt.org", "ca.example.com"]
- order_1.challenge_data_dns_account | length == 1
- order_1.challenge_data_dns_account.values() | first | length == 1
- name: "({{ select_crypto_backend }}) Get order information"
community.crypto.acme_certificate_order_info:
acme_directory: "{{ acme_directory_url }}"
@@ -94,7 +112,7 @@
ansible.builtin.debug:
var: order_info_1
- name: "({{ select_crypto_backend }}) Check order information"
- name: "({{ select_crypto_backend }}) Check order information (1/2)"
ansible.builtin.assert:
that:
- order_info_1 is not changed
@@ -119,6 +137,13 @@
- order_info_1.order_uri == order_1.order_uri
- order_info_1.account_uri == account.account_uri
- name: "({{ select_crypto_backend }}) Check order information (2/2)"
when: ansible_version.full is version('2.21', '>=')
ansible.builtin.assert:
that:
- (order_info_1.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-account-01') | first).status == 'pending'
- (order_info_1.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-persist-01') | first).status == 'pending'
- name: "({{ select_crypto_backend }}) Create HTTP challenges"
ansible.builtin.uri:
url: "http://{{ acme_host }}:5000/http/{{ item.identifier }}/{{ item.challenges['http-01'].resource[('.well-known/acme-challenge/' | length) :] }}"
@@ -161,7 +186,7 @@
ansible.builtin.debug:
var: order_info_2
- name: "({{ select_crypto_backend }}) Check order information"
- name: "({{ select_crypto_backend }}) Check order information (1/2)"
ansible.builtin.assert:
that:
- order_info_2 is not changed
@@ -186,6 +211,13 @@
- order_info_2.order_uri == order_1.order_uri
- order_info_2.account_uri == account.account_uri
- name: "({{ select_crypto_backend }}) Check order information (2/2)"
when: ansible_version.full is version('2.21', '>=')
ansible.builtin.assert:
that:
- (order_info_2.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-account-01') | map(attribute='status') | first | default('not there')) in ['pending', 'not there']
- (order_info_2.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-persist-01') | map(attribute='status') | first | default('not there')) in ['pending', 'not there']
- name: "({{ select_crypto_backend }}) Let the challenge be validated (idempotent)"
community.crypto.acme_certificate_order_validate:
acme_directory: "{{ acme_directory_url }}"
@@ -411,7 +443,7 @@
- replacement_order_1.account_uri == account.account_uri
- replacement_order_1.order_uri not in [order_1.order_uri]
- name: "({{ select_crypto_backend }}) Check replacement order 1 information"
- name: "({{ select_crypto_backend }}) Check replacement order 1 information (1/2)"
ansible.builtin.assert:
that:
- order_info_5 is not changed
@@ -436,6 +468,13 @@
- order_info_5.order_uri == replacement_order_1.order_uri
- order_info_5.account_uri == account.account_uri
- name: "({{ select_crypto_backend }}) Check replacement order 1 information (2/2)"
when: ansible_version.full is version('2.21', '>=')
ansible.builtin.assert:
that:
- (order_info_5.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-account-01') | first).status == 'pending'
- (order_info_5.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-persist-01') | first).status == 'pending'
# Right now Pebble does not reject duplicate replacement orders...
- when: false # TODO get Pebble improved
@@ -509,7 +548,7 @@
- >-
('Stop passing `replaces=' ~ cert_info.cert_id ~ '` due to error 409 urn:ietf:params:acme:error:alreadyReplaced when creating ACME order') in replacement_order_3.warnings
- name: "({{ select_crypto_backend }}) Check replacement order 3 information"
- name: "({{ select_crypto_backend }}) Check replacement order 3 information (1/2)"
ansible.builtin.assert:
that:
- order_info_6 is not changed
@@ -534,6 +573,13 @@
- order_info_6.order_uri == replacement_order_3.order_uri
- order_info_6.account_uri == account.account_uri
- name: "({{ select_crypto_backend }}) Check replacement order 3 information (2/2)"
when: ansible_version.full is version('2.21', '>=')
ansible.builtin.assert:
that:
- (order_info_6.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-account-01') | first).status == 'pending'
- (order_info_6.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-persist-01') | first).status == 'pending'
- name: "({{ select_crypto_backend }}) Deactivate authzs for replacement order 3"
community.crypto.acme_certificate_deactivate_authz:
acme_directory: "{{ acme_directory_url }}"
@@ -647,7 +693,7 @@
order_uri: "{{ replacement_order_5.order_uri }}"
register: order_info_7
- name: "({{ select_crypto_backend }}) Check replacement order 5"
- name: "({{ select_crypto_backend }}) Check replacement order 5 (1/2)"
ansible.builtin.assert:
that:
- replacement_order_5 is changed
@@ -670,7 +716,25 @@
- >-
('Stop passing `replaces=' ~ cert_info.cert_id ~ '` due to error 409 urn:ietf:params:acme:error:malformed when creating ACME order') in replacement_order_5.warnings
- name: "({{ select_crypto_backend }}) Check replacement order 5 information"
- name: "({{ select_crypto_backend }}) Check replacement order 5 (2/2)"
when: ansible_version.full is version('2.21', '>=')
ansible.builtin.assert:
that:
- "'dns-account-01' in replacement_order_5.challenge_data[0].challenges"
- "'dns-persist-01' in replacement_order_5.challenge_data[0].challenges"
- replacement_order_5.challenge_data[0].challenges['dns-account-01'].record.endswith('._acme-challenge.' ~ domain_name)
- replacement_order_5.challenge_data[0].challenges['dns-account-01'].resource.endswith('._acme-challenge')
- replacement_order_5.challenge_data[0].challenges['dns-account-01'].resource_value is string
- "'record' not in replacement_order_5.challenge_data[0].challenges['dns-persist-01']"
- "'resource' not in replacement_order_5.challenge_data[0].challenges['dns-persist-01']"
- "'resource_value' not in replacement_order_5.challenge_data[0].challenges['dns-persist-01']"
- replacement_order_5.challenge_data[0].challenges['dns-persist-01'].account_uri == replacement_order_5.account_uri
- replacement_order_5.challenge_data[0].challenges['dns-persist-01'].issuer_domain_names | length == 2
- replacement_order_5.challenge_data[0].challenges['dns-persist-01'].issuer_domain_names == ["pebble.letsencrypt.org", "ca.example.com"]
- replacement_order_5.challenge_data_dns_account | length == 1
- replacement_order_5.challenge_data_dns_account.values() | first | length == 1
- name: "({{ select_crypto_backend }}) Check replacement order 5 information (1/2)"
ansible.builtin.assert:
that:
- order_info_7 is not changed
@@ -695,6 +759,13 @@
- order_info_7.order_uri == replacement_order_5.order_uri
- order_info_7.account_uri == account.account_uri
- name: "({{ select_crypto_backend }}) Check replacement order 5 information (2/2)"
when: ansible_version.full is version('2.21', '>=')
ansible.builtin.assert:
that:
- (order_info_7.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-account-01') | first).status == 'pending'
- (order_info_7.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-persist-01') | first).status == 'pending'
- name: "({{ select_crypto_backend }}) Deactivate authzs for replacement order 5"
community.crypto.acme_certificate_deactivate_authz:
acme_directory: "{{ acme_directory_url }}"

View File

@@ -0,0 +1,6 @@
# 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
azp/generic/1
azp/posix/1

View File

@@ -0,0 +1,110 @@
---
# Copyright (c) 2026 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
- name: Basic tests
ansible.builtin.assert:
that:
- >-
'letsencrypt.org' | community.crypto.acme_dns_persist_record(
account_uri='https://acme-v02.api.letsencrypt.org/acme/acct/1234',
)
== "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234"
- >-
'letsencrypt.org' | community.crypto.acme_dns_persist_record(
account_uri='https://acme-v02.api.letsencrypt.org/acme/acct/1234',
persist_until=1234,
)
== "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234; persistUntil=1234"
- >-
'letsencrypt.org' | community.crypto.acme_dns_persist_record(
account_uri='https://acme-v02.api.letsencrypt.org/acme/acct/1234',
policy='wildcard',
)
== "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234; policy=wildcard"
- >-
'letsencrypt.org' | community.crypto.acme_dns_persist_record(
account_uri='https://acme-v02.api.letsencrypt.org/acme/acct/1234',
policy='wildcard',
persist_until=1234,
)
== "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234; policy=wildcard; persistUntil=1234"
- name: Test error (1/N)
ansible.builtin.debug:
msg: "{{ 42 | community.crypto.acme_dns_persist_record(account_uri='') }}"
ignore_errors: true
register: result
- name: Check error (1/N)
ansible.builtin.assert:
that:
- result is failed
- >-
"The input for the community.crypto.acme_dns_persist_record filter must be a string; got <class 'int'> instead" in result.msg
- name: Test error (2/N)
ansible.builtin.debug:
msg: "{{ '' | community.crypto.acme_dns_persist_record(account_uri=42) }}"
ignore_errors: true
register: result
- name: Check error (2/N)
ansible.builtin.assert:
that:
- result is failed
- >-
"The account_uri parameter for the community.crypto.acme_dns_persist_record filter must be a string; got <class 'int'> instead" in result.msg
- name: Test error (3/N)
ansible.builtin.debug:
msg: "{{ '' | community.crypto.acme_dns_persist_record(account_uri='', policy=42) }}"
ignore_errors: true
register: result
- name: Check error (3/N)
ansible.builtin.assert:
that:
- result is failed
- >-
'The policy parameter for the community.crypto.acme_dns_persist_record filter must be one of "wildcard"; got 42 instead' in result.msg
- name: Test error (4/N)
ansible.builtin.debug:
msg: "{{ '' | community.crypto.acme_dns_persist_record(account_uri='') }}"
ignore_errors: true
register: result
- name: Check error (4/N)
ansible.builtin.assert:
that:
- result is failed
- >-
"Error composing result for the community.crypto.acme_dns_persist_record filter: Invalid label ''" in result.msg
- name: Test error (5/N)
ansible.builtin.debug:
msg: "{{ '' | community.crypto.acme_dns_persist_record(account_uri='', persist_until=true) }}"
ignore_errors: true
register: result
- name: Check error (5/N)
ansible.builtin.assert:
that:
- result is failed
- >-
"The persist_until parameter for the community.crypto.acme_dns_persist_record filter must be an integer, a string, or a datetime object; got <class 'bool'> instead" in result.msg
- name: Test error (6/N)
ansible.builtin.debug:
msg: "{{ '' | community.crypto.acme_dns_persist_record(account_uri='', persist_until='foo bar') }}"
ignore_errors: true
register: result
- name: Check error (6/N)
ansible.builtin.assert:
that:
- result is failed
- >-
'Error parsing persist_until parameter for the community.crypto.acme_dns_persist_record filter: The time spec "foo bar" for persist_until is invalid' in result.msg

View File

@@ -0,0 +1,6 @@
# 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
azp/generic/1
azp/posix/1

View File

@@ -0,0 +1,113 @@
---
# Copyright (c) 2026 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
- name: Basic tests
ansible.builtin.assert:
that:
- >-
"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234"
| community.crypto.acme_dns_persist_record_parse
== {
"account_uri": "https://acme-v02.api.letsencrypt.org/acme/acct/1234",
"issuer_domain_name": "letsencrypt.org",
"persist_until": None,
"persist_until_str": None,
"policy": None,
}
- >-
"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234; policy=wildcard"
| community.crypto.acme_dns_persist_record_parse
== {
"account_uri": "https://acme-v02.api.letsencrypt.org/acme/acct/1234",
"issuer_domain_name": "letsencrypt.org",
"persist_until": None,
"persist_until_str": None,
"policy": "wildcard",
}
- >-
"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234; persistUntil=1234"
| community.crypto.acme_dns_persist_record_parse
== {
"account_uri": "https://acme-v02.api.letsencrypt.org/acme/acct/1234",
"issuer_domain_name": "letsencrypt.org",
"persist_until": 1234,
"persist_until_str": "19700101002034Z",
"policy": None,
}
- >-
"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234; policy=wildcard; persistUntil=1234"
| community.crypto.acme_dns_persist_record_parse
== {
"account_uri": "https://acme-v02.api.letsencrypt.org/acme/acct/1234",
"issuer_domain_name": "letsencrypt.org",
"persist_until": 1234,
"persist_until_str": "19700101002034Z",
"policy": "wildcard",
}
- name: Test error (1/N)
ansible.builtin.debug:
msg: "{{ 42 | community.crypto.acme_dns_persist_record_parse }}"
ignore_errors: true
register: result
- name: Check error (1/N)
ansible.builtin.assert:
that:
- result is failed
- >-
"The input for the community.crypto.acme_dns_persist_record_parse filter must be a string; got <class 'int'> instead" in result.msg
- name: Test error (2/N)
ansible.builtin.debug:
msg: "{{ ';' | community.crypto.acme_dns_persist_record_parse }}"
ignore_errors: true
register: result
- name: Check error (2/N)
ansible.builtin.assert:
that:
- result is failed
- >-
"community.crypto.acme_dns_persist_record_parse filter: domain name not present" in result.msg
- name: Test error (3/N)
ansible.builtin.debug:
msg: "{{ 'lets.encrypt; foo=bar' | community.crypto.acme_dns_persist_record_parse }}"
ignore_errors: true
register: result
- name: Check error (3/N)
ansible.builtin.assert:
that:
- result is failed
- >-
"community.crypto.acme_dns_persist_record_parse filter: cannot find account URI" in result.msg
- name: Test error (4/N)
ansible.builtin.debug:
msg: "{{ 'lets.encrypt; accounturi=foo; persistUntil=a' | community.crypto.acme_dns_persist_record_parse }}"
ignore_errors: true
register: result
- name: Check error (4/N)
ansible.builtin.assert:
that:
- result is failed
- >-
"community.crypto.acme_dns_persist_record_parse filter: error when parsing persistUntil: invalid literal for int() with base 10: 'a'" in result.msg
- name: Test error (1/N)
ansible.builtin.debug:
msg: "{{ 'a; b' | community.crypto.acme_dns_persist_record_parse }}"
ignore_errors: true
register: result
- name: Check error (1/N)
ansible.builtin.assert:
that:
- result is failed
- >-
"community.crypto.acme_dns_persist_record_parse filter could not parse value: 'b' is not of the form tag=value" in result.msg

View File

@@ -0,0 +1,180 @@
# 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
from __future__ import annotations
import re
import typing as t
import pytest
from ansible_collections.community.crypto.plugins.module_utils._caa import (
_check_domain_name,
_check_label,
_check_value,
join_issue_value,
parse_issue_value,
)
TEST_CHECK_VALUE: list[tuple[str, str | None]] = [
("", None),
("a", None),
(
"!\"#$%&'()*+,-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
None,
),
("a=b", None),
("\x1f", "Invalid value '\\x1f'"),
(" ", "Invalid value ' '"),
(";", "Invalid value ';'"),
("\x7f", "Invalid value '\\x7f'"),
("a b", "Invalid value 'a b'"),
]
@pytest.mark.parametrize("value, error", TEST_CHECK_VALUE)
def test_check_value(
value: str,
error: str | None,
) -> None:
if error is None:
_check_value(value)
else:
with pytest.raises(ValueError, match=f"^{re.escape(error)}$"):
_check_value(value)
TEST_CHECK_LABEL: list[tuple[str, str | None]] = [
("", "Invalid value ''"),
("a", None),
("0", None),
("a-", None),
("a=", "Invalid value 'a='"),
("-a", "Invalid value '-a'"),
(" ", "Invalid value ' '"),
("\t", "Invalid value '\\t'"),
("a0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-", None),
]
@pytest.mark.parametrize("value, error", TEST_CHECK_LABEL)
def test_check_label(
value: str,
error: str | None,
) -> None:
if error is None:
_check_label(value, "value")
else:
with pytest.raises(ValueError, match=f"^{re.escape(error)}$"):
_check_label(value, "value")
TEST_CHECK_DOMAIN_NAME: list[tuple[str, str | None]] = [
("", "Invalid label ''"),
("a.", "Invalid label ''"),
(".a", "Invalid label ''"),
("a.-", "Invalid label '-'"),
("a.b", None),
("a.b.c.d.e.f.g.h.i.j.k.l", None),
("letsencrypt.org", None),
]
@pytest.mark.parametrize("value, error", TEST_CHECK_DOMAIN_NAME)
def test_check_domain_name(
value: str,
error: str | None,
) -> None:
if error is None:
_check_domain_name(value)
else:
with pytest.raises(ValueError, match=f"^{re.escape(error)}$"):
_check_domain_name(value)
TEST_PARSE_ISSUE_VALUE: list[
tuple[str, dict[str, t.Any], str | None, list[tuple[str, str]]]
] = [
("", {}, None, []),
(";", {}, None, []),
("a=b", {"strict": False}, "a=b", []),
("; a=b", {"strict": False}, None, [("a", "b")]),
("a; a=b", {"strict": False}, "a", [("a", "b")]),
("a; a=b; c-d=e", {"strict": False}, "a", [("a", "b"), ("c-d", "e")]),
("ca1.example.net", {}, "ca1.example.net", []),
("ca1.example.net; account=230123", {}, "ca1.example.net", [("account", "230123")]),
]
@pytest.mark.parametrize(
"value, kwargs, expected_domain_name, expected_pairs", TEST_PARSE_ISSUE_VALUE
)
def test_parse_issue_value(
value: str,
kwargs: dict[str, t.Any],
expected_domain_name: str | None,
expected_pairs: list[tuple[str, str]],
) -> None:
assert parse_issue_value(value, **kwargs) == (expected_domain_name, expected_pairs)
TEST_PARSE_ISSUE_VALUE_FAIL: list[tuple[str, dict[str, t.Any], str]] = [
("a=b", {}, "Invalid label 'a=b'"),
("a; a.b=b", {}, "Invalid tag 'a.b'"),
("a; a=b; a=c", {}, "Tag 'a' appears multiple times"),
("%%%%%", {}, "Invalid label '%%%%%'"),
]
@pytest.mark.parametrize("value, kwargs, expected_error", TEST_PARSE_ISSUE_VALUE_FAIL)
def test_parse_issue_value_fail(
value: str, kwargs: dict[str, t.Any], expected_error: str
) -> None:
with pytest.raises(ValueError, match=f"^{re.escape(expected_error)}$"):
parse_issue_value(value, **kwargs)
TEST_JOIN_ISSUE_VALUE: list[
tuple[str | None, list[tuple[str, str]], dict[str, t.Any], str]
] = [
(None, [], {}, ""),
("a", [], {}, "a"),
(None, [("a", "b")], {}, "; a=b"),
("a", [("a", "b")], {}, "a; a=b"),
]
@pytest.mark.parametrize(
"domain_name, pairs, kwargs, expected_result", TEST_JOIN_ISSUE_VALUE
)
def test_join_issue_value(
domain_name: str | None,
pairs: list[tuple[str, str]],
kwargs: dict[str, t.Any],
expected_result: str,
) -> None:
assert join_issue_value(domain_name, pairs, **kwargs) == expected_result
TEST_JOIN_ISSUE_VALUE_FAIL: list[
tuple[str | None, list[tuple[str, str]], dict[str, t.Any], str]
] = [
("", [], {}, "Invalid label ''"),
(None, [("", "")], {}, "Invalid tag ''"),
(None, [("a", " ")], {}, "Invalid value ' '"),
(None, [("a", "a"), ("a", "b")], {}, "Tag 'a' appears multiple times"),
]
@pytest.mark.parametrize(
"domain_name, pairs, kwargs, expected_error", TEST_JOIN_ISSUE_VALUE_FAIL
)
def test_join_issue_value_fail(
domain_name: str | None,
pairs: list[tuple[str, str]],
kwargs: dict[str, t.Any],
expected_error: str,
) -> None:
with pytest.raises(ValueError, match=f"^{re.escape(expected_error)}$"):
join_issue_value(domain_name, pairs, **kwargs)