1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Yiisoft\Validator\Tests\Rule; |
||
6 | |||
7 | use DateTime; |
||
8 | use InvalidArgumentException; |
||
9 | use RuntimeException; |
||
10 | use stdClass; |
||
11 | use Stringable; |
||
12 | use Yiisoft\Validator\DataSetInterface; |
||
13 | use Yiisoft\Validator\DataWrapperInterface; |
||
14 | use Yiisoft\Validator\Rule\Compare; |
||
15 | use Yiisoft\Validator\Rule\CompareType; |
||
16 | use Yiisoft\Validator\RuleInterface; |
||
17 | use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase; |
||
18 | use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait; |
||
19 | use Yiisoft\Validator\Tests\Rule\Base\SkipOnErrorTestTrait; |
||
20 | use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait; |
||
0 ignored issues
–
show
Coding Style
introduced
by
![]() |
|||
21 | |||
22 | use Yiisoft\Validator\Tests\Support\Data\CompareObject; |
||
23 | |||
24 | use function is_string; |
||
25 | |||
26 | final class CompareTest extends RuleTestCase |
||
27 | { |
||
28 | use RuleWithOptionsTestTrait; |
||
29 | use SkipOnErrorTestTrait; |
||
30 | use WhenTestTrait; |
||
31 | |||
32 | public function testInitWithWrongType(): void |
||
33 | { |
||
34 | $this->expectException(InvalidArgumentException::class); |
||
35 | $message = 'Type "float" is not supported. The valid types are: "original", "string", "number".'; |
||
36 | $this->expectExceptionMessage($message); |
||
37 | |||
38 | new Compare(type: 'float'); |
||
39 | } |
||
40 | |||
41 | public function testInitWithWrongOperator(): void |
||
42 | { |
||
43 | $this->expectException(InvalidArgumentException::class); |
||
44 | $message = 'Operator "=" is not supported. The valid operators are: "==", "===", "!=", "!==", ">", ">=", ' . |
||
45 | '"<", "<=".'; |
||
46 | $this->expectExceptionMessage($message); |
||
47 | |||
48 | new Compare(1, operator: '='); |
||
49 | } |
||
50 | |||
51 | public function testGetName(): void |
||
52 | { |
||
53 | $rule = new Compare(); |
||
54 | $this->assertSame(Compare::class, $rule->getName()); |
||
55 | } |
||
56 | |||
57 | public function dataOptions(): array |
||
58 | { |
||
59 | return [ |
||
60 | [ |
||
61 | new Compare(1), |
||
62 | [ |
||
63 | 'targetValue' => 1, |
||
64 | 'targetAttribute' => null, |
||
65 | 'incorrectInputMessage' => [ |
||
66 | 'template' => 'The allowed types are integer, float, string, boolean, null and object ' . |
||
67 | 'implementing \Stringable interface or \DateTimeInterface.', |
||
68 | 'parameters' => [ |
||
69 | 'targetValue' => 1, |
||
70 | 'targetAttribute' => null, |
||
71 | 'targetValueOrAttribute' => 1, |
||
72 | ], |
||
73 | ], |
||
74 | 'incorrectDataSetTypeMessage' => [ |
||
75 | 'template' => 'The attribute value returned from a custom data set must have one of the ' . |
||
76 | 'following types: integer, float, string, boolean, null or an object implementing ' . |
||
77 | '\Stringable interface or \DateTimeInterface.', |
||
78 | 'parameters' => [ |
||
79 | 'targetValue' => 1, |
||
80 | 'targetAttribute' => null, |
||
81 | 'targetValueOrAttribute' => 1, |
||
82 | ], |
||
83 | ], |
||
84 | 'message' => [ |
||
85 | 'template' => 'Value must be equal to "{targetValueOrAttribute}".', |
||
86 | 'parameters' => [ |
||
87 | 'targetValue' => 1, |
||
88 | 'targetAttribute' => null, |
||
89 | 'targetValueOrAttribute' => 1, |
||
90 | ], |
||
91 | ], |
||
92 | 'type' => 'number', |
||
93 | 'operator' => '==', |
||
94 | 'skipOnEmpty' => false, |
||
95 | 'skipOnError' => false, |
||
96 | ], |
||
97 | ], |
||
98 | [ |
||
99 | new Compare( |
||
100 | new DateTime('2023-02-07 12:57:12'), |
||
101 | targetAttribute: 'test', |
||
102 | incorrectInputMessage: 'Custom message 1.', |
||
103 | incorrectDataSetTypeMessage: 'Custom message 2.', |
||
104 | message: 'Custom message 3.', |
||
105 | type: CompareType::ORIGINAL, |
||
106 | operator: '>=', |
||
107 | skipOnEmpty: true, |
||
108 | skipOnError: true, |
||
109 | when: static fn (): bool => true, |
||
110 | ), |
||
111 | [ |
||
112 | 'targetAttribute' => 'test', |
||
113 | 'incorrectInputMessage' => [ |
||
114 | 'template' => 'Custom message 1.', |
||
115 | 'parameters' => [ |
||
116 | 'targetAttribute' => 'test', |
||
117 | ], |
||
118 | ], |
||
119 | 'incorrectDataSetTypeMessage' => [ |
||
120 | 'template' => 'Custom message 2.', |
||
121 | 'parameters' => [ |
||
122 | 'targetAttribute' => 'test', |
||
123 | ], |
||
124 | ], |
||
125 | 'message' => [ |
||
126 | 'template' => 'Custom message 3.', |
||
127 | 'parameters' => [ |
||
128 | 'targetAttribute' => 'test', |
||
129 | ], |
||
130 | ], |
||
131 | 'type' => 'original', |
||
132 | 'operator' => '>=', |
||
133 | 'skipOnEmpty' => true, |
||
134 | 'skipOnError' => true, |
||
135 | ], |
||
136 | ], |
||
137 | 'targetAttribute priority with simple targetValue' => [ |
||
138 | new Compare( |
||
139 | 1, |
||
140 | targetAttribute: 'test', |
||
141 | ), |
||
142 | [ |
||
143 | 'targetValue' => 1, |
||
144 | 'targetAttribute' => 'test', |
||
145 | 'incorrectInputMessage' => [ |
||
146 | 'template' => 'The allowed types are integer, float, string, boolean, null and object ' . |
||
147 | 'implementing \Stringable interface or \DateTimeInterface.', |
||
148 | 'parameters' => [ |
||
149 | 'targetValue' => 1, |
||
150 | 'targetAttribute' => 'test', |
||
151 | 'targetValueOrAttribute' => 'test', |
||
152 | ], |
||
153 | ], |
||
154 | 'incorrectDataSetTypeMessage' => [ |
||
155 | 'template' => 'The attribute value returned from a custom data set must have one of the ' . |
||
156 | 'following types: integer, float, string, boolean, null or an object implementing ' . |
||
157 | '\Stringable interface or \DateTimeInterface.', |
||
158 | 'parameters' => [ |
||
159 | 'targetValue' => 1, |
||
160 | 'targetAttribute' => 'test', |
||
161 | 'targetValueOrAttribute' => 'test', |
||
162 | ], |
||
163 | ], |
||
164 | 'message' => [ |
||
165 | 'template' => 'Value must be equal to "{targetValueOrAttribute}".', |
||
166 | 'parameters' => [ |
||
167 | 'targetValue' => 1, |
||
168 | 'targetAttribute' => 'test', |
||
169 | 'targetValueOrAttribute' => 'test', |
||
170 | ], |
||
171 | ], |
||
172 | 'type' => 'number', |
||
173 | 'operator' => '==', |
||
174 | 'skipOnEmpty' => false, |
||
175 | 'skipOnError' => false, |
||
176 | ], |
||
177 | ], |
||
178 | ]; |
||
179 | } |
||
180 | |||
181 | public function dataValidationPassed(): array |
||
182 | { |
||
183 | $targetStringableFloat = new class () implements Stringable { |
||
184 | public function __toString(): string |
||
185 | { |
||
186 | return '100.5'; |
||
187 | } |
||
188 | }; |
||
189 | $stringableFloat = new class () implements Stringable { |
||
190 | public function __toString(): string |
||
191 | { |
||
192 | return '100.50'; |
||
193 | } |
||
194 | }; |
||
195 | $targetStringableUuid = new class () implements Stringable { |
||
196 | public function __toString(): string |
||
197 | { |
||
198 | return '3b98a689-7d49-48bb-8741-7e27f220b69a'; |
||
199 | } |
||
200 | }; |
||
201 | $stringableUuid = new class () implements Stringable { |
||
202 | public function __toString(): string |
||
203 | { |
||
204 | return 'd62f2b3f-707f-451a-8819-046ff8436a4f'; |
||
205 | } |
||
206 | }; |
||
207 | $dateTime = new DateTime('2023-02-07 12:57:12'); |
||
208 | $object = new CompareObject(a: 1, b: 2); |
||
209 | $objectWithDifferentPropertyType = new CompareObject(a: 1, b: '2'); |
||
210 | $array = [1, 2]; |
||
211 | |||
212 | return [ |
||
213 | // Number / string specific, expressions |
||
214 | |||
215 | 'target value: float, value: float with the same value as expression result, type: number, operator: ==' => [ |
||
216 | 1 - 0.83, |
||
217 | [new Compare(0.17)], |
||
218 | ], |
||
219 | 'target value: float, value: float with the same value as expression result, type: number, operator: ===' => [ |
||
220 | 1 - 0.83, |
||
221 | [new Compare(0.17, operator: '===')], |
||
222 | ], |
||
223 | 'target value: float, value: float with the same value as expression result, type: number, operator: >=' => [ |
||
224 | 1 - 0.83, |
||
225 | [new Compare(0.17, operator: '>=')], |
||
226 | ], |
||
227 | 'target value: float as expression result, value: float with the same value, type: number, operator: >=' => [ |
||
228 | 0.17, |
||
229 | [new Compare(1 - 0.83, operator: '>=')], |
||
230 | ], |
||
231 | 'target value: float, value: float with the same value as expression result, type: number, operator: <=' => [ |
||
232 | 1 - 0.83, |
||
233 | [new Compare(0.17, operator: '<=')], |
||
234 | ], |
||
235 | 'target value: float as expression result, value: float with the same value, type: number, operator: <=' => [ |
||
236 | 0.17, |
||
237 | [new Compare(1 - 0.83, operator: '<=')], |
||
238 | ], |
||
239 | 'target value: float, value: float with the same value as expression result, type: string, operator: ==' => [ |
||
240 | 1 - 0.83, |
||
241 | [new Compare(0.17, type: CompareType::STRING)], |
||
242 | ], |
||
243 | |||
244 | // Number / original specific, decimal places, directly provided values |
||
245 | |||
246 | 'target value: string float, value: string float with the same value, but extra decimal place (0), type: number, operator: ==' => [ |
||
247 | '100.50', |
||
248 | [new Compare('100.5')], |
||
249 | ], |
||
250 | 'target value: float, value: string float with the same value, but extra decimal place (0), type: number, operator: ==' => [ |
||
251 | '100.50', |
||
252 | [new Compare(100.5)], |
||
253 | ], |
||
254 | 'target value: string float, value: string float with the same value, but extra decimal place (0), type: number, operator: ===' => [ |
||
255 | '100.50', |
||
256 | [new Compare('100.5', operator: '===')], |
||
257 | ], |
||
258 | 'target value: string float, value: string float with the same value, but extra decimal place (0), type: original, operator: ==' => [ |
||
259 | '100.50', |
||
260 | [new Compare('100.5', type: CompareType::ORIGINAL)], |
||
261 | ], |
||
262 | |||
263 | // Number / original specific, decimal places, values provided via stringable objects |
||
264 | |||
265 | 'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: number, operator: ==' => [ |
||
266 | $stringableFloat, |
||
267 | [new Compare($targetStringableFloat)], |
||
268 | ], |
||
269 | 'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: number, operator: >=' => [ |
||
270 | $stringableFloat, |
||
271 | [new Compare($targetStringableFloat, operator: '>=')], |
||
272 | ], |
||
273 | |||
274 | // String / original specific, character order, directly provided values |
||
275 | |||
276 | 'target value: uuidv4, value: greater uuidv4, type: string, operator: >' => [ |
||
277 | 'd62f2b3f-707f-451a-8819-046ff8436a4f', |
||
278 | [new Compare('3b98a689-7d49-48bb-8741-7e27f220b69a', type: CompareType::STRING, operator: '>')], |
||
279 | ], |
||
280 | 'target value: character, value: character located further within alphabet, type: string, operator: >' => [ |
||
281 | 'b', |
||
282 | [new Compare('a', type: CompareType::STRING, operator: '>')], |
||
283 | ], |
||
284 | 'target value: character, value: character located further within alphabet, type: original, operator: >' => [ |
||
285 | 'b', |
||
286 | [new Compare('a', type: CompareType::ORIGINAL, operator: '>')], |
||
287 | ], |
||
288 | |||
289 | // String specific, character order, values provided via stringable objects |
||
290 | |||
291 | 'target value: stringable uuidv4, value: greater stringable uuidv4, type: string, operator: >' => [ |
||
292 | $stringableUuid, |
||
293 | [new Compare($targetStringableUuid, type: CompareType::STRING, operator: '>')], |
||
294 | ], |
||
295 | 'target value: stringable uuidv4, value: greater stringable uuidv4, type: string, operator: >=' => [ |
||
296 | $stringableUuid, |
||
297 | [new Compare($targetStringableUuid, type: CompareType::STRING, operator: '>=')], |
||
298 | ], |
||
299 | |||
300 | // Original specific, datetime |
||
301 | |||
302 | 'target value: DateTime object, value: DateTime object with the same value, type: original, operator: ==' => [ |
||
303 | new DateTime('2023-02-07 12:57:12'), |
||
304 | [new Compare(new DateTime('2023-02-07 12:57:12'), type: CompareType::ORIGINAL)], |
||
305 | ], |
||
306 | 'target value: DateTime object, value: the same DateTime object, type: original, operator: ===' => [ |
||
307 | $dateTime, |
||
308 | [new Compare($dateTime, type: CompareType::ORIGINAL)], |
||
309 | ], |
||
310 | 'target value: DateTime object, value: DateTime object with the same value, type: original, operator: !==' => [ |
||
311 | new DateTime('2023-02-07 12:57:12'), |
||
312 | [new Compare(new DateTime('2023-02-07 12:57:12'), type: CompareType::ORIGINAL, operator: '!==')], |
||
313 | ], |
||
314 | 'target value: DateTime object, value: DateTime object with the same value, type: original, operator: >=' => [ |
||
315 | new DateTime('2023-02-07 12:57:12'), |
||
316 | [new Compare(new DateTime('2023-02-07 12:57:12'), type: CompareType::ORIGINAL, operator: '>=')], |
||
317 | ], |
||
318 | 'target value: human-readable DateTime object, value: greater DateTime object, type: original, operator: >' => [ |
||
319 | new DateTime('2022-06-03'), |
||
320 | [new Compare(new DateTime('June 2nd, 2022'), type: CompareType::ORIGINAL, operator: '>')], |
||
321 | ], |
||
322 | |||
323 | // Number / string specific, DateTime object and Unix Timestamp |
||
324 | |||
325 | 'target value: Unix Timestamp string, value: DateTime object with the same value, type: number, operator: ==' => [ |
||
326 | $dateTime->format('U'), |
||
327 | [new Compare($dateTime)], |
||
328 | ], |
||
329 | 'target value: Unix Timestamp string, value: DateTime object with the same value, type: string, operator: ==' => [ |
||
330 | $dateTime->format('U'), |
||
331 | [new Compare($dateTime, type: CompareType::STRING)], |
||
332 | ], |
||
333 | 'target value: Unix Timestamp string, value: DateTime object with the same value, type: number, operator: >=' => [ |
||
334 | $dateTime->format('U'), |
||
335 | [new Compare($dateTime, operator: '>=')], |
||
336 | ], |
||
337 | 'target value: Unix Timestamp string, value: DateTime object with the same value, type: string, operator: >=' => [ |
||
338 | $dateTime->format('U'), |
||
339 | [new Compare($dateTime, type: CompareType::STRING, operator: '>=')], |
||
340 | ], |
||
341 | |||
342 | // Original specific, objects |
||
343 | |||
344 | 'target value: object, value: similar object in a different instance, type: original, operator: ==' => [ |
||
345 | new stdClass(), |
||
346 | [new Compare(new stdClass(), type: CompareType::ORIGINAL)], |
||
347 | ], |
||
348 | 'target value: object, value: the same object, type: original, operator: ===' => [ |
||
349 | $object, |
||
350 | [new Compare($object, type: CompareType::ORIGINAL, operator: '===')], |
||
351 | ], |
||
352 | 'target value: object, value: similar object but with different property type, type: original, operator: ===' => [ |
||
353 | $objectWithDifferentPropertyType, |
||
354 | [new Compare($object, type: CompareType::ORIGINAL)], |
||
355 | ], |
||
356 | |||
357 | // Original specific, arrays |
||
358 | |||
359 | 'target value: array, value: similar array declared separately, type: original, operator: ==' => [ |
||
360 | [1, 2], |
||
361 | [new Compare([1, 2], type: CompareType::ORIGINAL)], |
||
362 | ], |
||
363 | 'target value: array, value: similar array declared separately, type: original, operator: ===' => [ |
||
364 | [1, 2], |
||
365 | [new Compare([1, 2], type: CompareType::ORIGINAL, operator: '===')], |
||
366 | ], |
||
367 | 'target value: array, value: similar array but with different item type, type: original, operator: ==' => [ |
||
368 | [1, 2], |
||
369 | [new Compare([1, '2'], type: CompareType::ORIGINAL)], |
||
370 | ], |
||
371 | 'target value: array, value: the same array, type: original, operator: ===' => [ |
||
372 | $array, |
||
373 | [new Compare($array, type: CompareType::ORIGINAL)], |
||
374 | ], |
||
375 | ]; |
||
376 | } |
||
377 | |||
378 | public function dataValidationPassedWithDifferentTypes(): array |
||
379 | { |
||
380 | $customDataSet = new class () implements DataSetInterface { |
||
381 | public function getAttributeValue(string $attribute): mixed |
||
382 | { |
||
383 | return 100; |
||
384 | } |
||
385 | |||
386 | public function getData(): ?array |
||
387 | { |
||
388 | return null; |
||
389 | } |
||
390 | |||
391 | public function hasAttribute(string $attribute): bool |
||
392 | { |
||
393 | return true; |
||
394 | } |
||
395 | }; |
||
396 | $subFloatFromInt = static fn(int $value1, float $value2): int => $value1 - (int) $value2; |
||
397 | $initialData = [ |
||
398 | // Basic |
||
399 | |||
400 | 'target value: integer, value: integer with the same value, type: number, operator: ==' => [ |
||
401 | 100, |
||
402 | [new Compare(100)], |
||
403 | ], |
||
404 | 'target value: integer, value: integer with the same value, type: number, operator: ===' => [ |
||
405 | 100, |
||
406 | [new Compare(100, operator: '===')], |
||
407 | ], |
||
408 | 'target value: integer, value: lower integer, type: number, operator: !=' => [ |
||
409 | 99, |
||
410 | [new Compare(100, operator: '!=')], |
||
411 | ], |
||
412 | 'target value: integer, value: greater integer, type: number, operator: !=' => [ |
||
413 | 101, |
||
414 | [new Compare(100, operator: '!=')], |
||
415 | ], |
||
416 | 'target value: integer, value: lower integer, type: number, operator: !==' => [ |
||
417 | 101, |
||
418 | [new Compare(100, operator: '!==')], |
||
419 | ], |
||
420 | 'target value: integer, value: greater integer, type: number, operator: !==' => [ |
||
421 | 101, |
||
422 | [new Compare(100, operator: '!==')], |
||
423 | ], |
||
424 | 'target value: integer, value: greater integer, type: number, operator: >' => [ |
||
425 | 101, |
||
426 | [new Compare(100, operator: '>')], |
||
427 | ], |
||
428 | 'target value: integer, value: integer with the same value, type: number, operator: >=' => [ |
||
429 | 100, |
||
430 | [new Compare(100, operator: '>=')], |
||
431 | ], |
||
432 | 'target value: integer, value: greater integer, type: number, operator: >=' => [ |
||
433 | 101, |
||
434 | [new Compare(100, operator: '>=')], |
||
435 | ], |
||
436 | 'target value: integer, value: lower integer, type: number, operator: <' => [ |
||
437 | 99, |
||
438 | [new Compare(100, operator: '<')], |
||
439 | ], |
||
440 | 'target value: integer, value: integer with the same value, type: number, operator: <=' => [ |
||
441 | 100, |
||
442 | [new Compare(100, operator: '<=')], |
||
443 | ], |
||
444 | 'target value: integer, value: lower integer, type: number, operator: <=' => [ |
||
445 | 99, |
||
446 | [new Compare(100, operator: '<=')], |
||
447 | ], |
||
448 | |||
449 | // Boolean |
||
450 | |||
451 | 'target value: boolean (false), value: boolean (true), type: number, operator: >=' => [ |
||
452 | true, |
||
453 | [new Compare(false, operator: '>=')], |
||
454 | ], |
||
455 | |||
456 | // Different types for non-strict equality |
||
457 | |||
458 | 'target value: empty string, value: null, type: number, operator: ==' => [ |
||
459 | null, |
||
460 | [new Compare('')], |
||
461 | ], |
||
462 | 'target value: integer, value: string integer with the same value, type: number, operator: ==' => [ |
||
463 | '100', |
||
464 | [new Compare(100)], |
||
465 | ], |
||
466 | |||
467 | // Different types for non-strict inequality |
||
468 | |||
469 | 'target value: integer, value: float, type: number, operator: !=' => [ |
||
470 | 100.00001, |
||
471 | [new Compare(100, operator: '!=')], |
||
472 | ], |
||
473 | 'target value: integer, value: boolean, type: number, operator: !=' => [ |
||
474 | false, |
||
475 | [new Compare(100, operator: '!=')], |
||
476 | ], |
||
477 | |||
478 | // Different types for strict inequality |
||
479 | |||
480 | 'target value: integer, value: boolean, type: number, operator: !==' => [ |
||
481 | false, |
||
482 | [new Compare(100, operator: '!==')], |
||
483 | ], |
||
484 | 'target value: integer, value: string integer with the same value, type: number, operator: !==' => [ |
||
485 | '100', |
||
486 | [new Compare(100, operator: '!==')], |
||
487 | ], |
||
488 | 'target value: integer, value: float with the same value, but extra decimal place (0), type: number, operator: !==' => [ |
||
489 | 100.0, |
||
490 | [new Compare(100, operator: '!==')], |
||
491 | ], |
||
492 | |||
493 | // Large integers |
||
494 | |||
495 | 'target value: string with large integer, value: string with the same integer, type: number, operator: ===' => [ |
||
496 | PHP_INT_MAX . '0', |
||
497 | [new Compare(PHP_INT_MAX . '0', operator: '===')], |
||
498 | ], |
||
499 | 'target value: string with large integer, value: string with greater integer, type: number, operator: >' => [ |
||
500 | PHP_INT_MAX . '0', |
||
501 | [new Compare('-' . PHP_INT_MAX . '12', operator: '>')], |
||
502 | ], |
||
503 | 'target value: large integer in scientific notation, value: greater integer, type: number, operator: ===' => [ |
||
504 | 4.5e19, |
||
505 | [new Compare(4.5e19, operator: '===')], |
||
506 | ], |
||
507 | 'target value: large integer in scientific notation, value: greater integer, type: number, operator: >' => [ |
||
508 | 4.5e20, |
||
509 | [new Compare(-4.5e19, operator: '>')], |
||
510 | ], |
||
511 | 'target value: integer, value: the same integer as expression result, type: number, operator: ===' => [ |
||
512 | $subFloatFromInt(1_234_567_890, 1_234_567_890), |
||
0 ignored issues
–
show
|
|||
513 | [new Compare(0, operator: '===')], |
||
514 | ], |
||
515 | |||
516 | // Target attribute |
||
517 | |||
518 | 'target attribute: array key, target attribute value: integer, attribute value: integer with the same value, type: number, operator: ==' => [ |
||
519 | ['attribute' => 100, 'number' => 100], |
||
520 | ['number' => new Compare(targetAttribute: 'attribute')], |
||
521 | ], |
||
522 | 'target attribute: array key, target attribute value: integer, attribute value: lower integer, type: number, operator: <=' => [ |
||
523 | ['attribute' => 100, 'number' => 99], |
||
524 | ['number' => new Compare(targetAttribute: 'attribute', operator: '<=')], |
||
525 | ], |
||
526 | 'target attribute: object property, target attribute value: integer, attribute value: integer with the same value, type: number, operator: ==' => [ |
||
527 | new class () { |
||
528 | public int $attribute = 100; |
||
529 | public int $number = 100; |
||
530 | }, |
||
531 | ['number' => new Compare(targetAttribute: 'attribute', operator: '<=')], |
||
532 | ], |
||
533 | 'target attribute: custom data set attribute, target attribute value: integer, attribute value: integer with the same value, type: number, operator: ==' => [ |
||
534 | $customDataSet, |
||
535 | ['number' => new Compare(targetAttribute: 'attribute', operator: '<=')], |
||
536 | ], |
||
537 | ]; |
||
538 | |||
539 | return $this->extendDataWithDifferentTypes($initialData); |
||
540 | } |
||
541 | |||
542 | /** |
||
543 | * @dataProvider dataValidationPassed |
||
544 | * @dataProvider dataValidationPassedWithDifferentTypes |
||
545 | */ |
||
546 | public function testValidationPassed(mixed $data, ?array $rules = null): void |
||
547 | { |
||
548 | parent::testValidationPassed($data, $rules); |
||
549 | } |
||
550 | |||
551 | public function dataValidationFailed(): array |
||
552 | { |
||
553 | $incorrectDataSet = new class () implements DataWrapperInterface { |
||
554 | public function getAttributeValue(string $attribute): mixed |
||
555 | { |
||
556 | return new stdClass(); |
||
557 | } |
||
558 | |||
559 | public function getData(): ?array |
||
560 | { |
||
561 | return null; |
||
562 | } |
||
563 | |||
564 | public function getSource(): mixed |
||
565 | { |
||
566 | return false; |
||
567 | } |
||
568 | |||
569 | public function hasAttribute(string $attribute): bool |
||
570 | { |
||
571 | return false; |
||
572 | } |
||
573 | }; |
||
574 | $targetStringableFloat = new class () implements Stringable { |
||
575 | public function __toString(): string |
||
576 | { |
||
577 | return '100.5'; |
||
578 | } |
||
579 | }; |
||
580 | $stringableFloat = new class () implements Stringable { |
||
581 | public function __toString(): string |
||
582 | { |
||
583 | return '100.50'; |
||
584 | } |
||
585 | }; |
||
586 | $targetStringableUuid = new class () implements Stringable { |
||
587 | public function __toString(): string |
||
588 | { |
||
589 | return '3b98a689-7d49-48bb-8741-7e27f220b69a'; |
||
590 | } |
||
591 | }; |
||
592 | $stringableUuid = new class () implements Stringable { |
||
593 | public function __toString(): string |
||
594 | { |
||
595 | return 'd62f2b3f-707f-451a-8819-046ff8436a4f'; |
||
596 | } |
||
597 | }; |
||
598 | $dateTime = new DateTime('2023-02-07 12:57:12'); |
||
599 | $object = new CompareObject(a: 1, b: 2); |
||
600 | $objectWithDifferentPropertyValue = new CompareObject(a: 1, b: 3); |
||
601 | $objectWithDifferentPropertyType = new CompareObject(a: 1, b: '2'); |
||
602 | $array = [1, 2]; |
||
603 | $reversedArray = [2, 1]; |
||
604 | |||
605 | return [ |
||
606 | // Incorrect input |
||
607 | |||
608 | 'incorrect input' => [ |
||
609 | [], |
||
610 | [new Compare(false)], |
||
611 | [ |
||
612 | '' => [ |
||
613 | 'The allowed types are integer, float, string, boolean, null and object implementing ' . |
||
614 | '\Stringable interface or \DateTimeInterface.', |
||
615 | ], |
||
616 | ], |
||
617 | ], |
||
618 | 'custom incorrect input message' => [ |
||
619 | [], |
||
620 | [new Compare(false, incorrectInputMessage: 'Custom incorrect input message.')], |
||
621 | ['' => ['Custom incorrect input message.']], |
||
622 | ], |
||
623 | 'custom incorrect input message with parameters' => [ |
||
624 | [], |
||
625 | [new Compare(false, incorrectInputMessage: 'Attribute - {attribute}, type - {type}.')], |
||
626 | ['' => ['Attribute - , type - array.']], |
||
627 | ], |
||
628 | 'custom incorrect input message with parameters, attribute set' => [ |
||
629 | ['data' => []], |
||
630 | ['data' => new Compare(false, incorrectInputMessage: 'Attribute - {attribute}, type - {type}.')], |
||
631 | ['data' => ['Attribute - data, type - array.']], |
||
632 | ], |
||
633 | |||
634 | // Incorrect data set input |
||
635 | |||
636 | 'incorrect data set type' => [ |
||
637 | $incorrectDataSet, |
||
638 | [new Compare(targetAttribute: 'test')], |
||
639 | [ |
||
640 | '' => [ |
||
641 | 'The attribute value returned from a custom data set must have one of the following types: ' . |
||
642 | 'integer, float, string, boolean, null or an object implementing \Stringable interface ' . |
||
643 | 'or \DateTimeInterface.', |
||
644 | ], |
||
645 | ], |
||
646 | ], |
||
647 | 'custom incorrect data set type message' => [ |
||
648 | $incorrectDataSet, |
||
649 | [ |
||
650 | new Compare( |
||
651 | targetAttribute: 'test', |
||
652 | incorrectDataSetTypeMessage: 'Custom incorrect data set type message.', |
||
653 | ), |
||
654 | ], |
||
655 | ['' => ['Custom incorrect data set type message.']], |
||
656 | ], |
||
657 | 'custom incorrect data set type message with parameters' => [ |
||
658 | $incorrectDataSet, |
||
659 | [ |
||
660 | new Compare( |
||
661 | targetAttribute: 'test', |
||
662 | incorrectDataSetTypeMessage: 'Type - {type}.', |
||
663 | ), |
||
664 | ], |
||
665 | ['' => ['Type - stdClass.']], |
||
666 | ], |
||
667 | |||
668 | // Custom message |
||
669 | |||
670 | 'custom message' => [101, [new Compare(100, message: 'Custom message.')], ['' => ['Custom message.']]], |
||
671 | 'custom message with parameters, target value set' => [ |
||
672 | 101, |
||
673 | [ |
||
674 | new Compare( |
||
675 | 100, |
||
676 | message: 'Attribute - {attribute}, target value - {targetValue}, target attribute - ' . |
||
677 | '{targetAttribute}, target value or attribute - {targetValueOrAttribute}, value - {value}.', |
||
678 | ), |
||
679 | ], |
||
680 | [ |
||
681 | '' => [ |
||
682 | 'Attribute - , target value - 100, target attribute - , target value or attribute - 100, ' . |
||
683 | 'value - 101.', |
||
684 | ], |
||
685 | ], |
||
686 | ], |
||
687 | 'custom message with parameters, attribute and target attribute set' => [ |
||
688 | ['attribute' => 100, 'number' => 101], |
||
689 | [ |
||
690 | 'number' => new Compare( |
||
691 | targetAttribute: 'attribute', |
||
692 | message: 'Attribute - {attribute}, target value - {targetValue}, target attribute - ' . |
||
693 | '{targetAttribute}, target attribute value - {targetAttributeValue}, target value or ' . |
||
694 | 'attribute - {targetValueOrAttribute}, value - {value}.', |
||
695 | operator: '===', |
||
696 | ), |
||
697 | ], |
||
698 | [ |
||
699 | 'number' => [ |
||
700 | 'Attribute - number, target value - , target attribute - attribute, target attribute value ' . |
||
701 | '- 100, target value or attribute - attribute, value - 101.', |
||
702 | ], |
||
703 | ], |
||
704 | ], |
||
705 | |||
706 | // String / original specific, falsy values |
||
707 | |||
708 | 'target value: integer (0), value: null, type: string, operator: ==' => [ |
||
709 | null, |
||
710 | [new Compare(0, type: CompareType::STRING)], |
||
711 | ['' => ['Value must be equal to "0".']], |
||
712 | ], |
||
713 | |||
714 | // Number / string specific, expressions |
||
715 | |||
716 | 'target value: float, value: float with the same value as expression result, type: original, operator: ==' => [ |
||
717 | 1 - 0.83, |
||
718 | [new Compare(0.17, type: CompareType::ORIGINAL)], |
||
719 | ['' => ['Value must be equal to "0.17".']], |
||
720 | ], |
||
721 | 'target value: float epsilon, value: doubled float epsilon, type: number, operator: ==' => [ |
||
722 | PHP_FLOAT_EPSILON * 2, |
||
723 | [new Compare(PHP_FLOAT_EPSILON)], |
||
724 | ['' => ['Value must be equal to "2.2204460492503E-16".']], |
||
725 | ], |
||
726 | |||
727 | // Number / original specific, decimal places, directly provided values |
||
728 | |||
729 | 'target value: string float, value: string float with the same value, but extra decimal place (0), type: string, operator: ==' => [ |
||
730 | '100.50', [new Compare('100.5', type: CompareType::STRING)], ['' => ['Value must be equal to "100.5".']], |
||
731 | ], |
||
732 | 'target value: string float, value: string float with the same value, but extra decimal place (0), type: string, operator: ===' => [ |
||
733 | '100.50', [new Compare('100.5', type: CompareType::STRING, operator: '===')], ['' => ['Value must be strictly equal to "100.5".']], |
||
734 | ], |
||
735 | 'target value: string float, value: string float with the same value, but extra decimal place (0), type: original, operator: ===' => [ |
||
736 | '100.50', [new Compare('100.5', type: CompareType::ORIGINAL, operator: '===')], ['' => ['Value must be strictly equal to "100.5".']], |
||
737 | ], |
||
738 | |||
739 | // Number / original specific, decimal places, values provided via stringable objects |
||
740 | |||
741 | 'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: string, operator: ==' => [ |
||
742 | $stringableFloat, |
||
743 | [new Compare($targetStringableFloat, type: CompareType::STRING)], |
||
744 | ['' => ['Value must be equal to "100.5".']], |
||
745 | ], |
||
746 | 'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: string, operator: ===' => [ |
||
747 | $stringableFloat, |
||
748 | [new Compare($targetStringableFloat, type: CompareType::STRING, operator: '===')], |
||
749 | ['' => ['Value must be strictly equal to "100.5".']], |
||
750 | ], |
||
751 | 'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: original, operator: ==' => [ |
||
752 | $stringableFloat, |
||
753 | [new Compare($targetStringableFloat, type: CompareType::ORIGINAL)], |
||
754 | ['' => ['Value must be equal to "100.5".']], |
||
755 | ], |
||
756 | 'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: original, operator: ===' => [ |
||
757 | $stringableFloat, |
||
758 | [new Compare($targetStringableFloat, type: CompareType::ORIGINAL, operator: '===')], |
||
759 | ['' => ['Value must be strictly equal to "100.5".']], |
||
760 | ], |
||
761 | |||
762 | // String / original specific, character order, directly provided values |
||
763 | |||
764 | 'target value: character, value: character located further within alphabet, type: number, operator: >' => [ |
||
765 | 'b', |
||
766 | [new Compare('a', type: CompareType::NUMBER, operator: '>')], |
||
767 | ['' => ['Value must be greater than "a".']], |
||
768 | ], |
||
769 | |||
770 | // String specific, character order, values provided via stringable objects |
||
771 | |||
772 | 'target value: stringable uuidv4, value: greater stringable uuidv4, type: number, operator: >' => [ |
||
773 | $stringableUuid, |
||
774 | [new Compare($targetStringableUuid, type: CompareType::NUMBER, operator: '>')], |
||
775 | ['' => ['Value must be greater than "3b98a689-7d49-48bb-8741-7e27f220b69a".']], |
||
776 | ], |
||
777 | 'target value: stringable uuidv4, value: greater stringable uuidv4, type: original, operator: >' => [ |
||
778 | $stringableUuid, |
||
779 | [new Compare($targetStringableUuid, type: CompareType::ORIGINAL, operator: '>')], |
||
780 | ['' => ['Value must be greater than "3b98a689-7d49-48bb-8741-7e27f220b69a".']], |
||
781 | ], |
||
782 | |||
783 | // Original specific, datetime |
||
784 | |||
785 | 'target value: human-readable DateTime string, value: greater DateTime string, type: string, operator: >' => [ |
||
786 | '2022-06-03', |
||
787 | [new Compare('June 2nd, 2022', type: CompareType::STRING, operator: '>')], |
||
788 | ['' => ['Value must be greater than "June 2nd, 2022".']], |
||
789 | ], |
||
790 | |||
791 | // Number / string specific, DateTime object and Unix Timestamp |
||
792 | |||
793 | 'target value: Unix Timestamp string, value: DateTime object with the same value, type: original, operator: ==' => [ |
||
794 | $dateTime->format('U'), |
||
795 | [new Compare($dateTime, type: CompareType::ORIGINAL)], |
||
796 | ['' => ['Value must be equal to "1675774632".']], |
||
797 | ], |
||
798 | 'target value: Unix Timestamp string, value: DateTime object with the same value, type: original, operator: >=' => [ |
||
799 | $dateTime->format('U'), |
||
800 | [new Compare($dateTime, type: CompareType::ORIGINAL, operator: '>=')], |
||
801 | ['' => ['Value must be greater than or equal to "1675774632".']], |
||
802 | ], |
||
803 | |||
804 | // Original specific, objects |
||
805 | |||
806 | 'target value: object, value: similar object in a different instance, type: original, operator: ===' => [ |
||
807 | new stdClass(), |
||
808 | [new Compare(new stdClass(), type: CompareType::ORIGINAL, operator: '===')], |
||
809 | ['' => ['Value must be strictly equal to "stdClass".']], |
||
810 | ], |
||
811 | 'target value: object, value: similar object with different property value, type: original, operator: ==' => [ |
||
812 | $objectWithDifferentPropertyValue, |
||
813 | [new Compare($object, type: CompareType::ORIGINAL)], |
||
814 | ['' => [sprintf('Value must be equal to "%s".', CompareObject::class)]], |
||
815 | ], |
||
816 | 'target value: object, value: similar object with different property value, type: original, operator: ===' => [ |
||
817 | $objectWithDifferentPropertyValue, |
||
818 | [new Compare($object, type: CompareType::ORIGINAL, operator: '===')], |
||
819 | ['' => [sprintf('Value must be strictly equal to "%s".', CompareObject::class)]], |
||
820 | ], |
||
821 | 'target value: object, value: similar object but with different property type, type: original, operator: ===' => [ |
||
822 | $objectWithDifferentPropertyType, |
||
823 | [new Compare($object, type: CompareType::ORIGINAL, operator: '===')], |
||
824 | ['' => [sprintf('Value must be strictly equal to "%s".', CompareObject::class)]], |
||
825 | ], |
||
826 | |||
827 | // Original specific, arrays |
||
828 | |||
829 | 'target value: array, value: similar array but with different item type, type: original, operator: ===' => [ |
||
830 | [1, 2], |
||
831 | [new Compare([1, '2'], type: CompareType::ORIGINAL, operator: '===')], |
||
832 | ['' => ['Value must be strictly equal to "array".']], |
||
833 | ], |
||
834 | 'target value: array, value: similar array but with different items order, type: original, operator: ==' => [ |
||
835 | $reversedArray, |
||
836 | [new Compare($array, type: CompareType::ORIGINAL)], |
||
837 | ['' => ['Value must be equal to "array".']], |
||
838 | ], |
||
839 | 'target value: array, value: similar array but reversed, type: original, operator: ===' => [ |
||
840 | $reversedArray, |
||
841 | [new Compare($array, type: CompareType::ORIGINAL, operator: '===')], |
||
842 | ['' => ['Value must be strictly equal to "array".']], |
||
843 | ], |
||
844 | ]; |
||
845 | } |
||
846 | |||
847 | public function dataValidationFailedWithDifferentTypes(): array |
||
848 | { |
||
849 | $messageEqual = 'Value must be equal to "100".'; |
||
850 | $messageStrictlyEqual = 'Value must be strictly equal to "100".'; |
||
851 | $messageNotEqual = 'Value must not be equal to "100".'; |
||
852 | $messageNotStrictlyEqual = 'Value must not be strictly equal to "100".'; |
||
853 | $messageGreaterThan = 'Value must be greater than "100".'; |
||
854 | $messageGreaterOrEqualThan = 'Value must be greater than or equal to "100".'; |
||
855 | $messageLessThan = 'Value must be less than "100".'; |
||
856 | $messageLessOrEqualThan = 'Value must be less than or equal to "100".'; |
||
857 | $initialData = [ |
||
858 | // Basic |
||
859 | |||
860 | 'target value: integer, value: lower integer, type: number, operator: ==' => [ |
||
861 | 99, |
||
862 | [new Compare(100)], |
||
863 | ['' => [$messageEqual]], |
||
864 | ], |
||
865 | 'target value: integer, value: greater integer, type: number, operator: ==' => [ |
||
866 | 101, |
||
867 | [new Compare(100)], |
||
868 | ['' => [$messageEqual]], |
||
869 | ], |
||
870 | 'target value: integer, value: lower integer, type: number, operator: ===' => [ |
||
871 | 99, |
||
872 | [new Compare(100, operator: '===')], |
||
873 | ['' => [$messageStrictlyEqual]], |
||
874 | ], |
||
875 | 'target value: integer, value: greater integer, type: number, operator: ===' => [ |
||
876 | 101, |
||
877 | [new Compare(100, operator: '===')], |
||
878 | ['' => [$messageStrictlyEqual]], |
||
879 | ], |
||
880 | 'target value: integer, value: integer with the same value, type: number, operator: !=' => [ |
||
881 | 100, |
||
882 | [new Compare(100, operator: '!=')], |
||
883 | ['' => [$messageNotEqual]], |
||
884 | ], |
||
885 | 'target value: integer, value: integer with the same value, type: number, operator: !==' => [ |
||
886 | 100, |
||
887 | [new Compare(100, operator: '!==')], |
||
888 | ['' => [$messageNotStrictlyEqual]], |
||
889 | ], |
||
890 | 'target value: integer, value: integer with the same value, type: number, operator: >' => [ |
||
891 | 100, |
||
892 | [new Compare(100, operator: '>')], |
||
893 | ['' => [$messageGreaterThan]], |
||
894 | ], |
||
895 | 'target value: integer, value: lower integer, type: number, operator: >' => [ |
||
896 | 99, |
||
897 | [new Compare(100, operator: '>')], |
||
898 | ['' => [$messageGreaterThan]], |
||
899 | ], |
||
900 | 'target value: integer, value: lower integer, type: number, operator: >=' => [ |
||
901 | 99, |
||
902 | [new Compare(100, operator: '>=')], |
||
903 | ['' => [$messageGreaterOrEqualThan]], |
||
904 | ], |
||
905 | 'target value: integer, value: integer with the same value, type: number, operator: <' => [ |
||
906 | 100, |
||
907 | [new Compare(100, operator: '<')], |
||
908 | ['' => [$messageLessThan]], |
||
909 | ], |
||
910 | 'target value: integer, value: greater integer, type: number, operator: <' => [ |
||
911 | 101, |
||
912 | [new Compare(100, operator: '<')], |
||
913 | ['' => [$messageLessThan]], |
||
914 | ], |
||
915 | 'target value: integer, value: greater integer, type: number, operator: <=' => [ |
||
916 | 101, |
||
917 | [new Compare(100, operator: '<=')], |
||
918 | ['' => [$messageLessOrEqualThan]], |
||
919 | ], |
||
920 | |||
921 | // Different types for strict equality |
||
922 | |||
923 | 'target value: empty string, value: null, type: number, operator: ===' => [ |
||
924 | null, |
||
925 | [new Compare('', operator: '===')], |
||
926 | ['' => ['Value must be strictly equal to "".']], |
||
927 | ], |
||
928 | 'target value: integer, value: string integer with the same value, type: number, operator: ===' => [ |
||
929 | '100', |
||
930 | [new Compare(100, operator: '===')], |
||
931 | ['' => [$messageStrictlyEqual]], |
||
932 | ], |
||
933 | 'target value: integer, value: float with the same value, but extra decimal place (0), type: number, operator: ===' => [ |
||
934 | 100.0, |
||
935 | [new Compare(100, operator: '===')], |
||
936 | ['' => [$messageStrictlyEqual]], |
||
937 | ], |
||
938 | |||
939 | // Different types for non-strict inequality |
||
940 | |||
941 | 'target value: integer, value: string integer with the same value, type: number, operator: !=' => [ |
||
942 | '100', |
||
943 | [new Compare(100, operator: '!=')], |
||
944 | ['' => [$messageNotEqual]], |
||
945 | ], |
||
946 | 'target value: integer, value: float with the same value, but extra decimal place (0), type: number, operator: !=' => [ |
||
947 | 100.0, |
||
948 | [new Compare(100, operator: '!=')], |
||
949 | ['' => [$messageNotEqual]], |
||
950 | ], |
||
951 | |||
952 | // Target attribute |
||
953 | |||
954 | 'target attribute: array key, target attribute value: string integer, attribute value: integer with the same value, type: number, operator: ===' => [ |
||
955 | ['attribute' => '100', 'number' => 100], |
||
956 | ['number' => new Compare(targetAttribute: 'attribute', operator: '===')], |
||
957 | ['number' => ['Value must be strictly equal to "attribute".']], |
||
958 | ], |
||
959 | 'target attribute: array key, target attribute value: integer, attribute value: greater integer, type: number, operator: <=' => [ |
||
960 | ['attribute' => 100, 'number' => 101], |
||
961 | ['number' => new Compare(targetAttribute: 'attribute', operator: '<=')], |
||
962 | ['number' => ['Value must be less than or equal to "attribute".']], |
||
963 | ], |
||
964 | ]; |
||
965 | |||
966 | return $this->extendDataWithDifferentTypes($initialData); |
||
967 | } |
||
968 | |||
969 | /** |
||
970 | * @dataProvider dataValidationFailed |
||
971 | * @dataProvider dataValidationFailedWithDifferentTypes |
||
972 | */ |
||
973 | public function testValidationFailed( |
||
974 | mixed $data, |
||
975 | array|RuleInterface|null $rules, |
||
976 | array $errorMessagesIndexedByPath, |
||
977 | ): void { |
||
978 | parent::testValidationFailed($data, $rules, $errorMessagesIndexedByPath); |
||
979 | } |
||
980 | |||
981 | private function extendDataWithDifferentTypes(array $initialData): array |
||
982 | { |
||
983 | $dynamicData = []; |
||
984 | $mainType = CompareType::NUMBER; |
||
985 | $remainingTypes = [CompareType::ORIGINAL, CompareType::STRING]; |
||
986 | foreach ($remainingTypes as $type) { |
||
987 | foreach ($initialData as $key => $item) { |
||
988 | $rules = []; |
||
989 | foreach ($item[1] as $attribute => $rule) { |
||
990 | if (!$rule instanceof Compare) { |
||
991 | throw new RuntimeException('Wrong format for rule.'); |
||
992 | } |
||
993 | |||
994 | $rules[$attribute] = new Compare( |
||
995 | targetValue: $rule->getTargetValue(), |
||
996 | targetAttribute: $rule->getTargetAttribute(), |
||
997 | type: $type, |
||
998 | operator: $rule->getOperator(), |
||
999 | ); |
||
1000 | } |
||
1001 | |||
1002 | if (!is_string($key)) { |
||
1003 | throw new RuntimeException('Data set must have a string name.'); |
||
1004 | } |
||
1005 | |||
1006 | $newKey = str_replace(", type: $mainType,", ", type: $type,", $key); |
||
1007 | if ($key === $newKey) { |
||
1008 | throw new RuntimeException('Wrong format for type.'); |
||
1009 | } |
||
1010 | |||
1011 | $itemData = [$item[0], $rules]; |
||
1012 | if (isset($item[2])) { |
||
1013 | $itemData[] = $item[2]; |
||
1014 | } |
||
1015 | |||
1016 | $dynamicData[$newKey] = $itemData; |
||
1017 | } |
||
1018 | } |
||
1019 | |||
1020 | return array_merge($initialData, $dynamicData); |
||
1021 | } |
||
1022 | |||
1023 | public function testSkipOnError(): void |
||
1024 | { |
||
1025 | $this->testSkipOnErrorInternal(new Compare(), new Compare(skipOnError: true)); |
||
1026 | } |
||
1027 | |||
1028 | public function testWhen(): void |
||
1029 | { |
||
1030 | $when = static fn (mixed $value): bool => $value !== null; |
||
1031 | $this->testWhenInternal(new Compare(), new Compare(when: $when)); |
||
1032 | } |
||
1033 | } |
||
1034 |