summaryrefslogtreecommitdiffstats
path: root/ansible_collections/microsoft/ad/tests
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/microsoft/ad/tests')
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml6
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml20
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/main.yml19
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/ansible.cfg1
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml17
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/tests.yml107
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml243
-rw-r--r--ansible_collections/microsoft/ad/tests/requirements.yml2
-rw-r--r--ansible_collections/microsoft/ad/tests/sanity/ignore-2.13.txt0
-rw-r--r--ansible_collections/microsoft/ad/tests/sanity/ignore-2.17.txt (renamed from ansible_collections/microsoft/ad/tests/sanity/ignore-2.12.txt)0
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/compat/__init__.py0
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/compat/mock.py42
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/mock/__init__.py0
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/mock/loader.py116
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/mock/path.py8
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/mock/procenv.py90
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/mock/vault_helper.py39
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/mock/yaml_helper.py124
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/modules/__init__.py0
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/modules/utils.py50
-rw-r--r--ansible_collections/microsoft/ad/tests/unit/plugins/filter/test_ldap_converters.py152
-rwxr-xr-xansible_collections/microsoft/ad/tests/utils/shippable/lint.sh10
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