Skip to content

Commit b82e129

Browse files
committed
Add tests for MinIO storage plugin
1 parent f549d66 commit b82e129

File tree

10 files changed

+307
-8
lines changed

10 files changed

+307
-8
lines changed

turms-plugins/turms-plugin-minio/pom.xml

+7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@
3535
<artifactId>minio</artifactId>
3636
<version>${minio.version}</version>
3737
</dependency>
38+
<!-- Testing -->
39+
<dependency>
40+
<groupId>im.turms</groupId>
41+
<artifactId>server-test-common</artifactId>
42+
<version>${project.version}</version>
43+
<scope>test</scope>
44+
</dependency>
3845
</dependencies>
3946

4047
<build>

turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/MinioStorageServiceProvider.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,13 @@
102102
*/
103103
public class MinioStorageServiceProvider extends TurmsExtension implements StorageServiceProvider {
104104

105+
public static final String RESOURCE_ID = "id";
106+
public static final String RESOURCE_URL = "url";
107+
105108
private static final Logger LOGGER = LoggerFactory.getLogger(MinioStorageServiceProvider.class);
106109

107110
private static final int INIT_BUCKETS_TIMEOUT_SECONDS = 60;
108111
private static final Map<StorageResourceType, String> RESOURCE_TYPE_TO_BUCKET_NAME;
109-
private static final String RESOURCE_ID = "id";
110-
private static final String RESOURCE_URL = "url";
111112

112113
private static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type";
113114
private static final Mono<Map<String, String>> ERROR_NOT_UPLOADER_OR_SHARED_WITH_USER_TO_DOWNLOAD_MESSAGE_ATTACHMENT =

turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/properties/MinioStorageProperties.java

+6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
package im.turms.plugin.minio.properties;
1919

20+
import lombok.AllArgsConstructor;
21+
import lombok.Builder;
2022
import lombok.Data;
23+
import lombok.NoArgsConstructor;
2124
import org.springframework.boot.context.properties.ConfigurationProperties;
2225
import org.springframework.boot.context.properties.NestedConfigurationProperty;
2326

@@ -26,8 +29,11 @@
2629
/**
2730
* @author James Chen
2831
*/
32+
@AllArgsConstructor
33+
@Builder(toBuilder = true)
2934
@ConfigurationProperties("turms-plugin.minio")
3035
@Data
36+
@NoArgsConstructor
3137
public class MinioStorageProperties {
3238

3339
private boolean enabled = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
* Copyright (C) 2019 The Turms Project
3+
* https://github.com/turms-im/turms
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package im.turms.plugin.minio;
19+
20+
import java.io.ByteArrayInputStream;
21+
import java.time.Duration;
22+
import java.util.Arrays;
23+
import java.util.Collections;
24+
import java.util.Map;
25+
26+
import io.netty.handler.codec.http.HttpHeaderNames;
27+
import io.netty.handler.codec.http.HttpResponseStatus;
28+
import org.junit.jupiter.api.BeforeAll;
29+
import org.junit.jupiter.api.MethodOrderer;
30+
import org.junit.jupiter.api.Order;
31+
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.api.TestMethodOrder;
33+
import org.springframework.context.ApplicationContext;
34+
import reactor.core.publisher.Mono;
35+
import reactor.netty.http.client.HttpClient;
36+
import reactor.test.StepVerifier;
37+
38+
import im.turms.plugin.minio.properties.MinioStorageProperties;
39+
import im.turms.server.common.access.admin.web.MediaType;
40+
import im.turms.server.common.access.admin.web.MediaTypeConst;
41+
import im.turms.server.common.infra.cluster.node.Node;
42+
import im.turms.server.common.infra.property.TurmsProperties;
43+
import im.turms.server.common.infra.property.TurmsPropertiesManager;
44+
import im.turms.server.common.infra.property.env.service.ServiceProperties;
45+
import im.turms.server.common.infra.property.env.service.business.storage.StorageProperties;
46+
import im.turms.server.common.infra.property.env.service.env.database.TurmsMongoProperties;
47+
import im.turms.server.common.testing.BaseIntegrationTest;
48+
import im.turms.server.common.testing.environment.ServiceTestEnvironmentType;
49+
import im.turms.server.common.testing.properties.MinioTestEnvironmentProperties;
50+
import im.turms.server.common.testing.properties.TestProperties;
51+
import im.turms.service.domain.group.service.GroupMemberService;
52+
import im.turms.service.domain.user.service.UserRelationshipService;
53+
54+
import static org.assertj.core.api.Assertions.assertThat;
55+
import static org.mockito.Mockito.doReturn;
56+
import static org.mockito.Mockito.mock;
57+
import static org.mockito.Mockito.spy;
58+
import static org.mockito.Mockito.when;
59+
60+
/**
61+
* @author James Chen
62+
*/
63+
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
64+
class MinioStorageServiceProviderIT extends BaseIntegrationTest {
65+
66+
private static final long USER_ID = 1L;
67+
private static final byte[] FILE_FOR_UPLOADING = new byte[]{0, 1, 2, 3};
68+
private static final String FILE_MEDIA_TYPE = MediaTypeConst.IMAGE_PNG;
69+
70+
private static final Duration TIMEOUT_DURATION = Duration.ofMinutes(1);
71+
72+
private static MinioStorageServiceProvider serviceProvider;
73+
74+
@BeforeAll
75+
static void setup() {
76+
setupTestEnvironment(new TestProperties().toBuilder()
77+
.minio(new MinioTestEnvironmentProperties().toBuilder()
78+
.type(ServiceTestEnvironmentType.CONTAINER)
79+
.build())
80+
.build());
81+
}
82+
83+
@Order(0)
84+
@Test
85+
void test_start() {
86+
TurmsPropertiesManager turmsPropertiesManager = mock(TurmsPropertiesManager.class);
87+
when(turmsPropertiesManager.getLocalProperties())
88+
.thenReturn(new TurmsProperties().toBuilder()
89+
.service(new ServiceProperties().toBuilder()
90+
.storage(new StorageProperties().toBuilder()
91+
.build())
92+
.build())
93+
.build());
94+
95+
ApplicationContext applicationContext = mock(ApplicationContext.class);
96+
when(applicationContext.getBean(Node.class)).thenReturn(mock(Node.class));
97+
when(applicationContext.getBean(GroupMemberService.class))
98+
.thenReturn(mock(GroupMemberService.class));
99+
when(applicationContext.getBean(UserRelationshipService.class))
100+
.thenReturn(mock(UserRelationshipService.class));
101+
when(applicationContext.getBean(TurmsPropertiesManager.class))
102+
.thenReturn(turmsPropertiesManager);
103+
104+
MinioStorageServiceProvider provider = spy(new MinioStorageServiceProvider());
105+
doReturn(applicationContext).when(provider)
106+
.getContext();
107+
doReturn(new MinioStorageProperties().toBuilder()
108+
.endpoint(testEnvironmentManager.getMinioUri())
109+
.mongo(new TurmsMongoProperties(testEnvironmentManager.getMongoUri()))
110+
.build()).when(provider)
111+
.loadProperties(MinioStorageProperties.class);
112+
113+
StepVerifier.create(provider.start()
114+
.timeout(TIMEOUT_DURATION))
115+
.verifyComplete();
116+
117+
serviceProvider = provider;
118+
}
119+
120+
@Order(100)
121+
@Test
122+
void test_queryUserProfilePictureUploadInfo() {
123+
Mono<Map<String, String>> queryUploadInfo = serviceProvider
124+
.queryUserProfilePictureUploadInfo(USER_ID,
125+
null,
126+
MediaType.create(MediaTypeConst.IMAGE_PNG),
127+
Collections.emptyMap())
128+
.timeout(TIMEOUT_DURATION);
129+
StepVerifier.create(queryUploadInfo)
130+
.expectNextMatches(uploadInfo -> {
131+
String resourceId = uploadInfo.remove(MinioStorageServiceProvider.RESOURCE_ID);
132+
String resourceUploadUrl =
133+
uploadInfo.remove(MinioStorageServiceProvider.RESOURCE_URL);
134+
assertThat(resourceId).as("The resource ID should not be null")
135+
.isNotNull();
136+
assertThat(resourceUploadUrl).as("The resource upload URL should not be null")
137+
.isNotNull();
138+
139+
StepVerifier.create(HttpClient.create()
140+
.post()
141+
.uri(resourceUploadUrl)
142+
.sendForm((request, form) -> {
143+
request.requestHeaders()
144+
.remove(HttpHeaderNames.TRANSFER_ENCODING);
145+
form.multipart(true);
146+
for (Map.Entry<String, String> entry : uploadInfo.entrySet()) {
147+
form.attr(entry.getKey(), entry.getValue());
148+
}
149+
form.attr("Content-Type", MediaTypeConst.IMAGE_PNG)
150+
.attr("key", resourceId)
151+
.file("file",
152+
resourceId,
153+
new ByteArrayInputStream(FILE_FOR_UPLOADING),
154+
FILE_MEDIA_TYPE);
155+
})
156+
.responseSingle((response, responseBodyMono) -> {
157+
HttpResponseStatus status = response.status();
158+
if (status.equals(HttpResponseStatus.NO_CONTENT)) {
159+
return Mono.empty();
160+
}
161+
return responseBodyMono.asString()
162+
.switchIfEmpty(
163+
Mono.defer(() -> Mono.error(new RuntimeException(
164+
"The response status code should be 200, but got: "
165+
+ status.code()))))
166+
.flatMap(body -> Mono.error(new RuntimeException(
167+
"The response status code should be 200, but got: "
168+
+ status.code()
169+
+ ". Response body: \""
170+
+ body
171+
+ "\"")));
172+
})
173+
.timeout(TIMEOUT_DURATION))
174+
.verifyComplete();
175+
return true;
176+
})
177+
.verifyComplete();
178+
}
179+
180+
@Order(101)
181+
@Test
182+
void test_queryUserProfilePictureDownloadInfo() {
183+
Mono<Map<String, String>> queryDownloadInfo = serviceProvider
184+
.queryUserProfilePictureDownloadInfo(USER_ID, USER_ID, Collections.emptyMap())
185+
.timeout(TIMEOUT_DURATION);
186+
StepVerifier.create(queryDownloadInfo)
187+
.expectNextMatches(downloadInfo -> {
188+
String resourceDownloadUrl =
189+
downloadInfo.get(MinioStorageServiceProvider.RESOURCE_URL);
190+
assertThat(resourceDownloadUrl)
191+
.as("The resource download URL should not be null")
192+
.isNotNull();
193+
194+
StepVerifier.create(HttpClient.create()
195+
.get()
196+
.uri(resourceDownloadUrl)
197+
.responseSingle((response, responseBodyMono) -> {
198+
HttpResponseStatus status = response.status();
199+
if (status.equals(HttpResponseStatus.OK)) {
200+
return responseBodyMono.asByteArray()
201+
.switchIfEmpty(Mono
202+
.defer(() -> Mono.error(new RuntimeException(
203+
"The downloaded file should be the same with the upload file"))))
204+
.flatMap(bytes -> {
205+
if (Arrays.equals(bytes, FILE_FOR_UPLOADING)) {
206+
return Mono.empty();
207+
}
208+
return Mono.error(new RuntimeException(
209+
"The downloaded file should be the same with the upload file"));
210+
});
211+
}
212+
return responseBodyMono.asString()
213+
.switchIfEmpty(
214+
Mono.defer(() -> Mono.error(new RuntimeException(
215+
"The response status code should be 200, but got: "
216+
+ status.code()))))
217+
.flatMap(body -> Mono.error(new RuntimeException(
218+
"The response status code should be 200, but got: "
219+
+ status.code()
220+
+ ". Response body: \""
221+
+ body
222+
+ "\"")));
223+
})
224+
.timeout(TIMEOUT_DURATION))
225+
.verifyComplete();
226+
return true;
227+
})
228+
.verifyComplete();
229+
}
230+
231+
@Order(102)
232+
@Test
233+
void test_deleteUserProfilePicture() {
234+
Mono<Void> deleteUserProfilePicture =
235+
serviceProvider.deleteUserProfilePicture(USER_ID, Collections.emptyMap())
236+
.timeout(TIMEOUT_DURATION);
237+
StepVerifier.create(deleteUserProfilePicture)
238+
.verifyComplete();
239+
240+
Mono<Map<String, String>> queryDownloadInfo = serviceProvider
241+
.queryUserProfilePictureDownloadInfo(USER_ID, USER_ID, Collections.emptyMap())
242+
.timeout(TIMEOUT_DURATION);
243+
StepVerifier.create(queryDownloadInfo)
244+
.expectNextMatches(downloadInfo -> {
245+
String resourceDownloadUrl =
246+
downloadInfo.get(MinioStorageServiceProvider.RESOURCE_URL);
247+
assertThat(resourceDownloadUrl)
248+
.as("The resource download URL should not be null")
249+
.isNotNull();
250+
251+
StepVerifier.create(HttpClient.create()
252+
.get()
253+
.uri(resourceDownloadUrl)
254+
.response()
255+
.timeout(TIMEOUT_DURATION))
256+
.expectNextMatches(response -> {
257+
assertThat(response.status())
258+
.isEqualTo(HttpResponseStatus.NOT_FOUND);
259+
return true;
260+
})
261+
.as("The deleted file should not be found")
262+
.verifyComplete();
263+
return true;
264+
})
265+
.verifyComplete();
266+
}
267+
268+
}

turms-server-common/src/main/java/im/turms/server/common/infra/plugin/TurmsExtension.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import im.turms.server.common.infra.logging.core.logger.LoggerFactory;
3434
import im.turms.server.common.infra.property.TurmsPropertiesManager;
3535
import im.turms.server.common.infra.reactor.TaskScheduler;
36+
import im.turms.server.common.infra.test.VisibleForTesting;
3637

3738
/**
3839
* @author James Chen
@@ -70,7 +71,11 @@ void setRunning(boolean running) {
7071
this.running = running;
7172
}
7273

73-
protected <T> T loadProperties(Class<T> propertiesClass) {
74+
/**
75+
* The method should be protected, but it is public for testing purposes.
76+
*/
77+
@VisibleForTesting
78+
public <T> T loadProperties(Class<T> propertiesClass) {
7479
return context.getBean(TurmsPropertiesManager.class)
7580
.loadProperties(propertiesClass);
7681
}

turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/TestEnvironmentContainer.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ public static TestEnvironmentContainer create(
9898
return new TestEnvironmentContainer(
9999
dockerComposeFile,
100100
config,
101-
setupMongo
101+
setupMinio
102+
|| setupMongo
102103
|| setupRedis
103104
|| setupTurmsAdmin
104105
|| setupTurmsGateway

turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/TestEnvironmentManager.java

+5
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ public static TestEnvironmentManager fromPropertiesFile(String testPropertiesRes
167167
public void start() {
168168
testEnvironmentContainer.start();
169169
// TODO: Support checking the running state of local services
170+
if (getMinioTestEnvironmentType().equals(ServiceTestEnvironmentType.CONTAINER)
171+
&& !isMongoRunning()) {
172+
throw new IllegalStateException("The MinIO container is not running");
173+
}
170174
if (getMongoTestEnvironmentType().equals(ServiceTestEnvironmentType.CONTAINER)
171175
&& !isMongoRunning()) {
172176
throw new IllegalStateException("The MongoDB container is not running");
@@ -175,6 +179,7 @@ public void start() {
175179
&& !isRedisRunning()) {
176180
throw new IllegalStateException("The Redis container is not running");
177181
}
182+
log.info("MinIO server URI: \"{}\"", getMinioUri());
178183
log.info("MongoDB server URI: \"{}\"", getMongoUri());
179184
log.info("Redis server URI: \"{}\"", getRedisUri());
180185
}

turms-server-test-common/src/main/java/im/turms/server/common/testing/environment/minio/MinioTestEnvironmentAware.java

+7
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,11 @@ default boolean isMinioRunning() {
4141

4242
String getMinioPassword();
4343

44+
default String getMinioUri() {
45+
return "http://"
46+
+ getMinioHost()
47+
+ ":"
48+
+ getMinioPort();
49+
}
50+
4451
}

turms-server-test-common/src/main/java/im/turms/server/common/testing/properties/MinioLocalTestEnvironmentProperties.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class MinioLocalTestEnvironmentProperties {
3434

3535
private int port = 9000;
3636

37-
private String username = "";
37+
private String username = "minioadmin";
3838

39-
private String password = "";
39+
private String password = "minioadmin";
4040
}

0 commit comments

Comments
 (0)