diff options
Diffstat (limited to 'ansible_collections/microsoft/ad/tests')
22 files changed, 566 insertions, 480 deletions
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml index fb4eee366..2a403c3d5 100644 --- a/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml +++ b/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml @@ -122,6 +122,7 @@ trusted_for_delegation: true upn: MyComputer@{{ domain_realm }} path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + protect_from_deletion: true register: custom_comp - set_fact: @@ -137,6 +138,7 @@ - msDS-AllowedToActOnBehalfOfOtherIdentity - msDS-SupportedEncryptionTypes - objectSid + - ProtectedFromAccidentalDeletion - sAMAccountName - servicePrincipalName - userAccountControl @@ -174,6 +176,7 @@ - custom_comp_actual.objects[0]['msDS-SupportedEncryptionTypes_AnsibleFlags'] == ["AES128_CTS_HMAC_SHA1_96", "AES256_CTS_HMAC_SHA1_96"] - custom_comp_actual.objects[0].sAMAccountName == 'SamMyComputer$' - custom_comp_actual.objects[0].ObjectClass == 'computer' + - custom_comp_actual.objects[0].ProtectedFromAccidentalDeletion == true - custom_comp_actual.objects[0].servicePrincipalName == 'HTTP/MyComputer' - custom_comp_actual.objects[0].userPrincipalName == 'MyComputer@' ~ domain_realm - '"ADS_UF_ACCOUNTDISABLE" in custom_comp_actual.objects[0].userAccountControl_AnsibleFlags' @@ -197,6 +200,7 @@ sam_account_name: MyComputer2$ trusted_for_delegation: false upn: mycomputer@{{ domain_realm }} + protect_from_deletion: false register: change_comp - name: get result of change computer with custom options @@ -207,6 +211,7 @@ - location - msDS-AllowedToActOnBehalfOfOtherIdentity - msDS-SupportedEncryptionTypes + - ProtectedFromAccidentalDeletion - sAMAccountName - userAccountControl - userPrincipalName @@ -235,6 +240,7 @@ - change_comp_actual.objects[0].location == 'comp location' - change_comp_actual.objects[0]['msDS-SupportedEncryptionTypes'] == 20 - change_comp_actual.objects[0]['msDS-SupportedEncryptionTypes_AnsibleFlags'] == ["RC4_HMAC", "AES256_CTS_HMAC_SHA1_96"] + - change_comp_actual.objects[0].ProtectedFromAccidentalDeletion == false - change_comp_actual.objects[0].sAMAccountName == 'MyComputer2$' - change_comp_actual.objects[0].userPrincipalName == 'mycomputer@' ~ domain_realm - '"ADS_UF_ACCOUNTDISABLE" not in change_comp_actual.objects[0].userAccountControl_AnsibleFlags' diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml index bdb1b95b7..b40041b0d 100644 --- a/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml +++ b/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml @@ -88,6 +88,14 @@ - 3 - 4 + - name: create test group with long name + group: + name: MyGroup2-ReallyLongGroupNameHere + state: present + scope: global + path: '{{ ou_info.distinguished_name }}' + register: test_group + - name: fail to find members to add to a group group: name: MyGroup @@ -109,6 +117,7 @@ add: - my_user_1 - '{{ test_users.results[2].sid }}' + - MyGroup2-ReallyLongGroupNameHere register: add_member_check check_mode: true @@ -133,6 +142,7 @@ add: - my_user_1 - '{{ test_users.results[2].sid }}' + - MyGroup2-ReallyLongGroupNameHere register: add_member - name: get result of add members to a group @@ -146,9 +156,10 @@ assert: that: - add_member is changed - - add_member_actual.objects[0].member | length == 2 + - add_member_actual.objects[0].member | length == 3 - test_users.results[0].distinguished_name in add_member_actual.objects[0].member - test_users.results[2].distinguished_name in add_member_actual.objects[0].member + - test_group.distinguished_name in add_member_actual.objects[0].member - name: add members to a group - idempotent group: @@ -158,6 +169,7 @@ add: - user_1@{{ domain_realm }} - '{{ test_users.results[2].object_guid }}' + - MyGroup2-ReallyLongGroupNameHere register: add_member_again - name: assert add members to a group - idempotent @@ -186,7 +198,8 @@ assert: that: - remove_member is changed - - remove_member_actual.objects[0].member == test_users.results[2].distinguished_name + - test_users.results[2].distinguished_name in remove_member_actual.objects[0].member + - test_group.distinguished_name in remove_member_actual.objects[0].member - name: remove member from a group - idempotent group: @@ -226,9 +239,10 @@ assert: that: - add_remove_member is changed - - add_remove_member_actual.objects[0].member | length == 2 + - add_remove_member_actual.objects[0].member | length == 3 - test_users.results[0].distinguished_name in add_remove_member_actual.objects[0].member - test_users.results[1].distinguished_name in add_remove_member_actual.objects[0].member + - test_group.distinguished_name in add_remove_member_actual.objects[0].member - name: set members group: diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/main.yml index 86b6d75e9..7e0bb2e2b 100644 --- a/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/main.yml +++ b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/main.yml @@ -95,6 +95,23 @@ - import_tasks: invoke.yml vars: + scenario: LDAP through lookup templates + inventory: + plugin: microsoft.ad.ldap + server: !unsafe '{{ lookup("ansible.builtin.env", "LDAP_SERVER") }}' + username: !unsafe '{{ lookup("ansible.builtin.env", "LDAP_USERNAME") }}' + password: !unsafe '{{ lookup("ansible.builtin.env", "LDAP_PASSWORD") }}' + environment: + LDAP_SERVER: '{{ ldap_server }}' + LDAP_USERNAME: '{{ ldap_user }}' + LDAP_PASSWORD: '{{ ldap_pass }}' + +- name: assert LDAP through lookup templates + assert: + that: *default-assertion + +- import_tasks: invoke.yml + vars: scenario: LDAPS inventory: plugin: microsoft.ad.ldap @@ -434,7 +451,7 @@ nothing_member: this_member: this raw_member: raw - computer_membership: this | map("regex_search", '^CN=(?P<name>.+?)((?<!\\),)', '\g<name>') | flatten + computer_membership: this | microsoft.ad.parse_dn | map(attribute="0.1") compose: host_var: computer_sid groups: diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/ansible.cfg b/ansible_collections/microsoft/ad/tests/integration/targets/membership/ansible.cfg index 3a986973e..50093ac61 100644 --- a/ansible_collections/microsoft/ad/tests/integration/targets/membership/ansible.cfg +++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/ansible.cfg @@ -1,3 +1,4 @@ [defaults] inventory = inventory.yml retry_files_enabled = False +callback_result_format = yaml diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml index e4fa96c8e..f66985da9 100644 --- a/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml +++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml @@ -26,6 +26,23 @@ Get-ADComputer -Filter { Name -ne 'DC' } -Properties DistinguishedName, Name, Enabled | Select-Object -Property DistinguishedName, Name, Enabled +- name: join domain invalid OU + membership: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + domain_ou_path: CN=Invalid,{{ domain_dn_base }} + state: domain + reboot: true + ignore_errors: true + register: join_domain_invalid_ou + +- name: assert join domain invalid OU + assert: + that: + - join_domain_invalid_ou is failed + - join_domain_invalid_ou.msg.endswith('Check domain_ou_path is pointing to a valid OU in the target domain.') + - name: join domain - check mode membership: dns_domain_name: '{{ domain_realm }}' diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/tests.yml index b642ce6eb..b18160926 100644 --- a/ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/tests.yml +++ b/ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/tests.yml @@ -441,6 +441,59 @@ - move_ou_actual.objects[0].DistinguishedName == 'OU=TestOU 2,' ~ sub_ous.results[0].distinguished_name - move_ou_actual.objects[0].ProtectedFromAccidentalDeletion == true +- name: do not move object in non default path without path - check + object: + name: TestOU 2 + identity: '{{ sub_ous.results[1].object_guid }}' + type: organizationalUnit + attributes: + set: + description: Test comment + register: dont_move_no_path_check + check_mode: true + +- name: get result of do not move object in non default path without path - check + object_info: + identity: '{{ sub_ous.results[1].object_guid }}' + properties: + - description + register: dont_move_no_path_check_actual + +- name: assert do not move object in non default path without path - check + assert: + that: + - dont_move_no_path_check is changed + - dont_move_no_path_check.distinguished_name == 'OU=TestOU 2,' ~ sub_ous.results[0].distinguished_name + - dont_move_no_path_check_actual.objects[0].Name == 'TestOU 2' + - dont_move_no_path_check_actual.objects[0].DistinguishedName == 'OU=TestOU 2,' ~ sub_ous.results[0].distinguished_name + - dont_move_no_path_check_actual.objects[0].Description == None + +- name: do not move object in non default path without path + object: + name: TestOU 2 + identity: '{{ sub_ous.results[1].object_guid }}' + type: organizationalUnit + attributes: + set: + description: Test comment + register: dont_move_no_path + +- name: get result of do not move object in non default path without path + object_info: + identity: '{{ sub_ous.results[1].object_guid }}' + properties: + - description + register: dont_move_no_path_actual + +- name: assert do not move object in non default path without path - check + assert: + that: + - dont_move_no_path is changed + - dont_move_no_path.distinguished_name == 'OU=TestOU 2,' ~ sub_ous.results[0].distinguished_name + - dont_move_no_path_actual.objects[0].Name == 'TestOU 2' + - dont_move_no_path_actual.objects[0].DistinguishedName == 'OU=TestOU 2,' ~ sub_ous.results[0].distinguished_name + - dont_move_no_path_actual.objects[0].Description == 'Test comment' + - name: remove object that is protected from deletion - check object: name: My, Container @@ -1444,3 +1497,57 @@ assert: that: - not unset_normal_again is changed + +- name: move object back into the default path - check + object: + name: My, Container + identity: '{{ object_identity }}' + type: container + path: microsoft.ad.default_path + register: move_into_default_check + check_mode: true + +- name: get result of move object back into the default path - check + object_info: + identity: '{{ object_identity }}' + register: move_into_default_check_actual + +- name: assert move object back into the default path - check + assert: + that: + - move_into_default_check is changed + - move_into_default_check.distinguished_name == 'CN=My\, Container,' ~ setup_domain_info.output[0].defaultNamingContext + - move_into_default_check_actual.objects[0].DistinguishedName == 'CN=My\, Container,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + +- name: move object back into the default path + object: + name: My, Container + identity: '{{ object_identity }}' + type: container + path: microsoft.ad.default_path + register: move_into_default + +- name: get result of move object back into the default path + object_info: + identity: '{{ object_identity }}' + register: move_into_default_actual + +- name: assert move object back into the default path + assert: + that: + - move_into_default is changed + - move_into_default.distinguished_name == 'CN=My\, Container,' ~ setup_domain_info.output[0].defaultNamingContext + - move_into_default_actual.objects[0].DistinguishedName == 'CN=My\, Container,' ~ setup_domain_info.output[0].defaultNamingContext + +- name: move object back into the default path - idempotent + object: + name: My, Container + identity: '{{ object_identity }}' + type: container + path: microsoft.ad.default_path + register: move_into_default_again + +- name: assert move object back into the default path - idempotent + assert: + that: + - not move_into_default_again is changed diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml index e06c54959..98718da6f 100644 --- a/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml +++ b/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml @@ -177,11 +177,125 @@ that: - not move_user_again is changed +- name: update user not in default path by identity - check + user: + name: MyUser2 + identity: '{{ object_sid }}' + firstname: first name + register: dont_move_no_path_check + check_mode: true + +- name: get result of update user not in default path by identity - check + object_info: + identity: '{{ object_identity }}' + properties: + - givenName + register: dont_move_no_path_check_actual + check_mode: true + +- name: assert update user not in default path by identity - check + assert: + that: + - dont_move_no_path_check is changed + - dont_move_no_path_check.distinguished_name == 'CN=MyUser2,' ~ setup_domain_info.output[0].defaultNamingContext + - dont_move_no_path_check_actual.objects[0].DistinguishedName == 'CN=MyUser2,' ~ setup_domain_info.output[0].defaultNamingContext + - dont_move_no_path_check_actual.objects[0].Name == 'MyUser2' + - dont_move_no_path_check_actual.objects[0].givenName == None + +- name: update user not in default path by identity + user: + name: MyUser2 + identity: '{{ object_sid }}' + firstname: first name + register: dont_move_no_path + +- name: get result of update user not in default path by identity + object_info: + identity: '{{ object_identity }}' + properties: + - givenName + register: dont_move_no_path_actual + check_mode: true + +- name: assert update user not in default path by identity - check + assert: + that: + - dont_move_no_path is changed + - dont_move_no_path.distinguished_name == 'CN=MyUser2,' ~ setup_domain_info.output[0].defaultNamingContext + - dont_move_no_path_actual.objects[0].DistinguishedName == 'CN=MyUser2,' ~ setup_domain_info.output[0].defaultNamingContext + - dont_move_no_path_actual.objects[0].Name == 'MyUser2' + - dont_move_no_path_actual.objects[0].givenName == 'first name' + +- name: update user without name + user: + identity: MyUser + firstname: first name + register: check_by_identity + +- name: assert update user without name + assert: + that: + - not check_by_identity is changed + - check_by_identity.distinguished_name == 'CN=MyUser2,' ~ setup_domain_info.output[0].defaultNamingContext + +- name: move user back - check + user: + name: MyUser + identity: MyUser + path: microsoft.ad.default_path + register: move_with_path_sentinel_check + check_mode: true + +- name: get result of move user back - check + object_info: + identity: '{{ object_identity }}' + properties: + - sAMAccountName + register: move_with_path_sentinel_check_actual + +- name: assert move user back - check + assert: + that: + - move_with_path_sentinel_check is changed + - move_with_path_sentinel_check.distinguished_name == 'CN=MyUser,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - move_with_path_sentinel_check_actual.objects[0].DistinguishedName == 'CN=MyUser2,' ~ setup_domain_info.output[0].defaultNamingContext + - move_with_path_sentinel_check_actual.objects[0].Name == 'MyUser2' + - move_with_path_sentinel_check_actual.objects[0].sAMAccountName == 'MyUser' + - name: move user back user: name: MyUser - identity: MyUser # By sAMAccountName - path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + identity: MyUser + path: microsoft.ad.default_path + register: move_with_path_sentinel + +- name: get result of move user back + object_info: + identity: '{{ object_identity }}' + properties: + - sAMAccountName + register: move_with_path_sentinel_actual + +- name: assert move user back + assert: + that: + - move_with_path_sentinel is changed + - move_with_path_sentinel.distinguished_name == 'CN=MyUser,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - move_with_path_sentinel_actual.objects[0].DistinguishedName == 'CN=MyUser,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - move_with_path_sentinel_actual.objects[0].Name == 'MyUser' + - move_with_path_sentinel_actual.objects[0].sAMAccountName == 'MyUser' + +- name: move user back - idempotent + user: + name: MyUser + identity: MyUser + path: microsoft.ad.default_path + register: move_with_path_sentinel_again + +- name: assert move user back - idempotent + assert: + that: + - not move_with_path_sentinel_again is changed - name: update password from blank - skip for on_create user: @@ -274,6 +388,29 @@ - always_update_password is changed - always_update_password_actual.objects[0].pwdLastSet > change_pass_actual.objects[0].pwdLastSet +- name: expire account for subsequent password check + user: + name: MyUser + attributes: + set: + accountExpires: + type: date_time + value: '2000-01-01T00:00:00.0000000Z' + +# There's no way to validate a password on an expired account, this will +# result in a change even if the password is the same +- name: update password for expired account + user: + name: MyUser + password: Password123! + update_password: when_changed + register: update_password_on_expired_account + +- name: assert update password for expired account + assert: + that: + - update_password_on_expired_account is changed + - name: remove user - check user: name: MyUser @@ -392,6 +529,7 @@ password_never_expires: true path: '{{ setup_domain_info.output[0].defaultNamingContext }}' postal_code: 4000 + protect_from_deletion: false sam_account_name: MyUserSam spn: set: @@ -404,6 +542,9 @@ attributes: set: comment: My comment + accountExpires: + type: date_time + value: '3023-07-31T00:00:00.0000000Z' register: create_user_check check_mode: true @@ -441,6 +582,7 @@ password_never_expires: true path: '{{ setup_domain_info.output[0].defaultNamingContext }}' postal_code: 4000 + protect_from_deletion: false sam_account_name: MyUserSam spn: set: @@ -453,6 +595,9 @@ attributes: set: comment: My comment + accountExpires: + type: date_time + value: '3023-07-31T00:00:00.0000000Z' register: create_user - set_fact: @@ -463,6 +608,7 @@ object_info: identity: '{{ object_identity }}' properties: + - accountExpires - c - comment - company @@ -476,6 +622,7 @@ - objectSid - postalcode - primaryGroupID + - ProtectedFromAccidentalDeletion - pwdLastSet - sAMAccountName - servicePrincipalName @@ -512,6 +659,7 @@ - create_user_actual.objects[0].Description == 'User Description' - create_user_actual.objects[0].DisplayName == 'User Name' - create_user_actual.objects[0].DistinguishedName == 'CN=MyUser,' ~ setup_domain_info.output[0].defaultNamingContext + - create_user_actual.objects[0].accountExpires == 448921440000000000 - create_user_actual.objects[0].c == 'au' - create_user_actual.objects[0].comment == 'My comment' - create_user_actual.objects[0].company == 'Red Hat' @@ -522,6 +670,7 @@ - create_user_actual.objects[0].memberOf == 'CN=Domain Admins,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext - create_user_actual.objects[0].postalcode == '4000' - create_user_actual.objects[0].primaryGroupID == 513 # Domain Users + - create_user_actual.objects[0].ProtectedFromAccidentalDeletion == false - create_user_actual.objects[0].pwdLastSet > 0 - create_user_actual.objects[0].sAMAccountName == 'MyUserSam' - create_user_actual.objects[0].servicePrincipalName == 'HTTP/MyUser' @@ -555,6 +704,7 @@ password_never_expires: true path: '{{ setup_domain_info.output[0].defaultNamingContext }}' postal_code: 4000 + protect_from_deletion: false sam_account_name: MyUserSam spn: set: @@ -568,6 +718,9 @@ attributes: set: comment: My comment + accountExpires: + type: date_time + value: '3023-07-31T00:00:00.0000000Z' register: create_user_again - name: assert create user with extra info - idempotent @@ -575,6 +728,27 @@ that: - not create_user_again is changed +- name: update user by identity + user: + identity: MyUserSam + postal_code: 4001 + register: update_by_identity + +- name: get result of update user by identity + object_info: + identity: '{{ object_identity }}' + properties: + - postalcode + register: update_by_identity_actual + +- name: assert create user with extra info + assert: + that: + - update_by_identity is changed + - update_by_identity_actual.objects | length == 1 + - update_by_identity_actual.objects[0].DistinguishedName == 'CN=MyUser,' ~ setup_domain_info.output[0].defaultNamingContext + - update_by_identity_actual.objects[0].postalcode == '4001' + - name: update user settings - check user: name: MyUser @@ -609,6 +783,9 @@ attributes: set: comment: My Comment + accountExpires: + type: date_time + value: '3023-07-31T00:00:00.0000001Z' register: update_user_check check_mode: true @@ -616,6 +793,7 @@ object_info: identity: '{{ object_identity }}' properties: + - accountExpires - c - comment - company @@ -629,6 +807,7 @@ - objectSid - postalcode - primaryGroupID + - ProtectedFromAccidentalDeletion - pwdLastSet - sAMAccountName - servicePrincipalName @@ -649,6 +828,7 @@ - update_user_check_actual.objects[0].Description == 'User Description' - update_user_check_actual.objects[0].DisplayName == 'User Name' - update_user_check_actual.objects[0].DistinguishedName == 'CN=MyUser,' ~ setup_domain_info.output[0].defaultNamingContext + - update_user_check_actual.objects[0].accountExpires == 448921440000000000 - update_user_check_actual.objects[0].c == 'au' - update_user_check_actual.objects[0].comment == 'My comment' - update_user_check_actual.objects[0].company == 'Red Hat' @@ -657,8 +837,9 @@ - update_user_check_actual.objects[0].mail == 'user@EMAIL.COM' # Domain Users is the primaryGroupID entry - update_user_check_actual.objects[0].memberOf == 'CN=Domain Admins,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext - - update_user_check_actual.objects[0].postalcode == '4000' + - update_user_check_actual.objects[0].postalcode == '4001' - update_user_check_actual.objects[0].primaryGroupID == 513 # Domain Users + - update_user_check_actual.objects[0].ProtectedFromAccidentalDeletion == false - update_user_check_actual.objects[0].pwdLastSet > 0 - update_user_check_actual.objects[0].sAMAccountName == 'MyUserSam' - update_user_check_actual.objects[0].servicePrincipalName == 'HTTP/MyUser' @@ -703,12 +884,16 @@ attributes: set: comment: My Comment + accountExpires: + type: date_time + value: '3023-07-31T00:00:00.0000001Z' register: update_user - name: get result of update user settings object_info: identity: '{{ object_identity }}' properties: + - accountExpires - c - comment - company @@ -757,6 +942,7 @@ - update_user_actual.objects[0].Description == 'User description' - update_user_actual.objects[0].DisplayName == 'User name' - update_user_actual.objects[0].DistinguishedName == 'CN=MyUser,' ~ setup_domain_info.output[0].defaultNamingContext + - update_user_actual.objects[0].accountExpires == 448921440000000001 - update_user_actual.objects[0].c == 'us' - update_user_actual.objects[0].comment == 'My Comment' - update_user_actual.objects[0].company == 'Ansible' @@ -1063,3 +1249,54 @@ that: - spn_add is changed - spn_add_actual.objects[0].servicePrincipalName == ['HTTP/fake', 'HTTP/host.domain:8080', 'HTTP/host'] + +- name: remove user for next test + user: + identity: '{{ object_identity }}' + state: absent + +- name: create user by identity - check + user: + identity: UserId + password: Password123 + state: present + register: create_user_by_id_check + check_mode: true + +- name: get result of create user by identity - check + object_info: + ldap_filter: (sAMAccountName=MyUser) + register: create_user_by_id_actual_check + +- name: assert create user by identity - check + assert: + that: + - create_user_by_id_check is changed + - create_user_by_id_check.distinguished_name == 'CN=UserId,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - create_user_by_id_actual_check.objects == [] + +- name: create user by identity + user: + identity: UserId + password: Password123 + state: present + register: create_user_by_id + +- set_fact: + object_identity: '{{ create_user_by_id.object_guid }}' + +- name: get result for create user by identity + object_info: + identity: '{{ object_identity }}' + properties: + - sAMAccountName + register: create_user_by_id_actual + +- name: assert create user by identity + assert: + that: + - create_user_by_id is changed + - create_user_by_id.distinguished_name == 'CN=UserId,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - create_user_by_id_actual.objects[0].DistinguishedName == create_user_by_id.distinguished_name + - create_user_by_id_actual.objects[0].Name == 'UserId' + - create_user_by_id_actual.objects[0].sAMAccountName == 'UserId' diff --git a/ansible_collections/microsoft/ad/tests/requirements.yml b/ansible_collections/microsoft/ad/tests/requirements.yml index f5ed6c435..81463ee9f 100644 --- a/ansible_collections/microsoft/ad/tests/requirements.yml +++ b/ansible_collections/microsoft/ad/tests/requirements.yml @@ -1,2 +1,2 @@ collections: -- name: ansible.windows + - name: ansible.windows diff --git a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.13.txt b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.13.txt deleted file mode 100644 index e69de29bb..000000000 --- a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.13.txt +++ /dev/null diff --git a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.12.txt b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.17.txt index e69de29bb..e69de29bb 100644 --- a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.12.txt +++ b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.17.txt diff --git a/ansible_collections/microsoft/ad/tests/unit/compat/__init__.py b/ansible_collections/microsoft/ad/tests/unit/compat/__init__.py deleted file mode 100644 index e69de29bb..000000000 --- a/ansible_collections/microsoft/ad/tests/unit/compat/__init__.py +++ /dev/null diff --git a/ansible_collections/microsoft/ad/tests/unit/compat/mock.py b/ansible_collections/microsoft/ad/tests/unit/compat/mock.py deleted file mode 100644 index 3dcd2687f..000000000 --- a/ansible_collections/microsoft/ad/tests/unit/compat/mock.py +++ /dev/null @@ -1,42 +0,0 @@ -# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -''' -Compat module for Python3.x's unittest.mock module -''' - -# Python 2.7 - -# Note: Could use the pypi mock library on python3.x as well as python2.x. It -# is the same as the python3 stdlib mock library - -try: - # Allow wildcard import because we really do want to import all of mock's - # symbols into this compat shim - # pylint: disable=wildcard-import,unused-wildcard-import - from unittest.mock import * -except ImportError: - # Python 2 - # pylint: disable=wildcard-import,unused-wildcard-import - try: - from mock import * - except ImportError: - print('You need the mock library installed on python2.x to run tests') diff --git a/ansible_collections/microsoft/ad/tests/unit/mock/__init__.py b/ansible_collections/microsoft/ad/tests/unit/mock/__init__.py deleted file mode 100644 index e69de29bb..000000000 --- a/ansible_collections/microsoft/ad/tests/unit/mock/__init__.py +++ /dev/null diff --git a/ansible_collections/microsoft/ad/tests/unit/mock/loader.py b/ansible_collections/microsoft/ad/tests/unit/mock/loader.py deleted file mode 100644 index e5dff78c1..000000000 --- a/ansible_collections/microsoft/ad/tests/unit/mock/loader.py +++ /dev/null @@ -1,116 +0,0 @@ -# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os - -from ansible.errors import AnsibleParserError -from ansible.parsing.dataloader import DataLoader -from ansible.module_utils._text import to_bytes, to_text - - -class DictDataLoader(DataLoader): - - def __init__(self, file_mapping=None): - file_mapping = {} if file_mapping is None else file_mapping - assert type(file_mapping) == dict - - super(DictDataLoader, self).__init__() - - self._file_mapping = file_mapping - self._build_known_directories() - self._vault_secrets = None - - def load_from_file(self, path, cache=True, unsafe=False): - path = to_text(path) - if path in self._file_mapping: - return self.load(self._file_mapping[path], path) - return None - - # TODO: the real _get_file_contents returns a bytestring, so we actually convert the - # unicode/text it's created with to utf-8 - def _get_file_contents(self, file_name): - path = to_text(file_name) - if path in self._file_mapping: - return (to_bytes(self._file_mapping[path]), False) - else: - raise AnsibleParserError("file not found: %s" % path) - - def path_exists(self, path): - path = to_text(path) - return path in self._file_mapping or path in self._known_directories - - def is_file(self, path): - path = to_text(path) - return path in self._file_mapping - - def is_directory(self, path): - path = to_text(path) - return path in self._known_directories - - def list_directory(self, path): - ret = [] - path = to_text(path) - for x in (list(self._file_mapping.keys()) + self._known_directories): - if x.startswith(path): - if os.path.dirname(x) == path: - ret.append(os.path.basename(x)) - return ret - - def is_executable(self, path): - # FIXME: figure out a way to make paths return true for this - return False - - def _add_known_directory(self, directory): - if directory not in self._known_directories: - self._known_directories.append(directory) - - def _build_known_directories(self): - self._known_directories = [] - for path in self._file_mapping: - dirname = os.path.dirname(path) - while dirname not in ('/', ''): - self._add_known_directory(dirname) - dirname = os.path.dirname(dirname) - - def push(self, path, content): - rebuild_dirs = False - if path not in self._file_mapping: - rebuild_dirs = True - - self._file_mapping[path] = content - - if rebuild_dirs: - self._build_known_directories() - - def pop(self, path): - if path in self._file_mapping: - del self._file_mapping[path] - self._build_known_directories() - - def clear(self): - self._file_mapping = dict() - self._known_directories = [] - - def get_basedir(self): - return os.getcwd() - - def set_vault_secrets(self, vault_secrets): - self._vault_secrets = vault_secrets diff --git a/ansible_collections/microsoft/ad/tests/unit/mock/path.py b/ansible_collections/microsoft/ad/tests/unit/mock/path.py deleted file mode 100644 index 4f46ed913..000000000 --- a/ansible_collections/microsoft/ad/tests/unit/mock/path.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -from ansible_collections.microsoft.ad.tests.unit.compat.mock import MagicMock -from ansible.utils.path import unfrackpath - - -mock_unfrackpath_noop = MagicMock(spec_set=unfrackpath, side_effect=lambda x, *args, **kwargs: x) diff --git a/ansible_collections/microsoft/ad/tests/unit/mock/procenv.py b/ansible_collections/microsoft/ad/tests/unit/mock/procenv.py deleted file mode 100644 index 8652d2689..000000000 --- a/ansible_collections/microsoft/ad/tests/unit/mock/procenv.py +++ /dev/null @@ -1,90 +0,0 @@ -# (c) 2016, Matt Davis <mdavis@ansible.com> -# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com> -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import sys -import json - -from contextlib import contextmanager -from io import BytesIO, StringIO -from ansible_collections.microsoft.ad.tests.unit.compat import unittest -from ansible.module_utils.six import PY3 -from ansible.module_utils._text import to_bytes - - -@contextmanager -def swap_stdin_and_argv(stdin_data='', argv_data=tuple()): - """ - context manager that temporarily masks the test runner's values for stdin and argv - """ - real_stdin = sys.stdin - real_argv = sys.argv - - if PY3: - fake_stream = StringIO(stdin_data) - fake_stream.buffer = BytesIO(to_bytes(stdin_data)) - else: - fake_stream = BytesIO(to_bytes(stdin_data)) - - try: - sys.stdin = fake_stream - sys.argv = argv_data - - yield - finally: - sys.stdin = real_stdin - sys.argv = real_argv - - -@contextmanager -def swap_stdout(): - """ - context manager that temporarily replaces stdout for tests that need to verify output - """ - old_stdout = sys.stdout - - if PY3: - fake_stream = StringIO() - else: - fake_stream = BytesIO() - - try: - sys.stdout = fake_stream - - yield fake_stream - finally: - sys.stdout = old_stdout - - -class ModuleTestCase(unittest.TestCase): - def setUp(self, module_args=None): - if module_args is None: - module_args = {'_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False} - - args = json.dumps(dict(ANSIBLE_MODULE_ARGS=module_args)) - - # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually - self.stdin_swap = swap_stdin_and_argv(stdin_data=args) - self.stdin_swap.__enter__() - - def tearDown(self): - # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually - self.stdin_swap.__exit__(None, None, None) diff --git a/ansible_collections/microsoft/ad/tests/unit/mock/vault_helper.py b/ansible_collections/microsoft/ad/tests/unit/mock/vault_helper.py deleted file mode 100644 index dcce9c784..000000000 --- a/ansible_collections/microsoft/ad/tests/unit/mock/vault_helper.py +++ /dev/null @@ -1,39 +0,0 @@ -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -from ansible.module_utils._text import to_bytes - -from ansible.parsing.vault import VaultSecret - - -class TextVaultSecret(VaultSecret): - '''A secret piece of text. ie, a password. Tracks text encoding. - - The text encoding of the text may not be the default text encoding so - we keep track of the encoding so we encode it to the same bytes.''' - - def __init__(self, text, encoding=None, errors=None, _bytes=None): - super(TextVaultSecret, self).__init__() - self.text = text - self.encoding = encoding or 'utf-8' - self._bytes = _bytes - self.errors = errors or 'strict' - - @property - def bytes(self): - '''The text encoded with encoding, unless we specifically set _bytes.''' - return self._bytes or to_bytes(self.text, encoding=self.encoding, errors=self.errors) diff --git a/ansible_collections/microsoft/ad/tests/unit/mock/yaml_helper.py b/ansible_collections/microsoft/ad/tests/unit/mock/yaml_helper.py deleted file mode 100644 index 1ef172159..000000000 --- a/ansible_collections/microsoft/ad/tests/unit/mock/yaml_helper.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import io -import yaml - -from ansible.module_utils.six import PY3 -from ansible.parsing.yaml.loader import AnsibleLoader -from ansible.parsing.yaml.dumper import AnsibleDumper - - -class YamlTestUtils(object): - """Mixin class to combine with a unittest.TestCase subclass.""" - def _loader(self, stream): - """Vault related tests will want to override this. - - Vault cases should setup a AnsibleLoader that has the vault password.""" - return AnsibleLoader(stream) - - def _dump_stream(self, obj, stream, dumper=None): - """Dump to a py2-unicode or py3-string stream.""" - if PY3: - return yaml.dump(obj, stream, Dumper=dumper) - else: - return yaml.dump(obj, stream, Dumper=dumper, encoding=None) - - def _dump_string(self, obj, dumper=None): - """Dump to a py2-unicode or py3-string""" - if PY3: - return yaml.dump(obj, Dumper=dumper) - else: - return yaml.dump(obj, Dumper=dumper, encoding=None) - - def _dump_load_cycle(self, obj): - # Each pass though a dump or load revs the 'generation' - # obj to yaml string - string_from_object_dump = self._dump_string(obj, dumper=AnsibleDumper) - - # wrap a stream/file like StringIO around that yaml - stream_from_object_dump = io.StringIO(string_from_object_dump) - loader = self._loader(stream_from_object_dump) - # load the yaml stream to create a new instance of the object (gen 2) - obj_2 = loader.get_data() - - # dump the gen 2 objects directory to strings - string_from_object_dump_2 = self._dump_string(obj_2, - dumper=AnsibleDumper) - - # The gen 1 and gen 2 yaml strings - self.assertEqual(string_from_object_dump, string_from_object_dump_2) - # the gen 1 (orig) and gen 2 py object - self.assertEqual(obj, obj_2) - - # again! gen 3... load strings into py objects - stream_3 = io.StringIO(string_from_object_dump_2) - loader_3 = self._loader(stream_3) - obj_3 = loader_3.get_data() - - string_from_object_dump_3 = self._dump_string(obj_3, dumper=AnsibleDumper) - - self.assertEqual(obj, obj_3) - # should be transitive, but... - self.assertEqual(obj_2, obj_3) - self.assertEqual(string_from_object_dump, string_from_object_dump_3) - - def _old_dump_load_cycle(self, obj): - '''Dump the passed in object to yaml, load it back up, dump again, compare.''' - stream = io.StringIO() - - yaml_string = self._dump_string(obj, dumper=AnsibleDumper) - self._dump_stream(obj, stream, dumper=AnsibleDumper) - - yaml_string_from_stream = stream.getvalue() - - # reset stream - stream.seek(0) - - loader = self._loader(stream) - # loader = AnsibleLoader(stream, vault_password=self.vault_password) - obj_from_stream = loader.get_data() - - stream_from_string = io.StringIO(yaml_string) - loader2 = self._loader(stream_from_string) - # loader2 = AnsibleLoader(stream_from_string, vault_password=self.vault_password) - obj_from_string = loader2.get_data() - - stream_obj_from_stream = io.StringIO() - stream_obj_from_string = io.StringIO() - - if PY3: - yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper) - yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper) - else: - yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper, encoding=None) - yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper, encoding=None) - - yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue() - yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue() - - stream_obj_from_stream.seek(0) - stream_obj_from_string.seek(0) - - if PY3: - yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper) - yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper) - else: - yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper, encoding=None) - yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper, encoding=None) - - assert yaml_string == yaml_string_obj_from_stream - assert yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string - assert (yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string == yaml_string_stream_obj_from_stream == - yaml_string_stream_obj_from_string) - assert obj == obj_from_stream - assert obj == obj_from_string - assert obj == yaml_string_obj_from_stream - assert obj == yaml_string_obj_from_string - assert obj == obj_from_stream == obj_from_string == yaml_string_obj_from_stream == yaml_string_obj_from_string - return {'obj': obj, - 'yaml_string': yaml_string, - 'yaml_string_from_stream': yaml_string_from_stream, - 'obj_from_stream': obj_from_stream, - 'obj_from_string': obj_from_string, - 'yaml_string_obj_from_string': yaml_string_obj_from_string} diff --git a/ansible_collections/microsoft/ad/tests/unit/modules/__init__.py b/ansible_collections/microsoft/ad/tests/unit/modules/__init__.py deleted file mode 100644 index e69de29bb..000000000 --- a/ansible_collections/microsoft/ad/tests/unit/modules/__init__.py +++ /dev/null diff --git a/ansible_collections/microsoft/ad/tests/unit/modules/utils.py b/ansible_collections/microsoft/ad/tests/unit/modules/utils.py deleted file mode 100644 index 8c9633ea9..000000000 --- a/ansible_collections/microsoft/ad/tests/unit/modules/utils.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import json - -from ansible_collections.microsoft.ad.tests.unit.compat import unittest -from ansible_collections.microsoft.ad.tests.unit.compat.mock import patch -from ansible.module_utils import basic -from ansible.module_utils._text import to_bytes - - -def set_module_args(args): - if '_ansible_remote_tmp' not in args: - args['_ansible_remote_tmp'] = '/tmp' - if '_ansible_keep_remote_files' not in args: - args['_ansible_keep_remote_files'] = False - - args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) - basic._ANSIBLE_ARGS = to_bytes(args) - - -class AnsibleExitJson(Exception): - pass - - -class AnsibleFailJson(Exception): - pass - - -def exit_json(*args, **kwargs): - if 'changed' not in kwargs: - kwargs['changed'] = False - raise AnsibleExitJson(kwargs) - - -def fail_json(*args, **kwargs): - kwargs['failed'] = True - raise AnsibleFailJson(kwargs) - - -class ModuleTestCase(unittest.TestCase): - - def setUp(self): - self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) - self.mock_module.start() - self.mock_sleep = patch('time.sleep') - self.mock_sleep.start() - set_module_args({}) - self.addCleanup(self.mock_module.stop) - self.addCleanup(self.mock_sleep.stop) diff --git a/ansible_collections/microsoft/ad/tests/unit/plugins/filter/test_ldap_converters.py b/ansible_collections/microsoft/ad/tests/unit/plugins/filter/test_ldap_converters.py index 923d30b31..362e76b4a 100644 --- a/ansible_collections/microsoft/ad/tests/unit/plugins/filter/test_ldap_converters.py +++ b/ansible_collections/microsoft/ad/tests/unit/plugins/filter/test_ldap_converters.py @@ -8,7 +8,13 @@ import uuid import pytest from ansible.errors import AnsibleFilterError -from ansible_collections.microsoft.ad.plugins.filter.ldap_converters import as_sid, as_guid, as_datetime +from ansible_collections.microsoft.ad.plugins.filter.ldap_converters import ( + as_sid, + as_guid, + as_datetime, + dn_escape, + parse_dn, +) @pytest.mark.parametrize("type", ["int", "str", "bytes"]) @@ -37,7 +43,10 @@ def test_as_datetime_with_format() -> None: def test_as_datetime_from_list() -> None: actual = as_datetime([133220025750000000, 133220025751000020]) - assert actual == ["2023-02-27T20:16:15.000000+0000", "2023-02-27T20:16:15.100002+0000"] + assert actual == [ + "2023-02-27T20:16:15.000000+0000", + "2023-02-27T20:16:15.100002+0000", + ] @pytest.mark.parametrize("type", ["str", "bytes"]) @@ -83,10 +92,147 @@ def test_as_sid_from_list() -> None: def test_as_sid_too_little_data_auth_count() -> None: - with pytest.raises(AnsibleFilterError, match="Raw SID bytes must be at least 8 bytes long"): + with pytest.raises( + AnsibleFilterError, match="Raw SID bytes must be at least 8 bytes long" + ): as_sid(b"\x00\x00\x00\x00") def test_as_sid_too_little_data_sub_authorities() -> None: with pytest.raises(AnsibleFilterError, match="Not enough data to unpack SID"): as_sid(b"\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + + +@pytest.mark.parametrize( + "value, expected", + [ + ("Sue, Grabbit and Runn", "Sue\\, Grabbit and Runn"), + ("Before\rAfter", "Before\\0DAfter"), + ("Docs, Adatum", "Docs\\, Adatum"), + ("foo,bar", "foo\\,bar"), + ("foo+bar", "foo\\+bar"), + ('foo"bar', 'foo\\"bar'), + ("foo\\bar", "foo\\\\bar"), + ("foo<bar", "foo\\<bar"), + ("foo>bar", "foo\\>bar"), + ("foo;bar", "foo\\;bar"), + (" foo bar", "\\ foo bar"), + ("#foo bar", "\\#foo bar"), + ("# foo bar", "\\# foo bar"), + ("foo bar ", "foo bar\\ "), + ("foo bar ", "foo bar \\ "), + ("foo bar #", "foo bar #"), + ("foo\00bar", "foo\\00bar"), + ("foo\nbar", "foo\\0Abar"), + ("foo\rbar", "foo\\0Dbar"), + ("foo=bar", "foo\\3Dbar"), + ("foo/bar", "foo\\2Fbar"), + ], +) +def test_dn_escape(value: str, expected: str) -> None: + actual = dn_escape(value) + assert actual == expected + + +@pytest.mark.parametrize( + "value, expected", + [ + ( + "", + [], + ), + ( + "CN=foo", + [["CN", "foo"]], + ), + ( + r"CN=foo,DC=bar", + [["CN", "foo"], ["DC", "bar"]], + ), + ( + r"CN=foo, DC=bar", + [["CN", "foo"], ["DC", "bar"]], + ), + ( + r"CN=foo , DC=bar", + [["CN", "foo"], ["DC", "bar"]], + ), + ( + r"CN=foo , DC=bar", + [["CN", "foo"], ["DC", "bar"]], + ), + ( + r"UID=jsmith,DC=example,DC=net", + [["UID", "jsmith"], ["DC", "example"], ["DC", "net"]], + ), + ( + r"OU=Sales+CN=J. Smith,DC=example,DC=net", + [["OU", "Sales", "CN", "J. Smith"], ["DC", "example"], ["DC", "net"]], + ), + ( + r"OU=Sales + CN=J. Smith,DC=example,DC=net", + [["OU", "Sales", "CN", "J. Smith"], ["DC", "example"], ["DC", "net"]], + ), + ( + r"CN=James \"Jim\" Smith\, III,DC=example,DC=net", + [["CN", 'James "Jim" Smith, III'], ["DC", "example"], ["DC", "net"]], + ), + ( + r"CN=Before\0dAfter,DC=example,DC=net", + [["CN", "Before\rAfter"], ["DC", "example"], ["DC", "net"]], + ), + ( + r"1.3.6.1.4.1.1466.0=#FE04024869", + [["1.3.6.1.4.1.1466.0", "\udcfe\x04\x02Hi"]], + ), + ( + r"1.3.6.1.4.1.1466.0 = #FE04024869", + [["1.3.6.1.4.1.1466.0", "\udcfe\x04\x02Hi"]], + ), + ( + r"CN=Lu\C4\8Di\C4\87", + [["CN", "Lučić"]], + ), + ], +) +def test_parse_dn(value: str, expected: t.List[str]) -> None: + actual = parse_dn(value) + + assert actual == expected + + +def test_parse_dn_invalid_attribute_type() -> None: + expected = "Expecting attribute type in RDN entry from 'foo_invalid=test'" + with pytest.raises(AnsibleFilterError, match=expected): + parse_dn("foo_invalid=test") + + +def test_parse_dn_no_attribute_value() -> None: + expected = "Expecting attribute value in RDN entry from ''" + with pytest.raises(AnsibleFilterError, match=expected): + parse_dn("foo=") + + +def test_parse_dn_no_value_after_ava_delimiter() -> None: + expected = "Expecting attribute type in RDN entry from ''" + with pytest.raises(AnsibleFilterError, match=expected): + parse_dn("foo=bar+") + + +def test_parse_dn_unescaped_hash() -> None: + expected = "Found leading # for attribute value but does not match hexstring format at '#bar'" + with pytest.raises(AnsibleFilterError, match=expected): + parse_dn("foo=#bar") + + +@pytest.mark.parametrize("c", ["\00", '"', ";", "<", ">"]) +def test_parse_dn_unescaped_special_char(c: str) -> None: + expected = f"Found unescaped character '{c}' in attribute value at '{c}value'" + with pytest.raises(AnsibleFilterError, match=expected): + parse_dn(f"foo=test{c}value") + + +def test_parse_dn_invalid_attr_value_escape() -> None: + expected = r"Found invalid escape sequence in attribute value at '\\1z" + with pytest.raises(AnsibleFilterError, match=expected): + parse_dn("foo=bar \\1z") diff --git a/ansible_collections/microsoft/ad/tests/utils/shippable/lint.sh b/ansible_collections/microsoft/ad/tests/utils/shippable/lint.sh new file mode 100755 index 000000000..12b5b4cd2 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/utils/shippable/lint.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -o pipefail -eux + +# This is aligned with the galaxy-importer used by AH +# https://github.com/ansible/galaxy-importer/blob/d4b5e6d12088ba452f129f4824bd049be5543358/setup.cfg#L22C4-L22C33 +python -m pip install \ + 'ansible-lint>=6.2.2,<=6.14.3' + +ansible-lint |