Skip to content

Commit 241e450

Browse files
committed
XXX
1 parent 952ce31 commit 241e450

File tree

8 files changed

+348
-0
lines changed

8 files changed

+348
-0
lines changed

gitlab4j-api/README.adoc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
== Integration tests
2+
3+
=== Mock server
4+
5+
By default the integration tests are running resquests against a mock server started on port 9999 (using the https://github.com/xdev-software/mockserver-neolight[fork] of the https://www.mock-server.com/[mock-server] project)
6+
7+
You can force usage of the mock server with by setting following environement variable when running the tests:
8+
9+
```
10+
export MOCKSERVER_FORWARD_TO_REAL_SERVER=false
11+
```
12+
13+
Usually tests are exucuted using gradle, check the corresponding section in the `tasks.named('test')` section of the `build.gradle` file.
14+
15+
The mock-server expectation are stored in following folder: `gitlab4j-api/src/test/resources/mock-server/`.
16+
17+
Writing those expection files can be quite repetive work and hard to maintain.
18+
This is why it is recommended to capture REST requests and responses from a real GitLab server (see next section)
19+
20+
=== Real local GitLab server
21+
22+
```
23+
export MOCKSERVER_FORWARD_TO_REAL_SERVER=true
24+
export
25+
```
26+
27+
28+
The real GitLab server is execpted to run on a local port 8888.
29+
30+
If the server is running on a distant server (often using `https`) a reverse proxy tool like https://www.mitmproxy.org/[`mitmproxy`] can be used to forward the requests:
31+
32+
```
33+
mitmproxy -p 8888 --mode reverse:https://gitlab.com/
34+
```
35+
36+
Or with a Web-UI:
37+
38+
```
39+
mitmweb -p 8888 --mode reverse:https://gitlab.com/
40+
```
41+
42+
=== Configuration setup
43+
44+
Forwarding requests to a real local GitLab server is controled by
45+
46+
47+
In addition because credentials and specific ids.

gitlab4j-api/build.gradle

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,27 @@ dependencies {
1111
testImplementation 'org.mockito:mockito-junit-jupiter:5.2.0'
1212
testImplementation 'org.hamcrest:hamcrest-all:1.3'
1313
testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.0.2'
14+
testImplementation 'software.xdev.mockserver:server:1.1.1'
15+
testImplementation 'software.xdev.mockserver:client:1.1.1'
16+
testImplementation 'org.assertj:assertj-core:3.24.2'
1417
testImplementation "org.junit.jupiter:junit-jupiter-api:5.13.4"
1518
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.13.4"
1619
testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.13.4"
1720
}
1821

22+
tasks.named('compileTestJava') {
23+
javaCompiler.set(javaToolchains.compilerFor {
24+
languageVersion.set(JavaLanguageVersion.of(17))
25+
})
26+
}
27+
tasks.named('test') {
28+
// environment "MOCKSERVER_FORWARD_TO_REAL_SERVER", "true"
29+
// environment "MOCKDATA_FILE", "src/test/resources/mock-config-gitlabcom.properties"
30+
javaLauncher.set(javaToolchains.launcherFor {
31+
languageVersion.set(JavaLanguageVersion.of(17))
32+
})
33+
}
34+
1935
publishing {
2036
publications {
2137
mavenJava(MavenPublication) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.gitlab4j.api;
2+
3+
import java.io.IOException;
4+
5+
import org.assertj.core.api.Assertions;
6+
import org.gitlab4j.api.mockserver.GitlabMockBridge;
7+
import org.gitlab4j.api.mockserver.GitlabMockData;
8+
import org.gitlab4j.api.mockserver.MockserverUtil;
9+
import org.gitlab4j.api.models.User;
10+
import org.junit.jupiter.api.AfterEach;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Test;
13+
import org.junit.jupiter.api.TestInfo;
14+
15+
import software.xdev.mockserver.client.MockServerClient;
16+
17+
class UserTest {
18+
19+
private MockServerClient mockServer;
20+
private GitLabApi api;
21+
private GitlabMockData data;
22+
23+
@BeforeEach
24+
void init(TestInfo info) throws IOException {
25+
data = MockserverUtil.loadMockData();
26+
mockServer = MockserverUtil.initBeforeEach(info, data.getHostUrl());
27+
api = GitlabMockBridge.createClient(mockServer, data.getPersonalAccessToken());
28+
}
29+
30+
@AfterEach
31+
void recordExpectations(TestInfo info) throws IOException {
32+
MockserverUtil.recordExpectationsAfterEach(info, mockServer);
33+
}
34+
35+
@Test
36+
void testGetUser() throws Exception {
37+
User result = api.getUserApi().getUser(data.getUserId());
38+
Assertions.assertThat(result).isNotNull();
39+
}
40+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.gitlab4j.api.mockserver;
2+
3+
import org.gitlab4j.api.GitLabApi;
4+
5+
import software.xdev.mockserver.client.MockServerClient;
6+
7+
public class GitlabMockBridge {
8+
9+
public static GitLabApi createClient(MockServerClient mockServer, String personalAccessToken) {
10+
return new GitLabApi("http://localhost:" + mockServer.getPort(), personalAccessToken);
11+
}
12+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.gitlab4j.api.mockserver;
2+
3+
import java.io.FileInputStream;
4+
import java.io.IOException;
5+
import java.nio.file.Path;
6+
import java.util.Properties;
7+
8+
public class GitlabMockData {
9+
10+
private Properties data;
11+
12+
public GitlabMockData(Path file) {
13+
data = loadProperties(file);
14+
}
15+
16+
public String getHostUrl() {
17+
return get("HOST");
18+
}
19+
20+
public String getPersonalAccessToken() {
21+
return get("PAT");
22+
}
23+
24+
public Long getUserId() {
25+
return getLong("user.id");
26+
}
27+
28+
private Long getLong(String key) {
29+
String value = get(key);
30+
return Long.valueOf(value);
31+
}
32+
33+
private String get(String key) {
34+
if (!data.containsKey(key)) {
35+
throw new IllegalStateException("MockData properties file must contains key '" + key + "'");
36+
}
37+
String value = data.getProperty(key);
38+
if (value == null) {
39+
throw new IllegalStateException("MockData properties value for key '" + key + "' can't be null");
40+
}
41+
return value;
42+
}
43+
44+
private static Properties loadProperties(Path file) {
45+
try (FileInputStream inStream = new FileInputStream(file.toFile())) {
46+
Properties properties = new Properties();
47+
properties.load(inStream);
48+
return properties;
49+
} catch (IOException e) {
50+
throw new IllegalStateException("Can not load properties file " + file, e);
51+
}
52+
}
53+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package org.gitlab4j.api.mockserver;
2+
3+
import java.io.IOException;
4+
import java.lang.reflect.Method;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.nio.file.Paths;
8+
import java.util.List;
9+
10+
import org.junit.jupiter.api.TestInfo;
11+
12+
import com.fasterxml.jackson.databind.ObjectMapper;
13+
14+
import software.xdev.mockserver.client.MockServerClient;
15+
import software.xdev.mockserver.mock.Expectation;
16+
import software.xdev.mockserver.model.Header;
17+
import software.xdev.mockserver.model.Headers;
18+
import software.xdev.mockserver.model.HttpForward;
19+
import software.xdev.mockserver.model.HttpForward.Scheme;
20+
import software.xdev.mockserver.model.HttpRequest;
21+
import software.xdev.mockserver.netty.MockServer;
22+
import software.xdev.mockserver.serialization.ObjectMapperFactory;
23+
import software.xdev.mockserver.serialization.model.ExpectationDTO;
24+
import software.xdev.mockserver.serialization.model.HttpRequestDTO;
25+
import software.xdev.mockserver.serialization.model.HttpResponseDTO;
26+
import software.xdev.mockserver.serialization.model.RequestDefinitionDTO;
27+
28+
public class MockserverUtil {
29+
30+
private static final String MOCKSERVER_FORWARD_TO_REAL_SERVER = "MOCKSERVER_FORWARD_TO_REAL_SERVER";
31+
private static final String MOCKDATA_FILE = "MOCKDATA_FILE";
32+
33+
private static final Path MOCK_SERVER_EXPECTATIONS_ROOT = Paths.get("src/test/resources/mock-server");
34+
35+
public static MockServerClient initBeforeEach(TestInfo info, String remoteHost) {
36+
MockServer server = new MockServer(9999);
37+
MockServerClient mockServerClient = new MockServerClient("localhost", server.getLocalPort());
38+
39+
System.out.println("Mock server running on port " + server.getLocalPort());
40+
41+
mockServerClient.reset();
42+
43+
if (mockserverForwardToRealServer()) {
44+
mockServerClient
45+
.when(HttpRequest.request())
46+
.forward(HttpForward.forward()
47+
.withScheme(Scheme.HTTPS)
48+
.withHost("localhost")
49+
.withPort(8888));
50+
} else {
51+
if (Files.isDirectory(MOCK_SERVER_EXPECTATIONS_ROOT)) {
52+
ObjectMapper objectMapper = ObjectMapperFactory.createObjectMapper();
53+
String prefix = getExpectationFilePrefix(info);
54+
try {
55+
Files.list(MOCK_SERVER_EXPECTATIONS_ROOT)
56+
.filter(p -> p.getFileName().toString().startsWith(prefix))
57+
.sorted()
58+
.map(p -> readExpectation(objectMapper, p))
59+
.forEach(e -> mockServerClient.upsert(e));
60+
} catch (IOException e) {
61+
throw new IllegalStateException("Can not read expectation files", e);
62+
}
63+
} else {
64+
System.out.println("No expectactions found, because " + MOCK_SERVER_EXPECTATIONS_ROOT.toAbsolutePath()
65+
+ " is not a directory");
66+
}
67+
}
68+
69+
return mockServerClient;
70+
}
71+
72+
public static void recordExpectationsAfterEach(TestInfo info, MockServerClient mockServer) throws IOException {
73+
if (mockserverForwardToRealServer()) {
74+
Expectation[] list = mockServer.retrieveRecordedExpectations(HttpRequest.request());
75+
76+
String prefix = getExpectationFilePrefix(info);
77+
Files.createDirectories(MOCK_SERVER_EXPECTATIONS_ROOT);
78+
for (int i = 0; i < list.length; i++) {
79+
String id = prefix + String.format("%03d", i + 1);
80+
ExpectationDTO item = new ExpectationDTO(list[i]);
81+
item.setId(id);
82+
RequestDefinitionDTO request = item.getHttpRequest();
83+
if (request instanceof HttpRequestDTO) {
84+
HttpRequestDTO httpRequest = (HttpRequestDTO) request;
85+
httpRequest.setHeaders(new Headers());
86+
httpRequest.setKeepAlive(null);
87+
httpRequest.setProtocol(null);
88+
}
89+
HttpResponseDTO httpResponse = item.getHttpResponse();
90+
List<String> contentType = httpResponse.getHeaders().getValues("Content-Type");
91+
httpResponse.setHeaders(new Headers(new Header("Content-Type", contentType)));
92+
Path file = MOCK_SERVER_EXPECTATIONS_ROOT.resolve(id + ".json");
93+
Files.writeString(file, item.toString() + "\n");
94+
}
95+
}
96+
}
97+
98+
private static boolean mockserverForwardToRealServer() {
99+
String value = System.getenv(MOCKSERVER_FORWARD_TO_REAL_SERVER);
100+
return value != null && value.toLowerCase().equals("true");
101+
}
102+
103+
private static Expectation readExpectation(ObjectMapper objectMapper, Path file) {
104+
try {
105+
String content = Files.readString(file);
106+
ExpectationDTO dto = objectMapper.readValue(content, ExpectationDTO.class);
107+
return dto.buildObject();
108+
} catch (IOException e) {
109+
throw new IllegalStateException("Could not read the expectation file " + file, e);
110+
}
111+
}
112+
113+
private static String getExpectationFilePrefix(TestInfo info) {
114+
String testMethodName = getTestMethodName(info);
115+
String testMethodClass = getTestMethodClass(info);
116+
String prefix = testMethodClass + "_" + testMethodName + "_";
117+
return prefix;
118+
}
119+
120+
private static String getTestMethodClass(TestInfo info) {
121+
String testMethodName = info.getTestClass()
122+
.map(Class::getSimpleName)
123+
.orElseThrow(() -> new IllegalStateException("Could not find the test method class"));
124+
return testMethodName;
125+
}
126+
127+
private static String getTestMethodName(TestInfo info) {
128+
String testMethodName = info.getTestMethod()
129+
.map(Method::getName)
130+
.orElseThrow(() -> new IllegalStateException("Could not find the test method name"));
131+
return testMethodName;
132+
}
133+
134+
public static GitlabMockData loadMockData() {
135+
String value = System.getenv(MOCKDATA_FILE);
136+
Path file;
137+
if (value != null) {
138+
file = Paths.get(value);
139+
if (!Files.isReadable(file)) {
140+
throw new IllegalStateException("Can not load mock data file: " + file.toAbsolutePath() + ", check the "
141+
+ MOCKDATA_FILE + " environement value");
142+
}
143+
} else {
144+
file = Paths.get("src/test/resources/mock-config.properties");
145+
}
146+
return new GitlabMockData(file);
147+
}
148+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
HOST=gitlab.example.org
2+
PAT=glpat-**************
3+
user.id=24
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"httpRequest" : {
3+
"method" : "GET",
4+
"path" : "/api/v4/users/24",
5+
"queryStringParameters" : {
6+
"with_custom_attributes" : [ "false" ]
7+
}
8+
},
9+
"httpResponse" : {
10+
"statusCode" : 200,
11+
"reasonPhrase" : "OK",
12+
"headers" : {
13+
"Content-Type" : [ "application/json" ]
14+
},
15+
"body" : {
16+
"type" : "STRING",
17+
"string" : "{\"id\":24,\"username\":\"jeremie.bresson\",\"name\":\"Jérémie Bresson\",\"state\":\"active\",\"locked\":false,\"avatar_url\":\"https://secure.gravatar.com/avatar/000000000000000000000000000000000000000000000000000000?s=80&d=identicon\",\"web_url\":\"https://gitlab.example.org/jeremie.bresson\",\"created_at\":\"2022-04-05T11:54:33.313Z\",\"bio\":\"\",\"location\":\"\",\"public_email\":\"\",\"skype\":\"\",\"linkedin\":\"\",\"twitter\":\"\",\"discord\":\"\",\"website_url\":\"\",\"organization\":\"\",\"job_title\":\"\",\"pronouns\":\"\",\"bot\":false,\"work_information\":null,\"local_time\":null}",
18+
"contentType" : "application/json"
19+
}
20+
},
21+
"id" : "UserTest_testGetUser_001",
22+
"priority" : 0,
23+
"timeToLive" : {
24+
"unlimited" : true
25+
},
26+
"times" : {
27+
"remainingTimes" : 1
28+
}
29+
}

0 commit comments

Comments
 (0)