diff --git a/tests/Integration/Infra/Adapters/Directories/EloquentIncoSheetDirectoryTest.php b/tests/Integration/Infra/Adapters/Directories/EloquentIncoSheetDirectoryTest.php new file mode 100644 index 0000000..ea84475 --- /dev/null +++ b/tests/Integration/Infra/Adapters/Directories/EloquentIncoSheetDirectoryTest.php @@ -0,0 +1,160 @@ +delete(); + ArcadiaBrand::query()->firstOrCreate(['code' => 1], ['libelle' => 'Le Gaulois']); + ArcadiaBrand::query()->firstOrCreate(['code' => 2], ['libelle' => 'Maître CoQ']); + }); + + it('finds an inco sheet by IncoId and resolves its IncoBrand', function (): void { + // GIVEN + $resolver = app(BrandResolver::class); + $repository = app(BrandRepository::class); + $directory = new EloquentIncoSheetDirectory($resolver, $repository); + + IncoSheetModel::factory()->create([ + 'id' => '1000101', + 'name' => 'Poulet Rôti', + 'brand' => ['code' => 1, 'label' => 'Le Gaulois'], + ]); + + $incoId = new IncoId('1000101'); + + // WHEN + $result = $directory->find($incoId); + + // THEN + expect($result)->toBeInstanceOf(IncoSheet::class) + ->and((string) $result?->id->value)->toBe('1000101') + ->and($result?->name)->toBe('Poulet Rôti') + ->and($result?->brand->name->value)->toBe('Le Gaulois') + ->and($result?->brand->arcadiaBrandCode->value)->toBe(1) + ->and($result?->brand->code->value)->toMatch('/^[A-Z]{3}\d{2}$/'); + }); + + it('returns null when inco sheet is not found', function (): void { + // GIVEN + $resolver = app(BrandResolver::class); + $repository = app(BrandRepository::class); + $directory = new EloquentIncoSheetDirectory($resolver, $repository); + + // WHEN + $result = $directory->find(new IncoId('9999999')); + + // THEN + expect($result)->toBeNull(); + }); + + it('finds all inco sheets filtered by BrandCode', function (): void { + // GIVEN + $resolver = app(BrandResolver::class); + $repository = app(BrandRepository::class); + $directory = new EloquentIncoSheetDirectory($resolver, $repository); + + $brand1 = $resolver->resolve(new ArcadiaId(1)); + $brand2 = $resolver->resolve(new ArcadiaId(2)); + + IncoSheetModel::factory()->create([ + 'id' => '1000101', + 'brand' => ['code' => 1, 'label' => 'Le Gaulois'], + ]); + IncoSheetModel::factory()->create([ + 'id' => '1000102', + 'brand' => ['code' => 2, 'label' => 'Maître CoQ'], + ]); + + $pagination = Pagination::ofPage(1, 10); + $sorter = new IncoSheetSorter(IncoSheetSortColumn::incoSheetId, \SortDirection::Ascending); + $filter = new IncoSheetFilter(null, $brand1->code); + + // WHEN + $result = $directory->findAll($pagination, $sorter, $filter); + + // THEN + expect($result->getTotal())->toBe(1) + ->and($result->getItems())->toHaveCount(1) + ->and((string) $result->getItems()[0]->id->value)->toBe('1000101'); + }); + + it('searches inco sheets by id query and BrandCode', function (): void { + // GIVEN + $resolver = app(BrandResolver::class); + $repository = app(BrandRepository::class); + $directory = new EloquentIncoSheetDirectory($resolver, $repository); + + $brand1 = $resolver->resolve(new ArcadiaId(1)); + + IncoSheetModel::factory()->create([ + 'id' => '1234501', + 'brand' => ['code' => 1, 'label' => 'Le Gaulois'], + ]); + IncoSheetModel::factory()->create([ + 'id' => '1234502', + 'brand' => ['code' => 2, 'label' => 'Maître CoQ'], + ]); + IncoSheetModel::factory()->create([ + 'id' => '9999901', + 'brand' => ['code' => 1, 'label' => 'Le Gaulois'], + ]); + + $query = new IncoIdQuery('1234%'); + $pagination = Pagination::ofPage(1, 10); + + // WHEN + $result = $directory->search($query, $pagination, $brand1->code); + + // THEN + expect($result->getTotal())->toBe(1) + ->and($result->getItems())->toHaveCount(1) + ->and((string) $result->getItems()[0]->id->value)->toBe('1234501'); + }); + + it('returns empty paginated result when filtering by non-existent BrandCode', function (): void { + // GIVEN + $resolver = app(BrandResolver::class); + $repository = app(BrandRepository::class); + $directory = new EloquentIncoSheetDirectory($resolver, $repository); + + IncoSheetModel::factory()->create([ + 'id' => '1000101', + 'brand' => ['code' => 1, 'label' => 'Le Gaulois'], + ]); + + $unknownCode = new BrandCode('ZZZ01'); + $pagination = Pagination::ofPage(1, 10); + $sorter = new IncoSheetSorter(IncoSheetSortColumn::incoSheetId, \SortDirection::Ascending); + $filter = new IncoSheetFilter(null, $unknownCode); + + // WHEN + $result = $directory->findAll($pagination, $sorter, $filter); + + // THEN + expect($result->getTotal())->toBe(0) + ->and($result->getItems())->toBeEmpty(); + }); +}); diff --git a/tests/Integration/Infra/Adapters/Directories/EloquentIncoShortSheetDirectoryTest.php b/tests/Integration/Infra/Adapters/Directories/EloquentIncoShortSheetDirectoryTest.php new file mode 100644 index 0000000..58ed650 --- /dev/null +++ b/tests/Integration/Infra/Adapters/Directories/EloquentIncoShortSheetDirectoryTest.php @@ -0,0 +1,148 @@ +delete(); + ArcadiaBrand::query()->firstOrCreate(['code' => 1], ['libelle' => 'Le Gaulois']); + ArcadiaBrand::query()->firstOrCreate(['code' => 2], ['libelle' => 'Maître CoQ']); + }); + + it('finds an inco short sheet by IncoId', function (): void { + // GIVEN + $repository = app(BrandRepository::class); + $directory = new EloquentIncoShortSheetDirectory($repository); + + IncoSheetModel::factory()->create([ + 'id' => '1000101', + 'name' => 'Poulet Rôti Court', + ]); + + $incoId = new IncoId('1000101'); + + // WHEN + $result = $directory->find($incoId); + + // THEN + expect($result)->toBeInstanceOf(IncoShortSheet::class) + ->and((string) $result?->id->value)->toBe('1000101') + ->and($result?->name)->toBe('Poulet Rôti Court'); + }); + + it('returns null when inco short sheet is not found', function (): void { + // GIVEN + $repository = app(BrandRepository::class); + $directory = new EloquentIncoShortSheetDirectory($repository); + + // WHEN + $result = $directory->find(new IncoId('9999999')); + + // THEN + expect($result)->toBeNull(); + }); + + it('finds all inco short sheets filtered by BrandCode', function (): void { + // GIVEN + $resolver = app(BrandResolver::class); + $repository = app(BrandRepository::class); + $directory = new EloquentIncoShortSheetDirectory($repository); + + $brand1 = $resolver->resolve(new ArcadiaId(1)); + + IncoSheetModel::factory()->create([ + 'id' => '1000101', + 'brand' => ['code' => 1, 'label' => 'Le Gaulois'], + ]); + IncoSheetModel::factory()->create([ + 'id' => '1000102', + 'brand' => ['code' => 2, 'label' => 'Maître CoQ'], + ]); + + $pagination = Pagination::ofPage(1, 10); + $sorter = new IncoSheetSorter(IncoSheetSortColumn::incoSheetId, \SortDirection::Ascending); + $filter = new IncoSheetFilter(null, $brand1->code); + + // WHEN + $result = $directory->findAll($pagination, $sorter, $filter); + + // THEN + expect($result->getTotal())->toBe(1) + ->and($result->getItems())->toHaveCount(1) + ->and((string) $result->getItems()[0]->id->value)->toBe('1000101'); + }); + + it('searches inco short sheets by id query and BrandCode', function (): void { + // GIVEN + $resolver = app(BrandResolver::class); + $repository = app(BrandRepository::class); + $directory = new EloquentIncoShortSheetDirectory($repository); + + $brand1 = $resolver->resolve(new ArcadiaId(1)); + + IncoSheetModel::factory()->create([ + 'id' => '1234501', + 'brand' => ['code' => 1, 'label' => 'Le Gaulois'], + ]); + IncoSheetModel::factory()->create([ + 'id' => '1234502', + 'brand' => ['code' => 2, 'label' => 'Maître CoQ'], + ]); + + $query = new IncoIdQuery('1234%'); + $pagination = Pagination::ofPage(1, 10); + + // WHEN + $result = $directory->search($query, $pagination, $brand1->code); + + // THEN + expect($result->getTotal())->toBe(1) + ->and($result->getItems())->toHaveCount(1) + ->and((string) $result->getItems()[0]->id->value)->toBe('1234501'); + }); + + it('returns empty paginated short sheets when filtering by non-existent BrandCode', function (): void { + // GIVEN + $repository = app(BrandRepository::class); + $directory = new EloquentIncoShortSheetDirectory($repository); + + IncoSheetModel::factory()->create([ + 'id' => '1000101', + 'brand' => ['code' => 1, 'label' => 'Le Gaulois'], + ]); + + $unknownCode = new BrandCode('ZZZ01'); + $pagination = Pagination::ofPage(1, 10); + $sorter = new IncoSheetSorter(IncoSheetSortColumn::incoSheetId, \SortDirection::Ascending); + $filter = new IncoSheetFilter(null, $unknownCode); + + // WHEN + $result = $directory->findAll($pagination, $sorter, $filter); + + // THEN + expect($result->getTotal())->toBe(0) + ->and($result->getItems())->toBeEmpty(); + }); +}); diff --git a/tests/Integration/Infra/Adapters/Repositories/EloquentBrandRepositoryTest.php b/tests/Integration/Infra/Adapters/Repositories/EloquentBrandRepositoryTest.php new file mode 100644 index 0000000..e758291 --- /dev/null +++ b/tests/Integration/Infra/Adapters/Repositories/EloquentBrandRepositoryTest.php @@ -0,0 +1,259 @@ +delete(); + ArcadiaBrand::query()->delete(); +}); + +describe('EloquentBrandRepository Integration Tests', function (): void { + it('persists a new brand into database and invalidates cache', function (): void { + // GIVEN + $repository = new EloquentBrandRepository(); + $brandId = new BrandId(Uuid::uuid7()); + $brandCode = new BrandCode('LGL01'); + $arcadiaId = new ArcadiaId(1); + $name = new BrandName('Le Gaulois'); + + ArcadiaBrand::query()->firstOrCreate([ + 'code' => 1, + ], [ + 'libelle' => 'Le Gaulois', + ]); + + $brand = Brand::hydrate( + id: $brandId, + name: $name, + code: $brandCode, + arcadiaId: $arcadiaId, + ); + + // WHEN + $repository->persist($brand); + + // THEN + $found = $repository->find($brandId); + expect($found)->not->toBeNull() + ->and($found->id()->value)->toBe($brandId->value) + ->and($found->code->value)->toBe('LGL01') + ->and($found->arcadiaId->value)->toBe(1) + ->and($found->name->value)->toBe('Le Gaulois'); + }); + + it('finds a brand by BrandId and serves subsequent calls from cache', function (): void { + // GIVEN + $repository = new EloquentBrandRepository(); + $rawId = Uuid::uuid7()->toString(); + + ArcadiaBrand::query()->firstOrCreate(['code' => 10], ['libelle' => 'Maître CoQ']); + BrandModel::query()->create([ + 'id' => $rawId, + 'arcadia_id' => 10, + 'uwil_public_id' => 'MCQ01', + ]); + + $brandId = new BrandId(Uuid::fromString($rawId)); + + // WHEN + $firstResult = $repository->find($brandId); + + // Verify cache was populated + $cachedData = Cache::get("brand:id:{$rawId}"); + expect($cachedData)->toBeArray() + ->and($cachedData['code'])->toBe('MCQ01'); + + // Delete from DB directly to ensure second call is served from cache + BrandModel::query()->where('id', $rawId)->delete(); + $secondResult = $repository->find($brandId); + + // THEN + expect($firstResult)->not->toBeNull() + ->and($secondResult)->not->toBeNull() + ->and($secondResult->id()->value)->toBe($brandId->value) + ->and($secondResult->code->value)->toBe('MCQ01'); + }); + + it('finds a brand by BrandCode and serves subsequent calls from cache', function (): void { + // GIVEN + $repository = new EloquentBrandRepository(); + $rawId = Uuid::uuid7()->toString(); + + ArcadiaBrand::query()->firstOrCreate(['code' => 20], ['libelle' => 'Doux']); + BrandModel::query()->create([ + 'id' => $rawId, + 'arcadia_id' => 20, + 'uwil_public_id' => 'DOX01', + ]); + + $brandCode = new BrandCode('DOX01'); + + // WHEN + $firstResult = $repository->findByCode($brandCode); + + // Delete from DB directly to ensure second call is served from cache + BrandModel::query()->where('id', $rawId)->delete(); + $secondResult = $repository->findByCode($brandCode); + + // THEN + expect($firstResult)->not->toBeNull() + ->and($secondResult)->not->toBeNull() + ->and($secondResult->code->value)->toBe('DOX01'); + }); + + it('finds a brand by ArcadiaId and serves subsequent calls from cache', function (): void { + // GIVEN + $repository = new EloquentBrandRepository(); + $rawId = Uuid::uuid7()->toString(); + + ArcadiaBrand::query()->firstOrCreate(['code' => 30], ['libelle' => 'Père Dodu']); + BrandModel::query()->create([ + 'id' => $rawId, + 'arcadia_id' => 30, + 'uwil_public_id' => 'PRD01', + ]); + + $arcadiaId = new ArcadiaId(30); + + // WHEN + $firstResult = $repository->findByArcadiaId($arcadiaId); + + // Delete from DB directly to ensure second call is served from cache + BrandModel::query()->where('id', $rawId)->delete(); + $secondResult = $repository->findByArcadiaId($arcadiaId); + + // THEN + expect($firstResult)->not->toBeNull() + ->and($secondResult)->not->toBeNull() + ->and($secondResult->arcadiaId->value)->toBe(30); + }); + + it('finds all brands and caches the list', function (): void { + // GIVEN + $repository = new EloquentBrandRepository(); + + ArcadiaBrand::query()->firstOrCreate(['code' => 1], ['libelle' => 'Brand 1']); + ArcadiaBrand::query()->firstOrCreate(['code' => 2], ['libelle' => 'Brand 2']); + + BrandModel::query()->create([ + 'id' => Uuid::uuid7()->toString(), + 'arcadia_id' => 1, + 'uwil_public_id' => 'BRD01', + ]); + BrandModel::query()->create([ + 'id' => Uuid::uuid7()->toString(), + 'arcadia_id' => 2, + 'uwil_public_id' => 'BRD02', + ]); + + // WHEN + $all = $repository->findAll(); + + // THEN + expect($all)->toHaveCount(2) + ->and(Cache::has('brand:all'))->toBeTrue(); + }); + + it('gracefully recovers when cache contains corrupted or non-array data', function (): void { + // GIVEN + $repository = new EloquentBrandRepository(); + $rawId = Uuid::uuid7()->toString(); + + ArcadiaBrand::query()->firstOrCreate(['code' => 40], ['libelle' => 'Fermiers d\'Ancenis']); + BrandModel::query()->create([ + 'id' => $rawId, + 'arcadia_id' => 40, + 'uwil_public_id' => 'FDA01', + ]); + + $brandId = new BrandId(Uuid::fromString($rawId)); + $brandCode = new BrandCode('FDA01'); + $arcadiaId = new ArcadiaId(40); + + // Pollute cache with invalid data (simulating stale objects or corrupted cache) + Cache::put("brand:id:{$rawId}", 'corrupted_string'); + Cache::put('brand:code:FDA01', ['invalid_key' => 123]); + Cache::put('brand:arcadia:40', 99999); + + // WHEN + $byId = $repository->find($brandId); + $byCode = $repository->findByCode($brandCode); + $byArcadia = $repository->findByArcadiaId($arcadiaId); + + // THEN + expect($byId)->not->toBeNull() + ->and($byId->code->value)->toBe('FDA01') + ->and($byCode)->not->toBeNull() + ->and($byCode->code->value)->toBe('FDA01') + ->and($byArcadia)->not->toBeNull() + ->and($byArcadia->code->value)->toBe('FDA01'); + }); + + it('deletes an existing brand and invalidates cache', function (): void { + // GIVEN + $repository = new EloquentBrandRepository(); + $rawId = Uuid::uuid7()->toString(); + + ArcadiaBrand::query()->firstOrCreate(['code' => 50], ['libelle' => 'To Delete']); + BrandModel::query()->create([ + 'id' => $rawId, + 'arcadia_id' => 50, + 'uwil_public_id' => 'DEL01', + ]); + + $brand = Brand::hydrate( + id: new BrandId(Uuid::fromString($rawId)), + name: new BrandName('To Delete'), + code: new BrandCode('DEL01'), + arcadiaId: new ArcadiaId(50), + ); + + // Prime cache + $repository->find($brand->id()); + + // WHEN + $repository->delete($brand); + + // THEN + expect(BrandModel::query()->find($rawId))->toBeNull() + ->and(Cache::has("brand:id:{$rawId}"))->toBeFalse() + ->and(Cache::has('brand:code:DEL01'))->toBeFalse() + ->and(Cache::has('brand:arcadia:50'))->toBeFalse() + ->and(Cache::has('brand:all'))->toBeFalse(); + }); + + it('throws BrandNotFoundException when deleting non-existent brand', function (): void { + // GIVEN + $repository = new EloquentBrandRepository(); + $brand = Brand::hydrate( + id: new BrandId(Uuid::uuid7()), + name: new BrandName('Ghost'), + code: new BrandCode('GHO01'), + arcadiaId: new ArcadiaId(999), + ); + + // WHEN & THEN + expect(fn () => $repository->delete($brand)) + ->toThrow(BrandNotFoundException::class); + }); +});