diff --git a/turms-plugins/turms-plugin-minio/pom.xml b/turms-plugins/turms-plugin-minio/pom.xml index ddeb48bb58..84a5e902d1 100644 --- a/turms-plugins/turms-plugin-minio/pom.xml +++ b/turms-plugins/turms-plugin-minio/pom.xml @@ -35,6 +35,13 @@ minio ${minio.version} + + + im.turms + server-test-common + ${project.version} + test + diff --git a/turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/MinioStorageServiceProvider.java b/turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/MinioStorageServiceProvider.java index 81351f4e8d..41c6d89480 100644 --- a/turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/MinioStorageServiceProvider.java +++ b/turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/MinioStorageServiceProvider.java @@ -102,12 +102,13 @@ */ public class MinioStorageServiceProvider extends TurmsExtension implements StorageServiceProvider { + public static final String RESOURCE_ID = "id"; + public static final String RESOURCE_URL = "url"; + private static final Logger LOGGER = LoggerFactory.getLogger(MinioStorageServiceProvider.class); private static final int INIT_BUCKETS_TIMEOUT_SECONDS = 60; private static final Map RESOURCE_TYPE_TO_BUCKET_NAME; - private static final String RESOURCE_ID = "id"; - private static final String RESOURCE_URL = "url"; private static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type"; private static final Mono> ERROR_NOT_UPLOADER_OR_SHARED_WITH_USER_TO_DOWNLOAD_MESSAGE_ATTACHMENT = diff --git a/turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/properties/MinioStorageProperties.java b/turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/properties/MinioStorageProperties.java index 9d95b1acd0..e2451007ff 100644 --- a/turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/properties/MinioStorageProperties.java +++ b/turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/properties/MinioStorageProperties.java @@ -17,7 +17,10 @@ package im.turms.plugin.minio.properties; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; @@ -26,8 +29,11 @@ /** * @author James Chen */ +@AllArgsConstructor +@Builder(toBuilder = true) @ConfigurationProperties("turms-plugin.minio") @Data +@NoArgsConstructor public class MinioStorageProperties { private boolean enabled = true; diff --git a/turms-plugins/turms-plugin-minio/src/test/java/im/turms/plugin/minio/MinioStorageServiceProviderIT.java b/turms-plugins/turms-plugin-minio/src/test/java/im/turms/plugin/minio/MinioStorageServiceProviderIT.java new file mode 100644 index 0000000000..e44e46cc8c --- /dev/null +++ b/turms-plugins/turms-plugin-minio/src/test/java/im/turms/plugin/minio/MinioStorageServiceProviderIT.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2019 The Turms Project + * https://github.com/turms-im/turms + * + * Licensed 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 im.turms.plugin.minio; + +import java.io.ByteArrayInputStream; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.context.ApplicationContext; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.test.StepVerifier; + +import im.turms.plugin.minio.properties.MinioStorageProperties; +import im.turms.server.common.access.admin.web.MediaType; +import im.turms.server.common.access.admin.web.MediaTypeConst; +import im.turms.server.common.infra.cluster.node.Node; +import im.turms.server.common.infra.property.TurmsProperties; +import im.turms.server.common.infra.property.TurmsPropertiesManager; +import im.turms.server.common.infra.property.env.service.ServiceProperties; +import im.turms.server.common.infra.property.env.service.business.storage.StorageProperties; +import im.turms.server.common.infra.property.env.service.env.database.TurmsMongoProperties; +import im.turms.server.common.testing.BaseIntegrationTest; +import im.turms.server.common.testing.environment.ServiceTestEnvironmentType; +import im.turms.server.common.testing.properties.MinioTestEnvironmentProperties; +import im.turms.server.common.testing.properties.TestProperties; +import im.turms.service.domain.group.service.GroupMemberService; +import im.turms.service.domain.user.service.UserRelationshipService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +/** + * @author James Chen + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MinioStorageServiceProviderIT extends BaseIntegrationTest { + + private static final long USER_ID = 1L; + private static final byte[] FILE_FOR_UPLOADING = new byte[]{0, 1, 2, 3}; + private static final String FILE_MEDIA_TYPE = MediaTypeConst.IMAGE_PNG; + + private static final Duration DURATION_ONE_MINUTE = Duration.ofMinutes(1); + + private static MinioStorageServiceProvider serviceProvider; + + @BeforeAll + static void setup() { + setupTestEnvironment(new TestProperties().toBuilder() + .minio(new MinioTestEnvironmentProperties().toBuilder() + .type(ServiceTestEnvironmentType.CONTAINER) + .build()) + .build()); + } + + @Order(0) + @Test + void test_start() { + TurmsPropertiesManager turmsPropertiesManager = mock(TurmsPropertiesManager.class); + when(turmsPropertiesManager.getLocalProperties()) + .thenReturn(new TurmsProperties().toBuilder() + .service(new ServiceProperties().toBuilder() + .storage(new StorageProperties().toBuilder() + .build()) + .build()) + .build()); + + ApplicationContext applicationContext = mock(ApplicationContext.class); + when(applicationContext.getBean(Node.class)).thenReturn(mock(Node.class)); + when(applicationContext.getBean(GroupMemberService.class)) + .thenReturn(mock(GroupMemberService.class)); + when(applicationContext.getBean(UserRelationshipService.class)) + .thenReturn(mock(UserRelationshipService.class)); + when(applicationContext.getBean(TurmsPropertiesManager.class)) + .thenReturn(turmsPropertiesManager); + + MinioStorageServiceProvider provider = spy(new MinioStorageServiceProvider()); + doReturn(applicationContext).when(provider) + .getContext(); + doReturn(new MinioStorageProperties().toBuilder() + .endpoint(testEnvironmentManager.getMinioUri()) + .mongo(new TurmsMongoProperties(testEnvironmentManager.getMongoUri())) + .build()).when(provider) + .loadProperties(MinioStorageProperties.class); + + StepVerifier.create(provider.start() + .timeout(DURATION_ONE_MINUTE)) + .verifyComplete(); + + serviceProvider = provider; + } + + @Order(100) + @Test + void test_queryUserProfilePictureUploadInfo() { + Mono> queryUploadInfo = serviceProvider + .queryUserProfilePictureUploadInfo(USER_ID, + null, + MediaType.create(MediaTypeConst.IMAGE_PNG), + Collections.emptyMap()) + .timeout(DURATION_ONE_MINUTE); + StepVerifier.create(queryUploadInfo) + .expectNextMatches(uploadInfo -> { + String resourceId = uploadInfo.remove(MinioStorageServiceProvider.RESOURCE_ID); + String resourceUploadUrl = + uploadInfo.remove(MinioStorageServiceProvider.RESOURCE_URL); + assertThat(resourceId).as("The resource ID should not be null") + .isNotNull(); + assertThat(resourceUploadUrl).as("The resource upload URL should not be null") + .isNotNull(); + + StepVerifier.create(HttpClient.create() + .post() + .uri(resourceUploadUrl) + .sendForm((request, form) -> { + request.requestHeaders() + .remove(HttpHeaderNames.TRANSFER_ENCODING); + form.multipart(true); + for (Map.Entry entry : uploadInfo.entrySet()) { + form.attr(entry.getKey(), entry.getValue()); + } + form.attr("Content-Type", MediaTypeConst.IMAGE_PNG) + .attr("key", resourceId) + .file("file", + resourceId, + new ByteArrayInputStream(FILE_FOR_UPLOADING), + FILE_MEDIA_TYPE); + }) + .responseSingle((response, responseBodyMono) -> { + HttpResponseStatus status = response.status(); + if (status.equals(HttpResponseStatus.NO_CONTENT)) { + return Mono.empty(); + } + return responseBodyMono.asString() + .switchIfEmpty( + Mono.defer(() -> Mono.error(new RuntimeException( + "The response status code should be 200, but got: " + + status.code())))) + .flatMap(body -> Mono.error(new RuntimeException( + "The response status code should be 200, but got: " + + status.code() + + ". Response body: \"" + + body + + "\""))); + }) + .timeout(DURATION_ONE_MINUTE)) + .verifyComplete(); + return true; + }) + .verifyComplete(); + } + + @Order(101) + @Test + void test_queryUserProfilePictureDownloadInfo() { + Mono> queryDownloadInfo = serviceProvider + .queryUserProfilePictureDownloadInfo(USER_ID, USER_ID, Collections.emptyMap()) + .timeout(DURATION_ONE_MINUTE); + StepVerifier.create(queryDownloadInfo) + .expectNextMatches(downloadInfo -> { + String resourceDownloadUrl = + downloadInfo.get(MinioStorageServiceProvider.RESOURCE_URL); + assertThat(resourceDownloadUrl) + .as("The resource download URL should not be null") + .isNotNull(); + + StepVerifier.create(HttpClient.create() + .get() + .uri(resourceDownloadUrl) + .responseSingle((response, responseBodyMono) -> { + HttpResponseStatus status = response.status(); + if (status.equals(HttpResponseStatus.OK)) { + return responseBodyMono.asByteArray() + .switchIfEmpty(Mono + .defer(() -> Mono.error(new RuntimeException( + "The downloaded file should be the same with the upload file")))) + .flatMap(bytes -> { + if (Arrays.equals(bytes, FILE_FOR_UPLOADING)) { + return Mono.empty(); + } + return Mono.error(new RuntimeException( + "The downloaded file should be the same with the upload file")); + }); + } + return responseBodyMono.asString() + .switchIfEmpty( + Mono.defer(() -> Mono.error(new RuntimeException( + "The response status code should be 200, but got: " + + status.code())))) + .flatMap(body -> Mono.error(new RuntimeException( + "The response status code should be 200, but got: " + + status.code() + + ". Response body: \"" + + body + + "\""))); + }) + .timeout(DURATION_ONE_MINUTE)) + .verifyComplete(); + return true; + }) + .verifyComplete(); + } + + @Order(102) + @Test + void test_deleteUserProfilePicture() { + Mono deleteUserProfilePicture = + serviceProvider.deleteUserProfilePicture(USER_ID, Collections.emptyMap()) + .timeout(DURATION_ONE_MINUTE); + StepVerifier.create(deleteUserProfilePicture) + .verifyComplete(); + + Mono> queryDownloadInfo = serviceProvider + .queryUserProfilePictureDownloadInfo(USER_ID, USER_ID, Collections.emptyMap()) + .timeout(DURATION_ONE_MINUTE); + StepVerifier.create(queryDownloadInfo) + .expectNextMatches(downloadInfo -> { + String resourceDownloadUrl = + downloadInfo.get(MinioStorageServiceProvider.RESOURCE_URL); + assertThat(resourceDownloadUrl) + .as("The resource download URL should not be null") + .isNotNull(); + + StepVerifier.create(HttpClient.create() + .get() + .uri(resourceDownloadUrl) + .response() + .timeout(DURATION_ONE_MINUTE)) + .expectNextMatches(response -> { + assertThat(response.status()) + .isEqualTo(HttpResponseStatus.NOT_FOUND); + return true; + }) + .as("The deleted file should not be found") + .verifyComplete(); + return true; + }) + .verifyComplete(); + } + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/plugin/TurmsExtension.java b/turms-server-common/src/main/java/im/turms/server/common/infra/plugin/TurmsExtension.java index c6c626f7e9..71b1c67de3 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/plugin/TurmsExtension.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/plugin/TurmsExtension.java @@ -33,6 +33,7 @@ import im.turms.server.common.infra.logging.core.logger.LoggerFactory; import im.turms.server.common.infra.property.TurmsPropertiesManager; import im.turms.server.common.infra.reactor.TaskScheduler; +import im.turms.server.common.infra.test.VisibleForTesting; /** * @author James Chen @@ -70,7 +71,11 @@ void setRunning(boolean running) { this.running = running; } - protected T loadProperties(Class propertiesClass) { + /** + * The method should be protected, but it is public for testing purposes. + */ + @VisibleForTesting + public T loadProperties(Class propertiesClass) { return context.getBean(TurmsPropertiesManager.class) .loadProperties(propertiesClass); } diff --git a/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/TestEnvironmentContainer.java b/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/TestEnvironmentContainer.java index e6be86034a..2767b0dda3 100644 --- a/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/TestEnvironmentContainer.java +++ b/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/TestEnvironmentContainer.java @@ -98,7 +98,8 @@ public static TestEnvironmentContainer create( return new TestEnvironmentContainer( dockerComposeFile, config, - setupMongo + setupMinio + || setupMongo || setupRedis || setupTurmsAdmin || setupTurmsGateway diff --git a/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/TestEnvironmentManager.java b/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/TestEnvironmentManager.java index d530302eee..95967c6aeb 100644 --- a/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/TestEnvironmentManager.java +++ b/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/TestEnvironmentManager.java @@ -167,6 +167,10 @@ public static TestEnvironmentManager fromPropertiesFile(String testPropertiesRes public void start() { testEnvironmentContainer.start(); // TODO: Support checking the running state of local services + if (getMinioTestEnvironmentType().equals(ServiceTestEnvironmentType.CONTAINER) + && !isMongoRunning()) { + throw new IllegalStateException("The MinIO container is not running"); + } if (getMongoTestEnvironmentType().equals(ServiceTestEnvironmentType.CONTAINER) && !isMongoRunning()) { throw new IllegalStateException("The MongoDB container is not running"); @@ -175,6 +179,7 @@ public void start() { && !isRedisRunning()) { throw new IllegalStateException("The Redis container is not running"); } + log.info("MinIO server URI: \"{}\"", getMinioUri()); log.info("MongoDB server URI: \"{}\"", getMongoUri()); log.info("Redis server URI: \"{}\"", getRedisUri()); } diff --git a/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/minio/MinioTestEnvironmentAware.java b/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/minio/MinioTestEnvironmentAware.java index dfd4bf01dd..7dbe846d01 100644 --- a/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/minio/MinioTestEnvironmentAware.java +++ b/turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/minio/MinioTestEnvironmentAware.java @@ -41,4 +41,11 @@ default boolean isMinioRunning() { String getMinioPassword(); + default String getMinioUri() { + return "http://" + + getMinioHost() + + ":" + + getMinioPort(); + } + } \ No newline at end of file diff --git a/turms-server-test-common/src/main/java/im/turms/server/common/testing/properties/MinioLocalTestEnvironmentProperties.java b/turms-server-test-common/src/main/java/im/turms/server/common/testing/properties/MinioLocalTestEnvironmentProperties.java index 99196fdfc7..cce40b73ec 100644 --- a/turms-server-test-common/src/main/java/im/turms/server/common/testing/properties/MinioLocalTestEnvironmentProperties.java +++ b/turms-server-test-common/src/main/java/im/turms/server/common/testing/properties/MinioLocalTestEnvironmentProperties.java @@ -34,7 +34,7 @@ public class MinioLocalTestEnvironmentProperties { private int port = 9000; - private String username = ""; + private String username = "minioadmin"; - private String password = ""; + private String password = "minioadmin"; } \ No newline at end of file diff --git a/turms-server-test-common/src/main/resources/docker-compose.test.yml b/turms-server-test-common/src/main/resources/docker-compose.test.yml index b31017da8c..c4712341e8 100644 --- a/turms-server-test-common/src/main/resources/docker-compose.test.yml +++ b/turms-server-test-common/src/main/resources/docker-compose.test.yml @@ -54,8 +54,7 @@ services: environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin - ports: - - "9000:9000" + command: server /data healthcheck: test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ] interval: 10s