Skip to content

Commit b4a68b3

Browse files
committed
feat: introducing KubernetesMixedDispatcher
1 parent ace88b1 commit b4a68b3

File tree

4 files changed

+266
-0
lines changed

4 files changed

+266
-0
lines changed

kubernetes-server-mock/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@
6767
<artifactId>mockito-core</artifactId>
6868
<scope>test</scope>
6969
</dependency>
70+
<dependency>
71+
<groupId>org.assertj</groupId>
72+
<artifactId>assertj-core</artifactId>
73+
<scope>test</scope>
74+
</dependency>
7075

7176
</dependencies>
7277
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.kubernetes.client.server.mock;
17+
18+
import io.fabric8.mockwebserver.ServerRequest;
19+
import io.fabric8.mockwebserver.ServerResponse;
20+
import io.fabric8.mockwebserver.dsl.HttpMethod;
21+
import io.fabric8.mockwebserver.internal.MockDispatcher;
22+
import io.fabric8.mockwebserver.internal.SimpleRequest;
23+
import okhttp3.mockwebserver.Dispatcher;
24+
import okhttp3.mockwebserver.MockResponse;
25+
import okhttp3.mockwebserver.RecordedRequest;
26+
27+
import java.util.Map;
28+
import java.util.Queue;
29+
30+
/**
31+
* A composite dispatcher consistent of a {@link MockDispatcher} and a {@link KubernetesCrudDispatcher}.
32+
*
33+
* <p> Any request matching a pre-recorded response for the MockDispatcher is handled by the MockDispatcher.
34+
* The rest of requests are forwarded to the KubernetesCrudDispatcher.
35+
*
36+
* <p> This dispatcher is useful to use the KubernetesMockServer in a mixed CRUD mode.
37+
*/
38+
public class KubernetesMixedDispatcher extends Dispatcher {
39+
40+
private final Map<ServerRequest, Queue<ServerResponse>> responses;
41+
private final MockDispatcher mockDispatcher;
42+
private final KubernetesCrudDispatcher kubernetesCrudDispatcher;
43+
44+
public KubernetesMixedDispatcher(Map<ServerRequest, Queue<ServerResponse>> responses) {
45+
this.responses = responses;
46+
mockDispatcher = new MockDispatcher(responses);
47+
kubernetesCrudDispatcher = new KubernetesCrudDispatcher();
48+
}
49+
50+
@Override
51+
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
52+
final Queue<ServerResponse> responseQueue = responses.get(
53+
new SimpleRequest(HttpMethod.valueOf(request.getMethod()), request.getPath()));
54+
if (responseQueue != null && !responseQueue.isEmpty()) {
55+
return mockDispatcher.dispatch(request);
56+
}
57+
return kubernetesCrudDispatcher.dispatch(request);
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package io.fabric8.kubernetes.client.server.mock;
2+
3+
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
4+
import io.fabric8.kubernetes.client.utils.Serialization;
5+
import io.fabric8.mockwebserver.ServerRequest;
6+
import io.fabric8.mockwebserver.ServerResponse;
7+
import io.fabric8.mockwebserver.dsl.HttpMethod;
8+
import io.fabric8.mockwebserver.internal.SimpleRequest;
9+
import io.fabric8.mockwebserver.internal.SimpleResponse;
10+
import okhttp3.Headers;
11+
import okhttp3.mockwebserver.MockResponse;
12+
import okhttp3.mockwebserver.RecordedRequest;
13+
import okio.Buffer;
14+
import org.junit.jupiter.api.BeforeEach;
15+
import org.junit.jupiter.api.DisplayName;
16+
import org.junit.jupiter.api.Test;
17+
import org.mockito.Mockito;
18+
19+
import java.net.Socket;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.ArrayDeque;
22+
import java.util.Collections;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
import java.util.Queue;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
class KubernetesMixedDispatcherTest {
30+
31+
private static final Headers EMPTY_HEADERS = new Headers.Builder().build();
32+
33+
private Map<ServerRequest, Queue<ServerResponse>> responses;
34+
private KubernetesMixedDispatcher dispatcher;
35+
36+
private Socket socket;
37+
38+
@BeforeEach
39+
void setUp() {
40+
responses = new HashMap<>();
41+
dispatcher = new KubernetesMixedDispatcher(responses);
42+
socket = Mockito.mock(Socket.class, Mockito.RETURNS_DEEP_STUBS);
43+
}
44+
45+
@Test
46+
@DisplayName("dispatch, with matching expectation, returns expectation")
47+
void dispatchWithMatchingExpectation() throws Exception {
48+
// Given
49+
responses.compute(new SimpleRequest(HttpMethod.GET, "/api/v1/resources/my-resource"), (k, v) -> new ArrayDeque<>())
50+
.add(new SimpleResponse(true, 200, "resourceBody", null));
51+
// When
52+
final MockResponse result = dispatcher.dispatch(new RecordedRequest(
53+
"GET /api/v1/resources/my-resource HTTP/1.1", EMPTY_HEADERS, Collections.emptyList(),
54+
0, new Buffer(), 0, socket));
55+
// Then
56+
assertThat(result)
57+
.hasFieldOrPropertyWithValue("status", "HTTP/1.1 200 OK")
58+
.extracting(MockResponse::getBody).extracting(Buffer::readUtf8)
59+
.isEqualTo("resourceBody");
60+
}
61+
62+
@Test
63+
@DisplayName("dispatch, with existing CRUD resource, returns CRUD resource")
64+
void dispatchWithCrudExistentResource() throws Exception {
65+
// Given
66+
final Buffer requestBody = new Buffer();
67+
requestBody.writeString("{\"kind\": \"Resource\", \"apiVersion\": \"v1\",\"metadata\": {\"name\": \"my-resource\"}}", StandardCharsets.UTF_8);
68+
requestBody.flush();
69+
dispatcher.dispatch(new RecordedRequest(
70+
"POST /api/v1/resources HTTP/1.1", EMPTY_HEADERS, Collections.emptyList(),
71+
requestBody.size(), requestBody, 0, socket));
72+
// When
73+
final MockResponse result = dispatcher.dispatch(new RecordedRequest(
74+
"GET /api/v1/resources/my-resource HTTP/1.1", EMPTY_HEADERS, Collections.emptyList(),
75+
0, new Buffer(), 0, socket));
76+
// Then
77+
assertThat(result)
78+
.hasFieldOrPropertyWithValue("status", "HTTP/1.1 200 OK")
79+
.extracting(MockResponse::getBody).extracting(Buffer::readUtf8)
80+
.extracting(Serialization::unmarshal)
81+
.isInstanceOf(GenericKubernetesResource.class)
82+
.hasFieldOrPropertyWithValue("metadata.name", "my-resource");
83+
}
84+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.kubernetes.client.mock;
17+
18+
import io.fabric8.kubernetes.api.model.Pod;
19+
import io.fabric8.kubernetes.api.model.PodBuilder;
20+
import io.fabric8.kubernetes.client.KubernetesClient;
21+
import io.fabric8.kubernetes.client.KubernetesClientException;
22+
import io.fabric8.kubernetes.client.VersionInfo;
23+
import io.fabric8.kubernetes.client.server.mock.KubernetesMixedDispatcher;
24+
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
25+
import io.fabric8.kubernetes.client.utils.Serialization;
26+
import io.fabric8.mockwebserver.Context;
27+
import io.fabric8.mockwebserver.ServerRequest;
28+
import io.fabric8.mockwebserver.ServerResponse;
29+
import okhttp3.mockwebserver.MockWebServer;
30+
import org.junit.jupiter.api.AfterEach;
31+
import org.junit.jupiter.api.BeforeAll;
32+
import org.junit.jupiter.api.BeforeEach;
33+
import org.junit.jupiter.api.DisplayName;
34+
import org.junit.jupiter.api.Test;
35+
36+
import java.util.HashMap;
37+
import java.util.Map;
38+
import java.util.Queue;
39+
40+
import static org.assertj.core.api.Assertions.assertThat;
41+
import static org.junit.jupiter.api.Assertions.assertThrows;
42+
43+
class MixedCrudTest {
44+
45+
private KubernetesMockServer server;
46+
private KubernetesClient client;
47+
48+
@BeforeAll
49+
static void beforeAll() {
50+
}
51+
52+
@BeforeEach
53+
void setUp() {
54+
final Map<ServerRequest, Queue<ServerResponse>> responses = new HashMap<>();
55+
server = new KubernetesMockServer(new Context(Serialization.jsonMapper()),
56+
new MockWebServer(), responses, new KubernetesMixedDispatcher(responses), false);
57+
client = server.createClient();
58+
}
59+
60+
@AfterEach
61+
void tearDown() {
62+
client.close();
63+
server.destroy();
64+
}
65+
66+
@Test
67+
@DisplayName("client.getKubernetesVersion, with no expectations, should throw Exception")
68+
void versionWithNoExpectationsShouldFail() {
69+
// When
70+
final RuntimeException exception = assertThrows(RuntimeException.class, client::getKubernetesVersion);
71+
// Then
72+
assertThat(exception).isNotNull().isInstanceOf(KubernetesClientException.class);
73+
}
74+
75+
@Test
76+
@DisplayName("client.getKubernetesVersion, with expectation for version, should return version")
77+
void versionWithExpectationsShouldReturnVersion() {
78+
// Given
79+
server.expect().get().withPath("/version")
80+
.andReturn(200, "{\"major\": \"13\", \"minor\": \"37\"}").always();
81+
// When
82+
final VersionInfo result = client.getKubernetesVersion();
83+
// Then
84+
assertThat(result)
85+
.hasFieldOrPropertyWithValue("major", "13")
86+
.hasFieldOrPropertyWithValue("minor", "37");
87+
}
88+
89+
@Test
90+
@DisplayName("CRUD get, with no expectations, should return managed resource")
91+
void crudGetWithNoExpectations() {
92+
// Given
93+
client.pods().inNamespace("ns")
94+
.create(new PodBuilder().editOrNewMetadata().withName("my-pod").addToAnnotations("my", "pod").endMetadata().build());
95+
// When
96+
final Pod result = client.pods().inNamespace("ns").withName("my-pod").get();
97+
// Then
98+
assertThat(result)
99+
.hasFieldOrPropertyWithValue("metadata.annotations.my", "pod")
100+
.hasFieldOrPropertyWithValue("metadata.generation", 1L);
101+
}
102+
103+
@Test
104+
@DisplayName("CRUD get, with expectations, should return expectation")
105+
void crudGetWithExpectations() {
106+
// Given
107+
client.pods().inNamespace("ns")
108+
.create(new PodBuilder().editOrNewMetadata().withName("my-pod").addToAnnotations("my", "pod").endMetadata().build());
109+
server.expect().get().withPath("/api/v1/namespaces/ns/pods/my-pod")
110+
.andReturn(200, "{\"metadata\": {\"name\": \"override\"}}").always();
111+
// When
112+
final Pod result = client.pods().inNamespace("ns").withName("my-pod").get();
113+
// Then
114+
assertThat(result)
115+
.hasFieldOrPropertyWithValue("metadata.name", "override")
116+
.hasFieldOrPropertyWithValue("metadata.generation", null);
117+
}
118+
}

0 commit comments

Comments
 (0)