Skip to content

Commit 0b1a52f

Browse files
alxkmilayaperumalg
authored andcommitted
test: Add comprehensive tests for OpenAI API builder, mutation, and streaming functionality
Co-authored-by: Oleksandr Klymenko <[email protected]> Signed-off-by: Oleksandr Klymenko <[email protected]>
1 parent cae6364 commit 0b1a52f

File tree

3 files changed

+272
-0
lines changed

3 files changed

+272
-0
lines changed

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiApiBuilderTests.java

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,122 @@ void testInvalidResponseErrorHandler() {
159159
.hasMessageContaining("responseErrorHandler cannot be null");
160160
}
161161

162+
@Test
163+
void testBuilderWithAllCustomPaths() {
164+
OpenAiApi api = OpenAiApi.builder()
165+
.apiKey(TEST_API_KEY)
166+
.baseUrl(TEST_BASE_URL)
167+
.completionsPath("/custom/completions")
168+
.embeddingsPath("/custom/embeddings")
169+
.build();
170+
171+
assertThat(api).isNotNull();
172+
}
173+
174+
@Test
175+
void testBuilderImmutability() {
176+
OpenAiApi.Builder builder = OpenAiApi.builder().apiKey(TEST_API_KEY).baseUrl(TEST_BASE_URL);
177+
178+
OpenAiApi api1 = builder.build();
179+
OpenAiApi api2 = builder.build();
180+
181+
assertThat(api1).isNotNull();
182+
assertThat(api2).isNotNull();
183+
assertThat(api1).isNotSameAs(api2);
184+
}
185+
186+
@Test
187+
void testNullApiKeyValue() {
188+
assertThatThrownBy(() -> OpenAiApi.builder().apiKey((ApiKey) null).build())
189+
.isInstanceOf(IllegalArgumentException.class)
190+
.hasMessageContaining("apiKey cannot be null");
191+
}
192+
193+
@Test
194+
void testBuilderMethodChaining() {
195+
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
196+
headers.add("Test-Header", "test-value");
197+
198+
OpenAiApi api = OpenAiApi.builder()
199+
.apiKey(TEST_API_KEY)
200+
.baseUrl(TEST_BASE_URL)
201+
.completionsPath(TEST_COMPLETIONS_PATH)
202+
.embeddingsPath(TEST_EMBEDDINGS_PATH)
203+
.headers(headers)
204+
.restClientBuilder(RestClient.builder())
205+
.webClientBuilder(WebClient.builder())
206+
.responseErrorHandler(mock(ResponseErrorHandler.class))
207+
.build();
208+
209+
assertThat(api).isNotNull();
210+
}
211+
212+
@Test
213+
void testCustomHeadersPreservation() {
214+
MultiValueMap<String, String> customHeaders = new LinkedMultiValueMap<>();
215+
customHeaders.add("X-Custom-Header", "custom-value");
216+
customHeaders.add("X-Organization", "org-123");
217+
customHeaders.add("User-Agent", "Custom-Client/1.0");
218+
219+
OpenAiApi api = OpenAiApi.builder().apiKey(TEST_API_KEY).headers(customHeaders).build();
220+
221+
assertThat(api).isNotNull();
222+
}
223+
224+
@Test
225+
void testComplexMultiValueHeaders() {
226+
MultiValueMap<String, String> multiHeaders = new LinkedMultiValueMap<>();
227+
multiHeaders.add("Accept", "application/json");
228+
multiHeaders.add("Accept", "text/plain");
229+
multiHeaders.add("Cache-Control", "no-cache");
230+
multiHeaders.add("Cache-Control", "no-store");
231+
232+
OpenAiApi api = OpenAiApi.builder().apiKey(TEST_API_KEY).headers(multiHeaders).build();
233+
234+
assertThat(api).isNotNull();
235+
}
236+
237+
@Test
238+
void testPathValidationWithSlashes() {
239+
OpenAiApi api1 = OpenAiApi.builder()
240+
.apiKey(TEST_API_KEY)
241+
.completionsPath("/v1/completions")
242+
.embeddingsPath("/v1/embeddings")
243+
.build();
244+
245+
OpenAiApi api2 = OpenAiApi.builder()
246+
.apiKey(TEST_API_KEY)
247+
.completionsPath("v1/completions")
248+
.embeddingsPath("v1/embeddings")
249+
.build();
250+
251+
assertThat(api1).isNotNull();
252+
assertThat(api2).isNotNull();
253+
}
254+
255+
@Test
256+
void testInvalidPathsWithSpecialCharacters() {
257+
assertThatThrownBy(() -> OpenAiApi.builder().apiKey(TEST_API_KEY).completionsPath(" ").build())
258+
.isInstanceOf(IllegalArgumentException.class);
259+
}
260+
261+
@Test
262+
void testBuilderWithOnlyRequiredFields() {
263+
OpenAiApi api = OpenAiApi.builder().apiKey(TEST_API_KEY).build();
264+
265+
assertThat(api).isNotNull();
266+
}
267+
268+
@Test
269+
void testDifferentApiKeyTypes() {
270+
SimpleApiKey simpleKey = new SimpleApiKey("simple-key");
271+
OpenAiApi api1 = OpenAiApi.builder().apiKey(simpleKey).build();
272+
assertThat(api1).isNotNull();
273+
274+
OpenAiApi api2 = OpenAiApi.builder().apiKey(() -> "supplier-key").build();
275+
assertThat(api2).isNotNull();
276+
}
277+
162278
@Nested
163279
class MockRequests {
164280

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiChatModelMutateTests.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.util.LinkedMultiValueMap;
2525

2626
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
2728

2829
/*
2930
* Integration test for mutate/clone functionality on OpenAiApi and OpenAiChatModel.
@@ -190,4 +191,31 @@ void testMutateBuilderValidation() {
190191
assertThat(unchanged).isNotSameAs(this.baseModel);
191192
}
192193

194+
@Test
195+
void testMutateWithInvalidBaseUrl() {
196+
assertThatThrownBy(() -> this.baseApi.mutate().baseUrl("").build()).isInstanceOf(IllegalArgumentException.class)
197+
.hasMessageContaining("baseUrl");
198+
199+
assertThatThrownBy(() -> this.baseApi.mutate().baseUrl(null).build())
200+
.isInstanceOf(IllegalArgumentException.class)
201+
.hasMessageContaining("baseUrl");
202+
}
203+
204+
@Test
205+
void testMutateWithNullOpenAiApi() {
206+
assertThatThrownBy(() -> this.baseModel.mutate().openAiApi(null).build())
207+
.isInstanceOf(IllegalArgumentException.class);
208+
}
209+
210+
@Test
211+
void testMutatePreservesUnchangedFields() {
212+
String originalBaseUrl = this.baseApi.getBaseUrl();
213+
String newApiKey = "new-test-key";
214+
215+
OpenAiApi mutated = this.baseApi.mutate().apiKey(newApiKey).build();
216+
217+
assertThat(mutated.getBaseUrl()).isEqualTo(originalBaseUrl);
218+
assertThat(mutated.getApiKey().getValue()).isEqualTo(newApiKey);
219+
}
220+
193221
}

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiStreamFunctionCallingHelperTest.java

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.mockito.Mockito;
2626

2727
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
2829

2930
/**
3031
* Unit tests for {@link OpenAiStreamFunctionCallingHelper}
@@ -206,4 +207,131 @@ public void isStreamingToolFunctionCallFinishDetectsToolCallsFinishReason() {
206207
assertThat(this.helper.isStreamingToolFunctionCallFinish(chunk)).isTrue();
207208
}
208209

210+
@Test
211+
public void merge_whenBothChunksAreNull() {
212+
var result = this.helper.merge(null, null);
213+
assertThat(result).isNull();
214+
}
215+
216+
@Test
217+
public void merge_whenPreviousIsNull() {
218+
var current = new OpenAiApi.ChatCompletionChunk("id", Collections.emptyList(), System.currentTimeMillis(),
219+
"model", "default", "fingerprint", "object", null);
220+
221+
var result = this.helper.merge(null, current);
222+
assertThat(result).isEqualTo(current);
223+
}
224+
225+
@Test
226+
public void merge_whenCurrentIsNull() {
227+
var previous = new OpenAiApi.ChatCompletionChunk("id", Collections.emptyList(), System.currentTimeMillis(),
228+
"model", "default", "fingerprint", "object", null);
229+
230+
var result = this.helper.merge(previous, null);
231+
assertThat(result).isEqualTo(previous);
232+
}
233+
234+
@Test
235+
public void merge_partialFieldsFromEachChunk() {
236+
var choices = List.of(Mockito.mock(OpenAiApi.ChatCompletionChunk.ChunkChoice.class));
237+
var usage = Mockito.mock(OpenAiApi.Usage.class);
238+
239+
var previous = new OpenAiApi.ChatCompletionChunk(null, choices, 1L, "model1", null, "fp1", null, null);
240+
var current = new OpenAiApi.ChatCompletionChunk("id2", null, null, null, "tier2", null, "object2", usage);
241+
242+
var result = this.helper.merge(previous, current);
243+
244+
assertThat(result.id()).isEqualTo("id2");
245+
assertThat(result.choices()).isEqualTo(choices);
246+
assertThat(result.created()).isEqualTo(1L);
247+
assertThat(result.model()).isEqualTo("model1");
248+
assertThat(result.serviceTier()).isEqualTo("tier2");
249+
assertThat(result.systemFingerprint()).isEqualTo("fp1");
250+
assertThat(result.object()).isEqualTo("object2");
251+
assertThat(result.usage()).isEqualTo(usage);
252+
}
253+
254+
@Test
255+
public void isStreamingToolFunctionCall_withMultipleChoicesAndOnlyFirstHasToolCalls() {
256+
var toolCall = Mockito.mock(OpenAiApi.ChatCompletionMessage.ToolCall.class);
257+
var deltaWithToolCalls = new OpenAiApi.ChatCompletionMessage(null, null, null, null, List.of(toolCall), null,
258+
null, null);
259+
var deltaWithoutToolCalls = new OpenAiApi.ChatCompletionMessage(null, null);
260+
261+
var choice1 = new OpenAiApi.ChatCompletionChunk.ChunkChoice(null, null, deltaWithToolCalls, null);
262+
var choice2 = new OpenAiApi.ChatCompletionChunk.ChunkChoice(null, null, deltaWithoutToolCalls, null);
263+
264+
var chunk = new OpenAiApi.ChatCompletionChunk(null, List.of(choice1, choice2), null, null, null, null, null,
265+
null);
266+
267+
assertThat(this.helper.isStreamingToolFunctionCall(chunk)).isTrue();
268+
}
269+
270+
@Test
271+
public void isStreamingToolFunctionCall_withMultipleChoicesAndNoneHaveToolCalls() {
272+
var deltaWithoutToolCalls = new OpenAiApi.ChatCompletionMessage(null, null);
273+
274+
var choice1 = new OpenAiApi.ChatCompletionChunk.ChunkChoice(null, null, deltaWithoutToolCalls, null);
275+
var choice2 = new OpenAiApi.ChatCompletionChunk.ChunkChoice(null, null, deltaWithoutToolCalls, null);
276+
277+
var chunk = new OpenAiApi.ChatCompletionChunk(null, List.of(choice1, choice2), null, null, null, null, null,
278+
null);
279+
280+
assertThat(this.helper.isStreamingToolFunctionCall(chunk)).isFalse();
281+
}
282+
283+
@Test
284+
public void isStreamingToolFunctionCallFinish_withMultipleChoicesAndOnlyFirstIsToolCallsFinish() {
285+
var choice1 = new OpenAiApi.ChatCompletionChunk.ChunkChoice(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS,
286+
null, new OpenAiApi.ChatCompletionMessage(null, null), null);
287+
var choice2 = new OpenAiApi.ChatCompletionChunk.ChunkChoice(OpenAiApi.ChatCompletionFinishReason.STOP, null,
288+
new OpenAiApi.ChatCompletionMessage(null, null), null);
289+
290+
var chunk = new OpenAiApi.ChatCompletionChunk(null, List.of(choice1, choice2), null, null, null, null, null,
291+
null);
292+
293+
assertThat(this.helper.isStreamingToolFunctionCallFinish(chunk)).isTrue();
294+
}
295+
296+
@Test
297+
public void chunkToChatCompletion_whenChunkIsNull() {
298+
assertThatThrownBy(() -> this.helper.chunkToChatCompletion(null)).isInstanceOf(NullPointerException.class);
299+
}
300+
301+
@Test
302+
public void chunkToChatCompletion_withEmptyChoices() {
303+
var chunk = new OpenAiApi.ChatCompletionChunk("id", Collections.emptyList(), 1L, "model", "tier", "fp",
304+
"object", null);
305+
306+
var result = this.helper.chunkToChatCompletion(chunk);
307+
308+
assertThat(result.object()).isEqualTo("chat.completion");
309+
assertThat(result.choices()).isEmpty();
310+
assertThat(result.id()).isEqualTo("id");
311+
assertThat(result.created()).isEqualTo(1L);
312+
assertThat(result.model()).isEqualTo("model");
313+
}
314+
315+
@Test
316+
public void edgeCases_emptyStringFields() {
317+
var chunk = new OpenAiApi.ChatCompletionChunk("", Collections.emptyList(), 0L, "", "", "", "", null);
318+
319+
var result = this.helper.chunkToChatCompletion(chunk);
320+
321+
assertThat(result.id()).isEmpty();
322+
assertThat(result.model()).isEmpty();
323+
assertThat(result.serviceTier()).isEmpty();
324+
assertThat(result.systemFingerprint()).isEmpty();
325+
assertThat(result.created()).isEqualTo(0L);
326+
}
327+
328+
@Test
329+
public void isStreamingToolFunctionCall_withNullToolCallsList() {
330+
var delta = new OpenAiApi.ChatCompletionMessage(null, null, null, null, null, null, null, null);
331+
var choice = new OpenAiApi.ChatCompletionChunk.ChunkChoice(null, null, delta, null);
332+
var chunk = new OpenAiApi.ChatCompletionChunk(null, List.of(choice), null, null, null, null, null, null);
333+
334+
assertThat(this.helper.isStreamingToolFunctionCall(chunk)).isFalse();
335+
}
336+
209337
}

0 commit comments

Comments
 (0)