Skip to content

Commit

Permalink
Add tests for MinIO storage plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesChenX committed Dec 18, 2023
1 parent f549d66 commit d0e1f61
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 8 deletions.
7 changes: 7 additions & 0 deletions turms-plugins/turms-plugin-minio/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>im.turms</groupId>
<artifactId>server-test-common</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<StorageResourceType, String> 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<Map<String, String>> ERROR_NOT_UPLOADER_OR_SHARED_WITH_USER_TO_DOWNLOAD_MESSAGE_ATTACHMENT =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -26,8 +29,11 @@
/**
* @author James Chen
*/
@AllArgsConstructor
@Builder(toBuilder = true)
@ConfigurationProperties("turms-plugin.minio")
@Data
@NoArgsConstructor
public class MinioStorageProperties {

private boolean enabled = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Map<String, String>> 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<String, String> 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<Map<String, String>> 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<Void> deleteUserProfilePicture =
serviceProvider.deleteUserProfilePicture(USER_ID, Collections.emptyMap())
.timeout(DURATION_ONE_MINUTE);
StepVerifier.create(deleteUserProfilePicture)
.verifyComplete();

Mono<Map<String, String>> 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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -70,7 +71,11 @@ void setRunning(boolean running) {
this.running = running;
}

protected <T> T loadProperties(Class<T> propertiesClass) {
/**
* The method should be protected, but it is public for testing purposes.
*/
@VisibleForTesting
public <T> T loadProperties(Class<T> propertiesClass) {
return context.getBean(TurmsPropertiesManager.class)
.loadProperties(propertiesClass);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ public static TestEnvironmentContainer create(
return new TestEnvironmentContainer(
dockerComposeFile,
config,
setupMongo
setupMinio
|| setupMongo
|| setupRedis
|| setupTurmsAdmin
|| setupTurmsGateway
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,11 @@ default boolean isMinioRunning() {

String getMinioPassword();

default String getMinioUri() {
return "http://"
+ getMinioHost()
+ ":"
+ getMinioPort();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Loading

0 comments on commit d0e1f61

Please sign in to comment.