feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Tests\Unit\Application\Storage;
|
|
|
|
|
|
|
|
|
|
use App\Domain\Storage\Repository\StoragePathRepositoryInterface;
|
|
|
|
|
use App\Domain\Storage\StoragePath;
|
|
|
|
|
use App\Infrastructure\Storage\LocalStorageManager;
|
|
|
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
|
|
|
|
|
|
final class LocalStorageManagerTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
private StoragePathRepositoryInterface&MockObject $repo;
|
|
|
|
|
private LocalStorageManager $manager;
|
|
|
|
|
private string $tmpFile;
|
|
|
|
|
|
|
|
|
|
protected function setUp(): void
|
|
|
|
|
{
|
|
|
|
|
$this->repo = $this->createMock(StoragePathRepositoryInterface::class);
|
|
|
|
|
$this->manager = new LocalStorageManager($this->repo);
|
|
|
|
|
$this->tmpFile = sys_get_temp_dir().'/test-upload-'.uniqid().'.jpg';
|
|
|
|
|
file_put_contents($this->tmpFile, str_repeat('x', 100));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function tearDown(): void
|
|
|
|
|
{
|
|
|
|
|
if (file_exists($this->tmpFile)) {
|
|
|
|
|
unlink($this->tmpFile);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 22:44:16 +00:00
|
|
|
public function testStorePicksActivePathWithQuota(): void
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
{
|
|
|
|
|
$path = new StoragePath('Main', sys_get_temp_dir().'/storage-test-'.uniqid(), 1_000_000, 10);
|
|
|
|
|
mkdir($path->getBasePath(), recursive: true);
|
|
|
|
|
|
|
|
|
|
$this->repo->expects($this->once())
|
|
|
|
|
->method('findActiveSortedByPriority')
|
|
|
|
|
->willReturn([$path]);
|
|
|
|
|
|
|
|
|
|
$stored = $this->manager->store($this->tmpFile, 'photo.jpg');
|
|
|
|
|
|
|
|
|
|
$this->assertSame($path->getId()->toRfc4122(), $stored->storagePath->getId()->toRfc4122());
|
|
|
|
|
$this->assertStringEndsWith('.jpg', $stored->filename);
|
|
|
|
|
$this->assertFileExists($path->getBasePath().'/'.$stored->filename);
|
|
|
|
|
|
|
|
|
|
// cleanup
|
|
|
|
|
unlink($path->getBasePath().'/'.$stored->filename);
|
|
|
|
|
rmdir($path->getBasePath());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 22:44:16 +00:00
|
|
|
public function testThrowsWhenNoActivePath(): void
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
{
|
|
|
|
|
$this->repo->method('findActiveSortedByPriority')->willReturn([]);
|
|
|
|
|
|
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
|
|
|
$this->expectExceptionMessage('No active storage path');
|
|
|
|
|
|
|
|
|
|
$this->manager->store($this->tmpFile, 'photo.jpg');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 22:44:16 +00:00
|
|
|
public function testSkipsFullPathAndUsesNext(): void
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
{
|
|
|
|
|
$fullPath = new StoragePath('Full', sys_get_temp_dir().'/full-'.uniqid(), 50, 20);
|
|
|
|
|
$okPath = new StoragePath('OK', sys_get_temp_dir().'/ok-'.uniqid(), 1_000_000, 10);
|
|
|
|
|
mkdir($fullPath->getBasePath(), recursive: true);
|
|
|
|
|
mkdir($okPath->getBasePath(), recursive: true);
|
|
|
|
|
|
|
|
|
|
// fullPath quota (50 bytes) is less than file size (100 bytes) — skip it
|
|
|
|
|
$this->repo->method('findActiveSortedByPriority')->willReturn([$fullPath, $okPath]);
|
|
|
|
|
|
|
|
|
|
$stored = $this->manager->store($this->tmpFile, 'photo.jpg');
|
|
|
|
|
|
|
|
|
|
$this->assertSame($okPath->getId()->toRfc4122(), $stored->storagePath->getId()->toRfc4122());
|
|
|
|
|
|
|
|
|
|
unlink($okPath->getBasePath().'/'.$stored->filename);
|
|
|
|
|
rmdir($fullPath->getBasePath());
|
|
|
|
|
rmdir($okPath->getBasePath());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 22:44:16 +00:00
|
|
|
public function testGetFullPath(): void
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
{
|
|
|
|
|
$path = new StoragePath('Main', '/srv/storage', 1_000_000, 10);
|
|
|
|
|
$this->assertSame('/srv/storage/photo.jpg', $this->manager->getFullPath($path, 'photo.jpg'));
|
|
|
|
|
}
|
|
|
|
|
}
|