Skip to content

Commit 3b9b243

Browse files
committed
test: Enable GCP blob store conformance tests testDownloadWithKmsKey, testUploadWithKmsKey_happyPath, testUploadWithKmsKey_emptyKmsKeyId, testUploadWithKmsKey_nullKmsKeyId, testRangedReadWithKmsKey, testVersionedCopyFrom
1 parent a02dece commit 3b9b243

File tree

79 files changed

+2420
-13
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+2420
-13
lines changed

blob/blob-client/src/test/java/com/salesforce/multicloudj/blob/client/AbstractBlobStoreIT.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.ArrayList;
5656
import java.util.Arrays;
5757
import java.util.Collection;
58+
import java.util.Collections;
5859
import java.util.HashMap;
5960
import java.util.HashSet;
6061
import java.util.Iterator;
@@ -94,6 +95,10 @@ public interface Harness extends AutoCloseable {
9495

9596
// Returns the KMS key ID for encryption tests (provider-specific)
9697
String getKmsKeyId();
98+
99+
default List<String> getWiremockExtensions() {
100+
return Collections.emptyList();
101+
}
97102
}
98103

99104
protected abstract Harness createHarness();
@@ -108,7 +113,9 @@ public interface Harness extends AutoCloseable {
108113
@BeforeAll
109114
public void initializeWireMockServer() {
110115
harness = createHarness();
111-
TestsUtil.startWireMockServer("src/test/resources", harness.getPort());
116+
List<String> extensions = harness.getWiremockExtensions();
117+
TestsUtil.startWireMockServer("src/test/resources", harness.getPort(),
118+
extensions.toArray(new String[0]));
112119
}
113120

114121
/**
@@ -146,8 +153,8 @@ public void testNonexistentBucket() {
146153
// And run the tests given the non-existent bucket
147154
runOperationsThatShouldFail("testNonexistentBucket", bucketClient);
148155
if (!GCP_PROVIDER_ID.equals(harness.getProviderId())) {
149-
runOperationsThatShouldNotFail("testNonexistentBucket", bucketClient);
150-
}
156+
runOperationsThatShouldNotFail("testNonexistentBucket", bucketClient);
157+
}
151158
}
152159

153160
@Test
@@ -160,8 +167,8 @@ public void testInvalidCredentials() {
160167
// And run the tests given the invalid credentialsOverrider
161168
runOperationsThatShouldFail("testInvalidCredentials", bucketClient);
162169
if (!GCP_PROVIDER_ID.equals(harness.getProviderId())) {
163-
runOperationsThatShouldNotFail("testInvalidCredentials", bucketClient);
164-
}
170+
runOperationsThatShouldNotFail("testInvalidCredentials", bucketClient);
171+
}
165172
}
166173

167174
private void runOperationsThatShouldFail(String testName, BucketClient bucketClient) {
@@ -871,7 +878,7 @@ public TestConfig(String testName, Collection<String> keysToCreate, Collection<S
871878

872879
@Test
873880
public void testVersionedDelete_fileDoesNotExist() throws IOException {
874-
881+
Assumptions.assumeFalse(GCP_PROVIDER_ID.equals(harness.getProviderId()));
875882
// Create the BucketClient
876883
AbstractBlobStore blobStore = harness.createBlobStore(true, true, true);
877884
BucketClient bucketClient = new BucketClient(blobStore);
@@ -1356,8 +1363,6 @@ public void testCopyFrom() throws IOException {
13561363

13571364
@Test
13581365
public void testVersionedCopyFrom() throws IOException {
1359-
Assumptions.assumeFalse(GCP_PROVIDER_ID.equals(harness.getProviderId()));
1360-
13611366
String key = "conformance-tests/versionedCopyFrom/blob";
13621367
String destKeyV1 = "conformance-tests/versionedCopyFrom/copied-from-blob-v1";
13631368
String destKeyV2 = "conformance-tests/versionedCopyFrom/copied-from-blob-v2";
@@ -2737,22 +2742,19 @@ private void safeDeleteBlobs(BucketClient bucketClient, String... keys){
27372742

27382743
@Test
27392744
public void testUploadWithKmsKey_happyPath() {
2740-
Assumptions.assumeFalse(GCP_PROVIDER_ID.equals(harness.getProviderId()));
27412745
String key = "conformance-tests/kms/upload-happy-path";
27422746
String kmsKeyId = harness.getKmsKeyId();
27432747
runUploadWithKmsKeyTest(key, kmsKeyId, "Test data with KMS encryption".getBytes());
27442748
}
27452749

27462750
@Test
27472751
public void testUploadWithKmsKey_nullKmsKeyId() {
2748-
Assumptions.assumeFalse(GCP_PROVIDER_ID.equals(harness.getProviderId()));
27492752
String key = "conformance-tests/kms/upload-null-key";
27502753
runUploadWithKmsKeyTest(key, null, "Test data without KMS".getBytes());
27512754
}
27522755

27532756
@Test
27542757
public void testUploadWithKmsKey_emptyKmsKeyId() {
2755-
Assumptions.assumeFalse(GCP_PROVIDER_ID.equals(harness.getProviderId()));
27562758
String key = "conformance-tests/kms/upload-empty-key";
27572759
runUploadWithKmsKeyTest(key, "", "Test data with empty KMS key".getBytes());
27582760
}
@@ -2795,7 +2797,6 @@ private void runUploadWithKmsKeyTest(String key, String kmsKeyId, byte[] content
27952797

27962798
@Test
27972799
public void testDownloadWithKmsKey() throws IOException {
2798-
Assumptions.assumeFalse(GCP_PROVIDER_ID.equals(harness.getProviderId()));
27992800
String key = "conformance-tests/kms/download-happy-path";
28002801
String kmsKeyId = harness.getKmsKeyId();
28012802
byte[] content = "Test data for KMS download".getBytes(StandardCharsets.UTF_8);
@@ -2833,7 +2834,6 @@ public void testDownloadWithKmsKey() throws IOException {
28332834

28342835
@Test
28352836
public void testRangedReadWithKmsKey() throws IOException {
2836-
Assumptions.assumeFalse(GCP_PROVIDER_ID.equals(harness.getProviderId()));
28372837
String key = "conformance-tests/kms/ranged-read";
28382838
String kmsKeyId = harness.getKmsKeyId();
28392839
runRangedReadWithKmsKeyTest(key, kmsKeyId);

blob/blob-gcp/src/test/java/com/salesforce/multicloudj/blob/gcp/GcpBlobStoreIT.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.io.IOException;
1919
import java.net.URI;
20+
import java.util.List;
2021
import java.util.concurrent.ThreadLocalRandom;
2122

2223
public class GcpBlobStoreIT extends AbstractBlobStoreIT {
@@ -114,6 +115,11 @@ public String getKmsKeyId() {
114115
return "projects/substrate-sdk-gcp-poc1/locations/us/keyRings/chameleon-test/cryptoKeys/chameleon-test";
115116
}
116117

118+
@Override
119+
public List<String> getWiremockExtensions() {
120+
return List.of("com.salesforce.multicloudj.blob.gcp.util.ResumableUploadIdTransformer");
121+
}
122+
117123
@Override
118124
public void close() {
119125
try {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package com.salesforce.multicloudj.blob.gcp.util;
2+
3+
import com.github.tomakehurst.wiremock.common.FileSource;
4+
import com.github.tomakehurst.wiremock.extension.Parameters;
5+
import com.github.tomakehurst.wiremock.extension.StubMappingTransformer;
6+
import com.github.tomakehurst.wiremock.matching.ContentPattern;
7+
import com.github.tomakehurst.wiremock.matching.RequestPattern;
8+
import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder;
9+
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
10+
11+
import java.util.List;
12+
import java.util.regex.Pattern;
13+
14+
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
15+
16+
/**
17+
* Transformer that replaces exact upload_id parameter values in resumable upload URLs
18+
* with regex patterns to allow flexible matching during replay.
19+
*
20+
* This handles the dynamic upload_id values generated by GCP Storage for resumable uploads.
21+
* The transformer converts exact URL matches to URL pattern matches by replacing
22+
* the exact upload_id value with a regex pattern.
23+
*/
24+
public class ResumableUploadIdTransformer extends StubMappingTransformer {
25+
26+
// Pattern to match upload_id parameter: upload_id=<value>
27+
// The value can contain alphanumeric, dash, underscore, and equals characters
28+
private static final Pattern UPLOAD_ID_PATTERN = Pattern.compile("upload_id=[A-Za-z0-9_-]+");
29+
private static final String UPLOAD_ID_REPLACEMENT = "upload_id=[^&]+";
30+
31+
@Override
32+
public StubMapping transform(StubMapping stubMapping, FileSource files, Parameters parameters) {
33+
RequestPattern requestPattern = stubMapping.getRequest();
34+
String url = requestPattern.getUrl();
35+
String urlPattern = requestPattern.getUrlPattern();
36+
37+
// Check if this is a resumable upload request (PUT or POST with uploadType=resumable)
38+
boolean isResumableUpload = (url != null && url.contains("uploadType=resumable")) ||
39+
(urlPattern != null && urlPattern.contains("uploadType=resumable"));
40+
41+
if (isResumableUpload) {
42+
String transformedPattern = null;
43+
44+
if (url != null && url.contains("upload_id=")) {
45+
// Replace exact upload_id value with regex pattern and escape special characters
46+
transformedPattern = escapeUrlForRegex(url);
47+
} else if (urlPattern != null && urlPattern.contains("upload_id=")) {
48+
// If it's already a pattern, ensure upload_id is flexible
49+
transformedPattern = UPLOAD_ID_PATTERN.matcher(urlPattern).replaceAll(UPLOAD_ID_REPLACEMENT);
50+
}
51+
52+
if (transformedPattern != null) {
53+
// Rebuild the RequestPattern with the transformed URL pattern
54+
RequestPatternBuilder builder = RequestPatternBuilder.newRequestPattern(
55+
requestPattern.getMethod(),
56+
urlMatching(transformedPattern)
57+
);
58+
59+
// Copy body patterns if they exist
60+
List<ContentPattern<?>> bodyPatterns = requestPattern.getBodyPatterns();
61+
if (bodyPatterns != null && !bodyPatterns.isEmpty()) {
62+
for (ContentPattern<?> pattern : bodyPatterns) {
63+
builder.withRequestBody(pattern);
64+
}
65+
}
66+
67+
RequestPattern newRequestPattern = builder.build();
68+
69+
// Create a new StubMapping with the modified RequestPattern
70+
StubMapping newStubMapping = new StubMapping(newRequestPattern, stubMapping.getResponse());
71+
// Copy all other properties from the original stub
72+
newStubMapping.setId(stubMapping.getId());
73+
newStubMapping.setPriority(stubMapping.getPriority());
74+
newStubMapping.setScenarioName(stubMapping.getScenarioName());
75+
newStubMapping.setRequiredScenarioState(stubMapping.getRequiredScenarioState());
76+
newStubMapping.setNewScenarioState(stubMapping.getNewScenarioState());
77+
newStubMapping.setPersistent(stubMapping.isPersistent());
78+
return newStubMapping;
79+
}
80+
}
81+
82+
return stubMapping;
83+
}
84+
85+
/**
86+
* Escapes special regex characters in the URL while preserving our regex pattern for upload_id.
87+
* WireMock regex patterns need special characters escaped, except for our intentional regex pattern.
88+
*/
89+
private String escapeUrlForRegex(String url) {
90+
// Replace upload_id value with regex pattern first
91+
String withRegex = UPLOAD_ID_PATTERN.matcher(url).replaceAll(UPLOAD_ID_REPLACEMENT);
92+
93+
// Now escape special regex characters, but preserve our upload_id=[^&]+ pattern
94+
// We'll escape everything except the upload_id pattern we just inserted
95+
StringBuilder result = new StringBuilder();
96+
int uploadIdIndex = withRegex.indexOf("upload_id=[^&]+");
97+
98+
if (uploadIdIndex >= 0) {
99+
// Escape the part before upload_id pattern
100+
String before = withRegex.substring(0, uploadIdIndex);
101+
result.append(escapeRegexSpecialChars(before));
102+
103+
// Keep the upload_id regex pattern as-is
104+
result.append("upload_id=[^&]+");
105+
106+
// Escape the part after upload_id pattern
107+
String after = withRegex.substring(uploadIdIndex + "upload_id=[^&]+".length());
108+
result.append(escapeRegexSpecialChars(after));
109+
} else {
110+
// No upload_id pattern found, escape the whole URL
111+
result.append(escapeRegexSpecialChars(withRegex));
112+
}
113+
114+
return result.toString();
115+
}
116+
117+
/**
118+
* Escapes special regex characters that need to be literal in the URL pattern.
119+
*/
120+
private String escapeRegexSpecialChars(String str) {
121+
return str.replaceAll("([\\\\^$\\[\\]{}()*+?.|])", "\\\\$1");
122+
}
123+
124+
125+
@Override
126+
public String getName() {
127+
return "resumable-upload-id-transformer";
128+
}
129+
130+
@Override
131+
public boolean applyGlobally() {
132+
return true;
133+
}
134+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"id" : "531417ba-b6d4-4172-81f0-ee672fd5f557",
3+
"name" : "storage_v1_b_substrate-sdk-gcp-poc1-test-bucket_o_conformance-tests_kms_download-happy-path",
4+
"request" : {
5+
"url" : "/storage/v1/b/substrate-sdk-gcp-poc1-test-bucket/o/conformance-tests%2Fkms%2Fdownload-happy-path",
6+
"method" : "DELETE"
7+
},
8+
"response" : {
9+
"status" : 204,
10+
"headers" : {
11+
"Alt-Svc" : "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000",
12+
"Server" : "UploadServer",
13+
"Cache-Control" : "no-cache, no-store, max-age=0, must-revalidate",
14+
"X-GUploader-UploadID" : "AHVrFxOgb3W1p0IaAChnG5rSTCgvZ9ZLIXjPG-d9xQVqXArluIuhgrNd0Eh33DpDOiAu5x8mSzqezQH4vdjHPA",
15+
"Vary" : [ "Origin", "X-Origin" ],
16+
"Pragma" : "no-cache",
17+
"Expires" : "Mon, 01 Jan 1990 00:00:00 GMT",
18+
"Date" : "Mon, 29 Dec 2025 20:02:52 GMT",
19+
"Content-Type" : "application/json"
20+
}
21+
},
22+
"uuid" : "531417ba-b6d4-4172-81f0-ee672fd5f557",
23+
"persistent" : true,
24+
"insertionIndex" : 863
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"id" : "12b90d1c-427a-4afb-8b49-f65659fa5520",
3+
"name" : "storage_v1_b_substrate-sdk-gcp-poc1-test-bucket_o_conformance-tests_kms_ranged-read",
4+
"request" : {
5+
"url" : "/storage/v1/b/substrate-sdk-gcp-poc1-test-bucket/o/conformance-tests%2Fkms%2Franged-read",
6+
"method" : "DELETE"
7+
},
8+
"response" : {
9+
"status" : 204,
10+
"headers" : {
11+
"Alt-Svc" : "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000",
12+
"Server" : "UploadServer",
13+
"Cache-Control" : "no-cache, no-store, max-age=0, must-revalidate",
14+
"X-GUploader-UploadID" : "AHVrFxNmhg9_j3PjTOzJ_ue8p5DF-xnWzQPygCXJ-UcMswM-87WEEZX70N7byIlgZPhSXWSa",
15+
"Vary" : [ "Origin", "X-Origin" ],
16+
"Pragma" : "no-cache",
17+
"Expires" : "Mon, 01 Jan 1990 00:00:00 GMT",
18+
"Date" : "Mon, 29 Dec 2025 20:09:04 GMT",
19+
"Content-Type" : "application/json"
20+
}
21+
},
22+
"uuid" : "12b90d1c-427a-4afb-8b49-f65659fa5520",
23+
"persistent" : true,
24+
"insertionIndex" : 891
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"id" : "a04882ab-493f-4490-a8e9-064a9e827d16",
3+
"name" : "storage_v1_b_substrate-sdk-gcp-poc1-test-bucket_o_conformance-tests_kms_upload-null-key",
4+
"request" : {
5+
"url" : "/storage/v1/b/substrate-sdk-gcp-poc1-test-bucket/o/conformance-tests%2Fkms%2Fupload-null-key",
6+
"method" : "DELETE"
7+
},
8+
"response" : {
9+
"status" : 204,
10+
"headers" : {
11+
"Alt-Svc" : "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000",
12+
"Server" : "UploadServer",
13+
"Cache-Control" : "no-cache, no-store, max-age=0, must-revalidate",
14+
"X-GUploader-UploadID" : "AHVrFxOWNLPu7C3_wNABfMAfiJavsd4RO-CE2HhH3tIXYGgcX_cgIKxJXwbWq2J0LeRJ5UFe",
15+
"Vary" : [ "Origin", "X-Origin" ],
16+
"Pragma" : "no-cache",
17+
"Expires" : "Mon, 01 Jan 1990 00:00:00 GMT",
18+
"Date" : "Mon, 29 Dec 2025 20:08:16 GMT",
19+
"Content-Type" : "application/json"
20+
}
21+
},
22+
"uuid" : "a04882ab-493f-4490-a8e9-064a9e827d16",
23+
"persistent" : true,
24+
"insertionIndex" : 884
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"id" : "12490805-8dc1-4f5b-a110-7cef7a1f6e65",
3+
"name" : "storage_v1_b_substrate-sdk-gcp-poc1-test-bucket_o_conformance-tests_kms_upload-happy-path",
4+
"request" : {
5+
"url" : "/storage/v1/b/substrate-sdk-gcp-poc1-test-bucket/o/conformance-tests%2Fkms%2Fupload-happy-path",
6+
"method" : "DELETE"
7+
},
8+
"response" : {
9+
"status" : 204,
10+
"headers" : {
11+
"Alt-Svc" : "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000",
12+
"Server" : "UploadServer",
13+
"Cache-Control" : "no-cache, no-store, max-age=0, must-revalidate",
14+
"X-GUploader-UploadID" : "AHVrFxMzg4fSYxLMd3E0aO8oyhIPYW1-oFjveo5_vk09ictySqjJLKbg0BJo3QojvVtI_CjGDiqP8DR7ECeGGg",
15+
"Vary" : [ "Origin", "X-Origin" ],
16+
"Pragma" : "no-cache",
17+
"Expires" : "Mon, 01 Jan 1990 00:00:00 GMT",
18+
"Date" : "Mon, 29 Dec 2025 20:03:14 GMT",
19+
"Content-Type" : "application/json"
20+
}
21+
},
22+
"uuid" : "12490805-8dc1-4f5b-a110-7cef7a1f6e65",
23+
"persistent" : true,
24+
"insertionIndex" : 870
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"id" : "d27615af-1d5f-4849-9759-533ddb4b624d",
3+
"name" : "storage_v1_b_substrate-sdk-gcp-poc1-test-bucket-versioned_o_conformance-tests_versionedcopyfrom_blob",
4+
"request" : {
5+
"url" : "/storage/v1/b/substrate-sdk-gcp-poc1-test-bucket-versioned/o/conformance-tests%2FversionedCopyFrom%2Fblob",
6+
"method" : "DELETE"
7+
},
8+
"response" : {
9+
"status" : 204,
10+
"headers" : {
11+
"Alt-Svc" : "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000",
12+
"Server" : "UploadServer",
13+
"Cache-Control" : "no-cache, no-store, max-age=0, must-revalidate",
14+
"X-GUploader-UploadID" : "AHVrFxOCInV5IuowOoWIEWtFKct_wkFcxdKqsV8DHUiWI7Kfuk2D8MaQBzWIQxfRXgdzksZI",
15+
"Vary" : [ "Origin", "X-Origin" ],
16+
"Pragma" : "no-cache",
17+
"Expires" : "Mon, 01 Jan 1990 00:00:00 GMT",
18+
"Date" : "Mon, 29 Dec 2025 20:09:35 GMT",
19+
"Content-Type" : "application/json"
20+
}
21+
},
22+
"uuid" : "d27615af-1d5f-4849-9759-533ddb4b624d",
23+
"persistent" : true,
24+
"insertionIndex" : 910
25+
}

0 commit comments

Comments
 (0)