Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+
Expand Down Expand Up @@ -389,5 +401,4 @@ CREATE TABLE find_products (

## License

MIT License — see [LICENSE](LICENSE) for details
ываыва
MIT License — see [LICENSE](LICENSE) for details
16 changes: 16 additions & 0 deletions tests/Unit/Dto/KeepaInputDtoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
149 changes: 149 additions & 0 deletions tests/Unit/Dto/KeepaOutputDtoTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace App\Tests\Unit\Dto;

use App\Dto\KeepaOutputDto;
use PHPUnit\Framework\TestCase;

class KeepaOutputDtoTest extends TestCase
{
public function testFromArrayWithCompleteData(): void
{
$data = [
'asin' => '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);
}
}
186 changes: 186 additions & 0 deletions tests/Unit/Service/FinderProductsServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

namespace App\Tests\Unit\Service;

use App\Dto\KeepaInputDto;
use App\Dto\KeepaOutputDto;
use App\Service\FinderProductsService;
use App\Service\KeepaService;
use App\Service\Validator\MessageValidator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Envelope;

class FinderProductsServiceTest extends TestCase
{
/** @var LoggerInterface&MockObject */
private LoggerInterface $logger;

/** @var KeepaService&MockObject */
private KeepaService $keepaService;

/** @var MessageValidator&MockObject */
private MessageValidator $messageValidator;

/** @var MessageBusInterface&MockObject */
private MessageBusInterface $bus;

protected function setUp(): void
{
parent::setUp();

$this->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());
}
}
Loading