The single {@code GetExistTest} method is skipped in every language port + * because Storage Mover agents cannot be created via the RP — they self-register + * from a real VM. See {@code storage-mover-scenario-tests-cross-language} task + * note for the rationale shared across .NET, Python, JS, Go, and Java. + */ +public class AgentTests extends StorageMoverManagementTestBase { + + @Test + @Disabled("Agents cannot be created by the RP; this test requires a registered agent VM.") + public void getExist() { + // Body intentionally empty — see @Disabled reason. The .NET reference + // exercises agent get / list / patch (uploadLimitWeeklyRecurrences) on + // a pre-existing agent named "testagent1", which is impossible to set + // up hermetically. + } +} diff --git a/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/ConnectionTests.java b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/ConnectionTests.java new file mode 100644 index 000000000000..96f84dc76497 --- /dev/null +++ b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/ConnectionTests.java @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.resourcemanager.storagemover.scenario; + +import com.azure.core.management.Region; +import com.azure.resourcemanager.storagemover.models.Connection; +import com.azure.resourcemanager.storagemover.models.ConnectionProperties; +import com.azure.resourcemanager.storagemover.models.StorageMover; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.stream.StreamSupport; + +/** + * Mirrors {@code ConnectionTests.cs} from the .NET source-of-truth (row #32 of + * the cross-language matrix). Exercises Connection CRUD against the shared + * private-link service {@code test-pls-wcs}. + * + *
Operates in {@code westcentralus} because the upstream PLS is deployed + * there; the per-test resource group is provisioned in the same region so + * private endpoint provisioning stays in-region. + * + *
This test intentionally does not assert on {@code connectionStatus} — + * PLS provisioning is asynchronous and the status starts as {@code Pending}. + * The status assertion lives in the C2C-with-private-source scenario (row + * #31) where the PE is explicitly approved. + * + *
The {@code description} field on update is also intentionally not asserted + * to equal the new value — the RP echoes the stale (create-time) description + * for several seconds after a PATCH and the assertion would be flaky across + * languages (Python and JS hit the same behaviour). + */ +public class ConnectionTests extends StorageMoverManagementTestBase { + + @Override + protected Region testRegion() { + return WEST_CENTRAL_US; + } + + @Test + public void createGetListUpdateDelete() { + String storageMoverName = generateRandomResourceName("stomover-conn-", 24); + String connectionName = generateRandomResourceName("conn-", 24); + + StorageMover sm = storageMoverManager.storageMovers() + .define(storageMoverName) + .withRegion(testRegion()) + .withExistingResourceGroup(resourceGroupName) + .create(); + + try { + Connection created = storageMoverManager.connections() + .define(connectionName) + .withExistingStorageMover(resourceGroupName, sm.name()) + .withProperties(new ConnectionProperties().withDescription("initial description") + .withPrivateLinkServiceId(PRIVATE_LINK_SERVICE_ID)) + .create(); + Assertions.assertEquals(connectionName, created.name()); + Assertions.assertNotNull(created.properties()); + Assertions.assertEquals("initial description", created.properties().description()); + // The body-key sanitizer registered in the base redacts the + // subscription segment of $..privateLinkServiceId to all-zeros, so + // assert only the resource-group + name suffix (which is what the + // round-trip actually verifies). The sub segment is identical in + // record and playback after sanitization. + String expectedPlsSuffix = "/resourceGroups/" + PRIVATE_LINK_SERVICE_RG + + "/providers/Microsoft.Network/privateLinkServices/" + PRIVATE_LINK_SERVICE_NAME; + Assertions.assertNotNull(created.properties().privateLinkServiceId()); + Assertions.assertTrue(created.properties().privateLinkServiceId().endsWith(expectedPlsSuffix), + "expected privateLinkServiceId to end with " + expectedPlsSuffix + " but was " + + created.properties().privateLinkServiceId()); + + Connection fetched = storageMoverManager.connections().get(resourceGroupName, sm.name(), connectionName); + Assertions.assertEquals(connectionName, fetched.name()); + Assertions.assertNotNull(fetched.properties().privateLinkServiceId()); + Assertions.assertTrue(fetched.properties().privateLinkServiceId().endsWith(expectedPlsSuffix), + "expected fetched privateLinkServiceId to end with " + expectedPlsSuffix + " but was " + + fetched.properties().privateLinkServiceId()); + + boolean foundConnection = StreamSupport + .stream(storageMoverManager.connections().list(resourceGroupName, sm.name()).spliterator(), false) + .anyMatch(c -> connectionName.equals(c.name())); + Assertions.assertTrue(foundConnection, + "expected connection " + connectionName + " in list but it was not found"); + + // Update keeps the same PLS id (cannot be changed post-create) and + // changes only the description. The new description is intentionally + // NOT asserted because the RP echoes the create-time value for several + // seconds after the PATCH (cross-language flakiness — see playbook). + Connection updated = fetched.update() + .withProperties(new ConnectionProperties().withDescription("updated description") + .withPrivateLinkServiceId(PRIVATE_LINK_SERVICE_ID)) + .apply(); + Assertions.assertEquals(connectionName, updated.name()); + + Connection refreshed = updated.refresh(); + Assertions.assertEquals(connectionName, refreshed.name()); + + storageMoverManager.connections().delete(resourceGroupName, sm.name(), connectionName); + assertNotFound(() -> storageMoverManager.connections().get(resourceGroupName, sm.name(), connectionName)); + } finally { + // Best-effort cleanup in case the test failed before the explicit + // delete above. Idempotent — delete on a missing resource is a 204. + try { + storageMoverManager.connections().delete(resourceGroupName, sm.name(), connectionName); + } catch (RuntimeException ignored) { + // already deleted or never created. + } + } + } +} diff --git a/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/EndpointTests.java b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/EndpointTests.java new file mode 100644 index 000000000000..3b8a41e13787 --- /dev/null +++ b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/EndpointTests.java @@ -0,0 +1,475 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.resourcemanager.storagemover.scenario; + +import com.azure.core.management.exception.ManagementException; +import com.azure.resourcemanager.storagemover.models.AzureKeyVaultS3WithHmacCredentials; +import com.azure.resourcemanager.storagemover.models.AzureKeyVaultSmbCredentials; +import com.azure.resourcemanager.storagemover.models.AzureMultiCloudConnectorEndpointProperties; +import com.azure.resourcemanager.storagemover.models.AzureStorageBlobContainerEndpointProperties; +import com.azure.resourcemanager.storagemover.models.AzureStorageNfsFileShareEndpointProperties; +import com.azure.resourcemanager.storagemover.models.AzureStorageSmbFileShareEndpointProperties; +import com.azure.resourcemanager.storagemover.models.Endpoint; +import com.azure.resourcemanager.storagemover.models.EndpointKind; +import com.azure.resourcemanager.storagemover.models.EndpointType; +import com.azure.resourcemanager.storagemover.models.ManagedServiceIdentity; +import com.azure.resourcemanager.storagemover.models.ManagedServiceIdentityType; +import com.azure.resourcemanager.storagemover.models.NfsMountEndpointProperties; +import com.azure.resourcemanager.storagemover.models.S3WithHmacEndpointProperties; +import com.azure.resourcemanager.storagemover.models.S3WithHmacSourceType; +import com.azure.resourcemanager.storagemover.models.SmbMountEndpointProperties; +import com.azure.resourcemanager.storagemover.models.SmbMountEndpointUpdateProperties; +import com.azure.resourcemanager.storagemover.models.StorageMover; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.stream.StreamSupport; + +/** + * Mirrors {@code EndpointTests.cs} from the .NET source-of-truth (16 methods). + * + *
Each test self-provisions its own storage mover into the per-test resource + * group so tests stay independent. The {@code S3WithHmac} test is intentionally + * activated (the .NET {@code [Ignore]} on it is wrong — placeholder Key Vault / + * S3 URIs are accepted by the RP at metadata level; only running an actual data + * job would need real credentials). + */ +public class EndpointTests extends StorageMoverManagementTestBase { + + // ----------------------------------------------------------------------- + // Omnibus + per-type CRUD + // ----------------------------------------------------------------------- + + @Test + public void createUpdateGetDelete() { + String storageMoverName = createStorageMover("ep-omni"); + + String cEndpointName = generateRandomResourceName("conendpoint-", 24); + String nfsEndpointName = generateRandomResourceName("nfsendpoint-", 24); + String smbEndpointName = generateRandomResourceName("smbendpoint-", 24); + String fsEndpointName = generateRandomResourceName("fsendpoint-", 24); + + // Blob container endpoint. + Endpoint cEndpoint + = storageMoverManager.endpoints() + .define(cEndpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new AzureStorageBlobContainerEndpointProperties() + .withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID) + .withBlobContainerName("testcontainer") + .withDescription("New container endpoint")) + .create(); + Assertions.assertEquals(cEndpointName, cEndpoint.name()); + Assertions.assertEquals(EndpointType.AZURE_STORAGE_BLOB_CONTAINER, cEndpoint.properties().endpointType()); + + Endpoint cEndpointGet = storageMoverManager.endpoints().get(resourceGroupName, storageMoverName, cEndpointName); + Assertions.assertEquals(cEndpointName, cEndpointGet.name()); + Assertions.assertEquals(EndpointType.AZURE_STORAGE_BLOB_CONTAINER, cEndpointGet.properties().endpointType()); + + // NFS endpoint. + Endpoint nfsEndpoint = storageMoverManager.endpoints() + .define(nfsEndpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new NfsMountEndpointProperties().withHost("10.0.0.1") + .withExport("/") + .withDescription("New NFS endpoint")) + .create(); + Assertions.assertEquals(nfsEndpointName, nfsEndpoint.name()); + Assertions.assertEquals(EndpointType.NFS_MOUNT, nfsEndpoint.properties().endpointType()); + NfsMountEndpointProperties nfsProps = (NfsMountEndpointProperties) nfsEndpoint.properties(); + Assertions.assertEquals("/", nfsProps.export()); + // Note: $..host is rewritten by the framework's default body-key + // sanitizer (TestProxyUtils.JSON_BODY_KEYS_TO_REDACT contains "host"), + // so playback returns the sanitised value rather than the literal IP. + // The serialisation round-trip is covered by NfsMountEndpointPropertiesTests. + Assertions.assertNotNull(nfsProps.host()); + + // SMB endpoint with credentials. + AzureKeyVaultSmbCredentials credentials = new AzureKeyVaultSmbCredentials() + .withUsernameUri("https://examples-azureKeyVault.vault.azure.net/secrets/examples-username") + .withPasswordUri("https://examples-azureKeyVault.vault.azure.net/secrets/examples-password"); + Endpoint smbEndpoint = storageMoverManager.endpoints() + .define(smbEndpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new SmbMountEndpointProperties().withHost("10.0.0.1") + .withShareName("testshare") + .withCredentials(credentials) + .withDescription("New Smb mount endpoint")) + .create(); + SmbMountEndpointProperties smbProps = (SmbMountEndpointProperties) smbEndpoint.properties(); + Assertions.assertEquals("https://examples-azureKeyVault.vault.azure.net/secrets/examples-username", + smbProps.credentials().usernameUri()); + Assertions.assertEquals("https://examples-azureKeyVault.vault.azure.net/secrets/examples-password", + smbProps.credentials().passwordUri()); + // host: see comment on the NFS assertion above (sanitised in playback). + Assertions.assertNotNull(smbProps.host()); + Assertions.assertEquals("testshare", smbProps.shareName()); + + // SMB PATCH — workaround: must include identity:{type:None} at top + // level. RP regression in api-version 2025-12-01: PATCH validation + // requires non-null identity for SMB endpoints. See cross-language + // playbook + Storage-XDataMove-RP/src/UserRP/Controllers/EndpointController.cs. + AzureKeyVaultSmbCredentials clearedCredentials + = new AzureKeyVaultSmbCredentials().withUsernameUri("").withPasswordUri(""); + SmbMountEndpointUpdateProperties updateProps + = new SmbMountEndpointUpdateProperties().withCredentials(clearedCredentials) + .withDescription("Update endpoint"); + Endpoint updatedSmb = smbEndpoint.update() + .withIdentity(new ManagedServiceIdentity().withType(ManagedServiceIdentityType.NONE)) + .withProperties(updateProps) + .apply(); + SmbMountEndpointProperties updatedSmbProps = (SmbMountEndpointProperties) updatedSmb.properties(); + Assertions.assertEquals(smbEndpointName, updatedSmb.name()); + Assertions.assertEquals(EndpointType.SMB_MOUNT, updatedSmb.properties().endpointType()); + Assertions.assertEquals("", updatedSmbProps.credentials().passwordUri()); + Assertions.assertEquals("", updatedSmbProps.credentials().usernameUri()); + // host: see comment on the NFS assertion above (sanitised in playback). + Assertions.assertNotNull(updatedSmbProps.host()); + Assertions.assertEquals("testshare", updatedSmbProps.shareName()); + + storageMoverManager.endpoints().delete(resourceGroupName, storageMoverName, smbEndpointName); + + // SMB file share endpoint. + Endpoint fsEndpoint + = storageMoverManager.endpoints() + .define(fsEndpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new AzureStorageSmbFileShareEndpointProperties() + .withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID) + .withFileShareName("testfileshare") + .withDescription("new file share endpoint")) + .create(); + AzureStorageSmbFileShareEndpointProperties fsProps + = (AzureStorageSmbFileShareEndpointProperties) fsEndpoint.properties(); + Assertions.assertEquals(fsEndpointName, fsEndpoint.name()); + Assertions.assertEquals(EndpointType.AZURE_STORAGE_SMB_FILE_SHARE, fsEndpoint.properties().endpointType()); + Assertions.assertEquals("testfileshare", fsProps.fileShareName()); + Assertions.assertEquals("new file share endpoint", fsProps.description()); + + long count = StreamSupport + .stream(storageMoverManager.endpoints().list(resourceGroupName, storageMoverName).spliterator(), false) + .count(); + Assertions.assertTrue(count > 1, "expected more than one endpoint but found " + count); + + // Existence assertions: positive on c+nfs+fs, negative on smb (deleted) + // and on a never-created name. + Assertions.assertEquals(cEndpointName, + storageMoverManager.endpoints().get(resourceGroupName, storageMoverName, cEndpointName).name()); + assertNotFound( + () -> storageMoverManager.endpoints().get(resourceGroupName, storageMoverName, cEndpointName + "111")); + assertNotFound(() -> storageMoverManager.endpoints().get(resourceGroupName, storageMoverName, smbEndpointName)); + } + + @Test + public void multiCloudConnectorEndpointCreateGetDelete() { + String storageMoverName = createStorageMover("ep-mcc"); + String endpointName = generateRandomResourceName("mcc-", 24); + + AzureMultiCloudConnectorEndpointProperties props + = new AzureMultiCloudConnectorEndpointProperties().withMultiCloudConnectorId(MULTI_CLOUD_CONNECTOR_ID) + .withAwsS3BucketId(AWS_S3_BUCKET_ID) + .withDescription("Test multi-cloud connector endpoint"); + + Endpoint endpoint = storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(props) + .create(); + Assertions.assertEquals(endpointName, endpoint.name()); + Assertions.assertEquals(EndpointType.AZURE_MULTI_CLOUD_CONNECTOR, endpoint.properties().endpointType()); + + Endpoint fetched = storageMoverManager.endpoints().get(resourceGroupName, storageMoverName, endpointName); + AzureMultiCloudConnectorEndpointProperties fetchedProps + = (AzureMultiCloudConnectorEndpointProperties) fetched.properties(); + Assertions.assertEquals(endpointName, fetched.name()); + Assertions.assertEquals("Test multi-cloud connector endpoint", fetchedProps.description()); + Assertions.assertNotNull(fetchedProps.multiCloudConnectorId()); + Assertions.assertNotNull(fetchedProps.awsS3BucketId()); + + storageMoverManager.endpoints().delete(resourceGroupName, storageMoverName, endpointName); + assertNotFound(() -> storageMoverManager.endpoints().get(resourceGroupName, storageMoverName, endpointName)); + } + + /** + * Activated despite .NET's {@code [Ignore]}: placeholder S3 / Key Vault URIs + * are accepted by the RP at metadata level; only running a copy job would + * need real credentials. Verified live in the Python port. + */ + @Test + public void s3WithHmacEndpointCreateGetDelete() { + String storageMoverName = createStorageMover("ep-s3"); + String endpointName = generateRandomResourceName("s3hmac-", 24); + + AzureKeyVaultS3WithHmacCredentials credentials = new AzureKeyVaultS3WithHmacCredentials() + .withAccessKeyUri("https://examples-azureKeyVault.vault.azure.net/secrets/examples-accesskey") + .withSecretKeyUri("https://examples-azureKeyVault.vault.azure.net/secrets/examples-secretkey"); + S3WithHmacEndpointProperties props + = new S3WithHmacEndpointProperties().withSourceUri("https://s3.example.com/bucket") + .withSourceType(S3WithHmacSourceType.MINIO) + .withCredentials(credentials) + .withDescription("Test S3 with HMAC endpoint"); + + Endpoint endpoint = storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(props) + .create(); + Assertions.assertEquals(endpointName, endpoint.name()); + Assertions.assertEquals(EndpointType.S3WITH_HMAC, endpoint.properties().endpointType()); + + Endpoint fetched = storageMoverManager.endpoints().get(resourceGroupName, storageMoverName, endpointName); + S3WithHmacEndpointProperties fetchedProps = (S3WithHmacEndpointProperties) fetched.properties(); + Assertions.assertEquals(endpointName, fetched.name()); + Assertions.assertEquals("https://s3.example.com/bucket", fetchedProps.sourceUri()); + Assertions.assertEquals(S3WithHmacSourceType.MINIO, fetchedProps.sourceType()); + Assertions.assertEquals("Test S3 with HMAC endpoint", fetchedProps.description()); + Assertions.assertNotNull(fetchedProps.credentials()); + Assertions.assertEquals("https://examples-azureKeyVault.vault.azure.net/secrets/examples-accesskey", + fetchedProps.credentials().accessKeyUri()); + Assertions.assertEquals("https://examples-azureKeyVault.vault.azure.net/secrets/examples-secretkey", + fetchedProps.credentials().secretKeyUri()); + + storageMoverManager.endpoints().delete(resourceGroupName, storageMoverName, endpointName); + assertNotFound(() -> storageMoverManager.endpoints().get(resourceGroupName, storageMoverName, endpointName)); + } + + @Test + public void nfsFileShareEndpointCreateGetDelete() { + String storageMoverName = createStorageMover("ep-nfsfs"); + String endpointName = generateRandomResourceName("nfsfs-", 24); + + AzureStorageNfsFileShareEndpointProperties props + = new AzureStorageNfsFileShareEndpointProperties().withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID) + .withFileShareName("testnfsfileshare") + .withDescription("Test NFS file share endpoint"); + + Endpoint endpoint = storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(props) + .create(); + Assertions.assertEquals(endpointName, endpoint.name()); + Assertions.assertEquals(EndpointType.AZURE_STORAGE_NFS_FILE_SHARE, endpoint.properties().endpointType()); + + Endpoint fetched = storageMoverManager.endpoints().get(resourceGroupName, storageMoverName, endpointName); + AzureStorageNfsFileShareEndpointProperties fetchedProps + = (AzureStorageNfsFileShareEndpointProperties) fetched.properties(); + Assertions.assertEquals(endpointName, fetched.name()); + Assertions.assertEquals("testnfsfileshare", fetchedProps.fileShareName()); + Assertions.assertEquals("Test NFS file share endpoint", fetchedProps.description()); + Assertions.assertNotNull(fetchedProps.storageAccountResourceId()); + + storageMoverManager.endpoints().delete(resourceGroupName, storageMoverName, endpointName); + assertNotFound(() -> storageMoverManager.endpoints().get(resourceGroupName, storageMoverName, endpointName)); + } + + // ----------------------------------------------------------------------- + // EndpointKind validation — valid combinations should succeed + // ----------------------------------------------------------------------- + + @Test + public void nfsMountEndpointKindSourceSucceeds() { + String storageMoverName = createStorageMover("ek-nfs-src"); + String endpointName = generateRandomResourceName("nfs-src-", 24); + Endpoint endpoint = storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new NfsMountEndpointProperties().withHost("10.0.0.1") + .withExport("/") + .withDescription("NFS source endpoint") + .withEndpointKind(EndpointKind.SOURCE)) + .create(); + Assertions.assertEquals(EndpointKind.SOURCE, endpoint.properties().endpointKind()); + } + + @Test + public void smbMountEndpointKindSourceSucceeds() { + String storageMoverName = createStorageMover("ek-smb-src"); + String endpointName = generateRandomResourceName("smb-src-", 24); + Endpoint endpoint = storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new SmbMountEndpointProperties().withHost("10.0.0.1") + .withShareName("testshare") + .withDescription("SMB source endpoint") + .withEndpointKind(EndpointKind.SOURCE)) + .create(); + Assertions.assertEquals(EndpointKind.SOURCE, endpoint.properties().endpointKind()); + } + + @Test + public void multiCloudConnectorEndpointKindSourceSucceeds() { + String storageMoverName = createStorageMover("ek-mcc-src"); + String endpointName = generateRandomResourceName("mcc-src-", 24); + Endpoint endpoint = storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties( + new AzureMultiCloudConnectorEndpointProperties().withMultiCloudConnectorId(MULTI_CLOUD_CONNECTOR_ID) + .withAwsS3BucketId(AWS_S3_BUCKET_ID) + .withDescription("Multi-cloud connector source endpoint") + .withEndpointKind(EndpointKind.SOURCE)) + .create(); + Assertions.assertEquals(EndpointKind.SOURCE, endpoint.properties().endpointKind()); + } + + @Test + public void blobContainerEndpointKindSourceSucceeds() { + String storageMoverName = createStorageMover("ek-blob-src"); + String endpointName = generateRandomResourceName("blob-src-", 24); + Endpoint endpoint + = storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new AzureStorageBlobContainerEndpointProperties() + .withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID) + .withBlobContainerName("testcontainer") + .withDescription("Blob container source endpoint") + .withEndpointKind(EndpointKind.SOURCE)) + .create(); + Assertions.assertEquals(EndpointKind.SOURCE, endpoint.properties().endpointKind()); + } + + @Test + public void blobContainerEndpointKindTargetSucceeds() { + String storageMoverName = createStorageMover("ek-blob-tgt"); + String endpointName = generateRandomResourceName("blob-tgt-", 24); + Endpoint endpoint + = storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new AzureStorageBlobContainerEndpointProperties() + .withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID) + .withBlobContainerName("testcontainer") + .withDescription("Blob container target endpoint") + .withEndpointKind(EndpointKind.TARGET)) + .create(); + Assertions.assertEquals(EndpointKind.TARGET, endpoint.properties().endpointKind()); + } + + @Test + public void smbFileShareEndpointKindTargetSucceeds() { + String storageMoverName = createStorageMover("ek-smbfs-tgt"); + String endpointName = generateRandomResourceName("smbfs-tgt-", 24); + Endpoint endpoint + = storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new AzureStorageSmbFileShareEndpointProperties() + .withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID) + .withFileShareName("testfileshare") + .withDescription("SMB file share target endpoint") + .withEndpointKind(EndpointKind.TARGET)) + .create(); + Assertions.assertEquals(EndpointKind.TARGET, endpoint.properties().endpointKind()); + } + + @Test + public void nfsFileShareEndpointKindTargetSucceeds() { + String storageMoverName = createStorageMover("ek-nfsfs-tgt"); + String endpointName = generateRandomResourceName("nfsfs-tgt-", 24); + Endpoint endpoint + = storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new AzureStorageNfsFileShareEndpointProperties() + .withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID) + .withFileShareName("testnfsfileshare") + .withDescription("NFS file share target endpoint") + .withEndpointKind(EndpointKind.TARGET)) + .create(); + Assertions.assertEquals(EndpointKind.TARGET, endpoint.properties().endpointKind()); + } + + // ----------------------------------------------------------------------- + // EndpointKind validation — invalid combinations must fail + // ----------------------------------------------------------------------- + + @Test + public void nfsMountEndpointKindTargetFails() { + String storageMoverName = createStorageMover("ek-nfs-tgt-fail"); + String endpointName = generateRandomResourceName("nfs-tgt-", 24); + Assertions.assertThrows(ManagementException.class, + () -> storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new NfsMountEndpointProperties().withHost("10.0.0.1") + .withExport("/") + .withEndpointKind(EndpointKind.TARGET)) + .create()); + } + + @Test + public void smbMountEndpointKindTargetFails() { + String storageMoverName = createStorageMover("ek-smb-tgt-fail"); + String endpointName = generateRandomResourceName("smb-tgt-", 24); + Assertions.assertThrows(ManagementException.class, + () -> storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new SmbMountEndpointProperties().withHost("10.0.0.1") + .withShareName("testshare") + .withEndpointKind(EndpointKind.TARGET)) + .create()); + } + + @Test + public void multiCloudConnectorEndpointKindTargetFails() { + String storageMoverName = createStorageMover("ek-mcc-tgt-fail"); + String endpointName = generateRandomResourceName("mcc-tgt-", 24); + Assertions + .assertThrows(ManagementException.class, + () -> storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new AzureMultiCloudConnectorEndpointProperties() + .withMultiCloudConnectorId(MULTI_CLOUD_CONNECTOR_ID) + .withAwsS3BucketId(AWS_S3_BUCKET_ID) + .withEndpointKind(EndpointKind.TARGET)) + .create()); + } + + @Test + public void smbFileShareEndpointKindSourceFails() { + String storageMoverName = createStorageMover("ek-smbfs-src-fail"); + String endpointName = generateRandomResourceName("smbfs-src-", 24); + Assertions.assertThrows(ManagementException.class, + () -> storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new AzureStorageSmbFileShareEndpointProperties() + .withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID) + .withFileShareName("testfileshare") + .withEndpointKind(EndpointKind.SOURCE)) + .create()); + } + + @Test + public void nfsFileShareEndpointKindSourceFails() { + String storageMoverName = createStorageMover("ek-nfsfs-src-fail"); + String endpointName = generateRandomResourceName("nfsfs-src-", 24); + Assertions.assertThrows(ManagementException.class, + () -> storageMoverManager.endpoints() + .define(endpointName) + .withExistingStorageMover(resourceGroupName, storageMoverName) + .withProperties(new AzureStorageNfsFileShareEndpointProperties() + .withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID) + .withFileShareName("testnfsfileshare") + .withEndpointKind(EndpointKind.SOURCE)) + .create()); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + private String createStorageMover(String prefix) { + StorageMover sm = storageMoverManager.storageMovers() + .define(generateRandomResourceName("sm-" + prefix + "-", 24)) + .withRegion(DEFAULT_REGION) + .withExistingResourceGroup(resourceGroupName) + .create(); + return sm.name(); + } +} diff --git a/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/JobDefinitionJobRunTests.java b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/JobDefinitionJobRunTests.java new file mode 100644 index 000000000000..22954e494800 --- /dev/null +++ b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/JobDefinitionJobRunTests.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.resourcemanager.storagemover.scenario; + +import com.azure.core.management.exception.ManagementException; +import com.azure.resourcemanager.storagemover.models.AzureStorageBlobContainerEndpointProperties; +import com.azure.resourcemanager.storagemover.models.CopyMode; +import com.azure.resourcemanager.storagemover.models.Endpoint; +import com.azure.resourcemanager.storagemover.models.JobDefinition; +import com.azure.resourcemanager.storagemover.models.NfsMountEndpointProperties; +import com.azure.resourcemanager.storagemover.models.Project; +import com.azure.resourcemanager.storagemover.models.StorageMover; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.stream.StreamSupport; + +/** + * Mirrors {@code JobDefinitionJobRunTests.cs} from the .NET source-of-truth. + * + *
Self-provisions storage mover, project, source endpoint (NFS) and target + * endpoint (Blob), creates a job definition, and verifies CRUD behaviour. The + * final {@code startJob}/{@code stopJob} calls are expected to fail with a + * {@link ManagementException} because no agent is registered against the + * storage mover — see the cross-language playbook. + */ +public class JobDefinitionJobRunTests extends StorageMoverManagementTestBase { + + @Test + public void jobDefinitionJobRun() { + String storageMoverName = generateRandomResourceName("stomover-", 24); + String projectName = generateRandomResourceName("project-", 24); + String sourceEndpointName = generateRandomResourceName("nfsep-", 24); + String targetEndpointName = generateRandomResourceName("blobep-", 24); + String jobDefinitionName = generateRandomResourceName("jobdef-", 24); + + StorageMover sm = storageMoverManager.storageMovers() + .define(storageMoverName) + .withRegion(DEFAULT_REGION) + .withExistingResourceGroup(resourceGroupName) + .create(); + + Project project = storageMoverManager.projects() + .define(projectName) + .withExistingStorageMover(resourceGroupName, sm.name()) + .create(); + + Endpoint nfsEndpoint = storageMoverManager.endpoints() + .define(sourceEndpointName) + .withExistingStorageMover(resourceGroupName, sm.name()) + .withProperties(new NfsMountEndpointProperties().withHost("10.0.0.1").withExport("/")) + .create(); + + Endpoint blobEndpoint + = storageMoverManager.endpoints() + .define(targetEndpointName) + .withExistingStorageMover(resourceGroupName, sm.name()) + .withProperties(new AzureStorageBlobContainerEndpointProperties() + .withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID) + .withBlobContainerName("testcontainer")) + .create(); + + JobDefinition jobDefinition = storageMoverManager.jobDefinitions() + .define(jobDefinitionName) + .withExistingProject(resourceGroupName, sm.name(), project.name()) + .withCopyMode(CopyMode.ADDITIVE) + .withSourceName(nfsEndpoint.name()) + .withTargetName(blobEndpoint.name()) + .create(); + Assertions.assertEquals(jobDefinitionName, jobDefinition.name()); + Assertions.assertEquals(blobEndpoint.name(), jobDefinition.targetName()); + Assertions.assertEquals(nfsEndpoint.name(), jobDefinition.sourceName()); + Assertions.assertEquals(CopyMode.ADDITIVE, jobDefinition.copyMode()); + + JobDefinition fetched + = storageMoverManager.jobDefinitions().get(resourceGroupName, sm.name(), project.name(), jobDefinitionName); + Assertions.assertEquals(jobDefinitionName, fetched.name()); + + long count = StreamSupport.stream( + storageMoverManager.jobDefinitions().list(resourceGroupName, sm.name(), project.name()).spliterator(), + false).count(); + Assertions.assertTrue(count >= 1, "expected at least one job definition but found " + count); + + // Equivalence between collection-get and resource-self-refresh. + JobDefinition refreshed = jobDefinition.refresh(); + Assertions.assertEquals(jobDefinition.name(), refreshed.name()); + Assertions.assertEquals(jobDefinition.targetName(), refreshed.targetName()); + Assertions.assertEquals(jobDefinition.agentName(), refreshed.agentName()); + Assertions.assertEquals(jobDefinition.sourceName(), refreshed.sourceName()); + Assertions.assertEquals(jobDefinition.id(), refreshed.id()); + + // StartJob / StopJob require a registered agent — without one the RP + // returns a 4xx wrapped in ManagementException. + Assertions.assertThrows(ManagementException.class, jobDefinition::startJob); + Assertions.assertThrows(ManagementException.class, jobDefinition::stopJob); + } +} diff --git a/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/JobDefinitionPrivateSourceTests.java b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/JobDefinitionPrivateSourceTests.java new file mode 100644 index 000000000000..39d3f56a50cc --- /dev/null +++ b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/JobDefinitionPrivateSourceTests.java @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.resourcemanager.storagemover.scenario; + +import com.azure.core.management.Region; +import com.azure.resourcemanager.network.fluent.models.PrivateEndpointConnectionInner; +import com.azure.resourcemanager.storage.models.BlobContainer; +import com.azure.resourcemanager.storage.models.PublicAccess; +import com.azure.resourcemanager.storagemover.models.AzureMultiCloudConnectorEndpointProperties; +import com.azure.resourcemanager.storagemover.models.AzureStorageBlobContainerEndpointProperties; +import com.azure.resourcemanager.storagemover.models.Connection; +import com.azure.resourcemanager.storagemover.models.ConnectionProperties; +import com.azure.resourcemanager.storagemover.models.CopyMode; +import com.azure.resourcemanager.storagemover.models.DataIntegrityValidation; +import com.azure.resourcemanager.storagemover.models.Endpoint; +import com.azure.resourcemanager.storagemover.models.EndpointKind; +import com.azure.resourcemanager.storagemover.models.JobDefinition; +import com.azure.resourcemanager.storagemover.models.JobRun; +import com.azure.resourcemanager.storagemover.models.JobRunResourceId; +import com.azure.resourcemanager.storagemover.models.JobRunStatus; +import com.azure.resourcemanager.storagemover.models.JobType; +import com.azure.resourcemanager.storagemover.models.ManagedServiceIdentity; +import com.azure.resourcemanager.storagemover.models.ManagedServiceIdentityType; +import com.azure.resourcemanager.storagemover.models.Project; +import com.azure.resourcemanager.storagemover.models.StorageMover; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/** + * Mirrors the {@code StartC2CJobWithPrivateSourceTest} scenario (row #31 of + * the cross-language matrix). Drives the full 12-step private-source + * Cloud-to-Cloud flow end-to-end: + * + *
Lives in its own class (separate from {@link JobDefinitionJobRunTests}) so
+ * the per-test resource group can be pinned to {@code westcentralus} via
+ * {@link #testRegion()}; the other job-definition test runs in
+ * {@link #DEFAULT_REGION}.
+ */
+public class JobDefinitionPrivateSourceTests extends StorageMoverManagementTestBase {
+
+ /** Job-run terminal states (the polling loop exits on any of these). */
+ private static final Set All schedule {@code startDate}/{@code endDate} values are derived from
+ * {@link #scheduleStartDate()} (a {@code Z}-offset {@code OffsetDateTime} of
+ * {@code now() + 1 day} captured via {@code testResourceNamer.now()} so the
+ * value is recorded once and replayed deterministically) so playback recordings
+ * stay portable and the server does not see the {@code +00:00}-suffix that
+ * triggers an RP bug — see the cross-language playbook.
+ */
+public class JobDefinitionScheduleTests extends StorageMoverManagementTestBase {
+
+ @Test
+ public void createJobDefinitionWithWeeklySchedule() {
+ TestContext ctx = provisionParents();
+ String jobDefinitionName = generateRandomResourceName("jobdef-sched-", 24);
+
+ OffsetDateTime start = scheduleStartDate();
+ ScheduleInfo schedule = new ScheduleInfo().withFrequency(Frequency.WEEKLY)
+ .withIsActive(true)
+ .withExecutionTime(new SchedulerTime().withHour(2))
+ .withStartDate(start)
+ .withEndDate(start.plusDays(30))
+ .withDaysOfWeek(Arrays.asList("Monday", "Wednesday", "Friday"));
+
+ JobDefinition jobDefinition = storageMoverManager.jobDefinitions()
+ .define(jobDefinitionName)
+ .withExistingProject(resourceGroupName, ctx.storageMoverName, ctx.projectName)
+ .withCopyMode(CopyMode.ADDITIVE)
+ .withSourceName(ctx.sourceName)
+ .withTargetName(ctx.targetName)
+ .withDescription("Job definition with weekly schedule")
+ .withDataIntegrityValidation(DataIntegrityValidation.SAVE_VERIFY_FILE_MD5)
+ .withSchedule(schedule)
+ .create();
+
+ Assertions.assertEquals(jobDefinitionName, jobDefinition.name());
+ Assertions.assertEquals(ctx.sourceName, jobDefinition.sourceName());
+ Assertions.assertEquals(ctx.targetName, jobDefinition.targetName());
+ Assertions.assertEquals(CopyMode.ADDITIVE, jobDefinition.copyMode());
+ Assertions.assertEquals("Job definition with weekly schedule", jobDefinition.description());
+
+ Assertions.assertNotNull(jobDefinition.schedule());
+ Assertions.assertEquals(Frequency.WEEKLY, jobDefinition.schedule().frequency());
+ Assertions.assertEquals(Boolean.TRUE, jobDefinition.schedule().isActive());
+ Assertions.assertEquals(2, jobDefinition.schedule().executionTime().hour());
+ Assertions.assertEquals(3, jobDefinition.schedule().daysOfWeek().size());
+
+ JobDefinition refetched = storageMoverManager.jobDefinitions()
+ .get(resourceGroupName, ctx.storageMoverName, ctx.projectName, jobDefinitionName);
+ Assertions.assertEquals(jobDefinitionName, refetched.name());
+ Assertions.assertNotNull(refetched.schedule());
+ Assertions.assertEquals(Frequency.WEEKLY, refetched.schedule().frequency());
+
+ storageMoverManager.jobDefinitions()
+ .delete(resourceGroupName, ctx.storageMoverName, ctx.projectName, jobDefinitionName);
+ assertNotFound(() -> storageMoverManager.jobDefinitions()
+ .get(resourceGroupName, ctx.storageMoverName, ctx.projectName, jobDefinitionName));
+ }
+
+ @Test
+ public void createJobDefinitionWithDailyScheduleAndPreservePermissions() {
+ TestContext ctx = provisionParents();
+ String jobDefinitionName = generateRandomResourceName("jobdef-daily-", 24);
+
+ OffsetDateTime start = scheduleStartDate();
+ ScheduleInfo schedule = new ScheduleInfo().withFrequency(Frequency.DAILY)
+ .withIsActive(true)
+ .withExecutionTime(new SchedulerTime().withHour(0))
+ .withStartDate(start)
+ .withEndDate(start.plusDays(30));
+
+ JobDefinition jobDefinition = storageMoverManager.jobDefinitions()
+ .define(jobDefinitionName)
+ .withExistingProject(resourceGroupName, ctx.storageMoverName, ctx.projectName)
+ .withCopyMode(CopyMode.MIRROR)
+ .withSourceName(ctx.sourceName)
+ .withTargetName(ctx.targetName)
+ .withDescription("Job definition with daily schedule")
+ .withDataIntegrityValidation(DataIntegrityValidation.NONE)
+ .withPreservePermissions(true)
+ .withSchedule(schedule)
+ .create();
+
+ Assertions.assertEquals(jobDefinitionName, jobDefinition.name());
+ Assertions.assertEquals(CopyMode.MIRROR, jobDefinition.copyMode());
+ Assertions.assertNotNull(jobDefinition.schedule());
+ Assertions.assertEquals(Frequency.DAILY, jobDefinition.schedule().frequency());
+ Assertions.assertEquals(Boolean.TRUE, jobDefinition.schedule().isActive());
+
+ storageMoverManager.jobDefinitions()
+ .delete(resourceGroupName, ctx.storageMoverName, ctx.projectName, jobDefinitionName);
+ }
+
+ @Test
+ public void createJobDefinitionWithOnetimeSchedule() {
+ TestContext ctx = provisionParents();
+ String jobDefinitionName = generateRandomResourceName("jobdef-once-", 24);
+
+ // Onetime is the only frequency that may omit endDate.
+ ScheduleInfo schedule = new ScheduleInfo().withFrequency(Frequency.ONETIME)
+ .withIsActive(true)
+ .withExecutionTime(new SchedulerTime().withHour(10))
+ .withStartDate(scheduleStartDate())
+ .withDaysOfWeek(Collections. Without a registered agent no JobRun ever materializes against a job
+ * definition, so the .NET test's "get an existing job run" assertion is
+ * impossible to satisfy hermetically. Per the cross-language playbook (Python's
+ * final state) this collapses to two assertions on a freshly created job
+ * definition: the job-run list is empty AND a get of an arbitrary jobName
+ * returns 404.
+ */
+public class JobRunTests extends StorageMoverManagementTestBase {
+
+ @Test
+ public void getExist() {
+ String storageMoverName = generateRandomResourceName("stomover-jr-", 24);
+ String projectName = generateRandomResourceName("project-jr-", 24);
+ String sourceEndpointName = generateRandomResourceName("nfsep-", 24);
+ String targetEndpointName = generateRandomResourceName("blobep-", 24);
+ String jobDefinitionName = generateRandomResourceName("jobdef-jr-", 24);
+ // Constant from the .NET base class — any well-formed name works since
+ // we only use it for the negative get-by-name path.
+ String missingJobName = "6e8c0dfe-821a-427d-8d11-a9ed7f1c9c13";
+
+ StorageMover sm = storageMoverManager.storageMovers()
+ .define(storageMoverName)
+ .withRegion(DEFAULT_REGION)
+ .withExistingResourceGroup(resourceGroupName)
+ .create();
+ storageMoverManager.projects()
+ .define(projectName)
+ .withExistingStorageMover(resourceGroupName, sm.name())
+ .create();
+ storageMoverManager.endpoints()
+ .define(sourceEndpointName)
+ .withExistingStorageMover(resourceGroupName, sm.name())
+ .withProperties(new NfsMountEndpointProperties().withHost("10.0.0.1").withExport("/"))
+ .create();
+ storageMoverManager.endpoints()
+ .define(targetEndpointName)
+ .withExistingStorageMover(resourceGroupName, sm.name())
+ .withProperties(
+ new AzureStorageBlobContainerEndpointProperties().withStorageAccountResourceId(FAKE_STORAGE_ACCOUNT_ID)
+ .withBlobContainerName("testcontainer"))
+ .create();
+ JobDefinition jobDefinition = storageMoverManager.jobDefinitions()
+ .define(jobDefinitionName)
+ .withExistingProject(resourceGroupName, sm.name(), projectName)
+ .withCopyMode(CopyMode.ADDITIVE)
+ .withSourceName(sourceEndpointName)
+ .withTargetName(targetEndpointName)
+ .create();
+ Assertions.assertNotNull(jobDefinition);
+
+ long count = StreamSupport.stream(storageMoverManager.jobRuns()
+ .list(resourceGroupName, sm.name(), projectName, jobDefinitionName)
+ .spliterator(), false).count();
+ Assertions.assertEquals(0L, count, "no JobRun should exist without a registered agent");
+
+ assertNotFound(() -> storageMoverManager.jobRuns()
+ .get(resourceGroupName, sm.name(), projectName, jobDefinitionName, missingJobName));
+ }
+}
diff --git a/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/ProjectCollectionTests.java b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/ProjectCollectionTests.java
new file mode 100644
index 000000000000..7c1af521717f
--- /dev/null
+++ b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/ProjectCollectionTests.java
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.resourcemanager.storagemover.scenario;
+
+import com.azure.resourcemanager.storagemover.models.Project;
+import com.azure.resourcemanager.storagemover.models.StorageMover;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.stream.StreamSupport;
+
+/**
+ * Mirrors {@code ProjectCollectionTests.cs} from the .NET source-of-truth.
+ * The .NET method name carries a typo ({@code CrateGetExistTest}); the Java
+ * port spells it {@code createGetExist}.
+ */
+public class ProjectCollectionTests extends StorageMoverManagementTestBase {
+
+ @Test
+ public void createGetExist() {
+ String storageMoverName = generateRandomResourceName("stomover-", 24);
+ StorageMover storageMover = storageMoverManager.storageMovers()
+ .define(storageMoverName)
+ .withRegion(DEFAULT_REGION)
+ .withExistingResourceGroup(resourceGroupName)
+ .create();
+ Assertions.assertEquals(storageMoverName, storageMover.name());
+
+ String projectName = generateRandomResourceName("project-", 24);
+ Project created = storageMoverManager.projects()
+ .define(projectName)
+ .withExistingStorageMover(resourceGroupName, storageMoverName)
+ .create();
+ Assertions.assertEquals(projectName, created.name());
+ Assertions.assertNull(created.description());
+ Assertions.assertEquals("microsoft.storagemover/storagemovers/projects", created.type().toLowerCase());
+
+ Project fetched = storageMoverManager.projects().get(resourceGroupName, storageMoverName, projectName);
+ Assertions.assertEquals(projectName, fetched.name());
+ Assertions.assertNull(fetched.description());
+ Assertions.assertEquals("microsoft.storagemover/storagemovers/projects", fetched.type().toLowerCase());
+
+ long count = StreamSupport
+ .stream(storageMoverManager.projects().list(resourceGroupName, storageMoverName).spliterator(), false)
+ .count();
+ Assertions.assertTrue(count >= 1, "expected at least one project but found " + count);
+
+ // Existence positive case — get must succeed.
+ Assertions.assertEquals(projectName,
+ storageMoverManager.projects().get(resourceGroupName, storageMoverName, projectName).name());
+ }
+}
diff --git a/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/ProjectResourceTests.java b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/ProjectResourceTests.java
new file mode 100644
index 000000000000..6db32585ed7b
--- /dev/null
+++ b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/ProjectResourceTests.java
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.resourcemanager.storagemover.scenario;
+
+import com.azure.resourcemanager.storagemover.models.Project;
+import com.azure.resourcemanager.storagemover.models.StorageMover;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Mirrors {@code ProjectResourceTests.cs} from the .NET source-of-truth.
+ */
+public class ProjectResourceTests extends StorageMoverManagementTestBase {
+
+ @Test
+ public void getUpdateDelete() {
+ String storageMoverName = generateRandomResourceName("stomover-", 24);
+ StorageMover storageMover = storageMoverManager.storageMovers()
+ .define(storageMoverName)
+ .withRegion(DEFAULT_REGION)
+ .withExistingResourceGroup(resourceGroupName)
+ .create();
+
+ String projectName = generateRandomResourceName("project-", 24);
+ Project created = storageMoverManager.projects()
+ .define(projectName)
+ .withExistingStorageMover(resourceGroupName, storageMover.name())
+ .create();
+
+ Project fetched = storageMoverManager.projects().get(resourceGroupName, storageMover.name(), projectName);
+ Assertions.assertEquals(created.name(), fetched.name());
+ Assertions.assertEquals(created.description(), fetched.description());
+ Assertions.assertEquals(created.id(), fetched.id());
+
+ Project updated = fetched.update().withDescription("This is an updated project").apply();
+ Assertions.assertEquals("This is an updated project", updated.description());
+
+ storageMoverManager.projects().delete(resourceGroupName, storageMover.name(), projectName);
+ assertNotFound(() -> storageMoverManager.projects().get(resourceGroupName, storageMover.name(), projectName));
+ }
+}
diff --git a/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/StorageMoverCollectionTests.java b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/StorageMoverCollectionTests.java
new file mode 100644
index 000000000000..2432bdb4bfe0
--- /dev/null
+++ b/sdk/storagemover/azure-resourcemanager-storagemover/src/test/java/com/azure/resourcemanager/storagemover/scenario/StorageMoverCollectionTests.java
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.resourcemanager.storagemover.scenario;
+
+import com.azure.resourcemanager.storagemover.models.StorageMover;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.StreamSupport;
+
+/**
+ * Mirrors {@code StorageMoverCollectionTests.cs} from the .NET source-of-truth
+ * test suite.
+ */
+public class StorageMoverCollectionTests extends StorageMoverManagementTestBase {
+
+ @Test
+ public void createUpdateGetExists() {
+ String storageMoverName1 = generateRandomResourceName("testsm-", 24);
+ String storageMoverName2 = generateRandomResourceName("testsm-", 24);
+
+ Map Mirrors the cross-language test suite layout used by .NET
+ * ({@code Azure.ResourceManager.StorageMover/tests/Scenario}) and Python
+ * ({@code azure-mgmt-storagemover/tests}). Each concrete subclass focuses on
+ * one operation group.
+ */
+public abstract class StorageMoverManagementTestBase extends ResourceManagerTestProxyTestBase {
+
+ /**
+ * Default region. Overridable via {@code AZURE_TEST_LOCATION} (e.g. when a
+ * subscription does not have {@code eastus} enabled).
+ */
+ protected static final Region DEFAULT_REGION
+ = parseRegionOrDefault(Configuration.getGlobalConfiguration().get("AZURE_TEST_LOCATION"), Region.US_EAST);
+
+ /**
+ * Region where the cross-sub private-link / cpmoveraccount / AWS private
+ * bucket fixtures live. The Storage Mover {@code cpmoveraccount} and the
+ * {@code test-pls-wcs} PrivateLinkService are deployed in {@code
+ * westcentralus}. Private-link tests therefore self-provision their
+ * resource group in that region so PE traffic stays in-region.
+ */
+ protected static final Region WEST_CENTRAL_US = Region.US_WEST_CENTRAL;
+
+ /**
+ * Returns a {@code startDate} suitable for schedule tests.
+ *
+ * The Storage Mover RP rejects any {@code endDate} more than one year
+ * past wall-clock {@code now()}, so a hard-coded far-future date does not
+ * work. Uses {@link com.azure.core.test.utils.TestResourceNamer#now()}
+ * which records the value during {@code RECORD} mode and replays the
+ * same value during {@code PLAYBACK}, keeping recordings portable across
+ * dates without an extra body-key sanitizer.
+ *
+ * @return a UTC {@link OffsetDateTime} one day in the future, truncated to
+ * the day so the JSON payload is stable.
+ */
+ protected OffsetDateTime scheduleStartDate() {
+ return testResourceNamer.now().plusDays(1).truncatedTo(ChronoUnit.DAYS);
+ }
+
+ /**
+ * Cross-language shared fixture subscription
+ * ({@code XDataMove-Synthetics}). Hosts the private-link service, the
+ * private AWS S3 bucket, and the {@code cpmoveraccount} storage account
+ * used by the C2C-with-private-source scenario.
+ */
+ protected static final String XDATAMOVE_SYNTHETICS_SUB_ID = "b6b34ad8-ca89-4f85-beb7-c2ec13702dac";
+
+ /**
+ * Well-known resource ids used by the {@code AzureMultiCloudConnector}
+ * endpoint scenarios. Mirrors the constants in
+ * {@code StorageMoverManagementTestBase.cs} from .NET.
+ */
+ protected static final String MULTI_CLOUD_CONNECTOR_ID
+ = "/subscriptions/" + XDATAMOVE_SYNTHETICS_SUB_ID + "/resourceGroups/E2E-Management-RGsyn"
+ + "/providers/Microsoft.HybridConnectivity/publicCloudConnectors/e2e-sm-rp-connector";
+
+ protected static final String AWS_S3_BUCKET_ID = "/subscriptions/" + XDATAMOVE_SYNTHETICS_SUB_ID
+ + "/resourceGroups/aws_640698235822/providers/Microsoft.AWSConnector/s3Buckets/e2e-sm-rp-bucket";
+
+ /**
+ * AWS S3 bucket reachable only over the private endpoint (rows #31/#32 of
+ * the cross-language matrix).
+ */
+ protected static final String AWS_PRIVATE_S3_BUCKET_ID = "/subscriptions/" + XDATAMOVE_SYNTHETICS_SUB_ID
+ + "/resourceGroups/aws_640698235822/providers/Microsoft.AWSConnector/s3Buckets/e2e-sm-rp-private-bucket";
+
+ /**
+ * PrivateLinkService that fronts the AWS private bucket. The Connection
+ * resource references this id; PE-approval helpers live in this base.
+ */
+ protected static final String PRIVATE_LINK_SERVICE_RG = "E2E-Management-RGsyn";
+ protected static final String PRIVATE_LINK_SERVICE_NAME = "test-pls-wcs";
+ protected static final String PRIVATE_LINK_SERVICE_ID
+ = "/subscriptions/" + XDATAMOVE_SYNTHETICS_SUB_ID + "/resourceGroups/" + PRIVATE_LINK_SERVICE_RG
+ + "/providers/Microsoft.Network/privateLinkServices/" + PRIVATE_LINK_SERVICE_NAME;
+
+ /**
+ * Storage account holding the per-test target blob containers used by the
+ * C2C-with-private-source scenario.
+ */
+ protected static final String TEST_STORAGE_ACCOUNT_RG = "CP_Mover_IN_WCUS";
+ protected static final String TEST_STORAGE_ACCOUNT_NAME = "cpmoveraccount";
+ protected static final String TEST_STORAGE_ACCOUNT_ID
+ = "/subscriptions/" + XDATAMOVE_SYNTHETICS_SUB_ID + "/resourceGroups/" + TEST_STORAGE_ACCOUNT_RG
+ + "/providers/Microsoft.Storage/storageAccounts/" + TEST_STORAGE_ACCOUNT_NAME;
+
+ /**
+ * Placeholder storage account resource id used when an endpoint requires a
+ * storage-account ARM id but the test does not actually hit the storage
+ * account. The Storage Mover RP accepts any well-formed id at endpoint
+ * metadata level.
+ */
+ protected static final String FAKE_STORAGE_ACCOUNT_ID
+ = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/fakeRg"
+ + "/providers/Microsoft.Storage/storageAccounts/fakeaccount";
+
+ protected StorageMoverManager storageMoverManager;
+ protected ResourceManager resourceManager;
+ protected String resourceGroupName;
+
+ /**
+ * The HTTP pipeline shared across all managers in a test run (set by
+ * {@link #initializeClients}). Cross-subscription managers reuse this
+ * pipeline so requests flow through the same recorder / playback proxy.
+ */
+ private HttpPipeline sharedHttpPipeline;
+ private AzureProfile sharedTestProfile;
+
+ /** Cached cross-sub managers (one of each is sufficient for the suite). */
+ private NetworkManager xDataMoveNetworkManager;
+ private AuthorizationManager xDataMoveAuthorizationManager;
+ private StorageManager xDataMoveStorageManager;
+
+ /**
+ * Provider registration policy. Has to be a member field because the
+ * pipeline (and therefore the policy) must exist before the
+ * {@link ResourceManager} that supplies the provider list does — see the
+ * two-phase wiring in {@link #buildHttpPipeline} / {@link #initializeClients}.
+ */
+ private final ProviderRegistrationPolicy providerRegistrationPolicy = new ProviderRegistrationPolicy();
+
+ /**
+ * Region the per-test resource group is created in. Subclasses override to
+ * pin themselves to a non-default region (e.g. private-link scenarios that
+ * must run in {@code westcentralus}).
+ *
+ * @return the region for the per-test resource group.
+ */
+ protected Region testRegion() {
+ return DEFAULT_REGION;
+ }
+
+ @Override
+ protected HttpPipeline buildHttpPipeline(TokenCredential credential, AzureProfile profile,
+ HttpLogOptions httpLogOptions, List