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