diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 9ec08d10610e..62ad1b75e9d7 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -225,13 +225,6 @@ otel_exporter_deprecated_io.opentelemetry:opentelemetry-sdk-extension-autoconfig otel_exporter_deprecated_io.opentelemetry:opentelemetry-sdk-testing;1.43.0 otel_exporter_deprecated_io.opentelemetry.semconv:opentelemetry-semconv-incubating;1.26.0-alpha -# This is a unique dependency as it is the only test-jar dependency in the -# data track. It's also using a SNAPSHOT version which should be disallowed but there is -# going to be some investigation necessary to find, at the very least, a newer version -# which is, hopefully, not a SNAPSHOT. -# sdk\batch\microsoft-azure-batch\pom.xml -test_jar_com.microsoft.azure:azure-mgmt-resources;1.3.1-SNAPSHOT - # Special test dependencies for clientcore integrations clientcore_dep_tests_org.slf4j:slf4j-simple;2.0.16 diff --git a/sdk/batch/microsoft-azure-batch/pom.xml b/sdk/batch/microsoft-azure-batch/pom.xml index 747afb543a1d..7ea171671427 100644 --- a/sdk/batch/microsoft-azure-batch/pom.xml +++ b/sdk/batch/microsoft-azure-batch/pom.xml @@ -103,14 +103,6 @@ test - - com.microsoft.azure - azure-mgmt-resources - 1.3.1-SNAPSHOT - test-jar - test - - com.microsoft.azure azure-mgmt-resources diff --git a/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/BatchIntegrationTestBase.java b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/BatchIntegrationTestBase.java index d59be0da1627..c246ee4cddfa 100644 --- a/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/BatchIntegrationTestBase.java +++ b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/BatchIntegrationTestBase.java @@ -4,6 +4,8 @@ package com.microsoft.azure.batch; import com.microsoft.azure.batch.protocol.models.*; +import com.microsoft.azure.batch.recording.InterceptorManager; +import com.microsoft.azure.batch.recording.TestMode; import com.microsoft.azure.credentials.ApplicationTokenCredentials; import com.azure.core.util.Configuration; import com.microsoft.azure.AzureEnvironment; @@ -13,9 +15,6 @@ import com.microsoft.azure.batch.auth.BatchApplicationTokenCredentials; import com.microsoft.azure.batch.auth.BatchCredentials; import com.microsoft.azure.batch.auth.BatchSharedKeyCredentials; -import com.microsoft.azure.management.resources.core.InterceptorManager; -import com.microsoft.azure.management.resources.core.TestBase; -import com.microsoft.azure.management.resources.core.TestBase.TestMode; import com.microsoft.azure.management.resources.fluentcore.utils.ResourceManagerThrottlingInterceptor; import com.microsoft.azure.serializer.AzureJacksonAdapter; import com.microsoft.azure.storage.CloudStorageAccount; @@ -73,7 +72,7 @@ public enum AuthMode { } - private static TestBase.TestMode testMode = null; + private static TestMode testMode = null; private static final String PLAYBACK_URI_BASE = "http://localhost:"; protected static String playbackUri = null; protected static String alternativePlaybackUri = null; @@ -84,14 +83,14 @@ private static void initTestMode() throws IOException { Configuration.getGlobalConfiguration().get("AZURE_TEST_MODE"); if (azureTestMode != null) { if (azureTestMode.equalsIgnoreCase("Record")) { - testMode = TestBase.TestMode.RECORD; + testMode = TestMode.RECORD; } else if (azureTestMode.equalsIgnoreCase("Playback")) { - testMode = TestBase.TestMode.PLAYBACK; + testMode = TestMode.PLAYBACK; } else { throw new IOException("Unknown AZURE_TEST_MODE: " + azureTestMode); } } else { - testMode = TestBase.TestMode.PLAYBACK; + testMode = TestMode.PLAYBACK; } } @@ -115,7 +114,7 @@ static boolean isPlaybackMode() { throw new RuntimeException("Can't init test mode."); } } - return testMode == TestBase.TestMode.PLAYBACK; + return testMode == TestMode.PLAYBACK; } static boolean isRecordMode() { diff --git a/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/InterceptorManager.java b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/InterceptorManager.java new file mode 100644 index 000000000000..073bbc1debb1 --- /dev/null +++ b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/InterceptorManager.java @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.batch.recording; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.microsoft.azure.management.resources.fluentcore.utils.SdkContext; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.internal.Util; +import okio.Buffer; +import okio.BufferedSource; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpStatus; +import rx.schedulers.Schedulers; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +/** + * Created by vlashch on 7/13/2017. + */ +public class InterceptorManager { + + private final static String RECORD_FOLDER = "session-records/"; + + private final Map textReplacementRules = new HashMap<>(); + // Stores a map of all the HTTP properties in a session + // A state machine ensuring a test is always reset before another one is setup + + protected RecordedData recordedData; + + private final String testName; + + private final TestMode testMode; + + private InterceptorManager(String testName, TestMode testMode) { + this.testName = testName; + this.testMode = testMode; + } + + public void addTextReplacementRule(String regex, String replacement) { + textReplacementRules.put(regex, replacement); + } + + // factory method + public static InterceptorManager create(String testName, TestMode testMode) throws IOException { + InterceptorManager interceptorManager = new InterceptorManager(testName, testMode); + SdkContext.setResourceNamerFactory(new TestResourceNamerFactory(interceptorManager)); + SdkContext.setDelayProvider(new TestDelayProvider(interceptorManager.isRecordMode())); + SdkContext.setRxScheduler(Schedulers.trampoline()); + + return interceptorManager; + } + + public boolean isRecordMode() { + return testMode == TestMode.RECORD; + } + + public boolean isPlaybackMode() { + return testMode == TestMode.PLAYBACK; + } + + public Interceptor initInterceptor() throws IOException { + switch (testMode) { + case RECORD: + recordedData = new RecordedData(); + return new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + return record(chain); + } + }; + case PLAYBACK: + readDataFromFile(); + return new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + return playback(chain); + } + }; + default: + System.out.println("==> Unknown AZURE_TEST_MODE: " + testMode); + }; + return null; + } + + public void finalizeInterceptor() throws IOException { + switch (testMode) { + case RECORD: + writeDataToFile(); + break; + case PLAYBACK: + // Do nothing + break; + default: + System.out.println("==> Unknown AZURE_TEST_MODE: " + testMode); + }; + } + + private Response record(Interceptor.Chain chain) throws IOException { + Request request = chain.request(); + NetworkCallRecord networkCallRecord = new NetworkCallRecord(); + + networkCallRecord.Headers = new HashMap<>(); + + if (request.header("Content-Type") != null) { + networkCallRecord.Headers.put("Content-Type", request.header("Content-Type")); + } + if (request.header("x-ms-version") != null) { + networkCallRecord.Headers.put("x-ms-version", request.header("x-ms-version")); + } + if (request.header("User-Agent") != null) { + networkCallRecord.Headers.put("User-Agent", request.header("User-Agent")); + } + + networkCallRecord.Method = request.method(); + networkCallRecord.Uri = applyReplacementRule(request.url().toString().replaceAll("\\?$", "")); + + Response response = chain.proceed(request); + + networkCallRecord.Response = new HashMap<>(); + networkCallRecord.Response.put("StatusCode", Integer.toString(response.code())); + extractResponseData(networkCallRecord.Response, response); + + // remove pre-added header if this is a waiting or redirection + if (networkCallRecord.Response.get("Body").contains("InProgress") + || Integer.parseInt(networkCallRecord.Response.get("StatusCode")) == HttpStatus.SC_TEMPORARY_REDIRECT) { + // Do nothing + } else { + synchronized (recordedData.getNetworkCallRecords()) { + recordedData.getNetworkCallRecords().add(networkCallRecord); + } + } + + return response; + } + + private Response playback(Interceptor.Chain chain) throws IOException { + Request request = chain.request(); + String incomingUrl = applyReplacementRule(request.url().toString()); + String incomingMethod = request.method(); + + incomingUrl = removeHost(incomingUrl); + NetworkCallRecord networkCallRecord = null; + synchronized (recordedData) { + for (Iterator iterator = recordedData.getNetworkCallRecords().iterator(); iterator.hasNext(); ) { + NetworkCallRecord record = iterator.next(); + if (record.Method.equalsIgnoreCase(incomingMethod) && removeHost(record.Uri).equalsIgnoreCase(incomingUrl)) { + networkCallRecord = record; + iterator.remove(); + break; + } + } + } + + if (networkCallRecord == null) { + System.out.println("NOT FOUND - " + incomingMethod + " " + incomingUrl); + System.out.println("Remaining records " + recordedData.getNetworkCallRecords().size()); + throw new IOException("==> Unexpected request: " + incomingMethod + " " + incomingUrl); + } + + int recordStatusCode = Integer.parseInt(networkCallRecord.Response.get("StatusCode")); + + Response originalResponse = chain.proceed(request); + originalResponse.body().close(); + + Response.Builder responseBuilder = originalResponse.newBuilder() + .code(recordStatusCode).message("-"); + + for (Map.Entry pair : networkCallRecord.Response.entrySet()) { + if (!pair.getKey().equals("StatusCode") && !pair.getKey().equals("Body") && !pair.getKey().equals("Content-Length")) { + String rawHeader = pair.getValue(); + for (Map.Entry rule : textReplacementRules.entrySet()) { + if (rule.getValue() != null) { + rawHeader = rawHeader.replaceAll(rule.getKey(), rule.getValue()); + } + } + responseBuilder.addHeader(pair.getKey(), rawHeader); + } + } + + String rawBody = networkCallRecord.Response.get("Body"); + if (rawBody != null) { + for (Map.Entry rule : textReplacementRules.entrySet()) { + if (rule.getValue() != null) { + rawBody = rawBody.replaceAll(rule.getKey(), rule.getValue()); + } + } + + String rawContentType = networkCallRecord.Response.get("content-type"); + String contentType = rawContentType == null + ? "application/json; charset=utf-8" + : rawContentType; + + ResponseBody responseBody = ResponseBody.create(MediaType.parse(contentType), rawBody.getBytes()); + responseBuilder.body(responseBody); + responseBuilder.addHeader("Content-Length", String.valueOf(rawBody.getBytes("UTF-8").length)); + } + + return responseBuilder.build(); + } + + private void extractResponseData(Map responseData, Response response) throws IOException { + Map> headers = response.headers().toMultimap(); + boolean addedRetryAfter = false; + for (Map.Entry> header : headers.entrySet()) { + String headerValueToStore = header.getValue().get(0); + + if (header.getKey().equalsIgnoreCase("location") || header.getKey().equalsIgnoreCase("azure-asyncoperation")) { + headerValueToStore = applyReplacementRule(headerValueToStore); + } + if (header.getKey().equalsIgnoreCase("retry-after")) { + headerValueToStore = "0"; + addedRetryAfter = true; + } + responseData.put(header.getKey().toLowerCase(), headerValueToStore); + } + + if (!addedRetryAfter) { + responseData.put("retry-after", "0"); + } + + BufferedSource bufferedSource = response.body().source(); + bufferedSource.request(9223372036854775807L); + Buffer buffer = bufferedSource.buffer().clone(); + String content = null; + + if (response.header("Content-Encoding") == null) { + content = new String(buffer.readString(Util.UTF_8)); + } else if ("gzip".equalsIgnoreCase(response.header("Content-Encoding"))) { + GZIPInputStream gis = new GZIPInputStream(buffer.inputStream()); + content = IOUtils.toString(gis); + responseData.remove("Content-Encoding".toLowerCase()); + responseData.put("Content-Length".toLowerCase(), Integer.toString(content.length())); + } + + if (content != null) { + content = applyReplacementRule(content); + responseData.put("Body", content); + } + } + + private void readDataFromFile() throws IOException { + File recordFile = getRecordFile(testName); + ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + recordedData = mapper.readValue(recordFile, RecordedData.class); + System.out.println("Total records " + recordedData.getNetworkCallRecords().size()); + } + + private void writeDataToFile() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + File recordFile = getRecordFile(testName); + recordFile.createNewFile(); + mapper.writeValue(recordFile, recordedData); + } + + private File getRecordFile(String testName) { + URL folderUrl = InterceptorManager.class.getClassLoader().getResource("."); + File folderFile = new File(folderUrl.getPath() + RECORD_FOLDER); + if (!folderFile.exists()) { + folderFile.mkdir(); + } + String filePath = folderFile.getPath() + "/" + testName + ".json"; + System.out.println("==> Playback file path: " + filePath); + return new File(filePath); + } + + private String applyReplacementRule(String text) { + for (Map.Entry rule : textReplacementRules.entrySet()) { + if (rule.getValue() != null) { + text = text.replaceAll(rule.getKey(), rule.getValue()); + } + } + return text; + } + + private String removeHost(String url) { + URI uri = URI.create(url); + return String.format("%s?%s", uri.getPath(), uri.getQuery()); + } + + public void pushVariable(String variable) { + if (isRecordMode()) { + synchronized (recordedData.getVariables()) { + recordedData.getVariables().add(variable); + } + } + } + + public String popVariable() { + synchronized (recordedData.getVariables()) { + return recordedData.getVariables().remove(); + } + } +} diff --git a/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/NetworkCallRecord.java b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/NetworkCallRecord.java new file mode 100644 index 000000000000..c2882b781c0b --- /dev/null +++ b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/NetworkCallRecord.java @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.batch.recording; + +import java.util.Map; + +public class NetworkCallRecord { + public String Method; + public String Uri; + + public Map Headers; + public Map Response; +} diff --git a/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/RecordedData.java b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/RecordedData.java new file mode 100644 index 000000000000..5b7a9ab956cc --- /dev/null +++ b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/RecordedData.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.batch.recording; + +import java.util.LinkedList; + +public class RecordedData { + private final LinkedList networkCallRecords; + private final LinkedList variables; + + public RecordedData() { + networkCallRecords = new LinkedList<>(); + variables = new LinkedList<>(); + } + + public LinkedList getNetworkCallRecords() { + return networkCallRecords; + } + + public LinkedList getVariables() { + return variables; + } +} diff --git a/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestDelayProvider.java b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestDelayProvider.java new file mode 100644 index 000000000000..8a758bab7473 --- /dev/null +++ b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestDelayProvider.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.batch.recording; + +import com.microsoft.azure.management.resources.fluentcore.utils.DelayProvider; + +public class TestDelayProvider extends DelayProvider { + private final boolean isRecordMode; + public TestDelayProvider(boolean isRecordMode) { + this.isRecordMode = isRecordMode; + } + @Override + public void sleep(int milliseconds) { + if (isRecordMode) { + super.sleep(milliseconds); + } + } + +} diff --git a/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestMode.java b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestMode.java new file mode 100644 index 000000000000..9baed175fe42 --- /dev/null +++ b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestMode.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.batch.recording; + +public enum TestMode { + PLAYBACK, + RECORD +} diff --git a/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestResourceNamer.java b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestResourceNamer.java new file mode 100644 index 000000000000..9942fcd7bcec --- /dev/null +++ b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestResourceNamer.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.batch.recording; + +import com.microsoft.azure.management.resources.fluentcore.utils.ResourceNamer; + +public class TestResourceNamer extends ResourceNamer { + private final InterceptorManager interceptorManager; + + public TestResourceNamer(String name, InterceptorManager interceptorManager) { + super(name); + this.interceptorManager = interceptorManager; + } + + /** + * Gets a random name. + * + * @param prefix the prefix to be used if possible + * @param maxLen the max length for the random generated name + * @return the random name + */ + @Override + public String randomName(String prefix, int maxLen) { + if (interceptorManager.isPlaybackMode()) { + return interceptorManager.popVariable(); + } + String randomName = super.randomName(prefix, maxLen); + + interceptorManager.pushVariable(randomName); + + return randomName; + } + + @Override + public String randomUuid() { + if (interceptorManager.isPlaybackMode()) { + return interceptorManager.popVariable(); + } + String randomName = super.randomUuid(); + + interceptorManager.pushVariable(randomName); + + return randomName; + } +} diff --git a/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestResourceNamerFactory.java b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestResourceNamerFactory.java new file mode 100644 index 000000000000..045cca052cb1 --- /dev/null +++ b/sdk/batch/microsoft-azure-batch/src/test/java/com/microsoft/azure/batch/recording/TestResourceNamerFactory.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.batch.recording; + +import com.microsoft.azure.management.resources.fluentcore.utils.ResourceNamer; +import com.microsoft.azure.management.resources.fluentcore.utils.ResourceNamerFactory; + +public class TestResourceNamerFactory extends ResourceNamerFactory { + + private final InterceptorManager interceptorManager; + + TestResourceNamerFactory(InterceptorManager mockIntegrationTestBase) { + this.interceptorManager = mockIntegrationTestBase; + } + @Override + public ResourceNamer createResourceNamer(String name) { + return new TestResourceNamer(name, interceptorManager); + } +}