11<?php
22
3+ use Joomla \AI \Exception \AuthenticationException ;
4+ use Joomla \AI \Exception \ProviderException ;
5+ use Joomla \AI \Exception \RateLimitException ;
6+ use Joomla \AI \Exception \UnserializableResponseException ;
37use Joomla \Http \Http ;
48use Joomla \Http \HttpFactory ;
59use Joomla \AI \Provider \OpenAIProvider ;
@@ -10,7 +14,7 @@ class ChatTest extends TestCase
1014{
1115 public function testSimpleChatCompletion ()
1216 {
13- echo "Test 1: Test chat method with a simple prompt \n" ;
17+ echo "Test 1: Test chat method for successful completion \n" ;
1418
1519 $ fakeChatResponseBody = json_encode ([
1620 'id ' => 'chatcmpl-test ' ,
@@ -59,4 +63,187 @@ public function testSimpleChatCompletion()
5963 $ this ->assertArrayHasKey ('model ' , $ metadata );
6064 $ this ->assertArrayHasKey ('usage ' , $ metadata );
6165 }
66+
67+ public function testChatRaisesProviderException ()
68+ {
69+ echo "Test 2: Test chat method raises ProviderException on server error \n" ;
70+
71+ $ httpFactoryMock = $ this ->createMock (HttpFactory::class);
72+ $ httpClientMock = $ this ->createMock (Http::class);
73+ $ httpFactoryMock ->method ('getHttp ' )->with ([])->willReturn ($ httpClientMock );
74+
75+ $ moderationResponse = $ this ->createJsonResponse ([
76+ 'id ' => 'modr-test ' ,
77+ 'model ' => 'text-moderation-001 ' ,
78+ 'results ' => [['flagged ' => false ]],
79+ ]);
80+
81+ $ serverErrorResponse = $ this ->createJsonResponse ([
82+ 'error ' => [
83+ 'message ' => 'Internal server error ' ,
84+ 'type ' => 'server_error ' ,
85+ ],
86+ ], 500 );
87+
88+ $ httpClientMock ->method ('post ' )->willReturnOnConsecutiveCalls ($ moderationResponse , $ serverErrorResponse );
89+
90+ $ provider = new OpenAIProvider (['api_key ' => 'test-api-key ' ], $ httpFactoryMock );
91+
92+ $ this ->expectException (ProviderException::class);
93+ $ this ->expectExceptionMessage ('Internal server error ' );
94+ $ provider ->chat ('Hello! ' , ['model ' => 'gpt-4o-mini ' ]);
95+ }
96+
97+ public function testChatRaisesAuthenticationException ()
98+ {
99+ echo "Test 3: Test chat method raises AuthenticationException on unauthorized access \n" ;
100+
101+ $ httpFactoryMock = $ this ->createMock (HttpFactory::class);
102+ $ httpClientMock = $ this ->createMock (Http::class);
103+ $ httpFactoryMock ->method ('getHttp ' )->with ([])->willReturn ($ httpClientMock );
104+
105+ $ moderationResponse = $ this ->createJsonResponse ([
106+ 'id ' => 'modr-test ' ,
107+ 'model ' => 'text-moderation-001 ' ,
108+ 'results ' => [['flagged ' => false ]],
109+ ]);
110+
111+ $ unauthorizedResponse = $ this ->createJsonResponse ([
112+ 'error ' => [
113+ 'message ' => 'Incorrect API key provided ' ,
114+ 'type ' => 'invalid_request_error ' ,
115+ 'code ' => 'invalid_api_key ' ,
116+ ],
117+ ], 401 );
118+
119+ $ httpClientMock ->method ('post ' )->willReturnOnConsecutiveCalls ($ moderationResponse , $ unauthorizedResponse );
120+
121+ $ provider = new OpenAIProvider (['api_key ' => 'bad-key ' ], $ httpFactoryMock );
122+
123+ $ this ->expectException (AuthenticationException::class);
124+ $ this ->expectExceptionMessage ('Incorrect API key provided ' );
125+ $ provider ->chat ('Hello! ' , ['model ' => 'gpt-4o-mini ' ]);
126+ }
127+
128+ public function testChatRaisesRateLimitException ()
129+ {
130+ echo "Test 4: Test chat method raises RateLimitException on too many requests \n" ;
131+
132+ $ httpFactoryMock = $ this ->createMock (HttpFactory::class);
133+ $ httpClientMock = $ this ->createMock (Http::class);
134+ $ httpFactoryMock ->method ('getHttp ' )->with ([])->willReturn ($ httpClientMock );
135+
136+ $ moderationResponse = $ this ->createJsonResponse ([
137+ 'id ' => 'modr-test ' ,
138+ 'model ' => 'text-moderation-001 ' ,
139+ 'results ' => [['flagged ' => false ]],
140+ ]);
141+
142+ $ rateLimitResponse = $ this ->createJsonResponse ([
143+ 'error ' => [
144+ 'message ' => 'Rate limit exceeded ' ,
145+ 'type ' => 'rate_limit_error ' ,
146+ ],
147+ ], 429 );
148+
149+ $ httpClientMock ->method ('post ' )->willReturnOnConsecutiveCalls ($ moderationResponse , $ rateLimitResponse );
150+
151+ $ provider = new OpenAIProvider (['api_key ' => 'test-api-key ' ], $ httpFactoryMock );
152+
153+ $ this ->expectException (RateLimitException::class);
154+ $ this ->expectExceptionMessage ('Rate limit exceeded ' );
155+ $ provider ->chat ('Hello! ' , ['model ' => 'gpt-4o-mini ' ]);
156+ }
157+
158+ public function testChatRaisesUnserializableResponse ()
159+ {
160+ echo "Test 5: Test chat method raises UnserializableResponseException on invalid JSON \n" ;
161+
162+ $ httpFactoryMock = $ this ->createMock (HttpFactory::class);
163+ $ httpClientMock = $ this ->createMock (Http::class);
164+ $ httpFactoryMock ->method ('getHttp ' )->with ([])->willReturn ($ httpClientMock );
165+
166+ $ moderationResponse = $ this ->createJsonResponse ([
167+ 'id ' => 'modr-test ' ,
168+ 'model ' => 'text-moderation-001 ' ,
169+ 'results ' => [['flagged ' => false ]],
170+ ]);
171+
172+ $ invalidJson = new HttpResponse ('php://memory ' , 200 , ['Content-Type ' => 'application/json ' ]);
173+ $ invalidJson ->getBody ()->write ('{ not valid json ' );
174+
175+ $ httpClientMock ->method ('post ' )->willReturnOnConsecutiveCalls ($ moderationResponse , $ invalidJson );
176+
177+ $ provider = new OpenAIProvider (['api_key ' => 'test-api-key ' ], $ httpFactoryMock );
178+
179+ $ this ->expectException (UnserializableResponseException::class);
180+ $ this ->expectExceptionMessage ('Syntax error ' );
181+ $ provider ->chat ('Hello! ' , ['model ' => 'gpt-4o-mini ' ]);
182+ }
183+
184+ public function testChatHandlesBase64ContentInChoices ()
185+ {
186+ echo "Test 6: Test chat method handles base64 encoded content in choices metadata \n" ;
187+
188+ $ rawAudio = 'fake-audio-bytes ' ;
189+ $ encodedAudio = base64_encode ($ rawAudio );
190+
191+ $ moderationResponse = $ this ->createJsonResponse ([
192+ 'id ' => 'modr-test ' ,
193+ 'model ' => 'text-moderation-001 ' ,
194+ 'results ' => [['flagged ' => false ]],
195+ ]);
196+
197+ $ chatResponse = $ this ->createJsonResponse ([
198+ 'id ' => 'chatcmpl-audio ' ,
199+ 'object ' => 'chat.completion ' ,
200+ 'created ' => time (),
201+ 'model ' => 'gpt-4o-mini ' ,
202+ 'usage ' => [
203+ 'prompt_tokens ' => 9 ,
204+ 'completion_tokens ' => 12 ,
205+ 'total_tokens ' => 21 ,
206+ ],
207+ 'choices ' => [
208+ [
209+ 'index ' => 0 ,
210+ 'message ' => [
211+ 'role ' => 'assistant ' ,
212+ 'content ' => $ encodedAudio ,
213+ 'mime_type ' => 'audio/wav ' ,
214+ ],
215+ 'finish_reason ' => 'stop ' ,
216+ ],
217+ ],
218+ ]);
219+
220+ $ httpFactoryMock = $ this ->createMock (HttpFactory::class);
221+ $ httpClientMock = $ this ->createMock (Http::class);
222+ $ httpFactoryMock ->method ('getHttp ' )->with ([])->willReturn ($ httpClientMock );
223+ $ httpClientMock ->method ('post ' )->willReturnOnConsecutiveCalls ($ moderationResponse , $ chatResponse );
224+
225+ $ provider = new OpenAIProvider (['api_key ' => 'test-api-key ' ], $ httpFactoryMock );
226+
227+ $ response = $ provider ->chat ('Please return audio as base64. ' , ['model ' => 'gpt-4o-mini ' ]);
228+
229+ $ this ->assertSame ($ encodedAudio , $ response ->getContent ());
230+
231+ $ metadata = $ response ->getMetadata ();
232+ $ this ->assertSame ('gpt-4o-mini ' , $ metadata ['model ' ]);
233+ $ this ->assertSame ($ encodedAudio , $ metadata ['choices ' ][0 ]['message ' ]['content ' ]);
234+ $ this ->assertSame ('audio/wav ' , $ metadata ['choices ' ][0 ]['message ' ]['mime_type ' ]);
235+
236+ $ decoded = base64_decode ($ response ->getContent (), true );
237+ $ this ->assertNotFalse ($ decoded );
238+ $ this ->assertSame ($ rawAudio , $ decoded );
239+ }
240+
241+ private function createJsonResponse (array $ payload , int $ status = 200 ): HttpResponse
242+ {
243+ $ response = new HttpResponse ('php://memory ' , $ status , ['Content-Type ' => 'application/json ' ]);
244+ $ stream = $ response ->getBody ();
245+ $ stream ->write (json_encode ($ payload ));
246+
247+ return $ response ;
248+ }
62249}
0 commit comments