summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/crypto/tests
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/community/crypto/tests')
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/aliases10
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/meta/main.yml8
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tasks/impl.yml154
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tasks/main.yml40
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tests/validate.yml17
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/aliases10
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/meta/main.yml8
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/impl.yml145
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/main.yml40
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/obtain-cert.yml159
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tests/validate.yml47
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/impl.yml10
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/ownca.yml16
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/selfsigned.yml12
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_ownca.yml5
-rw-r--r--ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml5
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/backend_data.py89
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.txt38
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.txt.license3
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2-b.txt57
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2-b.txt.license3
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.pem19
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.pem.license3
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.txt56
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.txt.license3
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py57
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py59
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_utils.py97
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/crypto/test_math.py117
-rw-r--r--ansible_collections/community/crypto/tests/unit/plugins/module_utils/test_time.py323
30 files changed, 1609 insertions, 1 deletions
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/aliases b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/aliases
new file mode 100644
index 000000000..b7f6d4f48
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/aliases
@@ -0,0 +1,10 @@
+# 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
+cloud/acme
+
+# For some reason connecting to helper containers does not work on the Alpine VMs
+skip/alpine
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/meta/main.yml
new file mode 100644
index 000000000..2e8ad10b8
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/meta/main.yml
@@ -0,0 +1,8 @@
+---
+# 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
+
+dependencies:
+ - setup_acme
+ - setup_remote_tmp_dir
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tasks/impl.yml
new file mode 100644
index 000000000..28a889684
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tasks/impl.yml
@@ -0,0 +1,154 @@
+---
+# 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
+
+- vars:
+ certificate_name: cert-1
+ subject_alt_name: DNS:example.com
+ account_email: example@example.org
+ block:
+ - name: Generate account key
+ openssl_privatekey:
+ path: "{{ remote_tmp_dir }}/account-ec256.pem"
+ type: ECC
+ curve: secp256r1
+ force: true
+ - name: Create cert private key
+ openssl_privatekey:
+ path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key"
+ type: ECC
+ curve: secp256r1
+ force: true
+ - name: Create cert CSR
+ openssl_csr:
+ path: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr"
+ privatekey_path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key"
+ subject_alt_name: "{{ subject_alt_name }}"
+ - name: Start process of obtaining certificate
+ acme_certificate:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
+ modify_account: true
+ csr: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr"
+ dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem"
+ challenge: http-01
+ force: true
+ terms_agreed: true
+ account_email: "{{ account_email }}"
+ register: certificate_data
+
+- name: Inspect order
+ acme_inspect:
+ acme_directory: https://{{ acme_host }}:14000/dir
+ acme_version: 2
+ validate_certs: false
+ account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
+ account_uri: "{{ certificate_data.account_uri }}"
+ url: "{{ certificate_data.order_uri }}"
+ method: get
+ register: order_1
+- name: Show order
+ debug:
+ var: order_1.output_json
+
+- name: Deactivate order (check mode)
+ acme_certificate_deactivate_authz:
+ acme_directory: https://{{ acme_host }}:14000/dir
+ acme_version: 2
+ validate_certs: false
+ account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
+ account_uri: "{{ certificate_data.account_uri }}"
+ order_uri: "{{ certificate_data.order_uri }}"
+ check_mode: true
+ register: deactivate_1
+
+- name: Inspect order again
+ acme_inspect:
+ acme_directory: https://{{ acme_host }}:14000/dir
+ acme_version: 2
+ validate_certs: false
+ account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
+ account_uri: "{{ certificate_data.account_uri }}"
+ url: "{{ certificate_data.order_uri }}"
+ method: get
+ register: order_2
+- name: Show order
+ debug:
+ var: order_2.output_json
+
+- name: Deactivate order
+ acme_certificate_deactivate_authz:
+ acme_directory: https://{{ acme_host }}:14000/dir
+ acme_version: 2
+ validate_certs: false
+ account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
+ account_uri: "{{ certificate_data.account_uri }}"
+ order_uri: "{{ certificate_data.order_uri }}"
+ register: deactivate_2
+
+- name: Inspect order again
+ acme_inspect:
+ acme_directory: https://{{ acme_host }}:14000/dir
+ acme_version: 2
+ validate_certs: false
+ account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
+ account_uri: "{{ certificate_data.account_uri }}"
+ url: "{{ certificate_data.order_uri }}"
+ method: get
+ register: order_3
+- name: Show order
+ debug:
+ var: order_3.output_json
+
+- name: Deactivate order (check mode, idempotent)
+ acme_certificate_deactivate_authz:
+ acme_directory: https://{{ acme_host }}:14000/dir
+ acme_version: 2
+ validate_certs: false
+ account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
+ account_uri: "{{ certificate_data.account_uri }}"
+ order_uri: "{{ certificate_data.order_uri }}"
+ check_mode: true
+ register: deactivate_3
+
+- name: Inspect order again
+ acme_inspect:
+ acme_directory: https://{{ acme_host }}:14000/dir
+ acme_version: 2
+ validate_certs: false
+ account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
+ account_uri: "{{ certificate_data.account_uri }}"
+ url: "{{ certificate_data.order_uri }}"
+ method: get
+ register: order_4
+- name: Show order
+ debug:
+ var: order_4.output_json
+
+- name: Deactivate order (idempotent)
+ acme_certificate_deactivate_authz:
+ acme_directory: https://{{ acme_host }}:14000/dir
+ acme_version: 2
+ validate_certs: false
+ account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
+ account_uri: "{{ certificate_data.account_uri }}"
+ order_uri: "{{ certificate_data.order_uri }}"
+ register: deactivate_4
+
+- name: Inspect order again
+ acme_inspect:
+ acme_directory: https://{{ acme_host }}:14000/dir
+ acme_version: 2
+ validate_certs: false
+ account_key_src: "{{ remote_tmp_dir }}/account-ec256.pem"
+ account_uri: "{{ certificate_data.account_uri }}"
+ url: "{{ certificate_data.order_uri }}"
+ method: get
+ register: order_5
+- name: Show order
+ debug:
+ var: order_5.output_json
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tasks/main.yml
new file mode 100644
index 000000000..68d47973d
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tasks/main.yml
@@ -0,0 +1,40 @@
+---
+# 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
+
+####################################################################
+# WARNING: These are designed specifically for Ansible tests #
+# and should not be used as examples of how to write Ansible roles #
+####################################################################
+
+- block:
+ - name: Running tests with OpenSSL backend
+ include_tasks: impl.yml
+ vars:
+ select_crypto_backend: openssl
+
+ - import_tasks: ../tests/validate.yml
+
+ # Old 0.9.8 versions have insufficient CLI support for signing with EC keys
+ when: openssl_version.stdout is version('1.0.0', '>=')
+
+- name: Remove output directory
+ file:
+ path: "{{ remote_tmp_dir }}"
+ state: absent
+
+- name: Re-create output directory
+ file:
+ path: "{{ remote_tmp_dir }}"
+ state: directory
+
+- block:
+ - name: Running tests with cryptography backend
+ include_tasks: impl.yml
+ vars:
+ select_crypto_backend: cryptography
+
+ - import_tasks: ../tests/validate.yml
+
+ when: cryptography_version.stdout is version('1.5', '>=')
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tests/validate.yml
new file mode 100644
index 000000000..603c7d7cc
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_deactivate_authz/tests/validate.yml
@@ -0,0 +1,17 @@
+---
+# 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
+
+- name: Checks
+ assert:
+ that:
+ - order_1.output_json.status == 'pending'
+ - deactivate_1 is changed
+ - order_2.output_json.status == 'pending'
+ - deactivate_2 is changed
+ - order_3.output_json.status == 'deactivated'
+ - deactivate_3 is not changed
+ - order_4.output_json.status == 'deactivated'
+ - deactivate_4 is not changed
+ - order_5.output_json.status == 'deactivated'
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/aliases b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/aliases
new file mode 100644
index 000000000..b7f6d4f48
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/aliases
@@ -0,0 +1,10 @@
+# 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
+cloud/acme
+
+# For some reason connecting to helper containers does not work on the Alpine VMs
+skip/alpine
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/meta/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/meta/main.yml
new file mode 100644
index 000000000..2e8ad10b8
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/meta/main.yml
@@ -0,0 +1,8 @@
+---
+# 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
+
+dependencies:
+ - setup_acme
+ - setup_remote_tmp_dir
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/impl.yml
new file mode 100644
index 000000000..b30808ed5
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/impl.yml
@@ -0,0 +1,145 @@
+---
+# 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
+
+## SET UP ACCOUNT KEYS ########################################################################
+- block:
+ - name: Generate account keys
+ openssl_privatekey:
+ path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
+ type: "{{ item.type }}"
+ size: "{{ item.size | default(omit) }}"
+ curve: "{{ item.curve | default(omit) }}"
+ force: true
+ loop: "{{ account_keys }}"
+
+ vars:
+ account_keys:
+ - name: account-ec256
+ type: ECC
+ curve: secp256r1
+## CREATE ACCOUNTS AND OBTAIN CERTIFICATES ####################################################
+- name: Obtain cert 1
+ include_tasks: obtain-cert.yml
+ vars:
+ certgen_title: Certificate 1 for renewal check
+ certificate_name: cert-1
+ key_type: rsa
+ rsa_bits: "{{ default_rsa_key_size }}"
+ subject_alt_name: "DNS:example.com"
+ subject_alt_name_critical: false
+ account_key: account-ec256
+ challenge: http-01
+ modify_account: true
+ deactivate_authzs: false
+ force: true
+ remaining_days: "{{ omit }}"
+ terms_agreed: true
+ account_email: "example@example.org"
+## OBTAIN CERTIFICATE INFOS ###################################################################
+- name: Dump OpenSSL x509 info
+ command:
+ cmd: openssl x509 -in {{ remote_tmp_dir }}/cert-1.pem -noout -text
+- name: Obtain certificate information
+ x509_certificate_info:
+ path: "{{ remote_tmp_dir }}/cert-1.pem"
+ register: cert_1_info
+- name: Read certificate
+ slurp:
+ src: '{{ remote_tmp_dir }}/cert-1.pem'
+ register: slurp_cert_1
+- name: Obtain certificate information (1/9)
+ acme_certificate_renewal_info:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ # Certificate is valid for ~1826 days
+ register: cert_1_renewal_1
+- name: Obtain certificate information (2/9)
+ acme_certificate_renewal_info:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ # Certificate is valid for ~1826 days
+ remaining_days: 1000
+ remaining_percentage: 0.5
+ register: cert_1_renewal_2
+- name: Obtain certificate information (3/9)
+ acme_certificate_renewal_info:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ certificate_content: "{{ slurp_cert_1.content | b64decode }}"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ now: +1800d
+ # Certificate is valid for ~26 days
+ register: cert_1_renewal_3
+- name: Obtain certificate information (4/9)
+ acme_certificate_renewal_info:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ now: +1800d
+ # Certificate is valid for ~26 days
+ remaining_days: 30
+ remaining_percentage: 0.1
+ register: cert_1_renewal_4
+- name: Obtain certificate information (5/9)
+ acme_certificate_renewal_info:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ now: +1800d
+ # Certificate is valid for ~26 days
+ remaining_days: 30
+ remaining_percentage: 0.01
+ register: cert_1_renewal_5
+- name: Obtain certificate information (6/9)
+ acme_certificate_renewal_info:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ now: +1800d
+ # Certificate is valid for ~26 days
+ remaining_days: 10
+ remaining_percentage: 0.03
+ register: cert_1_renewal_6
+- name: Obtain certificate information (7/9)
+ acme_certificate_renewal_info:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ now: +1830d
+ # Certificate is no longer valid
+ register: cert_1_renewal_7
+- name: Obtain certificate information (8/9)
+ acme_certificate_renewal_info:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ now: +1830d
+ # Certificate is no longer valid
+ register: cert_1_renewal_8
+- name: Obtain certificate information (9/9)
+ acme_certificate_renewal_info:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ certificate_path: "{{ remote_tmp_dir }}/cert-does-not-exist.pem"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ # Certificate is no longer valid
+ register: cert_1_renewal_9
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/main.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/main.yml
new file mode 100644
index 000000000..68d47973d
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/main.yml
@@ -0,0 +1,40 @@
+---
+# 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
+
+####################################################################
+# WARNING: These are designed specifically for Ansible tests #
+# and should not be used as examples of how to write Ansible roles #
+####################################################################
+
+- block:
+ - name: Running tests with OpenSSL backend
+ include_tasks: impl.yml
+ vars:
+ select_crypto_backend: openssl
+
+ - import_tasks: ../tests/validate.yml
+
+ # Old 0.9.8 versions have insufficient CLI support for signing with EC keys
+ when: openssl_version.stdout is version('1.0.0', '>=')
+
+- name: Remove output directory
+ file:
+ path: "{{ remote_tmp_dir }}"
+ state: absent
+
+- name: Re-create output directory
+ file:
+ path: "{{ remote_tmp_dir }}"
+ state: directory
+
+- block:
+ - name: Running tests with cryptography backend
+ include_tasks: impl.yml
+ vars:
+ select_crypto_backend: cryptography
+
+ - import_tasks: ../tests/validate.yml
+
+ when: cryptography_version.stdout is version('1.5', '>=')
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/obtain-cert.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/obtain-cert.yml
new file mode 100644
index 000000000..6882e5339
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tasks/obtain-cert.yml
@@ -0,0 +1,159 @@
+---
+# 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
+
+## PRIVATE KEY ################################################################################
+- name: ({{ certgen_title }}) Create cert private key
+ openssl_privatekey:
+ path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key"
+ type: "{{ 'RSA' if key_type == 'rsa' else 'ECC' }}"
+ size: "{{ rsa_bits if key_type == 'rsa' else omit }}"
+ curve: >-
+ {{ omit if key_type == 'rsa' else
+ 'secp256r1' if key_type == 'ec256' else
+ 'secp384r1' if key_type == 'ec384' else
+ 'secp521r1' if key_type == 'ec521' else
+ 'invalid value for key_type!' }}
+ passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}"
+ cipher: "{{ 'auto' if certificate_passphrase | default() else omit }}"
+ force: true
+## CSR ########################################################################################
+- name: ({{ certgen_title }}) Create cert CSR
+ openssl_csr:
+ path: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr"
+ privatekey_path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key"
+ privatekey_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}"
+ subject_alt_name: "{{ subject_alt_name }}"
+ subject_alt_name_critical: "{{ subject_alt_name_critical }}"
+ return_content: true
+ register: csr_result
+## ACME STEP 1 ################################################################################
+- name: ({{ certgen_title }}) Obtain cert, step 1
+ acme_certificate:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}"
+ account_key_content: "{{ account_key_content | default(omit) }}"
+ account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}"
+ modify_account: "{{ modify_account }}"
+ csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}"
+ csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}"
+ dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem"
+ fullchain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-fullchain.pem"
+ chain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-chain.pem"
+ challenge: "{{ challenge }}"
+ deactivate_authzs: "{{ deactivate_authzs }}"
+ force: "{{ force }}"
+ remaining_days: "{{ remaining_days }}"
+ terms_agreed: "{{ terms_agreed }}"
+ account_email: "{{ account_email }}"
+ register: challenge_data
+- name: ({{ certgen_title }}) Print challenge data
+ debug:
+ var: challenge_data
+- name: ({{ certgen_title }}) Create HTTP challenges
+ uri:
+ url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}"
+ method: PUT
+ body_format: raw
+ body: "{{ item.value['http-01'].resource_value }}"
+ headers:
+ content-type: "application/octet-stream"
+ with_dict: "{{ challenge_data.challenge_data }}"
+ when: "challenge_data is changed and challenge == 'http-01'"
+- name: ({{ certgen_title }}) Create DNS challenges
+ uri:
+ url: "http://{{ acme_host }}:5000/dns/{{ item.key }}"
+ method: PUT
+ body_format: json
+ body: "{{ item.value }}"
+ with_dict: "{{ challenge_data.challenge_data_dns }}"
+ when: "challenge_data is changed and challenge == 'dns-01'"
+- name: ({{ certgen_title }}) Create TLS ALPN challenges (acme_challenge_cert_helper)
+ acme_challenge_cert_helper:
+ challenge: tls-alpn-01
+ challenge_data: "{{ item.value['tls-alpn-01'] }}"
+ private_key_src: "{{ remote_tmp_dir }}/{{ certificate_name }}.key"
+ private_key_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}"
+ with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else {} }}"
+ register: tls_alpn_challenges
+ when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')"
+- name: ({{ certgen_title }}) Read private key
+ slurp:
+ src: '{{ remote_tmp_dir }}/{{ certificate_name }}.key'
+ register: slurp
+ when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')"
+- name: ({{ certgen_title }}) Set TLS ALPN challenges (acme_challenge_cert_helper)
+ uri:
+ url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.domain }}/{{ item.identifier }}/certificate-and-key"
+ method: PUT
+ body_format: raw
+ body: "{{ item.challenge_certificate }}\n{{ slurp.content | b64decode }}"
+ headers:
+ content-type: "application/pem-certificate-chain"
+ with_items: "{{ tls_alpn_challenges.results if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else [] }}"
+ when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')"
+- name: ({{ certgen_title }}) Create TLS ALPN challenges (der-value-b64)
+ uri:
+ url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}/{{ item.value['tls-alpn-01'].resource_original }}/der-value-b64"
+ method: PUT
+ body_format: raw
+ body: "{{ item.value['tls-alpn-01'].resource_value }}"
+ headers:
+ content-type: "application/octet-stream"
+ with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64') else {} }}"
+ when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64')"
+## ACME STEP 2 ################################################################################
+- name: ({{ certgen_title }}) Obtain cert, step 2
+ acme_certificate:
+ select_crypto_backend: "{{ select_crypto_backend }}"
+ acme_version: 2
+ acme_directory: https://{{ acme_host }}:14000/dir
+ validate_certs: false
+ account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}"
+ account_key_content: "{{ account_key_content | default(omit) }}"
+ account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}"
+ account_uri: "{{ challenge_data.account_uri }}"
+ modify_account: "{{ modify_account }}"
+ csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}"
+ csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}"
+ dest: "{{ remote_tmp_dir }}/{{ certificate_name }}.pem"
+ fullchain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-fullchain.pem"
+ chain_dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-chain.pem"
+ challenge: "{{ challenge }}"
+ deactivate_authzs: "{{ deactivate_authzs }}"
+ force: "{{ force }}"
+ remaining_days: "{{ remaining_days }}"
+ terms_agreed: "{{ terms_agreed }}"
+ account_email: "{{ account_email }}"
+ data: "{{ challenge_data }}"
+ retrieve_all_alternates: "{{ retrieve_all_alternates | default(omit) }}"
+ select_chain: "{{ select_chain | default(omit) if select_crypto_backend == 'cryptography' else omit }}"
+ register: certificate_obtain_result
+ when: challenge_data is changed
+- name: ({{ certgen_title }}) Deleting HTTP challenges
+ uri:
+ url: "http://{{ acme_host }}:5000/http/{{ item.key }}/{{ item.value['http-01'].resource[('.well-known/acme-challenge/'|length):] }}"
+ method: DELETE
+ with_dict: "{{ challenge_data.challenge_data }}"
+ when: "challenge_data is changed and challenge == 'http-01'"
+- name: ({{ certgen_title }}) Deleting DNS challenges
+ uri:
+ url: "http://{{ acme_host }}:5000/dns/{{ item.key }}"
+ method: DELETE
+ with_dict: "{{ challenge_data.challenge_data_dns }}"
+ when: "challenge_data is changed and challenge == 'dns-01'"
+- name: ({{ certgen_title }}) Deleting TLS ALPN challenges
+ uri:
+ url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}"
+ method: DELETE
+ with_dict: "{{ challenge_data.challenge_data }}"
+ when: "challenge_data is changed and challenge == 'tls-alpn-01'"
+- name: ({{ certgen_title }}) Get root certificate
+ get_url:
+ url: "http://{{ acme_host }}:5000/root-certificate-for-ca/{{ acme_expected_root_number | default(0) if select_crypto_backend == 'cryptography' else 0 }}"
+ dest: "{{ remote_tmp_dir }}/{{ certificate_name }}-root.pem"
+###############################################################################################
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tests/validate.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tests/validate.yml
new file mode 100644
index 000000000..116e524c4
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_certificate_renewal_info/tests/validate.yml
@@ -0,0 +1,47 @@
+---
+# 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
+
+- name: Validate results
+ assert:
+ that:
+ - cert_1_renewal_1.should_renew == false
+ - cert_1_renewal_1.msg == 'The certificate is still valid and no condition was reached'
+ - cert_1_renewal_1.supports_ari == supports_ari
+ - cert_1_renewal_1.cert_id is string or not can_have_cert_id
+ - cert_1_renewal_2.should_renew == false
+ - cert_1_renewal_2.msg == 'The certificate is still valid and no condition was reached'
+ - cert_1_renewal_2.supports_ari == supports_ari
+ - cert_1_renewal_2.cert_id is string or not can_have_cert_id
+ - cert_1_renewal_3.should_renew == false
+ - cert_1_renewal_3.msg == 'The certificate is still valid and no condition was reached'
+ - cert_1_renewal_3.supports_ari == supports_ari
+ - cert_1_renewal_3.cert_id is string or not can_have_cert_id
+ - cert_1_renewal_4.should_renew == true
+ - cert_1_renewal_4.msg == 'The certificate expires in 25 days'
+ - cert_1_renewal_4.supports_ari == supports_ari
+ - cert_1_renewal_4.cert_id is string or not can_have_cert_id
+ - cert_1_renewal_5.should_renew == true
+ - cert_1_renewal_5.msg == 'The certificate expires in 25 days'
+ - cert_1_renewal_5.supports_ari == supports_ari
+ - cert_1_renewal_5.cert_id is string or not can_have_cert_id
+ - cert_1_renewal_6.should_renew == true
+ - cert_1_renewal_6.msg.startswith("The remaining percentage 3.0% of the certificate's lifespan was reached on ")
+ - cert_1_renewal_6.supports_ari == supports_ari
+ - cert_1_renewal_6.cert_id is string or not can_have_cert_id
+ - cert_1_renewal_7.should_renew == true
+ - cert_1_renewal_7.msg == 'The certificate has already expired'
+ - cert_1_renewal_7.supports_ari == false
+ - cert_1_renewal_7.cert_id is string or not can_have_cert_id
+ - cert_1_renewal_8.should_renew == true
+ - cert_1_renewal_8.msg == 'No certificate was specified'
+ - cert_1_renewal_8.supports_ari == false
+ - cert_1_renewal_8.cert_id is not defined
+ - cert_1_renewal_9.should_renew == true
+ - cert_1_renewal_9.msg == 'The certificate file does not exist'
+ - cert_1_renewal_9.supports_ari == false
+ - cert_1_renewal_9.cert_id is not defined
+ vars:
+ can_have_cert_id: cert_1_info.authority_key_identifier is string
+ supports_ari: false
diff --git a/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/impl.yml b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/impl.yml
index 4eed1031a..d87501884 100644
--- a/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/impl.yml
+++ b/ansible_collections/community/crypto/tests/integration/targets/acme_inspect/tasks/impl.yml
@@ -28,6 +28,7 @@
acme_version: 2
validate_certs: false
method: directory-only
+ select_crypto_backend: "{{ select_crypto_backend }}"
register: directory
- debug: var=directory
@@ -40,6 +41,7 @@
url: "{{ directory.directory.newAccount}}"
method: post
content: '{"termsOfServiceAgreed":true}'
+ select_crypto_backend: "{{ select_crypto_backend }}"
register: account_creation
# account_creation.headers.location contains the account URI
# if creation was successful
@@ -54,6 +56,7 @@
account_uri: "{{ account_creation.headers.location }}"
url: "{{ account_creation.headers.location }}"
method: get
+ select_crypto_backend: "{{ select_crypto_backend }}"
register: account_get
- debug: var=account_get
@@ -67,6 +70,7 @@
url: "{{ account_creation.headers.location }}"
method: post
content: '{{ account_info | to_json }}'
+ select_crypto_backend: "{{ select_crypto_backend }}"
vars:
account_info:
# For valid values, see
@@ -86,6 +90,7 @@
url: "{{ directory.directory.newOrder }}"
method: post
content: '{{ create_order | to_json }}'
+ select_crypto_backend: "{{ select_crypto_backend }}"
vars:
create_order:
# For valid values, see
@@ -108,6 +113,7 @@
account_uri: "{{ account_creation.headers.location }}"
url: "{{ new_order.headers.location }}"
method: get
+ select_crypto_backend: "{{ select_crypto_backend }}"
register: order
- debug: var=order
@@ -120,6 +126,7 @@
account_uri: "{{ account_creation.headers.location }}"
url: "{{ item }}"
method: get
+ select_crypto_backend: "{{ select_crypto_backend }}"
loop: "{{ order.output_json.authorizations }}"
register: authz
- debug: var=authz
@@ -133,6 +140,7 @@
account_uri: "{{ account_creation.headers.location }}"
url: "{{ (item.challenges | selectattr('type', 'equalto', 'http-01') | list)[0].url }}"
method: get
+ select_crypto_backend: "{{ select_crypto_backend }}"
register: http01challenge
loop: "{{ authz.results | map(attribute='output_json') | list }}"
- debug: var=http01challenge
@@ -147,6 +155,7 @@
url: "{{ item.url }}"
method: post
content: '{}'
+ select_crypto_backend: "{{ select_crypto_backend }}"
register: activation
loop: "{{ http01challenge.results | map(attribute='output_json') | list }}"
- debug: var=activation
@@ -160,6 +169,7 @@
account_uri: "{{ account_creation.headers.location }}"
url: "{{ item.url }}"
method: get
+ select_crypto_backend: "{{ select_crypto_backend }}"
register: validation_result
loop: "{{ http01challenge.results | map(attribute='output_json') | list }}"
until: "validation_result.output_json.status not in ['pending', 'processing']"
diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/ownca.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/ownca.yml
index 99832a517..4bbd818ee 100644
--- a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/ownca.yml
+++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/ownca.yml
@@ -249,10 +249,24 @@
ownca_not_after: 20191023133742Z
path: "{{ remote_tmp_dir }}/ownca_cert3.pem"
csr_path: "{{ remote_tmp_dir }}/csr.csr"
- privatekey_path: "{{ remote_tmp_dir }}/privatekey3.pem"
+ privatekey_path: "{{ remote_tmp_dir }}/privatekey.pem"
+ ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem'
+ ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem'
+ select_crypto_backend: '{{ select_crypto_backend }}'
+
+- name: (OwnCA, {{select_crypto_backend}}) Create ownca certificate with notBefore and notAfter (idempotent)
+ x509_certificate:
+ provider: ownca
+ ownca_not_before: 20181023133742Z
+ ownca_not_after: 20191023133742Z
+ ignore_timestamps: false
+ path: "{{ remote_tmp_dir }}/ownca_cert3.pem"
+ csr_path: "{{ remote_tmp_dir }}/csr.csr"
+ privatekey_path: "{{ remote_tmp_dir }}/privatekey.pem"
ownca_path: '{{ remote_tmp_dir }}/ca_cert.pem'
ownca_privatekey_path: '{{ remote_tmp_dir }}/ca_privatekey.pem'
select_crypto_backend: '{{ select_crypto_backend }}'
+ register: ownca_cert3_idem
- name: (OwnCA, {{select_crypto_backend}}) Create ownca certificate with relative notBefore and notAfter
x509_certificate:
diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/selfsigned.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/selfsigned.yml
index a0f23643b..eeea25ddd 100644
--- a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/selfsigned.yml
+++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tasks/selfsigned.yml
@@ -220,6 +220,18 @@
privatekey_path: "{{ remote_tmp_dir }}/privatekey3.pem"
select_crypto_backend: '{{ select_crypto_backend }}'
+- name: (Selfsigned, {{select_crypto_backend}}) Create certificate3 with notBefore and notAfter (idempotent)
+ x509_certificate:
+ provider: selfsigned
+ selfsigned_not_before: 20181023133742Z
+ selfsigned_not_after: 20191023133742Z
+ ignore_timestamps: false
+ path: "{{ remote_tmp_dir }}/cert3.pem"
+ csr_path: "{{ remote_tmp_dir }}/csr3.pem"
+ privatekey_path: "{{ remote_tmp_dir }}/privatekey3.pem"
+ select_crypto_backend: '{{ select_crypto_backend }}'
+ register: cert3_selfsigned_idem
+
- name: (Selfsigned, {{select_crypto_backend}}) Generate privatekey
openssl_privatekey:
path: '{{ remote_tmp_dir }}/privatekey_ecc.pem'
diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_ownca.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_ownca.yml
index ac25b6295..ade7e6f51 100644
--- a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_ownca.yml
+++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_ownca.yml
@@ -98,6 +98,11 @@
that:
- ownca_cert3_notAfter.stdout == 'Oct 23 13:37:42 2019'
+- name: (OwnCA validation, {{select_crypto_backend}}) Validate idempotency
+ assert:
+ that:
+ - ownca_cert3_idem is not changed
+
- name: (OwnCA validation, {{select_crypto_backend}}) Validate ownca ECC certificate (test - ownca certificate pubkey)
shell: '{{ openssl_binary }} x509 -noout -pubkey -in {{ remote_tmp_dir }}/ownca_cert_ecc.pem'
register: ownca_cert_ecc_pubkey
diff --git a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml
index c76310437..c7254eb3e 100644
--- a/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml
+++ b/ansible_collections/community/crypto/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml
@@ -139,6 +139,11 @@
that:
- cert3_notAfter.stdout == 'Oct 23 13:37:42 2019'
+- name: (Selfsigned validation, {{select_crypto_backend}}) Validate idempotency
+ assert:
+ that:
+ - cert3_selfsigned_idem is not changed
+
- name: (Selfsigned validation, {{select_crypto_backend}}) Validate ECC certificate (test - privatekey's pubkey)
shell: '{{ openssl_binary }} ec -pubout -in {{ remote_tmp_dir }}/privatekey_ecc.pem'
register: privatekey_ecc_pubkey
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/backend_data.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/backend_data.py
index 988bcdaeb..c4aa09a6a 100644
--- a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/backend_data.py
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/backend_data.py
@@ -9,8 +9,10 @@ __metaclass__ = type
import base64
import datetime
import os
+import sys
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
+ CertificateInformation,
CryptoBackend,
)
@@ -79,6 +81,12 @@ TEST_CSRS = [
TEST_CERT = load_fixture("cert_1.pem")
+TEST_CERT_2 = load_fixture("cert_2.pem")
+
+
+TEST_CERT_OPENSSL_OUTPUT = load_fixture("cert_1.txt") # OpenSSL 3.3.0 output
+TEST_CERT_OPENSSL_OUTPUT_2 = load_fixture("cert_2.txt") # OpenSSL 3.3.0 output
+TEST_CERT_OPENSSL_OUTPUT_2B = load_fixture("cert_2-b.txt") # OpenSSL 1.1.1f output
TEST_CERT_DAYS = [
@@ -88,6 +96,81 @@ TEST_CERT_DAYS = [
]
+TEST_CERT_INFO = CertificateInformation(
+ not_valid_after=datetime.datetime(2018, 11, 26, 15, 28, 24),
+ not_valid_before=datetime.datetime(2018, 11, 25, 15, 28, 23),
+ serial_number=1,
+ subject_key_identifier=b'\x98\xD2\xFD\x3C\xCC\xCD\x69\x45\xFB\xE2\x8C\x30\x2C\x54\x62\x18\x34\xB7\x07\x73',
+ authority_key_identifier=None,
+)
+
+
+TEST_CERT_INFO_2 = CertificateInformation(
+ not_valid_before=datetime.datetime(2024, 5, 4, 20, 42, 21),
+ not_valid_after=datetime.datetime(2029, 5, 4, 20, 42, 20),
+ serial_number=4218235397573492796,
+ subject_key_identifier=b'\x17\xE5\x83\x22\x14\xEF\x74\xD3\xBE\x7E\x30\x76\x56\x1F\x51\x74\x65\x1F\xE9\xF0',
+ authority_key_identifier=b'\x13\xC3\x4C\x3E\x59\x45\xDD\xE3\x63\x51\xA3\x46\x80\xC4\x08\xC7\x14\xC0\x64\x4E',
+)
+
+
+TEST_CERT_INFO = [
+ (TEST_CERT, TEST_CERT_INFO, TEST_CERT_OPENSSL_OUTPUT),
+ (TEST_CERT_2, TEST_CERT_INFO_2, TEST_CERT_OPENSSL_OUTPUT_2),
+ (TEST_CERT_2, TEST_CERT_INFO_2, TEST_CERT_OPENSSL_OUTPUT_2B),
+]
+
+
+TEST_PARSE_ACME_TIMESTAMP = [
+ (
+ '2024-01-01T00:11:22Z',
+ dict(year=2024, month=1, day=1, hour=0, minute=11, second=22),
+ ),
+ (
+ '2024-01-01T00:11:22.123Z',
+ dict(year=2024, month=1, day=1, hour=0, minute=11, second=22, microsecond=123000),
+ ),
+ (
+ '2024-04-17T06:54:13.333333334Z',
+ dict(year=2024, month=4, day=17, hour=6, minute=54, second=13, microsecond=333333),
+ ),
+]
+
+if sys.version_info >= (3, 5):
+ TEST_PARSE_ACME_TIMESTAMP.extend([
+ (
+ '2024-01-01T00:11:22+0100',
+ dict(year=2023, month=12, day=31, hour=23, minute=11, second=22),
+ ),
+ (
+ '2024-01-01T00:11:22.123+0100',
+ dict(year=2023, month=12, day=31, hour=23, minute=11, second=22, microsecond=123000),
+ ),
+ ])
+
+
+TEST_INTERPOLATE_TIMESTAMP = [
+ (
+ dict(year=2024, month=1, day=1, hour=0, minute=0, second=0),
+ dict(year=2024, month=1, day=1, hour=1, minute=0, second=0),
+ 0.0,
+ dict(year=2024, month=1, day=1, hour=0, minute=0, second=0),
+ ),
+ (
+ dict(year=2024, month=1, day=1, hour=0, minute=0, second=0),
+ dict(year=2024, month=1, day=1, hour=1, minute=0, second=0),
+ 0.5,
+ dict(year=2024, month=1, day=1, hour=0, minute=30, second=0),
+ ),
+ (
+ dict(year=2024, month=1, day=1, hour=0, minute=0, second=0),
+ dict(year=2024, month=1, day=1, hour=1, minute=0, second=0),
+ 1.0,
+ dict(year=2024, month=1, day=1, hour=1, minute=0, second=0),
+ ),
+]
+
+
class FakeBackend(CryptoBackend):
def parse_key(self, key_file=None, key_content=None, passphrase=None):
raise BackendException('Not implemented in fake backend')
@@ -98,6 +181,9 @@ class FakeBackend(CryptoBackend):
def create_mac_key(self, alg, key):
raise BackendException('Not implemented in fake backend')
+ def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
+ raise BackendException('Not implemented in fake backend')
+
def get_csr_identifiers(self, csr_filename=None, csr_content=None):
raise BackendException('Not implemented in fake backend')
@@ -106,3 +192,6 @@ class FakeBackend(CryptoBackend):
def create_chain_matcher(self, criterium):
raise BackendException('Not implemented in fake backend')
+
+ def get_cert_information(self, cert_filename=None, cert_content=None):
+ raise BackendException('Not implemented in fake backend')
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.txt b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.txt
new file mode 100644
index 000000000..e989d914d
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.txt
@@ -0,0 +1,38 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 1 (0x1)
+ Signature Algorithm: ecdsa-with-SHA256
+ Issuer: CN=ansible.com
+ Validity
+ Not Before: Nov 25 15:28:23 2018 GMT
+ Not After : Nov 26 15:28:24 2018 GMT
+ Subject: CN=ansible.com
+ Subject Public Key Info:
+ Public Key Algorithm: id-ecPublicKey
+ Public-Key: (256 bit)
+ pub:
+ 04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35:
+ f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a:
+ d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90:
+ e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31:
+ 38:e3:f1:29:9b
+ ASN1 OID: prime256v1
+ NIST CURVE: P-256
+ X509v3 extensions:
+ X509v3 Subject Alternative Name:
+ DNS:example.com, DNS:example.org
+ X509v3 Basic Constraints: critical
+ CA:FALSE
+ X509v3 Key Usage: critical
+ Digital Signature
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication
+ X509v3 Subject Key Identifier:
+ 98:D2:FD:3C:CC:CD:69:45:FB:E2:8C:30:2C:54:62:18:34:B7:07:73
+ Signature Algorithm: ecdsa-with-SHA256
+ Signature Value:
+ 30:46:02:21:00:bc:fb:52:bf:7a:93:2d:0e:7c:ce:43:f4:cc:
+ 05:98:28:36:8d:c7:2a:9b:f5:20:94:62:3d:fb:82:9e:38:42:
+ 32:02:21:00:c0:55:f8:b5:d9:65:41:2a:dd:d4:76:3f:8c:cb:
+ 07:c1:d2:b9:c0:7d:c9:90:af:fd:f9:f1:b0:c9:13:f5:d5:52
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.txt.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_1.txt.license
@@ -0,0 +1,3 @@
+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
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2-b.txt b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2-b.txt
new file mode 100644
index 000000000..78326443b
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2-b.txt
@@ -0,0 +1,57 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 4218235397573492796 (0x3a8a2ebeb358c03c)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: CN = Pebble Intermediate CA 734609
+ Validity
+ Not Before: May 4 20:42:21 2024 GMT
+ Not After : May 4 20:42:20 2029 GMT
+ Subject: CN = example.com
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (1024 bit)
+ Modulus:
+ 00:c1:43:a5:f9:ad:00:b7:bb:1b:73:27:00:b3:a2:
+ 4e:27:0d:ff:ae:64:3e:a0:7e:f9:28:56:48:47:21:
+ 9e:0f:d8:fb:69:b5:21:e8:98:84:60:6c:aa:73:b9:
+ 6e:d9:f6:19:ad:85:e0:c2:f6:80:d3:22:b8:5a:d6:
+ 3a:89:3e:2a:7a:fc:1d:bf:fc:69:20:e5:91:b8:34:
+ 52:26:c8:15:74:e1:36:0c:cd:ab:01:4a:ad:83:f5:
+ 0b:77:96:31:cf:1c:ea:6f:88:75:23:ac:51:a6:d8:
+ 77:43:1b:b3:44:93:2c:8d:05:25:fb:77:41:36:94:
+ 81:d5:ca:56:ff:b5:23:b2:a5
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Key Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ X509v3 Basic Constraints: critical
+ CA:FALSE
+ X509v3 Subject Key Identifier:
+ 17:E5:83:22:14:EF:74:D3:BE:7E:30:76:56:1F:51:74:65:1F:E9:F0
+ X509v3 Authority Key Identifier:
+ keyid:13:C3:4C:3E:59:45:DD:E3:63:51:A3:46:80:C4:08:C7:14:C0:64:4E
+
+ Authority Information Access:
+ OCSP - URI:http://10.88.0.74:5000/ocsp
+
+ X509v3 Subject Alternative Name:
+ DNS:example.com
+ Signature Algorithm: sha256WithRSAEncryption
+ 31:43:de:b6:48:f4:b8:30:46:25:65:e6:91:22:33:1b:d1:ba:
+ 3f:60:f8:c3:18:32:72:e9:f8:d1:88:11:5a:0a:86:dc:1d:6d:
+ a5:ea:58:cd:05:ea:cd:5e:40:86:c1:ae:d5:cd:2e:8a:ca:50:
+ ee:df:bd:cf:6c:d9:20:3b:4b:49:f8:d5:8a:e3:be:f3:dd:24:
+ b2:7f:3f:3b:bf:e6:8d:7a:f8:8f:4b:6e:25:60:80:33:6f:0f:
+ 53:b7:7d:94:2a:d2:4a:db:3a:2f:70:79:d7:bf:05:ed:df:10:
+ 61:e7:24:ac:b2:fc:03:bd:ad:8c:e1:f3:1d:cc:78:99:e3:22:
+ 59:bf:c5:92:57:95:92:56:35:fc:05:8b:26:10:c5:1b:87:17:
+ 64:0b:bd:33:a9:54:d5:c0:2b:43:56:1b:52:d3:4f:8b:6f:25:
+ 06:58:7f:6f:aa:27:35:05:d5:57:6d:83:a0:73:de:40:3f:67:
+ 1c:5a:92:c6:37:e6:8f:c7:b8:91:d7:50:b9:4d:d4:f2:92:1f:
+ 8b:93:0c:e2:b4:b8:d7:1d:8e:ce:6d:19:dc:8f:12:8e:c0:f2:
+ 92:3b:95:5a:8c:c8:69:0e:0b:f7:fa:1f:55:62:80:7c:e2:f6:
+ 41:3f:7d:69:36:9e:7c:90:7e:d7:3b:e6:a3:15:de:a4:7d:95:
+ 13:46:c6:1a
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2-b.txt.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2-b.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2-b.txt.license
@@ -0,0 +1,3 @@
+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
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.pem b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.pem
new file mode 100644
index 000000000..92aecb621
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDDjCCAfagAwIBAgIIOoouvrNYwDwwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
+AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA3MzQ2MDkwHhcNMjQwNTA0MjA0MjIx
+WhcNMjkwNTA0MjA0MjIwWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCBnzANBgkq
+hkiG9w0BAQEFAAOBjQAwgYkCgYEAwUOl+a0At7sbcycAs6JOJw3/rmQ+oH75KFZI
+RyGeD9j7abUh6JiEYGyqc7lu2fYZrYXgwvaA0yK4WtY6iT4qevwdv/xpIOWRuDRS
+JsgVdOE2DM2rAUqtg/ULd5Yxzxzqb4h1I6xRpth3QxuzRJMsjQUl+3dBNpSB1cpW
+/7UjsqUCAwEAAaOB0TCBzjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB
+BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFBflgyIU73TT
+vn4wdlYfUXRlH+nwMB8GA1UdIwQYMBaAFBPDTD5ZRd3jY1GjRoDECMcUwGROMDcG
+CCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovLzEwLjg4LjAuNzQ6NTAw
+MC9vY3NwMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IB
+AQAxQ962SPS4MEYlZeaRIjMb0bo/YPjDGDJy6fjRiBFaCobcHW2l6ljNBerNXkCG
+wa7VzS6KylDu373PbNkgO0tJ+NWK477z3SSyfz87v+aNeviPS24lYIAzbw9Tt32U
+KtJK2zovcHnXvwXt3xBh5ySssvwDva2M4fMdzHiZ4yJZv8WSV5WSVjX8BYsmEMUb
+hxdkC70zqVTVwCtDVhtS00+LbyUGWH9vqic1BdVXbYOgc95AP2ccWpLGN+aPx7iR
+11C5TdTykh+LkwzitLjXHY7ObRncjxKOwPKSO5VajMhpDgv3+h9VYoB84vZBP31p
+Np58kH7XO+ajFd6kfZUTRsYa
+-----END CERTIFICATE-----
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.pem.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.pem.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.pem.license
@@ -0,0 +1,3 @@
+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
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.txt b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.txt
new file mode 100644
index 000000000..3cda74955
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.txt
@@ -0,0 +1,56 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 4218235397573492796 (0x3a8a2ebeb358c03c)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: CN=Pebble Intermediate CA 734609
+ Validity
+ Not Before: May 4 20:42:21 2024 GMT
+ Not After : May 4 20:42:20 2029 GMT
+ Subject: CN=example.com
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (1024 bit)
+ Modulus:
+ 00:c1:43:a5:f9:ad:00:b7:bb:1b:73:27:00:b3:a2:
+ 4e:27:0d:ff:ae:64:3e:a0:7e:f9:28:56:48:47:21:
+ 9e:0f:d8:fb:69:b5:21:e8:98:84:60:6c:aa:73:b9:
+ 6e:d9:f6:19:ad:85:e0:c2:f6:80:d3:22:b8:5a:d6:
+ 3a:89:3e:2a:7a:fc:1d:bf:fc:69:20:e5:91:b8:34:
+ 52:26:c8:15:74:e1:36:0c:cd:ab:01:4a:ad:83:f5:
+ 0b:77:96:31:cf:1c:ea:6f:88:75:23:ac:51:a6:d8:
+ 77:43:1b:b3:44:93:2c:8d:05:25:fb:77:41:36:94:
+ 81:d5:ca:56:ff:b5:23:b2:a5
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Key Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ X509v3 Basic Constraints: critical
+ CA:FALSE
+ X509v3 Subject Key Identifier:
+ 17:E5:83:22:14:EF:74:D3:BE:7E:30:76:56:1F:51:74:65:1F:E9:F0
+ X509v3 Authority Key Identifier:
+ 13:C3:4C:3E:59:45:DD:E3:63:51:A3:46:80:C4:08:C7:14:C0:64:4E
+ Authority Information Access:
+ OCSP - URI:http://10.88.0.74:5000/ocsp
+ X509v3 Subject Alternative Name:
+ DNS:example.com
+ Signature Algorithm: sha256WithRSAEncryption
+ Signature Value:
+ 31:43:de:b6:48:f4:b8:30:46:25:65:e6:91:22:33:1b:d1:ba:
+ 3f:60:f8:c3:18:32:72:e9:f8:d1:88:11:5a:0a:86:dc:1d:6d:
+ a5:ea:58:cd:05:ea:cd:5e:40:86:c1:ae:d5:cd:2e:8a:ca:50:
+ ee:df:bd:cf:6c:d9:20:3b:4b:49:f8:d5:8a:e3:be:f3:dd:24:
+ b2:7f:3f:3b:bf:e6:8d:7a:f8:8f:4b:6e:25:60:80:33:6f:0f:
+ 53:b7:7d:94:2a:d2:4a:db:3a:2f:70:79:d7:bf:05:ed:df:10:
+ 61:e7:24:ac:b2:fc:03:bd:ad:8c:e1:f3:1d:cc:78:99:e3:22:
+ 59:bf:c5:92:57:95:92:56:35:fc:05:8b:26:10:c5:1b:87:17:
+ 64:0b:bd:33:a9:54:d5:c0:2b:43:56:1b:52:d3:4f:8b:6f:25:
+ 06:58:7f:6f:aa:27:35:05:d5:57:6d:83:a0:73:de:40:3f:67:
+ 1c:5a:92:c6:37:e6:8f:c7:b8:91:d7:50:b9:4d:d4:f2:92:1f:
+ 8b:93:0c:e2:b4:b8:d7:1d:8e:ce:6d:19:dc:8f:12:8e:c0:f2:
+ 92:3b:95:5a:8c:c8:69:0e:0b:f7:fa:1f:55:62:80:7c:e2:f6:
+ 41:3f:7d:69:36:9e:7c:90:7e:d7:3b:e6:a3:15:de:a4:7d:95:
+ 13:46:c6:1a
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.txt.license b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.txt.license
new file mode 100644
index 000000000..edff8c768
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/fixtures/cert_2.txt.license
@@ -0,0 +1,3 @@
+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
+SPDX-FileCopyrightText: Ansible Project
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py
index 59da68a3b..9186e2430 100644
--- a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py
@@ -16,11 +16,22 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryp
CryptographyBackend,
)
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ ensure_utc_timezone,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
+ CRYPTOGRAPHY_TIMEZONE,
+)
+
from .backend_data import (
TEST_KEYS,
TEST_CSRS,
TEST_CERT,
TEST_CERT_DAYS,
+ TEST_CERT_INFO,
+ TEST_PARSE_ACME_TIMESTAMP,
+ TEST_INTERPOLATE_TIMESTAMP,
)
@@ -64,3 +75,49 @@ def test_certdays_cryptography(now, expected_days, tmpdir):
assert days == expected_days
days = backend.get_cert_days(cert_content=TEST_CERT, now=now)
assert days == expected_days
+
+
+@pytest.mark.parametrize("cert_content, expected_cert_info, openssl_output", TEST_CERT_INFO)
+def test_get_cert_information(cert_content, expected_cert_info, openssl_output, tmpdir):
+ fn = tmpdir / 'test-cert.pem'
+ fn.write(cert_content)
+ module = MagicMock()
+ backend = CryptographyBackend(module)
+
+ if CRYPTOGRAPHY_TIMEZONE:
+ expected_cert_info = expected_cert_info._replace(
+ not_valid_after=ensure_utc_timezone(expected_cert_info.not_valid_after),
+ not_valid_before=ensure_utc_timezone(expected_cert_info.not_valid_before),
+ )
+
+ cert_info = backend.get_cert_information(cert_filename=str(fn))
+ assert cert_info == expected_cert_info
+ cert_info = backend.get_cert_information(cert_content=cert_content)
+ assert cert_info == expected_cert_info
+
+
+def test_now():
+ module = MagicMock()
+ backend = CryptographyBackend(module)
+ now = backend.get_now()
+ assert CRYPTOGRAPHY_TIMEZONE == (now.tzinfo is not None)
+
+
+@pytest.mark.parametrize("input, expected", TEST_PARSE_ACME_TIMESTAMP)
+def test_parse_acme_timestamp(input, expected):
+ module = MagicMock()
+ backend = CryptographyBackend(module)
+ ts_expected = backend.get_utc_datetime(**expected)
+ timestamp = backend.parse_acme_timestamp(input)
+ assert ts_expected == timestamp
+
+
+@pytest.mark.parametrize("start, end, percentage, expected", TEST_INTERPOLATE_TIMESTAMP)
+def test_interpolate_timestamp(start, end, percentage, expected):
+ module = MagicMock()
+ backend = CryptographyBackend(module)
+ ts_start = backend.get_utc_datetime(**start)
+ ts_end = backend.get_utc_datetime(**end)
+ ts_expected = backend.get_utc_datetime(**expected)
+ timestamp = backend.interpolate_timestamp(ts_start, ts_end, percentage)
+ assert ts_expected == timestamp
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py
index dd30cf795..5138a6202 100644
--- a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py
@@ -18,6 +18,12 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.backend_open
from .backend_data import (
TEST_KEYS,
TEST_CSRS,
+ TEST_CERT,
+ TEST_CERT_OPENSSL_OUTPUT,
+ TEST_CERT_DAYS,
+ TEST_CERT_INFO,
+ TEST_PARSE_ACME_TIMESTAMP,
+ TEST_INTERPOLATE_TIMESTAMP,
)
@@ -61,3 +67,56 @@ def test_normalize_ip(ip, result):
module = MagicMock()
backend = OpenSSLCLIBackend(module, openssl_binary='openssl')
assert backend._normalize_ip(ip) == result
+
+
+@pytest.mark.parametrize("now, expected_days", TEST_CERT_DAYS)
+def test_certdays_cryptography(now, expected_days, tmpdir):
+ fn = tmpdir / 'test-cert.pem'
+ fn.write(TEST_CERT)
+ module = MagicMock()
+ module.run_command = MagicMock(return_value=(0, TEST_CERT_OPENSSL_OUTPUT, 0))
+ backend = OpenSSLCLIBackend(module, openssl_binary='openssl')
+ days = backend.get_cert_days(cert_filename=str(fn), now=now)
+ assert days == expected_days
+ days = backend.get_cert_days(cert_content=TEST_CERT, now=now)
+ assert days == expected_days
+
+
+@pytest.mark.parametrize("cert_content, expected_cert_info, openssl_output", TEST_CERT_INFO)
+def test_get_cert_information(cert_content, expected_cert_info, openssl_output, tmpdir):
+ fn = tmpdir / 'test-cert.pem'
+ fn.write(cert_content)
+ module = MagicMock()
+ module.run_command = MagicMock(return_value=(0, openssl_output, 0))
+ backend = OpenSSLCLIBackend(module, openssl_binary='openssl')
+ cert_info = backend.get_cert_information(cert_filename=str(fn))
+ assert cert_info == expected_cert_info
+ cert_info = backend.get_cert_information(cert_content=cert_content)
+ assert cert_info == expected_cert_info
+
+
+def test_now():
+ module = MagicMock()
+ backend = OpenSSLCLIBackend(module, openssl_binary='openssl')
+ now = backend.get_now()
+ assert now.tzinfo is None
+
+
+@pytest.mark.parametrize("input, expected", TEST_PARSE_ACME_TIMESTAMP)
+def test_parse_acme_timestamp(input, expected):
+ module = MagicMock()
+ backend = OpenSSLCLIBackend(module, openssl_binary='openssl')
+ ts_expected = backend.get_utc_datetime(**expected)
+ timestamp = backend.parse_acme_timestamp(input)
+ assert ts_expected == timestamp
+
+
+@pytest.mark.parametrize("start, end, percentage, expected", TEST_INTERPOLATE_TIMESTAMP)
+def test_interpolate_timestamp(start, end, percentage, expected):
+ module = MagicMock()
+ backend = OpenSSLCLIBackend(module, openssl_binary='openssl')
+ ts_start = backend.get_utc_datetime(**start)
+ ts_end = backend.get_utc_datetime(**end)
+ ts_expected = backend.get_utc_datetime(**expected)
+ timestamp = backend.interpolate_timestamp(ts_start, ts_end, percentage)
+ assert ts_expected == timestamp
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_utils.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_utils.py
index 9bdd8eb6e..5cc318ac2 100644
--- a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_utils.py
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/acme/test_utils.py
@@ -6,12 +6,20 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
+import datetime
+
import pytest
+from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
+ CertificateInformation,
+)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
nopad_b64,
pem_to_der,
+ process_links,
+ parse_retry_after,
+ compute_cert_id,
)
from .backend_data import (
@@ -27,6 +35,73 @@ NOPAD_B64 = [
]
+TEST_LINKS_HEADER = [
+ (
+ {},
+ [],
+ ),
+ (
+ {
+ 'link': '<foo>; rel="bar"'
+ },
+ [
+ ('foo', 'bar'),
+ ],
+ ),
+ (
+ {
+ 'link': '<foo>; rel="bar", <baz>; rel="bam"'
+ },
+ [
+ ('foo', 'bar'),
+ ('baz', 'bam'),
+ ],
+ ),
+ (
+ {
+ 'link': '<https://one.example.com>; rel="preconnect", <https://two.example.com>; rel="preconnect", <https://three.example.com>; rel="preconnect"'
+ },
+ [
+ ('https://one.example.com', 'preconnect'),
+ ('https://two.example.com', 'preconnect'),
+ ('https://three.example.com', 'preconnect'),
+ ],
+ ),
+]
+
+
+TEST_RETRY_AFTER_HEADER = [
+ ('120', datetime.datetime(2024, 4, 29, 0, 2, 0)),
+ ('Wed, 21 Oct 2015 07:28:00 GMT', datetime.datetime(2015, 10, 21, 7, 28, 0)),
+]
+
+
+TEST_COMPUTE_CERT_ID = [
+ (
+ CertificateInformation(
+ not_valid_after=datetime.datetime(2018, 11, 26, 15, 28, 24),
+ not_valid_before=datetime.datetime(2018, 11, 25, 15, 28, 23),
+ serial_number=1,
+ subject_key_identifier=None,
+ authority_key_identifier=b'\x00\xff',
+ ),
+ 'AP8.AQ',
+ ),
+ (
+ # AKI, serial number, and expected result taken from
+ # https://letsencrypt.org/2024/04/25/guide-to-integrating-ari-into-existing-acme-clients.html#step-3-constructing-the-ari-certid
+ CertificateInformation(
+ not_valid_after=datetime.datetime(2018, 11, 26, 15, 28, 24),
+ not_valid_before=datetime.datetime(2018, 11, 25, 15, 28, 23),
+ serial_number=0x87654321,
+ subject_key_identifier=None,
+ authority_key_identifier=b'\x69\x88\x5B\x6B\x87\x46\x40\x41\xE1\xB3\x7B\x84\x7B\xA0\xAE\x2C\xDE\x01\xC8\xD4',
+ ),
+ 'aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE',
+ ),
+]
+
+
@pytest.mark.parametrize("value, result", NOPAD_B64)
def test_nopad_b64(value, result):
assert nopad_b64(value.encode('utf-8')) == result
@@ -37,3 +112,25 @@ def test_pem_to_der(pem, der, tmpdir):
fn = tmpdir / 'test.pem'
fn.write(pem)
assert pem_to_der(str(fn)) == der
+
+
+@pytest.mark.parametrize("value, expected_result", TEST_LINKS_HEADER)
+def test_process_links(value, expected_result):
+ data = []
+
+ def callback(url, rel):
+ data.append((url, rel))
+
+ process_links(value, callback)
+
+ assert expected_result == data
+
+
+@pytest.mark.parametrize("value, expected_result", TEST_RETRY_AFTER_HEADER)
+def test_parse_retry_after(value, expected_result):
+ assert expected_result == parse_retry_after(value, now=datetime.datetime(2024, 4, 29, 0, 0, 0))
+
+
+@pytest.mark.parametrize("cert_info, expected_result", TEST_COMPUTE_CERT_ID)
+def test_compute_cert_id(cert_info, expected_result):
+ assert expected_result == compute_cert_id(backend=None, cert_info=cert_info)
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/crypto/test_math.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/crypto/test_math.py
new file mode 100644
index 000000000..4fd917713
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/crypto/test_math.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024, 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.crypto.plugins.module_utils.crypto.math import (
+ binary_exp_mod,
+ simple_gcd,
+ quick_is_not_prime,
+ convert_int_to_bytes,
+ convert_int_to_hex,
+ convert_bytes_to_int,
+)
+
+
+@pytest.mark.parametrize('f, e, m, result', [
+ (0, 0, 5, 1),
+ (0, 1, 5, 0),
+ (2, 1, 5, 2),
+ (2, 2, 5, 4),
+ (2, 3, 5, 3),
+ (2, 10, 5, 4),
+])
+def test_binary_exp_mod(f, e, m, result):
+ value = binary_exp_mod(f, e, m)
+ print(value)
+ assert value == result
+
+
+@pytest.mark.parametrize('a, b, result', [
+ (0, -123, -123),
+ (0, 123, 123),
+ (-123, 0, -123),
+ (123, 0, 123),
+ (-123, 1, 1),
+ (123, 1, 1),
+ (1, -123, -1),
+ (1, 123, 1),
+ (1024, 10, 2),
+])
+def test_simple_gcd(a, b, result):
+ value = simple_gcd(a, b)
+ print(value)
+ assert value == result
+
+
+@pytest.mark.parametrize('n, result', [
+ (-2, True),
+ (0, True),
+ (1, True),
+ (2, False),
+ (3, False),
+ (4, True),
+ (5, False),
+ (6, True),
+ (7, False),
+ (8, True),
+ (9, True),
+ (10, True),
+ (211, False), # the smallest prime number >= 200
+])
+def test_quick_is_not_prime(n, result):
+ value = quick_is_not_prime(n)
+ print(value)
+ assert value == result
+
+
+@pytest.mark.parametrize('no, count, result', [
+ (0, None, b''),
+ (0, 1, b'\x00'),
+ (0, 2, b'\x00\x00'),
+ (1, None, b'\x01'),
+ (1, 2, b'\x00\x01'),
+ (255, None, b'\xff'),
+ (256, None, b'\x01\x00'),
+])
+def test_convert_int_to_bytes(no, count, result):
+ value = convert_int_to_bytes(no, count=count)
+ print(value)
+ assert value == result
+
+
+@pytest.mark.parametrize('no, digits, result', [
+ (0, None, '0'),
+ (1, None, '1'),
+ (16, None, '10'),
+ (1, 3, '001'),
+ (255, None, 'ff'),
+ (256, None, '100'),
+ (256, 2, '100'),
+ (256, 3, '100'),
+ (256, 4, '0100'),
+])
+def test_convert_int_to_hex(no, digits, result):
+ value = convert_int_to_hex(no, digits=digits)
+ print(value)
+ assert value == result
+
+
+@pytest.mark.parametrize('data, result', [
+ (b'', 0),
+ (b'\x00', 0),
+ (b'\x00\x01', 1),
+ (b'\x01', 1),
+ (b'\xff', 255),
+ (b'\x01\x00', 256),
+])
+def test_convert_bytes_to_int(data, result):
+ value = convert_bytes_to_int(data)
+ print(value)
+ assert value == result
diff --git a/ansible_collections/community/crypto/tests/unit/plugins/module_utils/test_time.py b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/test_time.py
new file mode 100644
index 000000000..35a83f4e4
--- /dev/null
+++ b/ansible_collections/community/crypto/tests/unit/plugins/module_utils/test_time.py
@@ -0,0 +1,323 @@
+# 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 absolute_import, division, print_function
+__metaclass__ = type
+
+
+import datetime
+import sys
+
+import pytest
+
+
+from ansible_collections.community.crypto.plugins.module_utils.time import (
+ add_or_remove_timezone,
+ get_now_datetime,
+ convert_relative_to_datetime,
+ ensure_utc_timezone,
+ from_epoch_seconds,
+ get_epoch_seconds,
+ get_relative_time_option,
+ remove_timezone,
+ UTC,
+)
+
+
+TEST_REMOVE_TIMEZONE = [
+ (
+ datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=UTC),
+ datetime.datetime(2024, 1, 1, 0, 1, 2),
+ ),
+ (
+ datetime.datetime(2024, 1, 1, 0, 1, 2),
+ datetime.datetime(2024, 1, 1, 0, 1, 2),
+ ),
+]
+
+TEST_UTC_TIMEZONE = [
+ (
+ datetime.datetime(2024, 1, 1, 0, 1, 2),
+ datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=UTC),
+ ),
+ (
+ datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=UTC),
+ datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=UTC),
+ ),
+]
+
+TEST_EPOCH_SECONDS = [
+ (0, dict(year=1970, day=1, month=1, hour=0, minute=0, second=0, microsecond=0)),
+ (1E-6, dict(year=1970, day=1, month=1, hour=0, minute=0, second=0, microsecond=1)),
+ (1E-3, dict(year=1970, day=1, month=1, hour=0, minute=0, second=0, microsecond=1000)),
+ (3691.2, dict(year=1970, day=1, month=1, hour=1, minute=1, second=31, microsecond=200000)),
+]
+
+TEST_EPOCH_TO_SECONDS = [
+ (datetime.datetime(1970, 1, 1, 0, 1, 2, 0), 62),
+ (datetime.datetime(1970, 1, 1, 0, 1, 2, 0, tzinfo=UTC), 62),
+]
+
+TEST_CONVERT_RELATIVE_TO_DATETIME = [
+ (
+ '+0',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ ),
+ (
+ '+1s',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC),
+ datetime.datetime(2024, 1, 1, 0, 0, 1),
+ ),
+ (
+ '-10w20d30h40m50s',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC),
+ datetime.datetime(2023, 10, 1, 17, 19, 10),
+ ),
+ (
+ '+0',
+ True,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC),
+ ),
+ (
+ '+1s',
+ True,
+ datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC),
+ datetime.datetime(2024, 1, 1, 0, 0, 1, tzinfo=UTC),
+ ),
+ (
+ '-10w20d30h40m50s',
+ True,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2023, 10, 1, 17, 19, 10, tzinfo=UTC),
+ ),
+]
+
+TEST_GET_RELATIVE_TIME_OPTION = [
+ (
+ '+1d2h3m4s',
+ 'foo',
+ 'cryptography',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 2, 2, 3, 4),
+ ),
+ (
+ '-1w10d24h',
+ 'foo',
+ 'cryptography',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2023, 12, 14, 0, 0, 0),
+ ),
+ (
+ '20240102040506Z',
+ 'foo',
+ 'cryptography',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 2, 4, 5, 6),
+ ),
+ (
+ '202401020405Z',
+ 'foo',
+ 'cryptography',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 2, 4, 5, 0),
+ ),
+ (
+ '+1d2h3m4s',
+ 'foo',
+ 'cryptography',
+ True,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 2, 2, 3, 4, tzinfo=UTC),
+ ),
+ (
+ '-1w10d24h',
+ 'foo',
+ 'cryptography',
+ True,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2023, 12, 14, 0, 0, 0, tzinfo=UTC),
+ ),
+ (
+ '20240102040506Z',
+ 'foo',
+ 'cryptography',
+ True,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 2, 4, 5, 6, tzinfo=UTC),
+ ),
+ (
+ '202401020405Z',
+ 'foo',
+ 'cryptography',
+ True,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 2, 4, 5, 0, tzinfo=UTC),
+ ),
+ (
+ '+1d2h3m4s',
+ 'foo',
+ 'pyopenssl',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ '20240102020304Z',
+ ),
+ (
+ '-1w10d24h',
+ 'foo',
+ 'pyopenssl',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ '20231214000000Z',
+ ),
+ (
+ '20240102040506Z',
+ 'foo',
+ 'pyopenssl',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ '20240102040506Z',
+ ),
+ (
+ '202401020405Z',
+ 'foo',
+ 'pyopenssl',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ '202401020405Z',
+ ),
+]
+
+
+if sys.version_info >= (3, 5):
+ ONE_HOUR_PLUS = datetime.timezone(datetime.timedelta(hours=1))
+
+ TEST_REMOVE_TIMEZONE.extend([
+ (
+ datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=ONE_HOUR_PLUS),
+ datetime.datetime(2023, 12, 31, 23, 1, 2),
+ ),
+ ])
+ TEST_UTC_TIMEZONE.extend([
+ (
+ datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=ONE_HOUR_PLUS),
+ datetime.datetime(2023, 12, 31, 23, 1, 2, tzinfo=UTC),
+ ),
+ ])
+ TEST_EPOCH_TO_SECONDS.extend([
+ (datetime.datetime(1970, 1, 1, 0, 1, 2, 0, tzinfo=ONE_HOUR_PLUS), 62 - 3600),
+ ])
+ TEST_GET_RELATIVE_TIME_OPTION.extend([
+ (
+ '20240102040506+0100',
+ 'foo',
+ 'cryptography',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 2, 3, 5, 6),
+ ),
+ (
+ '202401020405+0100',
+ 'foo',
+ 'cryptography',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 2, 3, 5, 0),
+ ),
+ (
+ '20240102040506+0100',
+ 'foo',
+ 'cryptography',
+ True,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 2, 3, 5, 6, tzinfo=UTC),
+ ),
+ (
+ '202401020405+0100',
+ 'foo',
+ 'cryptography',
+ True,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ datetime.datetime(2024, 1, 2, 3, 5, 0, tzinfo=UTC),
+ ),
+ (
+ '20240102040506+0100',
+ 'foo',
+ 'pyopenssl',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ '20240102040506+0100',
+ ),
+ (
+ '202401020405+0100',
+ 'foo',
+ 'pyopenssl',
+ False,
+ datetime.datetime(2024, 1, 1, 0, 0, 0),
+ '202401020405+0100',
+ ),
+ ])
+
+
+@pytest.mark.parametrize("input, expected", TEST_REMOVE_TIMEZONE)
+def test_remove_timezone(input, expected):
+ output_1 = remove_timezone(input)
+ assert expected == output_1
+ output_2 = add_or_remove_timezone(input, with_timezone=False)
+ assert expected == output_2
+
+
+@pytest.mark.parametrize("input, expected", TEST_UTC_TIMEZONE)
+def test_utc_timezone(input, expected):
+ output_1 = ensure_utc_timezone(input)
+ assert expected == output_1
+ output_2 = add_or_remove_timezone(input, with_timezone=True)
+ assert expected == output_2
+
+
+def test_get_now_datetime():
+ output_1 = get_now_datetime(with_timezone=False)
+ assert output_1.tzinfo is None
+ output_2 = get_now_datetime(with_timezone=True)
+ assert output_2.tzinfo is not None
+ assert output_2.tzinfo == UTC
+
+
+@pytest.mark.parametrize("seconds, timestamp", TEST_EPOCH_SECONDS)
+def test_epoch_seconds(seconds, timestamp):
+ ts_wo_tz = datetime.datetime(**timestamp)
+ assert seconds == get_epoch_seconds(ts_wo_tz)
+ timestamp_w_tz = dict(timestamp)
+ timestamp_w_tz['tzinfo'] = UTC
+ ts_w_tz = datetime.datetime(**timestamp_w_tz)
+ assert seconds == get_epoch_seconds(ts_w_tz)
+ output_1 = from_epoch_seconds(seconds, with_timezone=False)
+ assert ts_wo_tz == output_1
+ output_2 = from_epoch_seconds(seconds, with_timezone=True)
+ assert ts_w_tz == output_2
+
+
+@pytest.mark.parametrize("timestamp, expected_seconds", TEST_EPOCH_TO_SECONDS)
+def test_epoch_to_seconds(timestamp, expected_seconds):
+ assert expected_seconds == get_epoch_seconds(timestamp)
+
+
+@pytest.mark.parametrize("relative_time_string, with_timezone, now, expected", TEST_CONVERT_RELATIVE_TO_DATETIME)
+def test_convert_relative_to_datetime(relative_time_string, with_timezone, now, expected):
+ output = convert_relative_to_datetime(relative_time_string, with_timezone=with_timezone, now=now)
+ assert expected == output
+
+
+@pytest.mark.parametrize("input_string, input_name, backend, with_timezone, now, expected", TEST_GET_RELATIVE_TIME_OPTION)
+def test_get_relative_time_option(input_string, input_name, backend, with_timezone, now, expected):
+ output = get_relative_time_option(input_string, input_name, backend=backend, with_timezone=with_timezone, now=now)
+ assert expected == output