false, 'timeout' => 3, ]))->request('GET', $url); } catch (GuzzleException) { return false; } return $result->getStatusCode() === $code; } /** * Проверяет доступность API мониторинга * * @return bool */ protected function isMonitoringOnline(): bool { return $this->ping('https://testonline.atol.ru/api/auth/v1/gettoken', 400); } /** * Пропускает текущий тест если API мониторинга недоступен */ protected function skipIfMonitoringIsOffline(): void { if (!$this->isMonitoringOnline()) { $this->markTestSkipped($this->getName() . ': Monitoring API is inaccessible. Skipping test.'); } } //------------------------------------------------------------------------------------------------------------------ // Дополнительные ассерты //------------------------------------------------------------------------------------------------------------------ /** * Тестирует является ли объект приводимым к json-строке согласно схеме АТОЛ Онлайн * * @param Entity|EntityCollection $entity * @param array|null $json_structure * @covers \AtolOnline\Entities\Entity::__toString * @covers \AtolOnline\Entities\Entity::toArray * @covers \AtolOnline\Entities\Entity::jsonSerialize * @covers \AtolOnline\Collections\EntityCollection::jsonSerialize * @throws Exception */ public function assertIsAtolable(Entity | EntityCollection $entity, ?array $json_structure = []): void { $this->assertIsArray($entity->jsonSerialize()); $this->assertIsArray($entity->toArray()); $this->assertSame($entity->jsonSerialize(), $entity->toArray()); $this->assertIsString((string)$entity); $this->assertJson((string)$entity); if (!empty($json_structure)) { $this->assertSame(json_encode($json_structure), (string)$entity); } } //------------------------------------------------------------------------------------------------------------------ // Ассерты проверки наследования //------------------------------------------------------------------------------------------------------------------ /** * Тестирует идентичность двух классов * * @param object|string $expected Ожидаемый класс * @param object|string $actual Фактический класс */ public function assertIsSameClass(object | string $expected, object | string $actual): void { $this->assertTrue($this->checkisSameClass($expected, $actual)); } /** * Проверяет идентичность двух классов * * @param object|string $class1 * @param object|string $class2 * @return bool */ private function checkisSameClass(object | string $class1, object | string $class2): bool { return (is_object($class1) ? $class1::class : $class1) === (is_object($class2) ? $class2::class : $class2); } /** * Тестирует наследование класса (объекта) от указанных классов * * @param array $expected Массив ожидаемых имён классов-родителей * @param object|string $actual Объект или имя класса для проверки */ public function assertExtendsClasses(array $expected, object | string $actual): void { $this->assertTrue($this->checkExtendsClasses($expected, $actual)); } /** * Проверяет наследование класса (объекта) от указанных классов * * @param string[] $parents Имена классов-родителей * @param object|string $class Объект или имя класса для проверки */ private function checkExtendsClasses(array $parents, object | string $class): bool { return !empty(array_intersect($parents, is_object($class) ? class_parents($class) : [$class])); } /** * Тестирует имплементацию классом (объектом) указанных интерфейсов * * @param string[] $expected Массив ожидаемых имён интерфейсов * @param object|string $actual Объект или имя класса для проверки */ public function assertImplementsInterfaces(array $expected, object | string $actual): void { $this->assertTrue($this->checkImplementsInterfaces($expected, $actual)); } /** * Проверяет имплементацию классом (объектом) указанных интерфейсов * * @param string[] $interfaces Имена классов-интерфейсов * @param object|string $class Объект или имя класса для проверки * @see https://www.php.net/manual/ru/function.class-implements.php */ private function checkImplementsInterfaces(array $interfaces, object | string $class): bool { return !empty(array_intersect($interfaces, is_object($class) ? class_implements($class) : [$class])); } /** * Тестирует использование классом (объектом) указанных трейтов * * @param string[] $expected Массив ожидаемых имён трейтов * @param object|string $actual Объект или имя класса для проверки */ public function assertUsesTraits(array $expected, object | string $actual): void { $this->assertTrue($this->checkUsesTraits($expected, $actual)); } /** * Проверяет использование классом (объектом) указанных трейтов (исключает родителей) * * @param string[] $traits Массив ожидаемых имён трейтов * @param object|string $class Объект или имя класса для проверки * @return bool * @see https://www.php.net/manual/ru/function.class-uses.php#110752 */ private function checkUsesTraits(array $traits, object | string $class): bool { $found_traits = []; $check_class = is_object($class) ? $class::class : $class; do { $found_traits = array_merge(class_uses($check_class), $found_traits); } while ($check_class = get_parent_class($check_class)); foreach ($found_traits as $trait => $same) { $found_traits = array_merge(class_uses($trait), $found_traits); } return !empty(array_intersect(array_unique($found_traits), $traits)); } /** * Тестирует, является ли объект коллекцией * * @param mixed $value */ public function assertIsCollection(mixed $value): void { $this->assertIsObject($value); $this->assertIsIterable($value); $this->assertTrue( $this->checkisSameClass(Collection::class, $value) || $this->checkExtendsClasses([Collection::class], $value) ); } //------------------------------------------------------------------------------------------------------------------ // Провайдеры данных для прогона тестов //------------------------------------------------------------------------------------------------------------------ /** * Провайдер строк, которые приводятся к null * * @return array */ public function providerNullableStrings(): array { return [ [''], [' '], [null], ["\n\r\t"], ]; } /** * Провайдер валидных телефонов * * @return array> */ public function providerValidPhones(): array { return [ ['+79991234567', '+79991234567'], ['79991234567', '+79991234567'], ['89991234567', '+89991234567'], ['+7 999 123 45 67', '+79991234567'], ['+7 (999) 123-45-67', '+79991234567'], ["+7 %(?9:9\"9')abc\r123\n45\t67\0", '+79991234567'], ]; } /** * Провайдер телефонов, которые приводятся к null * * @return array> */ public function providerNullablePhones(): array { return array_merge( $this->providerNullableStrings(), [ [Helpers::randomStr(10, false)], ["asdfgvs \n\rtt\t*/(*&%^*$%"], ] ); } /** * Провайдер валидных email-ов * * @return array> */ public function providerValidEmails(): array { return [ ['abc@mail.com'], ['abc-d@mail.com'], ['abc.def@mail.com'], ['abc.def@mail.org'], ['abc.def@mail-archive.com'], ]; } /** * Провайдер невалидных email-ов * * @return array> */ public function providerInvalidEmails(): array { return [ ['@example'], [Helpers::randomStr(15)], ['@example.com'], ['abc.def@mail'], ['.abc@mail.com'], ['example@example'], ['abc..def@mail.com'], ['abc.def@mail..com'], ['abc.def@mail#archive.com'], ]; } //------------------------------------------------------------------------------------------------------------------ // Генераторы тестовых объектов //------------------------------------------------------------------------------------------------------------------ /** * Генерирует массив тестовых объектов предметов расчёта * * @param int $count * @return Item[] * @throws EmptyItemNameException * @throws NegativeItemPriceException * @throws NegativeItemQuantityException * @throws TooHighItemPriceException * @throws TooLongItemNameException * @throws TooManyException * @throws Exception */ protected function generateItemObjects(int $count = 1): iterable { for ($i = 0; $i < abs($count); ++$i) { yield new Item(Helpers::randomStr(), random_int(1, 100), random_int(1, 10)); } } /** * Генерирует массив тестовых объектов оплаты * * @param int $count * @return Payment[] * @throws NegativePaymentSumException * @throws TooHighPaymentSumException * @throws Exception */ protected function generatePaymentObjects(int $count = 1): iterable { $types = PaymentType::cases(); for ($i = 0; $i < abs($count); ++$i) { yield new Payment( $types[random_int(0, count($types) - 1)], random_int(1, 100) * 2 / 3 ); } } /** * Генерирует массив тестовых объектов ставок НДС * * @param int $count * @return Vat[] * @throws InvalidEnumValueException * @throws Exception */ protected function generateVatObjects(int $count = 1): iterable { $types = VatType::cases(); for ($i = 0; $i < abs($count); ++$i) { yield new Vat( $types[random_int(0, count($types) - 1)], random_int(1, 100) * 2 / 3 ); } } /** * Возвращает валидный тестовый объект чека прихода * * @return Receipt * @throws EmptyItemNameException * @throws EmptyItemsException * @throws InvalidEntityInCollectionException * @throws NegativeItemPriceException * @throws NegativeItemQuantityException * @throws NegativePaymentSumException * @throws TooHighItemPriceException * @throws TooHighPaymentSumException * @throws TooLongItemNameException * @throws TooManyException */ protected function newReceipt(): Receipt { return new Receipt( new Client('John Doe', '+79501234567', 'john@example.com', '1234567890'), new Company('1234567890', SnoType::OSN, 'https://example.com', 'company@example.com'), new Items($this->generateItemObjects(2)), new Payments($this->generatePaymentObjects()) ); } /** * Возвращает валидный тестовый объект чека * * @return Correction * @throws InvalidEntityInCollectionException * @throws InvalidEnumValueException * @throws NegativePaymentSumException * @throws TooHighPaymentSumException * @throws EmptyCorrectionNumberException * @throws InvalidCorrectionDateException */ protected function newCorrection(): Correction { return new Correction( new Company('1234567890', SnoType::OSN, 'https://example.com', 'company@example.com'), new CorrectionInfo(CorrectionType::SELF, '01.01.2021', Helpers::randomStr()), new Payments($this->generatePaymentObjects(2)), new Vats($this->generateVatObjects(2)), ); } }