diff --git a/README.md b/README.md index 03d0ff7..9f7bf73 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,18 @@ ![architecture-Keepa_API_Data_Extractor___Architecture.png](architecture-Keepa_API_Data_Extractor___Architecture.png) +### Применяемые паттерны проектирования + +В проекте активно используются следующие паттерны: + +- **DTO (Data Transfer Object)**: Для передачи типизированных данных между слоями (`KeepaInputDto`, `KeepaOutputDto`). +- **Facade (Фасад)**: `KeepaService` скрывает сложность взаимодействия с внешней библиотекой Keepa API и управления токенами. +- **Service Layer (Сервисный слой)**: Разделение бизнес-логики (`FinderProductsService`) и логики работы с API (`KeepaService`). +- **Orchestrator (Оркестратор)**: `FinderProductsService` управляет потоком: валидация -> поиск -> отправка. +- **Command Handler**: Обработка сообщений через `KeepaInputMessageHandler` (CQRS-style в Symfony Messenger). +- **Iterator / Generator**: Использование `yield` для экономии памяти при переборе больших списков товаров. +- **Dependency Injection**: Внедрение зависимостей для обеспечения тестируемости и гибкости. + ## Требования - PHP 8.3+ @@ -389,5 +401,4 @@ CREATE TABLE find_products ( ## License -MIT License — see [LICENSE](LICENSE) for details -ываыва \ No newline at end of file +MIT License — see [LICENSE](LICENSE) for details \ No newline at end of file diff --git a/tests/Unit/Dto/KeepaInputDtoTest.php b/tests/Unit/Dto/KeepaInputDtoTest.php index 38c5c4c..d071043 100644 --- a/tests/Unit/Dto/KeepaInputDtoTest.php +++ b/tests/Unit/Dto/KeepaInputDtoTest.php @@ -45,4 +45,20 @@ public function testFromArrayNormalizesAndCastsFields(): void 'step' => 3, ], $dto->toArray()); } + + public function testGetBrandReturnsNormalizedBrand(): void + { + $dto = KeepaInputDto::fromArray([ + 'id' => '10', + 'domain_id' => '3', + 'brand' => " Nike ", + 'time_from' => '2025-02-01', + 'time_to' => '2025-02-28', + 'version' => '1', + 'status' => ' IN_PROGRESS ', + 'step' => '1', + ]); + + self::assertSame('Nike', $dto->getBrand()); + } } diff --git a/tests/Unit/Dto/KeepaOutputDtoTest.php b/tests/Unit/Dto/KeepaOutputDtoTest.php new file mode 100644 index 0000000..88ba88d --- /dev/null +++ b/tests/Unit/Dto/KeepaOutputDtoTest.php @@ -0,0 +1,149 @@ + 'B08N5WRWNW', + 'brand' => 'Adidas', + 'title' => 'Adidas Ultraboost 21', + 'upc_list' => ['123456789012', '234567890123'], + 'ean_list' => ['1234567890123', '2345678901234'], + 'search_request_id' => 42, + 'domain_id' => 1, + 'json_data' => ['key' => 'value'], + 'time_from' => '2025-01-01', + 'time_to' => '2025-01-31', + 'version' => 1, + ]; + + $dto = KeepaOutputDto::fromArray($data); + + self::assertSame('B08N5WRWNW', $dto->asin); + self::assertSame('Adidas', $dto->brand); + self::assertSame('Adidas Ultraboost 21', $dto->title); + self::assertSame(['123456789012', '234567890123'], $dto->upc_list); + self::assertSame(['1234567890123', '2345678901234'], $dto->ean_list); + self::assertSame(42, $dto->search_request_id); + self::assertSame(1, $dto->domain_id); + self::assertSame(['key' => 'value'], $dto->json_data); + self::assertSame('2025-01-01', $dto->time_from); + self::assertSame('2025-01-31', $dto->time_to); + self::assertSame(1, $dto->version); + } + + public function testFromArrayWithMissingFields(): void + { + $data = [ + 'asin' => 'B08N5WRWNW', + 'brand' => 'Adidas', + ]; + + $dto = KeepaOutputDto::fromArray($data); + + self::assertSame('B08N5WRWNW', $dto->asin); + self::assertSame('Adidas', $dto->brand); + self::assertNull($dto->title); + self::assertNull($dto->upc_list); + self::assertNull($dto->ean_list); + self::assertNull($dto->search_request_id); + self::assertNull($dto->domain_id); + self::assertNull($dto->json_data); + self::assertNull($dto->time_from); + self::assertNull($dto->time_to); + self::assertNull($dto->version); + } + + public function testFromArrayCastsIntegerFields(): void + { + $data = [ + 'asin' => 'B08N5WRWNW', + 'search_request_id' => '42', + 'domain_id' => '1', + 'version' => '5', + ]; + + $dto = KeepaOutputDto::fromArray($data); + + self::assertSame(42, $dto->search_request_id); + self::assertSame(1, $dto->domain_id); + self::assertSame(5, $dto->version); + } + + public function testToArrayReturnsAllProperties(): void + { + $dto = new KeepaOutputDto(); + $dto->asin = 'B08N5WRWNW'; + $dto->brand = 'Nike'; + $dto->title = 'Nike Air Max'; + $dto->upc_list = ['111111111111']; + $dto->ean_list = ['2222222222222']; + $dto->search_request_id = 100; + $dto->domain_id = 2; + $dto->json_data = ['test' => 'data']; + $dto->time_from = '2025-02-01'; + $dto->time_to = '2025-02-28'; + $dto->version = 2; + + $array = $dto->toArray(); + + self::assertSame('B08N5WRWNW', $array['asin']); + self::assertSame('Nike', $array['brand']); + self::assertSame('Nike Air Max', $array['title']); + self::assertSame(['111111111111'], $array['upc_list']); + self::assertSame(['2222222222222'], $array['ean_list']); + self::assertSame(100, $array['search_request_id']); + self::assertSame(2, $array['domain_id']); + self::assertSame(['test' => 'data'], $array['json_data']); + self::assertSame('2025-02-01', $array['time_from']); + self::assertSame('2025-02-28', $array['time_to']); + self::assertSame(2, $array['version']); + } + + public function testToArrayWithNullProperties(): void + { + $dto = new KeepaOutputDto(); + + $array = $dto->toArray(); + + self::assertNull($array['asin']); + self::assertNull($array['brand']); + self::assertNull($array['title']); + self::assertNull($array['upc_list']); + self::assertNull($array['ean_list']); + self::assertNull($array['search_request_id']); + self::assertNull($array['domain_id']); + self::assertNull($array['json_data']); + self::assertNull($array['time_from']); + self::assertNull($array['time_to']); + self::assertNull($array['version']); + } + + public function testRoundTripConversion(): void + { + $originalData = [ + 'asin' => 'B08N5WRWNW', + 'brand' => 'Puma', + 'title' => 'Puma Suede Classic', + 'upc_list' => ['333333333333'], + 'ean_list' => ['4444444444444'], + 'search_request_id' => 999, + 'domain_id' => 3, + 'json_data' => ['nested' => ['data' => 'here']], + 'time_from' => '2025-03-01', + 'time_to' => '2025-03-31', + 'version' => 10, + ]; + + $dto = KeepaOutputDto::fromArray($originalData); + $resultArray = $dto->toArray(); + + self::assertSame($originalData, $resultArray); + } +} diff --git a/tests/Unit/Service/FinderProductsServiceTest.php b/tests/Unit/Service/FinderProductsServiceTest.php new file mode 100644 index 0000000..2af0a06 --- /dev/null +++ b/tests/Unit/Service/FinderProductsServiceTest.php @@ -0,0 +1,186 @@ +logger = $this->createMock(LoggerInterface::class); + $this->keepaService = $this->createMock(KeepaService::class); + $this->messageValidator = $this->createMock(MessageValidator::class); + $this->bus = $this->createMock(MessageBusInterface::class); + } + + public function testExecuteReturnsDefaultStatsWhenInputIsInvalid(): void + { + $dto = KeepaInputDto::fromArray([ + 'id' => 1, + 'domain_id' => 1, + 'brand' => 'adidas', + 'time_from' => '2025-01-01', + 'time_to' => '2025-01-31', + 'version' => 1, + 'status' => 'PENDING', + 'step' => 0, + ]); + + $this->messageValidator + ->expects(self::once()) + ->method('validateInput') + ->with($dto) + ->willReturn(null); + + $this->logger + ->expects(self::once()) + ->method('warning') + ->with( + self::stringContains('Invalid Input Kafka message'), + self::anything(), + ); + + $this->keepaService + ->expects(self::never()) + ->method('execute'); + + $this->bus + ->expects(self::never()) + ->method('dispatch'); + + $service = new FinderProductsService( + $this->logger, + $this->keepaService, + $this->messageValidator, + $this->bus, + ); + + $result = $service->execute($dto); + + self::assertSame( + [ + 'asinsFound' => 0, + 'tokensUsed' => 0, + 'pages' => 0, + 'sleepSeconds' => 0, + ], + $result, + ); + } + + public function testExecuteDispatchesValidOutputsAndReturnsMappedStats(): void + { + $inputDto = KeepaInputDto::fromArray([ + 'id' => 2, + 'domain_id' => 3, + 'brand' => 'nike', + 'time_from' => '2025-02-01', + 'time_to' => '2025-02-28', + 'version' => 1, + 'status' => 'IN_PROGRESS', + 'step' => 1, + ]); + + $validatedInput = clone $inputDto; + + $this->messageValidator + ->expects(self::once()) + ->method('validateInput') + ->with($inputDto) + ->willReturn($validatedInput); + + $product = new KeepaOutputDto(); + $product->asin = 'B000TEST00'; + + $generator = (function () use ($product) { + yield $product; + })(); + + $this->keepaService + ->expects(self::once()) + ->method('execute') + ->with($validatedInput) + ->willReturn($generator); + + $validatedOutput = clone $product; + + $this->messageValidator + ->expects(self::once()) + ->method('validateOutput') + ->with($product) + ->willReturn($validatedOutput); + + $this->bus + ->expects(self::once()) + ->method('dispatch') + ->with($validatedOutput) + ->willReturn(new Envelope($validatedOutput)); + + $this->keepaService + ->expects(self::once()) + ->method('getStats') + ->willReturn([ + 'asinsFound' => 5, + 'tokensUsed' => 42, + 'pagesProcessed' => 3, + 'sleepSeconds' => 7, + 'tokensLeft' => 100, + ]); + + $service = new FinderProductsService( + $this->logger, + $this->keepaService, + $this->messageValidator, + $this->bus, + ); + + $result = $service->execute($inputDto); + + self::assertSame( + [ + 'asinsFound' => 5, + 'tokensUsed' => 42, + 'pages' => 3, + 'sleepSeconds' => 7, + 'tokensLeft' => 100, + ], + $result, + ); + } + + public function testGetKeepaServiceReturnsInjectedInstance(): void + { + $service = new FinderProductsService( + $this->logger, + $this->keepaService, + $this->messageValidator, + $this->bus, + ); + + self::assertSame($this->keepaService, $service->getKeepaService()); + } +}