1"""Directory-related unit tests common to different directory types.
2
3Simple AD directories are used for test data, but the operations are
4common to the other directory types.
5"""
6from datetime import datetime, timezone
7
8import boto3
9from botocore.exceptions import ClientError
10import pytest
11
12from moto import mock_ds
13from moto.core.utils import get_random_hex
14from moto.ec2 import mock_ec2
15
16from .test_ds_simple_ad_directory import create_test_directory, TEST_REGION
17
18
19@mock_ec2
20@mock_ds
21def test_ds_delete_directory():
22    """Test good and bad invocations of delete_directory()."""
23    client = boto3.client("ds", region_name=TEST_REGION)
24
25    # Delete a directory when there are none.
26    random_directory_id = f"d-{get_random_hex(10)}"
27    with pytest.raises(ClientError) as exc:
28        client.delete_directory(DirectoryId=random_directory_id)
29    err = exc.value.response["Error"]
30    assert err["Code"] == "EntityDoesNotExistException"
31    assert f"Directory {random_directory_id} does not exist" in err["Message"]
32
33    # Delete an existing directory.
34    ec2_client = boto3.client("ec2", region_name=TEST_REGION)
35    directory_id = create_test_directory(client, ec2_client)
36    result = client.delete_directory(DirectoryId=directory_id)
37    assert result["DirectoryId"] == directory_id
38
39    # Attempt to delete a non-existent directory.
40    nonexistent_id = f"d-{get_random_hex(10)}"
41    with pytest.raises(ClientError) as exc:
42        client.delete_directory(DirectoryId=nonexistent_id)
43    err = exc.value.response["Error"]
44    assert err["Code"] == "EntityDoesNotExistException"
45    assert f"Directory {nonexistent_id} does not exist" in err["Message"]
46
47    # Attempt to use an invalid directory ID.
48    bad_id = get_random_hex(3)
49    with pytest.raises(ClientError) as exc:
50        client.delete_directory(DirectoryId=bad_id)
51    err = exc.value.response["Error"]
52    assert err["Code"] == "ValidationException"
53    assert "1 validation error detected" in err["Message"]
54    assert (
55        f"Value '{bad_id}' at 'directoryId' failed to satisfy constraint: "
56        f"Member must satisfy regular expression pattern: ^d-[0-9a-f]{{10}}$"
57    ) in err["Message"]
58
59
60@mock_ec2
61@mock_ds
62def test_ds_get_directory_limits():
63    """Test return value for directory limits."""
64    client = boto3.client("ds", region_name=TEST_REGION)
65    ec2_client = boto3.client("ec2", region_name=TEST_REGION)
66
67    limits = client.get_directory_limits()["DirectoryLimits"]
68    assert limits["CloudOnlyDirectoriesCurrentCount"] == 0
69    assert limits["CloudOnlyDirectoriesLimit"] > 0
70    assert not limits["CloudOnlyDirectoriesLimitReached"]
71
72    # Create a bunch of directories and verify the current count has been
73    # updated.
74    for _ in range(limits["CloudOnlyDirectoriesLimit"]):
75        create_test_directory(client, ec2_client)
76    limits = client.get_directory_limits()["DirectoryLimits"]
77    assert (
78        limits["CloudOnlyDirectoriesLimit"]
79        == limits["CloudOnlyDirectoriesCurrentCount"]
80    )
81    assert limits["CloudOnlyDirectoriesLimitReached"]
82    assert not limits["CloudOnlyMicrosoftADCurrentCount"]
83    assert not limits["ConnectedDirectoriesCurrentCount"]
84
85
86@mock_ec2
87@mock_ds
88def test_ds_describe_directories():
89    """Test good and bad invocations of describe_directories()."""
90    client = boto3.client("ds", region_name=TEST_REGION)
91    ec2_client = boto3.client("ec2", region_name=TEST_REGION)
92
93    expected_ids = set()
94    limit = 10
95    for _ in range(limit):
96        expected_ids.add(create_test_directory(client, ec2_client))
97
98    # Test that if no directory IDs are specified, all are returned.
99    result = client.describe_directories()
100    directories = result["DirectoryDescriptions"]
101    directory_ids = [x["DirectoryId"] for x in directories]
102
103    assert len(directories) == limit
104    assert set(directory_ids) == expected_ids
105    for idx, dir_info in enumerate(directories):
106        assert dir_info["DesiredNumberOfDomainControllers"] == 0
107        assert not dir_info["SsoEnabled"]
108        assert dir_info["DirectoryId"] == directory_ids[idx]
109        assert dir_info["Name"].startswith("test-")
110        assert dir_info["Size"] == "Large"
111        assert dir_info["Alias"] == directory_ids[idx]
112        assert dir_info["AccessUrl"] == f"{directory_ids[idx]}.awsapps.com"
113        assert dir_info["Stage"] == "Active"
114        assert dir_info["LaunchTime"] <= datetime.now(timezone.utc)
115        assert dir_info["StageLastUpdatedDateTime"] <= datetime.now(timezone.utc)
116        assert dir_info["Type"] == "SimpleAD"
117        assert dir_info["VpcSettings"]["VpcId"].startswith("vpc-")
118        assert len(dir_info["VpcSettings"]["SubnetIds"]) == 2
119        assert len(dir_info["DnsIpAddrs"]) == 2
120    assert "NextToken" not in result
121
122    # Test with a specific directory ID.
123    result = client.describe_directories(DirectoryIds=[directory_ids[5]])
124    assert len(result["DirectoryDescriptions"]) == 1
125    assert result["DirectoryDescriptions"][0]["DirectoryId"] == directory_ids[5]
126
127    # Test with a bad directory ID.
128    bad_id = get_random_hex(3)
129    with pytest.raises(ClientError) as exc:
130        client.describe_directories(DirectoryIds=[bad_id])
131    err = exc.value.response["Error"]
132    assert err["Code"] == "ValidationException"
133    assert (
134        f"Value '{bad_id}' at 'directoryId' failed to satisfy constraint: "
135        f"Member must satisfy regular expression pattern: ^d-[0-9a-f]{{10}}$"
136    ) in err["Message"]
137
138    # Test with an invalid next token.
139    with pytest.raises(ClientError) as exc:
140        client.describe_directories(NextToken="bogus")
141    err = exc.value.response["Error"]
142    assert err["Code"] == "InvalidNextTokenException"
143    assert "Invalid value passed for the NextToken parameter" in err["Message"]
144
145    # Test with a limit.
146    result = client.describe_directories(Limit=5)
147    assert len(result["DirectoryDescriptions"]) == 5
148    directories = result["DirectoryDescriptions"]
149    for idx in range(5):
150        assert directories[idx]["DirectoryId"] == directory_ids[idx]
151    assert result["NextToken"]
152
153    result = client.describe_directories(Limit=1, NextToken=result["NextToken"])
154    assert len(result["DirectoryDescriptions"]) == 1
155    assert result["DirectoryDescriptions"][0]["DirectoryId"] == directory_ids[5]
156
157
158@mock_ec2
159@mock_ds
160def test_ds_create_alias():
161    """Test good and bad invocations of create_alias()."""
162    client = boto3.client("ds", region_name=TEST_REGION)
163    ec2_client = boto3.client("ec2", region_name=TEST_REGION)
164
165    # Create a directory we can test against.
166    directory_id = create_test_directory(client, ec2_client)
167
168    # Bad format.
169    bad_alias = f"d-{get_random_hex(10)}"
170    with pytest.raises(ClientError) as exc:
171        client.create_alias(DirectoryId=directory_id, Alias=bad_alias)
172    err = exc.value.response["Error"]
173    assert err["Code"] == "ValidationException"
174    assert (
175        fr"Value '{bad_alias}' at 'alias' failed to satisfy constraint: "
176        fr"Member must satisfy regular expression pattern: "
177        fr"^(?!D-|d-)([\da-zA-Z]+)([-]*[\da-zA-Z])*$"
178    ) in err["Message"]
179
180    # Too long.
181    bad_alias = f"d-{get_random_hex(62)}"
182    with pytest.raises(ClientError) as exc:
183        client.create_alias(DirectoryId=directory_id, Alias=bad_alias)
184    err = exc.value.response["Error"]
185    assert err["Code"] == "ValidationException"
186    assert (
187        f"Value '{bad_alias}' at 'alias' failed to satisfy constraint: "
188        f"Member must have length less than or equal to 62"
189    ) in err["Message"]
190
191    # Just right.
192    good_alias = f"{get_random_hex(10)}"
193    result = client.create_alias(DirectoryId=directory_id, Alias=good_alias)
194    assert result["DirectoryId"] == directory_id
195    assert result["Alias"] == good_alias
196    result = client.describe_directories()
197    directory = result["DirectoryDescriptions"][0]
198    assert directory["Alias"] == good_alias
199    assert directory["AccessUrl"] == f"{good_alias}.awsapps.com"
200
201    # Attempt to create another alias for the same directory.
202    another_good_alias = f"{get_random_hex(10)}"
203    with pytest.raises(ClientError) as exc:
204        client.create_alias(DirectoryId=directory_id, Alias=another_good_alias)
205    err = exc.value.response["Error"]
206    assert err["Code"] == "InvalidParameterException"
207    assert (
208        "The directory in the request already has an alias. That alias must "
209        "be deleted before a new alias can be created."
210    ) in err["Message"]
211
212    # Create a second directory we can test against.
213    directory_id2 = create_test_directory(client, ec2_client)
214    with pytest.raises(ClientError) as exc:
215        client.create_alias(DirectoryId=directory_id2, Alias=good_alias)
216    err = exc.value.response["Error"]
217    assert err["Code"] == "EntityAlreadyExistsException"
218    assert f"Alias '{good_alias}' already exists." in err["Message"]
219
220
221@mock_ec2
222@mock_ds
223def test_ds_enable_sso():
224    """Test good and bad invocations of enable_sso()."""
225    client = boto3.client("ds", region_name=TEST_REGION)
226    ec2_client = boto3.client("ec2", region_name=TEST_REGION)
227
228    # Create a directory we can test against.
229    directory_id = create_test_directory(client, ec2_client)
230
231    # Need an alias before setting SSO.
232    with pytest.raises(ClientError) as exc:
233        client.enable_sso(DirectoryId=directory_id)
234    err = exc.value.response["Error"]
235    assert err["Code"] == "ClientException"
236    assert (
237        f"An alias is required before enabling SSO. DomainId={directory_id}"
238    ) in err["Message"]
239
240    # Add the alias to continue testing.
241    client.create_alias(DirectoryId=directory_id, Alias="anything-goes")
242
243    # Password must be less than 128 chars in length.
244    good_username = "test"
245    bad_password = f"bad_password{get_random_hex(128)}"
246    with pytest.raises(ClientError) as exc:
247        client.enable_sso(
248            DirectoryId=directory_id, UserName=good_username, Password=bad_password
249        )
250    err = exc.value.response["Error"]
251    assert err["Code"] == "ValidationException"
252    assert (
253        "Value at 'ssoPassword' failed to satisfy constraint: Member must "
254        "have length less than or equal to 128"
255    ) in err["Message"]
256
257    # Username has constraints.
258    bad_username = "@test"
259    good_password = "password"
260    with pytest.raises(ClientError) as exc:
261        client.enable_sso(
262            DirectoryId=directory_id, UserName=bad_username, Password=good_password
263        )
264    err = exc.value.response["Error"]
265    assert err["Code"] == "ValidationException"
266    assert (
267        fr"Value '{bad_username}' at 'userName' failed to satisfy constraint: "
268        fr"Member must satisfy regular expression pattern: ^[a-zA-Z0-9._-]+$"
269    ) in err["Message"]
270
271    # Valid execution.
272    client.enable_sso(DirectoryId=directory_id)
273    result = client.describe_directories()
274    directory = result["DirectoryDescriptions"][0]
275    assert directory["SsoEnabled"]
276
277
278@mock_ec2
279@mock_ds
280def test_ds_disable_sso():
281    """Test good and bad invocations of disable_sso()."""
282    client = boto3.client("ds", region_name=TEST_REGION)
283    ec2_client = boto3.client("ec2", region_name=TEST_REGION)
284
285    # Create a directory we can test against.
286    directory_id = create_test_directory(client, ec2_client)
287
288    # Password must be less than 128 chars in length.
289    good_username = "test"
290    bad_password = f"bad_password{get_random_hex(128)}"
291    with pytest.raises(ClientError) as exc:
292        client.disable_sso(
293            DirectoryId=directory_id, UserName=good_username, Password=bad_password
294        )
295    err = exc.value.response["Error"]
296    assert err["Code"] == "ValidationException"
297    assert (
298        "Value at 'ssoPassword' failed to satisfy constraint: Member must "
299        "have length less than or equal to 128"
300    ) in err["Message"]
301
302    # Username has constraints.
303    bad_username = "@test"
304    good_password = "password"
305    with pytest.raises(ClientError) as exc:
306        client.disable_sso(
307            DirectoryId=directory_id, UserName=bad_username, Password=good_password
308        )
309    err = exc.value.response["Error"]
310    assert err["Code"] == "ValidationException"
311    assert (
312        fr"Value '{bad_username}' at 'userName' failed to satisfy constraint: "
313        fr"Member must satisfy regular expression pattern: ^[a-zA-Z0-9._-]+$"
314    ) in err["Message"]
315
316    # Valid execution.  First enable SSO, as the default is disabled SSO.
317    client.create_alias(DirectoryId=directory_id, Alias="anything-goes")
318    client.enable_sso(DirectoryId=directory_id)
319    client.disable_sso(DirectoryId=directory_id)
320    result = client.describe_directories()
321    directory = result["DirectoryDescriptions"][0]
322    assert not directory["SsoEnabled"]
323