Skip to content

Commit eca7cfd

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

File tree

71 files changed

+1527
-422
lines changed

Some content is hidden

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

71 files changed

+1527
-422
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/AbortIncompleteMultipartUpload.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@
2020
import com.fasterxml.jackson.annotation.JsonProperty;
2121

2222
/**
23-
* Last validation: 2025-04.
2423
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortIncompleteMultipartUpload.html">API Reference</a>.
2524
*/
2625
@S3Verified(year = 2025)
2726
public record AbortIncompleteMultipartUpload(
28-
@JsonProperty("DaysAfterInitiation")
29-
Integer daysAfterInitiation
27+
@JsonProperty("DaysAfterInitiation") Integer daysAfterInitiation
3028
) {
3129

3230
}

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

Lines changed: 13 additions & 6 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,6 +16,7 @@
1616

1717
package com.adobe.testing.s3mock.dto;
1818

19+
import com.adobe.testing.S3Verified;
1920
import com.adobe.testing.s3mock.store.BucketMetadata;
2021
import com.fasterxml.jackson.annotation.JsonIgnore;
2122
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -24,16 +25,22 @@
2425
/**
2526
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_Bucket.html">API Reference</a>.
2627
*/
27-
public record Bucket(@JsonIgnore Path path,
28-
@JsonProperty("Name") String name,
29-
@JsonProperty("CreationDate") String creationDate) {
28+
@S3Verified(year = 2025)
29+
public record Bucket(
30+
@JsonProperty("BucketRegion") String bucketRegion,
31+
@JsonProperty("CreationDate") String creationDate,
32+
@JsonProperty("Name") String name,
33+
@JsonIgnore Path path
34+
) {
3035

3136
public static Bucket from(BucketMetadata bucketMetadata) {
3237
if (bucketMetadata == null) {
3338
return null;
3439
}
35-
return new Bucket(bucketMetadata.path(),
40+
return new Bucket(bucketMetadata.bucketRegion(),
41+
bucketMetadata.creationDate(),
3642
bucketMetadata.name(),
37-
bucketMetadata.creationDate());
43+
bucketMetadata.path()
44+
);
3845
}
3946
}
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+
}

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

Lines changed: 5 additions & 5 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,6 +16,7 @@
1616

1717
package com.adobe.testing.s3mock.dto;
1818

19+
import com.adobe.testing.S3Verified;
1920
import com.fasterxml.jackson.annotation.JsonProperty;
2021
import com.fasterxml.jackson.annotation.JsonRootName;
2122
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
@@ -26,14 +27,13 @@
2627
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_BucketLifecycleConfiguration.html">API Reference</a>.
2728
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_LifecycleConfiguration.html">API Reference</a>.
2829
*/
30+
@S3Verified(year = 2025)
2931
@JsonRootName("LifecycleConfiguration")
3032
public record BucketLifecycleConfiguration(
31-
@JsonProperty("Rule")
3233
@JacksonXmlElementWrapper(useWrapping = false)
33-
List<LifecycleRule> rules,
34+
@JsonProperty("Rule") List<LifecycleRule> rules,
3435
//workaround for adding xmlns attribute to root element only.
35-
@JacksonXmlProperty(isAttribute = true, localName = "xmlns")
36-
String xmlns
36+
@JacksonXmlProperty(isAttribute = true, localName = "xmlns") String xmlns
3737
) {
3838
public BucketLifecycleConfiguration {
3939
if (xmlns == null) {

0 commit comments

Comments
 (0)