Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace aiprovider_openai;
18
 
19
use aiprovider_openai\test\testcase_helper_trait;
20
use core_ai\aiactions\base;
21
use core_ai\provider;
22
use GuzzleHttp\Psr7\Response;
23
 
24
/**
25
 * Test response_base OpenAI provider methods.
26
 *
27
 * @package    aiprovider_openai
28
 * @copyright  2024 Matt Porritt <matt.porritt@moodle.com>
29
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30
 * @covers     \aiprovider_openai\provider
31
 * @covers     \aiprovider_openai\process_generate_image
32
 * @covers     \aiprovider_openai\abstract_processor
33
 */
34
final class process_generate_image_test extends \advanced_testcase {
35
 
36
    use testcase_helper_trait;
37
 
38
    /** @var string A successful response in JSON format. */
39
    protected string $responsebodyjson;
40
 
41
    /** @var \core_ai\manager */
42
    private $manager;
43
 
44
    /** @var provider The provider that will process the action. */
45
    protected provider $provider;
46
 
47
    /** @var base The action to process. */
48
    protected base $action;
49
 
50
    /**
51
     * Set up the test.
52
     */
53
    protected function setUp(): void {
54
        parent::setUp();
55
        $this->resetAfterTest();
56
        // Load a response body from a file.
57
        $this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_openai', 'image_request_success.json'));
58
        $this->manager = \core\di::get(\core_ai\manager::class);
59
        $this->provider = $this->create_provider(
60
            actionclass: \core_ai\aiactions\generate_image::class,
61
            actionconfig: [
62
                'model' => 'dall-e-3',
63
            ],
64
        );
65
        $this->create_action();
66
    }
67
 
68
    /**
69
     * Create the action object.
70
     * @param int $userid The user id to use in the action.
71
     */
72
    private function create_action(int $userid = 1): void {
73
        $this->action = new \core_ai\aiactions\generate_image(
74
            contextid: 1,
75
            userid: $userid,
76
            prompttext: 'This is a test prompt',
77
            quality: 'hd',
78
            aspectratio: 'square',
79
            numimages: 1,
80
            style: 'vivid',
81
        );
82
    }
83
 
84
    /**
85
     * Test calculate_size.
86
     */
87
    public function test_calculate_size(): void {
88
        $processor = new process_generate_image($this->provider, $this->action);
89
 
90
        // We're working with a private method here, so we need to use reflection.
91
        $method = new \ReflectionMethod($processor, 'calculate_size');
92
 
93
        $ratio = 'square';
94
        $size = $method->invoke($processor, $ratio);
95
        $this->assertEquals('1024x1024', $size);
96
 
97
        $ratio = 'portrait';
98
        $size = $method->invoke($processor, $ratio);
99
        $this->assertEquals('1024x1792', $size);
100
 
101
        $ratio = 'landscape';
102
        $size = $method->invoke($processor, $ratio);
103
        $this->assertEquals('1792x1024', $size);
104
    }
105
 
106
    /**
107
     * Test create_request_object
108
     */
109
    public function test_create_request_object(): void {
110
        $processor = new process_generate_image($this->provider, $this->action);
111
 
112
        // We're working with a private method here, so we need to use reflection.
113
        $method = new \ReflectionMethod($processor, 'create_request_object');
114
        $request = $method->invoke($processor, 1);
115
 
116
        $requestdata = (object) json_decode($request->getBody()->getContents());
117
 
118
        $this->assertEquals('This is a test prompt', $requestdata->prompt);
119
        $this->assertEquals('dall-e-3', $requestdata->model);
120
        $this->assertEquals('1', $requestdata->n);
121
        $this->assertEquals('hd', $requestdata->quality);
122
        $this->assertEquals('url', $requestdata->response_format);
123
        $this->assertEquals('1024x1024', $requestdata->size);
124
    }
125
 
126
    /**
127
     * Test create_request_object with extra model settings.
128
     */
129
    public function test_create_request_object_with_model_settings(): void {
130
        $this->provider = $this->create_provider(
131
            actionclass: \core_ai\aiactions\generate_image::class,
132
            actionconfig: [
133
                'model' => 'dall-e-3',
134
                'temperature' => '0.5',
135
                'max_completion_tokens' => '100',
136
            ],
137
        );
138
        $processor = new process_generate_image($this->provider, $this->action);
139
 
140
        // We're working with a private method here, so we need to use reflection.
141
        $method = new \ReflectionMethod($processor, 'create_request_object');
142
        $request = $method->invoke($processor, 1);
143
 
144
        $body = (object) json_decode($request->getBody()->getContents());
145
 
146
        $this->assertEquals('dall-e-3', $body->model);
147
        $this->assertEquals('0.5', $body->temperature);
148
        $this->assertEquals('100', $body->max_completion_tokens);
149
 
150
        $this->provider = $this->create_provider(
151
            actionclass: \core_ai\aiactions\generate_image::class,
152
            actionconfig: [
153
                'model' => 'my-custom-gpt',
154
                'modelextraparams' => '{"temperature": 0.5,"max_completion_tokens": 100}',
155
            ],
156
        );
157
        $processor = new process_generate_image($this->provider, $this->action);
158
 
159
        // We're working with a private method here, so we need to use reflection.
160
        $method = new \ReflectionMethod($processor, 'create_request_object');
161
        $request = $method->invoke($processor, 1);
162
 
163
        $body = (object) json_decode($request->getBody()->getContents());
164
 
165
        $this->assertEquals('my-custom-gpt', $body->model);
166
        $this->assertEquals('0.5', $body->temperature);
167
        $this->assertEquals('100', $body->max_completion_tokens);
168
    }
169
 
170
    /**
171
     * Test the API error response handler method.
172
     */
173
    public function test_handle_api_error(): void {
174
        $responses = [
175
            500 => new Response(500, ['Content-Type' => 'application/json']),
176
            503 => new Response(503, ['Content-Type' => 'application/json']),
177
            401 => new Response(401, ['Content-Type' => 'application/json'],
178
                '{"error": {"message": "Invalid Authentication"}}'),
179
            404 => new Response(404, ['Content-Type' => 'application/json'],
180
                '{"error": {"message": "You must be a member of an organization to use the API"}}'),
181
            429 => new Response(429, ['Content-Type' => 'application/json'],
182
                '{"error": {"message": "Rate limit reached for requests"}}'),
183
        ];
184
 
185
        $processor = new process_generate_image($this->provider, $this->action);
186
        $method = new \ReflectionMethod($processor, 'handle_api_error');
187
 
188
        foreach ($responses as $status => $response) {
189
            $result = $method->invoke($processor, $response);
190
            $this->assertEquals($status, $result['errorcode']);
191
            if ($status == 500) {
192
                $this->assertEquals('Internal Server Error', $result['errormessage']);
193
            } else if ($status == 503) {
194
                $this->assertEquals('Service Unavailable', $result['errormessage']);
195
            } else {
196
                $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']);
197
            }
198
        }
199
    }
200
 
201
    /**
202
     * Test the API success response handler method.
203
     */
204
    public function test_handle_api_success(): void {
205
        $response = new Response(
206
            200,
207
            ['Content-Type' => 'application/json'],
208
            $this->responsebodyjson,
209
        );
210
 
211
        // We're testing a private method, so we need to setup reflector magic.
212
        $processor = new process_generate_image($this->provider, $this->action);
213
        $method = new \ReflectionMethod($processor, 'handle_api_success');
214
 
215
        $result = $method->invoke($processor, $response);
216
 
217
        $this->stringContains('An image that represents the concept of a \'test\'.', $result['revisedprompt']);
218
        $this->stringContains('oaidalleapiprodscus.blob.core.windows.net', $result['sourceurl']);
219
        $this->assertEquals('dall-e-3', $result['model']);
220
    }
221
 
222
    /**
223
     * Test query_ai_api for a successful call.
224
     */
225
    public function test_query_ai_api_success(): void {
226
        // Mock the http client to return a successful response.
227
        ['mock' => $mock] = $this->get_mocked_http_client();
228
 
229
        // The response from OpenAI.
230
        $mock->append(new Response(
231
            200,
232
            ['Content-Type' => 'application/json'],
233
            $this->responsebodyjson,
234
        ));
235
 
236
        $mock->append(new Response(
237
            200,
238
            ['Content-Type' => 'image/jpeg'],
239
            \GuzzleHttp\Psr7\Utils::streamFor(fopen(
240
                self::get_fixture_path('aiprovider_openai', 'test.jpg'),
241
                'r',
242
            )),
243
        ));
244
 
245
        $this->setAdminUser();
246
 
247
        $processor = new process_generate_image($this->provider, $this->action);
248
        $method = new \ReflectionMethod($processor, 'query_ai_api');
249
        $result = $method->invoke($processor);
250
 
251
        $this->stringContains('An image that represents the concept of a \'test\'.', $result['revisedprompt']);
252
        $this->stringContains('oaidalleapiprodscus.blob.core.windows.net', $result['sourceurl']);
253
        $this->assertEquals('dall-e-3', $result['model']);
254
    }
255
 
256
    /**
257
     * Test prepare_response success.
258
     */
259
    public function test_prepare_response_success(): void {
260
        $processor = new process_generate_image($this->provider, $this->action);
261
 
262
        // We're working with a private method here, so we need to use reflection.
263
        $method = new \ReflectionMethod($processor, 'prepare_response');
264
 
265
        $response = [
266
            'success' => true,
267
            'revisedprompt' => 'An image that represents the concept of a \'test\'.',
268
            'imageurl' => 'oaidalleapiprodscus.blob.core.windows.net',
269
            'model' => 'dall-e-3',
270
        ];
271
 
272
        $result = $method->invoke($processor, $response);
273
 
274
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
275
        $this->assertTrue($result->get_success());
276
        $this->assertEquals('generate_image', $result->get_actionname());
277
        $this->assertEquals($response['success'], $result->get_success());
278
        $this->assertEquals($response['revisedprompt'], $result->get_response_data()['revisedprompt']);
279
        $this->assertEquals($response['model'], $result->get_response_data()['model']);
280
    }
281
 
282
    /**
283
     * Test prepare_response error.
284
     */
285
    public function test_prepare_response_error(): void {
286
        $processor = new process_generate_image($this->provider, $this->action);
287
 
288
        // We're working with a private method here, so we need to use reflection.
289
        $method = new \ReflectionMethod($processor, 'prepare_response');
290
 
291
        $response = [
292
            'success' => false,
293
            'errorcode' => 500,
294
            'errormessage' => 'Internal server error.',
295
        ];
296
 
297
        $result = $method->invoke($processor, $response);
298
 
299
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
300
        $this->assertFalse($result->get_success());
301
        $this->assertEquals('generate_image', $result->get_actionname());
302
        $this->assertEquals($response['errorcode'], $result->get_errorcode());
303
        $this->assertEquals($response['errormessage'], $result->get_errormessage());
304
    }
305
 
306
    /**
307
     * Test url_to_file.
308
     */
309
    public function test_url_to_file(): void {
310
        // Log in user.
311
        $this->setUser($this->getDataGenerator()->create_user());
312
 
313
        $processor = new process_generate_image($this->provider, $this->action);
314
        // We're working with a private method here, so we need to use reflection.
315
        $method = new \ReflectionMethod($processor, 'url_to_file');
316
 
317
        $contextid = 1;
318
        $url = $this->getExternalTestFileUrl('/test.jpg', false);
319
        $filenobj = $method->invoke($processor, $contextid, $url);
320
 
321
        $this->assertEquals('test.jpg', $filenobj->get_filename());
322
    }
323
 
324
    /**
325
     * Test process.
326
     */
327
    public function test_process(): void {
328
        // Log in user.
329
        $this->setUser($this->getDataGenerator()->create_user());
330
 
331
        // Mock the http client to return a successful response.
332
        ['mock' => $mock] = $this->get_mocked_http_client();
333
 
334
        $url = 'https://example.com/test.jpg';
335
 
336
        // The response from OpenAI.
337
        $mock->append(new Response(
338
            200,
339
            ['Content-Type' => 'application/json'],
340
            json_encode([
341
                'created' => 1719140500,
342
                'data' => [
343
                    (object) [
344
                        'revised_prompt' => 'An image that represents the concept of a \'test\'.',
345
                        'url' => $url,
346
                    ],
347
                ],
348
            ]),
349
        ));
350
 
351
        // The image downloaded from the server successfully.
352
        $mock->append(new Response(
353
            200,
354
            ['Content-Type' => 'image/jpeg'],
355
            \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
356
        ));
357
 
358
        // Create a request object.
359
        $contextid = 1;
360
        $userid = 1;
361
        $prompttext = 'This is a test prompt';
362
        $aspectratio = 'square';
363
        $quality = 'hd';
364
        $numimages = 1;
365
        $style = 'vivid';
366
        $this->action = new \core_ai\aiactions\generate_image(
367
            contextid: $contextid,
368
            userid: $userid,
369
            prompttext: $prompttext,
370
            quality: $quality,
371
            aspectratio: $aspectratio,
372
            numimages: $numimages,
373
            style: $style,
374
        );
375
 
376
        $processor = new process_generate_image($this->provider, $this->action);
377
        $result = $processor->process();
378
 
379
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
380
        $this->assertTrue($result->get_success());
381
        $this->assertEquals('generate_image', $result->get_actionname());
382
        $this->assertEquals('An image that represents the concept of a \'test\'.', $result->get_response_data()['revisedprompt']);
383
        $this->assertEquals($url, $result->get_response_data()['sourceurl']);
384
    }
385
 
386
    /**
387
     * Test process method with error.
388
     */
389
    public function test_process_error(): void {
390
        // Log in user.
391
        $this->setUser($this->getDataGenerator()->create_user());
392
 
393
        // Mock the http client to return a successful response.
394
        ['mock' => $mock] = $this->get_mocked_http_client();
395
 
396
        // The response from OpenAI.
397
        $mock->append(new Response(
398
            401,
399
            ['Content-Type' => 'application/json'],
400
            json_encode(['error' => ['message' => 'Invalid Authentication']]),
401
        ));
402
 
403
        $processor = new process_generate_image($this->provider, $this->action);
404
        $result = $processor->process();
405
 
406
        $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result);
407
        $this->assertFalse($result->get_success());
408
        $this->assertEquals('generate_image', $result->get_actionname());
409
        $this->assertEquals(401, $result->get_errorcode());
410
        $this->assertEquals('Invalid Authentication', $result->get_errormessage());
411
    }
412
 
413
    /**
414
     * Test process method with user rate limiter.
415
     */
416
    public function test_process_with_user_rate_limiter(): void {
417
        // Create users.
418
        $user1 = $this->getDataGenerator()->create_user();
419
        $user2 = $this->getDataGenerator()->create_user();
420
        // Log in user1.
421
        $this->setUser($user1);
422
        // Mock clock.
423
        $clock = $this->mock_clock_with_frozen();
424
 
425
        // Set the user rate limiter.
426
        $config = [
427
            'apikey' => '123',
428
            'enableuserratelimit' => true,
429
            'userratelimit' => 1,
430
        ];
431
        $provider = $this->manager->create_provider_instance(
432
            classname: '\aiprovider_openai\provider',
433
            name: 'dummy',
434
            config: $config,
435
            actionconfig: [
436
                \core_ai\aiactions\generate_image::class => [
437
                    'settings' => [
438
                        'model' => 'dall-e-3',
439
                        'endpoint' => "https://api.openai.com/v1/chat/completions",
440
                    ],
441
                ],
442
            ],
443
        );
444
 
445
        // Mock the http client to return a successful response.
446
        ['mock' => $mock] = $this->get_mocked_http_client();
447
        $url = 'https://example.com/test.jpg';
448
 
449
        // Case 1: User rate limit has not been reached.
450
        $this->create_action($user1->id);
451
        // The response from OpenAI.
452
        $mock->append(new Response(
453
            200,
454
            ['Content-Type' => 'application/json'],
455
            json_encode([
456
                'created' => 1719140500,
457
                'data' => [
458
                    (object) [
459
                        'revised_prompt' => 'An image that represents the concept of a \'test\'.',
460
                        'url' => $url,
461
                    ],
462
                ],
463
            ]),
464
        ));
465
        // The image downloaded from the server successfully.
466
        $mock->append(new Response(
467
            200,
468
            ['Content-Type' => 'image/jpeg'],
469
            \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
470
        ));
471
        $processor = new process_generate_image($provider, $this->action);
472
        $result = $processor->process();
473
        $this->assertTrue($result->get_success());
474
 
475
        // Case 2: User rate limit has been reached.
476
        $clock->bump(HOURSECS - 10);
477
        // The response from OpenAI.
478
        $mock->append(new Response(
479
            200,
480
            ['Content-Type' => 'application/json'],
481
            json_encode([
482
                'created' => 1719140500,
483
                'data' => [
484
                    (object) [
485
                        'revised_prompt' => 'An image that represents the concept of a \'test\'.',
486
                        'url' => $url,
487
                    ],
488
                ],
489
            ]),
490
        ));
491
        // The image downloaded from the server successfully.
492
        $mock->append(new Response(
493
            200,
494
            ['Content-Type' => 'image/jpeg'],
495
            \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
496
        ));
497
        $this->create_action($user1->id);
498
        $processor = new process_generate_image($provider, $this->action);
499
        $result = $processor->process();
500
        $this->assertEquals(429, $result->get_errorcode());
501
        $this->assertEquals('User rate limit exceeded', $result->get_errormessage());
502
        $this->assertFalse($result->get_success());
503
 
504
        // Case 3: User rate limit has not been reached for a different user.
505
        // Log in user2.
506
        $this->setUser($user2);
507
        $this->create_action($user2->id);
508
        // The response from OpenAI.
509
        $mock->append(new Response(
510
            200,
511
            ['Content-Type' => 'application/json'],
512
            json_encode([
513
                'created' => 1719140500,
514
                'data' => [
515
                    (object) [
516
                        'revised_prompt' => 'An image that represents the concept of a \'test\'.',
517
                        'url' => $url,
518
                    ],
519
                ],
520
            ]),
521
        ));
522
        // The image downloaded from the server successfully.
523
        $mock->append(new Response(
524
            200,
525
            ['Content-Type' => 'image/jpeg'],
526
            \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
527
        ));
528
        $processor = new process_generate_image($this->provider, $this->action);
529
        $result = $processor->process();
530
        $this->assertTrue($result->get_success());
531
 
532
        // Case 4: Time window has passed, user rate limit should be reset.
533
        $clock->bump(11);
534
        // Log in user1.
535
        $this->setUser($user1);
536
        // The response from OpenAI.
537
        $mock->append(new Response(
538
            200,
539
            ['Content-Type' => 'application/json'],
540
            json_encode([
541
                'created' => 1719140500,
542
                'data' => [
543
                    (object) [
544
                        'revised_prompt' => 'An image that represents the concept of a \'test\'.',
545
                        'url' => $url,
546
                    ],
547
                ],
548
            ]),
549
        ));
550
        // The image downloaded from the server successfully.
551
        $mock->append(new Response(
552
            200,
553
            ['Content-Type' => 'image/jpeg'],
554
            \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
555
        ));
556
        $this->create_action($user1->id);
557
        $processor = new process_generate_image($provider, $this->action);
558
        $result = $processor->process();
559
        $this->assertTrue($result->get_success());
560
    }
561
 
562
    /**
563
     * Test process method with global rate limiter.
564
     */
565
    public function test_process_with_global_rate_limiter(): void {
566
        // Create users.
567
        $user1 = $this->getDataGenerator()->create_user();
568
        $user2 = $this->getDataGenerator()->create_user();
569
        // Log in user1.
570
        $this->setUser($user1);
571
        // Mock clock.
572
        $clock = $this->mock_clock_with_frozen();
573
 
574
        // Set the global rate limiter.
575
        $config = [
576
            'apikey' => '123',
577
            'enableglobalratelimit' => true,
578
            'globalratelimit' => 1,
579
        ];
580
        $provider = $this->manager->create_provider_instance(
581
            classname: '\aiprovider_openai\provider',
582
            name: 'dummy',
583
            config: $config,
584
            actionconfig: [
585
                \core_ai\aiactions\generate_image::class => [
586
                    'settings' => [
587
                        'model' => 'dall-e-3',
588
                        'endpoint' => "https://api.openai.com/v1/chat/completions",
589
                    ],
590
                ],
591
            ],
592
        );
593
 
594
        // Mock the http client to return a successful response.
595
        ['mock' => $mock] = $this->get_mocked_http_client();
596
        $url = 'https://example.com/test.jpg';
597
 
598
        // Case 1: Global rate limit has not been reached.
599
        $this->create_action($user1->id);
600
        // The response from OpenAI.
601
        $mock->append(new Response(
602
            200,
603
            ['Content-Type' => 'application/json'],
604
            json_encode([
605
                'created' => 1719140500,
606
                'data' => [
607
                    (object) [
608
                        'revised_prompt' => 'An image that represents the concept of a \'test\'.',
609
                        'url' => $url,
610
                    ],
611
                ],
612
            ]),
613
        ));
614
        // The image downloaded from the server successfully.
615
        $mock->append(new Response(
616
            200,
617
            ['Content-Type' => 'image/jpeg'],
618
            \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
619
        ));
620
        $processor = new process_generate_image($provider, $this->action);
621
        $result = $processor->process();
622
        $this->assertTrue($result->get_success());
623
 
624
        // Case 2: Global rate limit has been reached.
625
        $clock->bump(HOURSECS - 10);
626
        // The response from OpenAI.
627
        $mock->append(new Response(
628
            200,
629
            ['Content-Type' => 'application/json'],
630
            json_encode([
631
                'created' => 1719140500,
632
                'data' => [
633
                    (object) [
634
                        'revised_prompt' => 'An image that represents the concept of a \'test\'.',
635
                        'url' => $url,
636
                    ],
637
                ],
638
            ]),
639
        ));
640
        // The image downloaded from the server successfully.
641
        $mock->append(new Response(
642
            200,
643
            ['Content-Type' => 'image/jpeg'],
644
            \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
645
        ));
646
        $this->provider = $this->create_provider(\core_ai\aiactions\generate_image::class);
647
        $this->create_action($user1->id);
648
        $processor = new process_generate_image($provider, $this->action);
649
        $result = $processor->process();
650
        $this->assertEquals(429, $result->get_errorcode());
651
        $this->assertEquals('Global rate limit exceeded', $result->get_errormessage());
652
        $this->assertFalse($result->get_success());
653
 
654
        // Case 3: Global rate limit has been reached for a different user too.
655
        // Log in user2.
656
        $this->setUser($user2);
657
        $this->provider = $this->create_provider(\core_ai\aiactions\generate_image::class);
658
        $this->create_action($user2->id);
659
        // The response from OpenAI.
660
        $mock->append(new Response(
661
            200,
662
            ['Content-Type' => 'application/json'],
663
            json_encode([
664
                'created' => 1719140500,
665
                'data' => [
666
                    (object) [
667
                        'revised_prompt' => 'An image that represents the concept of a \'test\'.',
668
                        'url' => $url,
669
                    ],
670
                ],
671
            ]),
672
        ));
673
        // The image downloaded from the server successfully.
674
        $mock->append(new Response(
675
            200,
676
            ['Content-Type' => 'image/jpeg'],
677
            \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
678
        ));
679
        $processor = new process_generate_image($provider, $this->action);
680
        $result = $processor->process();
681
        $this->assertFalse($result->get_success());
682
 
683
        // Case 4: Time window has passed, global rate limit should be reset.
684
        $clock->bump(11);
685
        // Log in user1.
686
        $this->setUser($user1);
687
        // The response from OpenAI.
688
        $mock->append(new Response(
689
            200,
690
            ['Content-Type' => 'application/json'],
691
            json_encode([
692
                'created' => 1719140500,
693
                'data' => [
694
                    (object) [
695
                        'revised_prompt' => 'An image that represents the concept of a \'test\'.',
696
                        'url' => $url,
697
                    ],
698
                ],
699
            ]),
700
        ));
701
        // The image downloaded from the server successfully.
702
        $mock->append(new Response(
703
            200,
704
            ['Content-Type' => 'image/jpeg'],
705
            \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')),
706
        ));
707
        $this->provider = $this->create_provider(\core_ai\aiactions\generate_image::class);
708
        $this->create_action($user1->id);
709
        $processor = new process_generate_image($provider, $this->action);
710
        $result = $processor->process();
711
        $this->assertTrue($result->get_success());
712
    }
713
}