Skip to content

Commit cf3e487

Browse files
committed
test: extract shared HTTP helpers into OpenAiServerTestSupport base
Both OpenAiCompatServerHttpTest and OpenAiCompatServerIntegrationTest duplicated the post/get/read/readAll helpers and the Response holder. Move them into a new abstract OpenAiServerTestSupport (intentionally not named *Test, so the harness never runs it on its own); both classes now extend it and call the inherited post(port, path, body, auth) / get(port, path, auth). Behaviour is unchanged: the fake-backend HTTP tests pass and the model-gated integration test still self-skips when the model is absent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014L2dLbAtwdq7C6a2gFRsQQ
1 parent f5158a3 commit cf3e487

3 files changed

Lines changed: 107 additions & 121 deletions

File tree

src/test/java/net/ladenthin/llama/server/OpenAiCompatServerHttpTest.java

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,22 @@
44

55
package net.ladenthin.llama.server;
66

7-
import static java.nio.charset.StandardCharsets.UTF_8;
87
import static org.hamcrest.MatcherAssert.assertThat;
98
import static org.hamcrest.Matchers.containsString;
109
import static org.hamcrest.Matchers.is;
1110

1211
import com.fasterxml.jackson.databind.JsonNode;
13-
import java.io.ByteArrayOutputStream;
1412
import java.io.IOException;
15-
import java.io.InputStream;
16-
import java.io.OutputStream;
17-
import java.net.HttpURLConnection;
18-
import java.net.URL;
1913
import org.junit.jupiter.api.Test;
2014

2115
/**
2216
* End-to-end HTTP tests for {@link OpenAiCompatServer} driven over a real socket with a
2317
* {@link FakeChatBackend} — no native library and no model are loaded. Exercises routing,
2418
* authentication, the non-streaming and Server-Sent-Events paths, heartbeats, and error statuses.
19+
*
20+
* <p>HTTP request plumbing is inherited from {@link OpenAiServerTestSupport}.
2521
*/
26-
public class OpenAiCompatServerHttpTest {
22+
public class OpenAiCompatServerHttpTest extends OpenAiServerTestSupport {
2723

2824
private static final String CHAT_BODY = "{\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}";
2925

@@ -129,61 +125,6 @@ public void authRequiredWhenApiKeyConfigured() throws IOException {
129125
}
130126
}
131127

132-
// ----- HTTP helpers -----
133-
134-
private static Response post(int port, String path, String body, String auth) throws IOException {
135-
HttpURLConnection conn = open(port, path, auth);
136-
conn.setRequestMethod("POST");
137-
conn.setDoOutput(true);
138-
conn.setRequestProperty("Content-Type", "application/json");
139-
try (OutputStream os = conn.getOutputStream()) {
140-
os.write(body.getBytes(UTF_8));
141-
}
142-
return read(conn);
143-
}
144-
145-
private static Response get(int port, String path, String auth) throws IOException {
146-
HttpURLConnection conn = open(port, path, auth);
147-
conn.setRequestMethod("GET");
148-
return read(conn);
149-
}
150-
151-
private static HttpURLConnection open(int port, String path, String auth) throws IOException {
152-
HttpURLConnection conn = (HttpURLConnection) new URL("http://127.0.0.1:" + port + path).openConnection();
153-
if (!auth.isEmpty()) {
154-
conn.setRequestProperty("Authorization", auth);
155-
}
156-
return conn;
157-
}
158-
159-
private static Response read(HttpURLConnection conn) throws IOException {
160-
int code = conn.getResponseCode();
161-
InputStream is = code < 400 ? conn.getInputStream() : conn.getErrorStream();
162-
String body = is == null ? "" : readAll(is);
163-
return new Response(code, body);
164-
}
165-
166-
private static String readAll(InputStream is) throws IOException {
167-
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
168-
byte[] chunk = new byte[1024];
169-
int read;
170-
while ((read = is.read(chunk)) != -1) {
171-
buffer.write(chunk, 0, read);
172-
}
173-
return new String(buffer.toByteArray(), UTF_8);
174-
}
175-
176-
/** Minimal HTTP response holder. */
177-
private static final class Response {
178-
private final int code;
179-
private final String body;
180-
181-
Response(int code, String body) {
182-
this.code = code;
183-
this.body = body;
184-
}
185-
}
186-
187128
/** Deterministic backend that returns canned OpenAI shapes. */
188129
static final class FakeChatBackend implements ChatBackend {
189130
@Override

src/test/java/net/ladenthin/llama/server/OpenAiCompatServerIntegrationTest.java

Lines changed: 7 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,15 @@
44

55
package net.ladenthin.llama.server;
66

7-
import static java.nio.charset.StandardCharsets.UTF_8;
87
import static org.hamcrest.MatcherAssert.assertThat;
98
import static org.hamcrest.Matchers.containsString;
109
import static org.hamcrest.Matchers.greaterThan;
1110
import static org.hamcrest.Matchers.is;
1211

1312
import com.fasterxml.jackson.databind.JsonNode;
1413
import com.fasterxml.jackson.databind.ObjectMapper;
15-
import java.io.ByteArrayOutputStream;
1614
import java.io.File;
1715
import java.io.IOException;
18-
import java.io.InputStream;
19-
import java.io.OutputStream;
20-
import java.net.HttpURLConnection;
21-
import java.net.URL;
2216
import net.ladenthin.llama.LlamaModel;
2317
import net.ladenthin.llama.TestConstants;
2418
import net.ladenthin.llama.parameters.ModelParameters;
@@ -37,9 +31,10 @@
3731
* <p>Assertions are deliberately structural (valid OpenAI shapes, stream terminates) rather than
3832
* content-specific — a 0.6B model's exact wording and whether it elects to call a tool are not
3933
* deterministic. The deterministic chunk/tool-call plumbing is covered by
40-
* {@link OpenAiCompatServerHttpTest} with a fake backend.
34+
* {@link OpenAiCompatServerHttpTest} with a fake backend. HTTP request plumbing is inherited from
35+
* {@link OpenAiServerTestSupport}.
4136
*/
42-
public class OpenAiCompatServerIntegrationTest {
37+
public class OpenAiCompatServerIntegrationTest extends OpenAiServerTestSupport {
4338

4439
private static final ObjectMapper MAPPER = new ObjectMapper();
4540
private static final String MODEL_ID = "qwen3-local";
@@ -81,7 +76,7 @@ public static void tearDown() {
8176
public void nonStreamingChatReturnsValidCompletion() throws IOException {
8277
String body = "{\"model\":\"" + MODEL_ID + "\",\"max_tokens\":16,"
8378
+ "\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in one word.\"}]}";
84-
Response response = post("/v1/chat/completions", body);
79+
Response response = post(port, "/v1/chat/completions", body, "");
8580
assertThat(response.code, is(200));
8681
JsonNode json = MAPPER.readTree(response.body);
8782
assertThat(json.path("object").asText(), is("chat.completion"));
@@ -93,7 +88,7 @@ public void nonStreamingChatReturnsValidCompletion() throws IOException {
9388
public void streamingChatEmitsChunksAndDone() throws IOException {
9489
String body = "{\"model\":\"" + MODEL_ID + "\",\"stream\":true,\"max_tokens\":16,"
9590
+ "\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in one word.\"}]}";
96-
Response response = post("/v1/chat/completions", body);
91+
Response response = post(port, "/v1/chat/completions", body, "");
9792
assertThat(response.code, is(200));
9893
assertThat(response.body, containsString("chat.completion.chunk"));
9994
assertThat(response.body, containsString("data: [DONE]"));
@@ -109,63 +104,16 @@ public void toolRequestRoundTripsThroughTheJinjaPath() throws IOException {
109104
+ "\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\","
110105
+ "\"description\":\"Get the weather for a city\",\"parameters\":{\"type\":\"object\","
111106
+ "\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}}]}";
112-
Response response = post("/v1/chat/completions", body);
107+
Response response = post(port, "/v1/chat/completions", body, "");
113108
assertThat(response.code, is(200));
114109
JsonNode message = MAPPER.readTree(response.body).path("choices").get(0).path("message");
115110
assertThat(message.isObject(), is(true));
116111
}
117112

118113
@Test
119114
public void modelsEndpointAdvertisesTheServedModel() throws IOException {
120-
Response response = get("/v1/models");
115+
Response response = get(port, "/v1/models", "");
121116
assertThat(response.code, is(200));
122117
assertThat(response.body, containsString(MODEL_ID));
123118
}
124-
125-
// ----- HTTP helpers -----
126-
127-
private static Response post(String path, String body) throws IOException {
128-
HttpURLConnection conn = (HttpURLConnection) new URL("http://127.0.0.1:" + port + path).openConnection();
129-
conn.setRequestMethod("POST");
130-
conn.setDoOutput(true);
131-
conn.setRequestProperty("Content-Type", "application/json");
132-
try (OutputStream os = conn.getOutputStream()) {
133-
os.write(body.getBytes(UTF_8));
134-
}
135-
return read(conn);
136-
}
137-
138-
private static Response get(String path) throws IOException {
139-
HttpURLConnection conn = (HttpURLConnection) new URL("http://127.0.0.1:" + port + path).openConnection();
140-
conn.setRequestMethod("GET");
141-
return read(conn);
142-
}
143-
144-
private static Response read(HttpURLConnection conn) throws IOException {
145-
int code = conn.getResponseCode();
146-
InputStream is = code < 400 ? conn.getInputStream() : conn.getErrorStream();
147-
String body = is == null ? "" : readAll(is);
148-
return new Response(code, body);
149-
}
150-
151-
private static String readAll(InputStream is) throws IOException {
152-
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
153-
byte[] chunk = new byte[1024];
154-
int read;
155-
while ((read = is.read(chunk)) != -1) {
156-
buffer.write(chunk, 0, read);
157-
}
158-
return new String(buffer.toByteArray(), UTF_8);
159-
}
160-
161-
/** Minimal HTTP response holder. */
162-
private static final class Response {
163-
private final int code;
164-
private final String body;
165-
166-
Response(int code, String body) {
167-
this.code = code;
168-
this.body = body;
169-
}
170-
}
171119
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
package net.ladenthin.llama.server;
6+
7+
import static java.nio.charset.StandardCharsets.UTF_8;
8+
9+
import java.io.ByteArrayOutputStream;
10+
import java.io.IOException;
11+
import java.io.InputStream;
12+
import java.io.OutputStream;
13+
import java.net.HttpURLConnection;
14+
import java.net.URL;
15+
16+
/**
17+
* Shared HTTP plumbing for {@link OpenAiCompatServer} tests: tiny helpers that POST/GET against a
18+
* server on {@code 127.0.0.1:<port>} and capture the status code and body.
19+
*
20+
* <p>Abstract (and not named {@code *Test}) so the harness never runs it on its own; subclasses
21+
* supply their own fixtures and assertions — {@link OpenAiCompatServerHttpTest} drives a fake backend,
22+
* and {@code OpenAiCompatServerIntegrationTest} drives a real model.
23+
*/
24+
abstract class OpenAiServerTestSupport {
25+
26+
/**
27+
* POST a JSON body to {@code path}.
28+
*
29+
* @param port the server port
30+
* @param path the request path (e.g. {@code /v1/chat/completions})
31+
* @param body the JSON request body
32+
* @param auth an {@code Authorization} header value, or {@code ""} to send none
33+
* @return the captured response
34+
* @throws IOException on transport failure
35+
*/
36+
Response post(int port, String path, String body, String auth) throws IOException {
37+
HttpURLConnection conn = open(port, path, auth);
38+
conn.setRequestMethod("POST");
39+
conn.setDoOutput(true);
40+
conn.setRequestProperty("Content-Type", "application/json");
41+
try (OutputStream os = conn.getOutputStream()) {
42+
os.write(body.getBytes(UTF_8));
43+
}
44+
return read(conn);
45+
}
46+
47+
/**
48+
* GET {@code path}.
49+
*
50+
* @param port the server port
51+
* @param path the request path
52+
* @param auth an {@code Authorization} header value, or {@code ""} to send none
53+
* @return the captured response
54+
* @throws IOException on transport failure
55+
*/
56+
Response get(int port, String path, String auth) throws IOException {
57+
HttpURLConnection conn = open(port, path, auth);
58+
conn.setRequestMethod("GET");
59+
return read(conn);
60+
}
61+
62+
private static HttpURLConnection open(int port, String path, String auth) throws IOException {
63+
HttpURLConnection conn = (HttpURLConnection) new URL("http://127.0.0.1:" + port + path).openConnection();
64+
if (!auth.isEmpty()) {
65+
conn.setRequestProperty("Authorization", auth);
66+
}
67+
return conn;
68+
}
69+
70+
private static Response read(HttpURLConnection conn) throws IOException {
71+
int code = conn.getResponseCode();
72+
InputStream is = code < 400 ? conn.getInputStream() : conn.getErrorStream();
73+
String body = is == null ? "" : readAll(is);
74+
return new Response(code, body);
75+
}
76+
77+
private static String readAll(InputStream is) throws IOException {
78+
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
79+
byte[] chunk = new byte[1024];
80+
int read;
81+
while ((read = is.read(chunk)) != -1) {
82+
buffer.write(chunk, 0, read);
83+
}
84+
return new String(buffer.toByteArray(), UTF_8);
85+
}
86+
87+
/** Captured HTTP response: status code and body text. */
88+
static final class Response {
89+
final int code;
90+
final String body;
91+
92+
Response(int code, String body) {
93+
this.code = code;
94+
this.body = body;
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)