diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java index c305fe99c95..a375988d660 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketLifecycleHandler.java @@ -93,7 +93,7 @@ public Response deleteBucketLifecycleConfiguration(S3RequestContext context, Str protected void deleteLifecycleConfiguration(S3RequestContext context, String bucketName) throws IOException, OS3Exception { try { - context.getVolume().getBucket(bucketName).deleteLifecycleConfiguration(); + context.getBucket(bucketName).deleteLifecycleConfiguration(); } catch (OMException ex) { // DeleteBucketLifecycle is idempotent: deleting a missing config // must still return 204, not 404 — same as normal key deletion. @@ -114,7 +114,7 @@ private void verifyBucketOwner(S3RequestContext context, String bucketName) thro } try { - String actualOwner = context.getVolume().getBucket(bucketName).getOwner(); + String actualOwner = context.getBucket(bucketName).getOwner(); if (actualOwner != null && !actualOwner.equals(expectedBucketOwner)) { LOG.debug("Bucket: {}, ExpectedBucketOwner: {}, ActualBucketOwner: {}", bucketName, expectedBucketOwner, actualOwner); @@ -130,7 +130,7 @@ public Response putBucketLifecycleConfiguration(S3RequestContext context, String throws IOException, OS3Exception { verifyBucketOwner(context, bucketName); S3LifecycleConfiguration s3LifecycleConfiguration; - OzoneBucket ozoneBucket = context.getVolume().getBucket(bucketName); + OzoneBucket ozoneBucket = context.getBucket(bucketName); OmLifecycleConfiguration lcc; try { s3LifecycleConfiguration = new PutBucketLifecycleConfigurationUnmarshaller().readFrom(body); diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java index ebcb773cef4..6fab0a75c23 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java @@ -21,8 +21,10 @@ import java.io.IOException; import org.apache.hadoop.ozone.audit.AuditLogger.PerformanceStringBuilder; import org.apache.hadoop.ozone.audit.S3GAction; +import org.apache.hadoop.ozone.client.OzoneBucket; import org.apache.hadoop.ozone.client.OzoneVolume; import org.apache.hadoop.util.Time; +import org.apache.ratis.util.Preconditions; class S3RequestContext { private final long startNanos; @@ -30,6 +32,7 @@ class S3RequestContext { private final EndpointBase endpoint; private S3GAction action; private OzoneVolume volume; + private OzoneBucket bucket; S3RequestContext(EndpointBase endpoint, S3GAction action) { this.endpoint = endpoint; @@ -54,6 +57,15 @@ OzoneVolume getVolume() throws IOException { return volume; } + OzoneBucket getBucket(String name) throws IOException { + if (bucket == null) { + bucket = getVolume().getBucket(name); + } else { + Preconditions.assertEquals(bucket.getName(), name, "Multiple buckets in one request are not supported"); + } + return bucket; + } + S3GAction getAction() { return action; } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationDelete.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationDelete.java index 5080f192b9c..0775af8fa8e 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationDelete.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationDelete.java @@ -19,21 +19,42 @@ import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_LIFECYCLE_CONFIGURATION; +import static org.apache.hadoop.ozone.s3.util.S3Consts.EXPECTED_BUCKET_OWNER_HEADER; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Collections; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; import org.apache.hadoop.ozone.client.ObjectStore; +import org.apache.hadoop.ozone.client.OzoneBucket; import org.apache.hadoop.ozone.client.OzoneClient; import org.apache.hadoop.ozone.client.OzoneClientStub; +import org.apache.hadoop.ozone.client.OzoneVolume; +import org.apache.hadoop.ozone.client.protocol.ClientProtocol; +import org.apache.hadoop.ozone.om.exceptions.OMException; +import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes; import org.apache.hadoop.ozone.s3.exception.OS3Exception; +import org.apache.hadoop.ozone.s3.exception.S3ErrorTable; import org.apache.hadoop.ozone.s3.util.S3Consts; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; /** * Testing for DeleteBucketLifecycleConfiguration. @@ -93,4 +114,108 @@ private static InputStream getBody() { return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = "owner") + public void testDeleteLifecycleLookupCount(String expectedOwner) throws Exception { + ClientProtocol proxy = mock(ClientProtocol.class); + HttpHeaders headers = mock(HttpHeaders.class); + when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner); + BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers); + + try (Response response = endpoint.delete("bucket1")) { + assertThat(response.getStatus()).isEqualTo(HTTP_NO_CONTENT); + } + + verify(proxy).getBucketDetails("volume1", "bucket1"); + verify(proxy).deleteLifecycleConfiguration("volume1", "bucket1"); + } + + @Test + public void testDeleteLifecycleWithoutHeaders() throws Exception { + ClientProtocol proxy = mock(ClientProtocol.class); + BucketEndpoint endpoint = newProtocolEndpoint(proxy, null); + try (Response response = endpoint.delete("bucket1")) { + assertThat(response.getStatus()).isEqualTo(HTTP_NO_CONTENT); + } + verify(proxy).deleteLifecycleConfiguration("volume1", "bucket1"); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = "owner") + public void testDeleteLifecycleMissingConfigurationIsIdempotent(String expectedOwner) throws Exception { + ClientProtocol proxy = mock(ClientProtocol.class); + HttpHeaders headers = mock(HttpHeaders.class); + when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner); + BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers); + doThrow(new OMException("Missing configuration", ResultCodes.LIFECYCLE_CONFIGURATION_NOT_FOUND)) + .when(proxy).deleteLifecycleConfiguration("volume1", "bucket1"); + try (Response response = endpoint.delete("bucket1")) { + assertThat(response.getStatus()).isEqualTo(HTTP_NO_CONTENT); + } + } + + @ParameterizedTest + @CsvSource({", BUCKET_NOT_FOUND, NO_SUCH_BUCKET", "owner, BUCKET_NOT_FOUND, NO_SUCH_BUCKET", + ", VOLUME_NOT_FOUND, NO_SUCH_BUCKET", "owner, VOLUME_NOT_FOUND, NO_SUCH_BUCKET", + ", PERMISSION_DENIED, ACCESS_DENIED", "owner, PERMISSION_DENIED, ACCESS_DENIED", + ", INTERNAL_ERROR, INTERNAL_ERROR", "owner, INTERNAL_ERROR, INTERNAL_ERROR"}) + public void testDeleteLifecycleProtocolErrors( + String expectedOwner, ResultCodes code, S3ErrorTable expected) throws Exception { + ClientProtocol proxy = mock(ClientProtocol.class); + HttpHeaders headers = mock(HttpHeaders.class); + when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner); + BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers); + doThrow(new OMException("Delete failed", code)).when(proxy).deleteLifecycleConfiguration("volume1", "bucket1"); + assertErrorResponse(expected, () -> endpoint.delete("bucket1")); + } + + @Test + public void testDeleteLifecycleRejectsOwnerBeforeDeleting() throws Exception { + ClientProtocol proxy = mock(ClientProtocol.class); + HttpHeaders headers = mock(HttpHeaders.class); + when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn("other-owner"); + BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers); + assertErrorResponse(S3ErrorTable.ACCESS_DENIED, () -> endpoint.delete("bucket1")); + verify(proxy, never()).deleteLifecycleConfiguration("volume1", "bucket1"); + } + + @ParameterizedTest + @CsvSource({", BUCKET_NOT_FOUND, NO_SUCH_BUCKET", "owner, BUCKET_NOT_FOUND, ACCESS_DENIED", + ", VOLUME_NOT_FOUND, NO_SUCH_BUCKET", "owner, VOLUME_NOT_FOUND, ACCESS_DENIED", + ", PERMISSION_DENIED, ACCESS_DENIED", "owner, PERMISSION_DENIED, ACCESS_DENIED", + ", INTERNAL_ERROR, INTERNAL_ERROR", "owner, INTERNAL_ERROR, ACCESS_DENIED", + "'', PERMISSION_DENIED, ACCESS_DENIED"}) + public void testDeleteLifecycleLookupFailurePreventsDeletion( + String expectedOwner, ResultCodes code, S3ErrorTable expected) throws Exception { + ClientProtocol proxy = mock(ClientProtocol.class); + HttpHeaders headers = mock(HttpHeaders.class); + when(headers.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner); + BucketEndpoint endpoint = newProtocolEndpoint(proxy, headers); + when(proxy.getBucketDetails("volume1", "bucket1")).thenThrow(new OMException("Lookup failed", code)); + assertErrorResponse(expected, () -> endpoint.delete("bucket1")); + verify(proxy, never()).deleteLifecycleConfiguration("volume1", "bucket1"); + } + + private BucketEndpoint newProtocolEndpoint(ClientProtocol proxy, HttpHeaders requestHeaders) throws Exception { + OzoneConfiguration conf = new OzoneConfiguration(); + OzoneVolume volume = OzoneVolume.newBuilder(conf, proxy) + .setName("volume1").setAcls(Collections.emptyList()).build(); + OzoneBucket bucket = OzoneBucket.newBuilder(conf, proxy) + .setVolumeName("volume1").setName("bucket1").setOwner("owner").build(); + when(proxy.getBucketDetails("volume1", "bucket1")).thenReturn(bucket); + ObjectStore store = mock(ObjectStore.class); + when(store.getS3Volume()).thenReturn(volume); + when(store.getClientProxy()).thenReturn(proxy); + OzoneClient client = mock(OzoneClient.class); + when(client.getObjectStore()).thenReturn(store); + when(client.getProxy()).thenReturn(proxy); + BucketEndpoint endpoint = EndpointBuilder.newBucketEndpointBuilder() + .setClient(client).setHeaders(requestHeaders).build(); + endpoint.queryParamsForTest().set(S3Consts.QueryParams.LIFECYCLE, ""); + return endpoint; + } + } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java index 0891b1dd8b9..f38575bf925 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3LifecycleConfigurationPut.java @@ -22,6 +22,7 @@ import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_OK; +import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.ACCESS_DENIED; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INTERNAL_ERROR; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INVALID_ARGUMENT; @@ -30,6 +31,7 @@ import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_BUCKET; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.QUOTA_EXCEEDED; import static org.apache.hadoop.ozone.s3.util.S3Consts.EXPECTED_BUCKET_OWNER_HEADER; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -37,14 +39,18 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.function.Supplier; import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; import org.apache.hadoop.ozone.client.BucketArgs; import org.apache.hadoop.ozone.client.ObjectStore; import org.apache.hadoop.ozone.client.OzoneBucket; @@ -53,14 +59,19 @@ import org.apache.hadoop.ozone.client.OzoneVolume; import org.apache.hadoop.ozone.client.protocol.ClientProtocol; import org.apache.hadoop.ozone.om.exceptions.OMException; +import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes; import org.apache.hadoop.ozone.om.helpers.BucketLayout; import org.apache.hadoop.ozone.om.helpers.OmLCExpiration; +import org.apache.hadoop.ozone.om.helpers.OmLifecycleConfiguration; import org.apache.hadoop.ozone.s3.exception.OS3Exception; +import org.apache.hadoop.ozone.s3.exception.S3ErrorTable; import org.apache.hadoop.ozone.s3.util.S3Consts; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; /** @@ -820,4 +831,83 @@ private static InputStream withAbortDaysAfterInitiationOverflow() { return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); } + + @ParameterizedTest + @CsvSource({", OBJECT_STORE", "'', OBJECT_STORE", "owner, OBJECT_STORE", + ", FILE_SYSTEM_OPTIMIZED", "owner, FILE_SYSTEM_OPTIMIZED"}) + public void testPutLifecycleReusesBucket(String expectedOwner, BucketLayout layout) throws Exception { + ClientProtocol proxy = mock(ClientProtocol.class); + HttpHeaders requestHeaders = mock(HttpHeaders.class); + when(requestHeaders.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner); + BucketEndpoint endpoint = newProtocolEndpoint(proxy, requestHeaders, layout); + + try (Response response = endpoint.put("bucket1", onePrefix())) { + assertThat(response.getStatus()).isEqualTo(HTTP_OK); + } + + verify(proxy).getBucketDetails("volume1", "bucket1"); + ArgumentCaptor config = ArgumentCaptor.forClass(OmLifecycleConfiguration.class); + verify(proxy).setLifecycleConfiguration(config.capture()); + assertThat(config.getValue().getVolume()).isEqualTo("volume1"); + assertThat(config.getValue().getBucket()).isEqualTo("bucket1"); + assertThat(config.getValue().getBucketLayout()).isEqualTo(layout); + } + + @Test + public void testPutLifecycleOwnerMismatchPrecedesXmlValidation() throws Exception { + ClientProtocol proxy = mock(ClientProtocol.class); + HttpHeaders requestHeaders = mock(HttpHeaders.class); + when(requestHeaders.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn("other-owner"); + BucketEndpoint endpoint = newProtocolEndpoint(proxy, requestHeaders, BucketLayout.OBJECT_STORE); + assertErrorResponse(ACCESS_DENIED, () -> endpoint.put("bucket1", null)); + verify(proxy, never()).setLifecycleConfiguration(any()); + } + + @ParameterizedTest + @CsvSource({", BUCKET_NOT_FOUND, NO_SUCH_BUCKET", "owner, BUCKET_NOT_FOUND, ACCESS_DENIED", + ", INTERNAL_ERROR, INTERNAL_ERROR", "owner, INTERNAL_ERROR, ACCESS_DENIED", + ", PERMISSION_DENIED, ACCESS_DENIED", "owner, PERMISSION_DENIED, ACCESS_DENIED", + "'', PERMISSION_DENIED, ACCESS_DENIED", ", VOLUME_NOT_FOUND, NO_SUCH_BUCKET", + "owner, VOLUME_NOT_FOUND, ACCESS_DENIED"}) + public void testPutLifecycleLookupFailurePrecedesXmlValidation( + String expectedOwner, ResultCodes code, S3ErrorTable expected) throws Exception { + ClientProtocol proxy = mock(ClientProtocol.class); + HttpHeaders requestHeaders = mock(HttpHeaders.class); + when(requestHeaders.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn(expectedOwner); + BucketEndpoint endpoint = newProtocolEndpoint(proxy, requestHeaders, BucketLayout.OBJECT_STORE); + when(proxy.getBucketDetails("volume1", "bucket1")).thenThrow(new OMException("Lookup failed", code)); + assertErrorResponse(expected, () -> endpoint.put("bucket1", null)); + verify(proxy, never()).setLifecycleConfiguration(any()); + } + + @Test + public void testPutLifecycleMatchingOwnerStillValidatesXml() throws Exception { + ClientProtocol proxy = mock(ClientProtocol.class); + HttpHeaders requestHeaders = mock(HttpHeaders.class); + when(requestHeaders.getHeaderString(EXPECTED_BUCKET_OWNER_HEADER)).thenReturn("owner"); + BucketEndpoint endpoint = newProtocolEndpoint(proxy, requestHeaders, BucketLayout.OBJECT_STORE); + assertErrorResponse(MALFORMED_XML, () -> endpoint.put("bucket1", null)); + verify(proxy, never()).setLifecycleConfiguration(any()); + } + + private BucketEndpoint newProtocolEndpoint(ClientProtocol proxy, HttpHeaders requestHeaders, BucketLayout layout) + throws Exception { + OzoneConfiguration conf = new OzoneConfiguration(); + OzoneVolume volume = OzoneVolume.newBuilder(conf, proxy) + .setName("volume1").setAcls(Collections.emptyList()).build(); + OzoneBucket bucket = OzoneBucket.newBuilder(conf, proxy) + .setVolumeName("volume1").setName("bucket1").setOwner("owner").setBucketLayout(layout).build(); + when(proxy.getBucketDetails("volume1", "bucket1")).thenReturn(bucket); + ObjectStore store = mock(ObjectStore.class); + when(store.getS3Volume()).thenReturn(volume); + when(store.getClientProxy()).thenReturn(proxy); + OzoneClient client = mock(OzoneClient.class); + when(client.getObjectStore()).thenReturn(store); + when(client.getProxy()).thenReturn(proxy); + BucketEndpoint endpoint = EndpointBuilder.newBucketEndpointBuilder() + .setClient(client).setHeaders(requestHeaders).build(); + endpoint.queryParamsForTest().set(S3Consts.QueryParams.LIFECYCLE, ""); + return endpoint; + } + } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3RequestContext.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3RequestContext.java new file mode 100644 index 00000000000..dc96fc0cde2 --- /dev/null +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3RequestContext.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.ozone.s3.endpoint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import org.apache.hadoop.ozone.audit.S3GAction; +import org.apache.hadoop.ozone.client.OzoneBucket; +import org.apache.hadoop.ozone.client.OzoneVolume; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Tests bucket reuse within a request. */ +class TestS3RequestContext { + private EndpointBase endpoint; + private OzoneVolume volume; + private OzoneBucket bucket; + private S3RequestContext context; + + @BeforeEach + void setup() throws IOException { + endpoint = mock(EndpointBase.class); + volume = mock(OzoneVolume.class); + bucket = mock(OzoneBucket.class); + when(bucket.getName()).thenReturn("bucket1"); + when(endpoint.getVolume()).thenReturn(volume); + when(volume.getBucket("bucket1")).thenReturn(bucket); + context = new S3RequestContext(endpoint, S3GAction.PUT_BUCKET_LIFECYCLE); + } + + @Test + void reusesBucketWithinRequest() throws IOException { + assertThat(context.getBucket("bucket1")).isSameAs(bucket); + assertThat(context.getBucket("bucket1")).isSameAs(bucket); + verify(volume).getBucket("bucket1"); + verify(endpoint).getVolume(); + } + + @Test + void rejectsDifferentBucket() throws IOException { + context.getBucket("bucket1"); + assertThrows(IllegalStateException.class, () -> context.getBucket("bucket2")); + verify(volume, never()).getBucket("bucket2"); + assertThat(context.getBucket("bucket1")).isSameAs(bucket); + verify(volume).getBucket("bucket1"); + } + + @Test + void retriesFailedLookup() throws IOException { + when(volume.getBucket("bucket1")).thenThrow(new IOException("Lookup failed")).thenReturn(bucket); + assertThrows(IOException.class, () -> context.getBucket("bucket1")); + assertThat(context.getBucket("bucket1")).isSameAs(bucket); + assertThat(context.getBucket("bucket1")).isSameAs(bucket); + verify(volume, times(2)).getBucket("bucket1"); + } + + @Test + void doesNotShareBucketBetweenRequests() throws IOException { + S3RequestContext second = new S3RequestContext(endpoint, S3GAction.PUT_BUCKET_LIFECYCLE); + assertThat(context.getBucket("bucket1")).isSameAs(bucket); + assertThat(second.getBucket("bucket1")).isSameAs(bucket); + verify(volume, times(2)).getBucket("bucket1"); + } +}