Skip to content

Commit 39d866b

Browse files
committed
API consistency: Bucket API / DTOs
Checking AWS APIs for changes and S3Mock for implementations and tests. Fixes #2340
1 parent bd319a8 commit 39d866b

Some content is hidden

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

44 files changed

+1151
-139
lines changed

integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ import software.amazon.awssdk.awscore.exception.AwsServiceException
2626
import software.amazon.awssdk.services.s3.S3Client
2727
import software.amazon.awssdk.services.s3.model.AbortIncompleteMultipartUpload
2828
import software.amazon.awssdk.services.s3.model.BucketLifecycleConfiguration
29+
import software.amazon.awssdk.services.s3.model.BucketType
2930
import software.amazon.awssdk.services.s3.model.BucketVersioningStatus
31+
import software.amazon.awssdk.services.s3.model.DataRedundancy
3032
import software.amazon.awssdk.services.s3.model.DeleteBucketRequest
3133
import software.amazon.awssdk.services.s3.model.ExpirationStatus
3234
import software.amazon.awssdk.services.s3.model.GetBucketLifecycleConfigurationRequest
3335
import software.amazon.awssdk.services.s3.model.LifecycleExpiration
3436
import software.amazon.awssdk.services.s3.model.LifecycleRule
3537
import software.amazon.awssdk.services.s3.model.LifecycleRuleFilter
38+
import software.amazon.awssdk.services.s3.model.LocationType
3639
import software.amazon.awssdk.services.s3.model.MFADelete
3740
import software.amazon.awssdk.services.s3.model.MFADeleteStatus
3841
import software.amazon.awssdk.services.s3.model.NoSuchBucketException
@@ -68,6 +71,35 @@ internal class BucketIT : S3TestBase() {
6871
}
6972
}
7073

74+
@Test
75+
@S3VerifiedSuccess(year = 2025)
76+
fun `creating a bucket with configuration is successful`(testInfo: TestInfo) {
77+
val bucketName = bucketName(testInfo)
78+
val createBucketResponse = s3Client.createBucket {
79+
it.bucket(bucketName)
80+
it.createBucketConfiguration {
81+
it.locationConstraint("ap-southeast-5")
82+
it.bucket {
83+
it.dataRedundancy(DataRedundancy.SINGLE_AVAILABILITY_ZONE)
84+
it.type(BucketType.DIRECTORY)
85+
}
86+
it.location {
87+
it.name("SomeName")
88+
it.type(LocationType.AVAILABILITY_ZONE)
89+
}
90+
}
91+
}
92+
assertThat(createBucketResponse.sdkHttpResponse().statusCode()).isEqualTo(200)
93+
assertThat(createBucketResponse.location()).isEqualTo("/$bucketName")
94+
95+
val bucketCreated = s3Client.waiter().waitUntilBucketExists { it.bucket(bucketName) }
96+
val bucketCreatedResponse = bucketCreated.matched().response().get()
97+
assertThat(bucketCreatedResponse).isNotNull
98+
99+
//does not throw exception if bucket exists.
100+
s3Client.headBucket { it.bucket(bucketName) }
101+
}
102+
71103
@Test
72104
@S3VerifiedSuccess(year = 2025)
73105
fun `deleting a non-empty bucket fails`(testInfo: TestInfo) {

server/src/main/java/com/adobe/testing/s3mock/BucketController.java

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
package com.adobe.testing.s3mock;
1818

19+
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_BUCKET_LOCATION_NAME;
20+
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_BUCKET_LOCATION_TYPE;
1921
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_BUCKET_OBJECT_LOCK_ENABLED;
22+
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_BUCKET_REGION;
2023
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_OBJECT_OWNERSHIP;
2124
import static com.adobe.testing.s3mock.util.AwsHttpParameters.CONTINUATION_TOKEN;
2225
import static com.adobe.testing.s3mock.util.AwsHttpParameters.ENCODING_TYPE;
@@ -39,15 +42,21 @@
3942
import static com.adobe.testing.s3mock.util.AwsHttpParameters.VERSION_ID_MARKER;
4043
import static org.springframework.http.MediaType.APPLICATION_XML_VALUE;
4144

45+
import com.adobe.testing.S3Verified;
4246
import com.adobe.testing.s3mock.dto.BucketLifecycleConfiguration;
47+
import com.adobe.testing.s3mock.dto.BucketType;
48+
import com.adobe.testing.s3mock.dto.CreateBucketConfiguration;
4349
import com.adobe.testing.s3mock.dto.ListAllMyBucketsResult;
4450
import com.adobe.testing.s3mock.dto.ListBucketResult;
4551
import com.adobe.testing.s3mock.dto.ListBucketResultV2;
4652
import com.adobe.testing.s3mock.dto.ListVersionsResult;
4753
import com.adobe.testing.s3mock.dto.LocationConstraint;
4854
import com.adobe.testing.s3mock.dto.ObjectLockConfiguration;
55+
import com.adobe.testing.s3mock.dto.ObjectOwnership;
56+
import com.adobe.testing.s3mock.dto.Region;
4957
import com.adobe.testing.s3mock.dto.VersioningConfiguration;
5058
import com.adobe.testing.s3mock.service.BucketService;
59+
import com.adobe.testing.s3mock.store.BucketMetadata;
5160
import org.springframework.http.ResponseEntity;
5261
import org.springframework.stereotype.Controller;
5362
import org.springframework.web.bind.annotation.CrossOrigin;
@@ -60,8 +69,6 @@
6069
import org.springframework.web.bind.annotation.RequestMapping;
6170
import org.springframework.web.bind.annotation.RequestMethod;
6271
import org.springframework.web.bind.annotation.RequestParam;
63-
import software.amazon.awssdk.regions.Region;
64-
import software.amazon.awssdk.services.s3.model.ObjectOwnership;
6572

6673
/**
6774
* Handles requests related to buckets.
@@ -123,15 +130,34 @@ public ResponseEntity<ListAllMyBucketsResult> listBuckets() {
123130
NOT_VERSIONING
124131
}
125132
)
133+
@S3Verified(year = 2025)
126134
public ResponseEntity<Void> createBucket(@PathVariable final String bucketName,
127135
@RequestHeader(value = X_AMZ_BUCKET_OBJECT_LOCK_ENABLED,
128136
required = false, defaultValue = "false") boolean objectLockEnabled,
129137
@RequestHeader(value = X_AMZ_OBJECT_OWNERSHIP,
130-
required = false, defaultValue = "BucketOwnerEnforced") ObjectOwnership objectOwnership) {
138+
required = false, defaultValue = "BucketOwnerEnforced") ObjectOwnership objectOwnership,
139+
@RequestBody(required = false) CreateBucketConfiguration createBucketRequest) {
131140
bucketService.verifyBucketNameIsAllowed(bucketName);
132141
bucketService.verifyBucketDoesNotExist(bucketName);
133-
bucketService.createBucket(bucketName, objectLockEnabled, objectOwnership);
134-
return ResponseEntity.ok().build();
142+
bucketService.createBucket(bucketName,
143+
objectLockEnabled,
144+
objectOwnership,
145+
getRegion(createBucketRequest),
146+
createBucketRequest != null ? createBucketRequest.bucket() : null,
147+
createBucketRequest != null ? createBucketRequest.location() : null
148+
);
149+
return ResponseEntity.ok()
150+
.header(LOCATION, "/" + bucketName)
151+
.build();
152+
}
153+
154+
private String getRegion(CreateBucketConfiguration createBucketRequest) {
155+
if (createBucketRequest != null
156+
&& createBucketRequest.locationConstraint() != null
157+
&& createBucketRequest.locationConstraint().region() != null) {
158+
return createBucketRequest.locationConstraint().region().toString();
159+
}
160+
return this.region.toString();
135161
}
136162

137163
/**
@@ -151,11 +177,24 @@ public ResponseEntity<Void> createBucket(@PathVariable final String bucketName,
151177
},
152178
method = RequestMethod.HEAD
153179
)
180+
@S3Verified(year = 2025)
154181
public ResponseEntity<Void> headBucket(@PathVariable final String bucketName) {
155-
bucketService.verifyBucketExists(bucketName);
156-
//return bucket region
157-
//return bucket location
158-
return ResponseEntity.ok().build();
182+
BucketMetadata bucketMetadata = bucketService.verifyBucketExists(bucketName);
183+
return ResponseEntity
184+
.ok()
185+
.header(X_AMZ_BUCKET_REGION, bucketMetadata.bucketRegion())
186+
.headers(h -> {
187+
if (bucketMetadata.bucketInfo() != null
188+
&& bucketMetadata.bucketInfo().type() != null
189+
&& bucketMetadata.bucketInfo().type() == BucketType.DIRECTORY
190+
&& bucketMetadata.locationInfo() != null
191+
&& bucketMetadata.locationInfo().name() != null
192+
&& bucketMetadata.locationInfo().type() != null) {
193+
h.add(X_AMZ_BUCKET_LOCATION_NAME, bucketMetadata.locationInfo().name());
194+
h.add(X_AMZ_BUCKET_LOCATION_TYPE, bucketMetadata.locationInfo().type().toString());
195+
}
196+
})
197+
.build();
159198
}
160199

161200
/**
@@ -392,8 +431,11 @@ public ResponseEntity<Void> deleteBucketLifecycleConfiguration(
392431
)
393432
public ResponseEntity<LocationConstraint> getBucketLocation(
394433
@PathVariable String bucketName) {
395-
bucketService.verifyBucketExists(bucketName);
396-
return ResponseEntity.ok(new LocationConstraint(region));
434+
BucketMetadata bucketMetadata = bucketService.verifyBucketExists(bucketName);
435+
String bucketRegion = bucketMetadata.bucketRegion() != null
436+
? bucketMetadata.bucketRegion()
437+
: region.toString();
438+
return ResponseEntity.ok(new LocationConstraint(bucketRegion));
397439
}
398440

399441
/**

server/src/main/java/com/adobe/testing/s3mock/ObjectCannedAclHeaderConverter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2024 Adobe.
2+
* Copyright 2017-2025 Adobe.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,11 +16,11 @@
1616

1717
package com.adobe.testing.s3mock;
1818

19+
import com.adobe.testing.s3mock.dto.ObjectCannedACL;
1920
import com.adobe.testing.s3mock.util.AwsHttpHeaders;
2021
import org.springframework.core.convert.converter.Converter;
2122
import org.springframework.lang.NonNull;
2223
import org.springframework.lang.Nullable;
23-
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
2424

2525
/**
2626
* Converts values of the {@link AwsHttpHeaders#X_AMZ_ACL} which is sent by the Amazon client.

server/src/main/java/com/adobe/testing/s3mock/ObjectController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
import com.adobe.testing.s3mock.dto.GetObjectAttributesOutput;
8383
import com.adobe.testing.s3mock.dto.LegalHold;
8484
import com.adobe.testing.s3mock.dto.ObjectAttributes;
85+
import com.adobe.testing.s3mock.dto.ObjectCannedACL;
8586
import com.adobe.testing.s3mock.dto.ObjectKey;
8687
import com.adobe.testing.s3mock.dto.Owner;
8788
import com.adobe.testing.s3mock.dto.Retention;
@@ -124,7 +125,6 @@
124125
import org.springframework.web.bind.annotation.RequestPart;
125126
import org.springframework.web.multipart.MultipartFile;
126127
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
127-
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
128128

129129
/**
130130
* Handles requests related to objects.

server/src/main/java/com/adobe/testing/s3mock/ObjectOwnershipHeaderConverter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2024 Adobe.
2+
* Copyright 2017-2025 Adobe.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,11 +16,11 @@
1616

1717
package com.adobe.testing.s3mock;
1818

19+
import com.adobe.testing.s3mock.dto.ObjectOwnership;
1920
import com.adobe.testing.s3mock.util.AwsHttpHeaders;
2021
import org.springframework.core.convert.converter.Converter;
2122
import org.springframework.lang.NonNull;
2223
import org.springframework.lang.Nullable;
23-
import software.amazon.awssdk.services.s3.model.ObjectOwnership;
2424

2525
/**
2626
* Converts values of the {@link AwsHttpHeaders#X_AMZ_OBJECT_OWNERSHIP} which is sent by the Amazon

server/src/main/java/com/adobe/testing/s3mock/S3MockProperties.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2022 Adobe.
2+
* Copyright 2017-2025 Adobe.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,9 +16,9 @@
1616

1717
package com.adobe.testing.s3mock;
1818

19+
import com.adobe.testing.s3mock.dto.Region;
1920
import org.springframework.boot.context.properties.ConfigurationProperties;
2021
import org.springframework.boot.context.properties.bind.DefaultValue;
21-
import software.amazon.awssdk.regions.Region;
2222

2323
@ConfigurationProperties("com.adobe.testing.s3mock")
2424
public record S3MockProperties(

server/src/main/java/com/adobe/testing/s3mock/dto/Bucket.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2024 Adobe.
2+
* Copyright 2017-2025 Adobe.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
2525
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Bucket.html">API Reference</a>.
2626
*/
2727
public record Bucket(@JsonIgnore Path path,
28+
@JsonProperty("BucketRegion") String bucketRegion,
2829
@JsonProperty("Name") String name,
2930
@JsonProperty("CreationDate") String creationDate) {
3031

@@ -33,6 +34,7 @@ public static Bucket from(BucketMetadata bucketMetadata) {
3334
return null;
3435
}
3536
return new Bucket(bucketMetadata.path(),
37+
bucketMetadata.bucketRegion(),
3638
bucketMetadata.name(),
3739
bucketMetadata.creationDate());
3840
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2017-2025 Adobe.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.adobe.testing.s3mock.dto;
18+
19+
import com.adobe.testing.S3Verified;
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
22+
/**
23+
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_BucketInfo.html">API Reference</a>.
24+
*/
25+
@S3Verified(year = 2025)
26+
public record BucketInfo(
27+
@JsonProperty("DataRedundancy")
28+
DataRedundancy dataRedundancy,
29+
@JsonProperty("Type")
30+
BucketType type
31+
) {
32+
33+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2017-2025 Adobe.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.adobe.testing.s3mock.dto;
18+
19+
import com.adobe.testing.S3Verified;
20+
import com.fasterxml.jackson.annotation.JsonCreator;
21+
import com.fasterxml.jackson.annotation.JsonValue;
22+
23+
/**
24+
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_BucketInfo.html">API Reference</a>.
25+
*/
26+
@S3Verified(year = 2025)
27+
public enum BucketType {
28+
DIRECTORY("Directory");
29+
30+
private final String value;
31+
32+
@JsonCreator
33+
BucketType(String value) {
34+
this.value = value;
35+
}
36+
37+
public static BucketType fromValue(String value) {
38+
return switch (value) {
39+
case "Directory" -> DIRECTORY;
40+
default -> null;
41+
};
42+
}
43+
44+
@Override
45+
@JsonValue
46+
public String toString() {
47+
return this.value;
48+
}
49+
50+
}

0 commit comments

Comments
 (0)