Files
michaelschiemer/tests/Unit/Domain/PreSave/PreSaveRepositoryTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

513 lines
16 KiB
PHP

<?php
declare(strict_types=1);
use App\Domain\PreSave\PreSaveCampaign;
use App\Domain\PreSave\PreSaveCampaignRepositoryInterface;
use App\Domain\PreSave\PreSaveRegistration;
use App\Domain\PreSave\PreSaveRegistrationRepositoryInterface;
use App\Domain\PreSave\ValueObjects\CampaignStatus;
use App\Domain\PreSave\ValueObjects\RegistrationStatus;
use App\Domain\PreSave\ValueObjects\StreamingPlatform;
use App\Domain\PreSave\ValueObjects\TrackUrl;
use App\Framework\Core\ValueObjects\Timestamp;
// Mock Repository Implementations for Testing
class InMemoryPreSaveCampaignRepository implements PreSaveCampaignRepositoryInterface
{
/** @var array<int, PreSaveCampaign> */
private array $campaigns = [];
private int $nextId = 1;
public function save(PreSaveCampaign $campaign): PreSaveCampaign
{
// If campaign has no ID (new), use a wrapper to create with ID
if ($campaign->id === null) {
$campaign = $this->assignId($campaign);
}
$this->campaigns[$campaign->id] = $campaign;
return $campaign;
}
private function assignId(PreSaveCampaign $campaign): PreSaveCampaign
{
$id = $this->nextId++;
return new PreSaveCampaign(
id: $id,
title: $campaign->title,
artistName: $campaign->artistName,
coverImageUrl: $campaign->coverImageUrl,
releaseDate: $campaign->releaseDate,
trackUrls: $campaign->trackUrls,
status: $campaign->status,
createdAt: $campaign->createdAt,
updatedAt: Timestamp::now(),
description: $campaign->description,
startDate: $campaign->startDate
);
}
public function findById(int $id): ?PreSaveCampaign
{
return $this->campaigns[$id] ?? null;
}
/** @return array<PreSaveCampaign> */
public function findAll(array $filters = []): array
{
return array_values($this->campaigns);
}
/** @return array<PreSaveCampaign> */
public function findReadyForRelease(): array
{
$now = Timestamp::now();
return array_filter(
$this->campaigns,
function (PreSaveCampaign $c) use ($now) {
return $c->status->equals(CampaignStatus::ACTIVE)
&& $c->releaseDate->isBefore($now);
}
);
}
/** @return array<PreSaveCampaign> */
public function findReadyForProcessing(): array
{
return array_filter(
$this->campaigns,
fn(PreSaveCampaign $c) => $c->status->equals(CampaignStatus::RELEASED)
);
}
public function delete(int $id): bool
{
if (isset($this->campaigns[$id])) {
unset($this->campaigns[$id]);
return true;
}
return false;
}
/** @return array<string, int> */
public function getStatistics(int $campaignId): array
{
return [
'total_registrations' => 0,
'completed' => 0,
'pending' => 0,
'failed' => 0,
];
}
public function clear(): void
{
$this->campaigns = [];
$this->nextId = 1;
}
}
class InMemoryPreSaveRegistrationRepository implements PreSaveRegistrationRepositoryInterface
{
/** @var array<int, PreSaveRegistration> */
private array $registrations = [];
private int $nextId = 1;
public function save(PreSaveRegistration $registration): PreSaveRegistration
{
if ($registration->id === null) {
$registration = $this->assignId($registration);
}
$this->registrations[$registration->id] = $registration;
return $registration;
}
private function assignId(PreSaveRegistration $registration): PreSaveRegistration
{
$id = $this->nextId++;
return new PreSaveRegistration(
id: $id,
campaignId: $registration->campaignId,
userId: $registration->userId,
platform: $registration->platform,
status: $registration->status,
registeredAt: $registration->registeredAt,
processedAt: $registration->processedAt,
errorMessage: $registration->errorMessage,
retryCount: $registration->retryCount
);
}
public function findForUserAndCampaign(
string $userId,
int $campaignId,
StreamingPlatform $platform
): ?PreSaveRegistration {
foreach ($this->registrations as $registration) {
if ($registration->userId === $userId
&& $registration->campaignId === $campaignId
&& $registration->platform->equals($platform)
) {
return $registration;
}
}
return null;
}
/** @return array<PreSaveRegistration> */
public function findByUserId(string $userId): array
{
return array_values(array_filter(
$this->registrations,
fn(PreSaveRegistration $r) => $r->userId === $userId
));
}
/** @return array<PreSaveRegistration> */
public function findPendingByCampaign(int $campaignId): array
{
return array_values(array_filter(
$this->registrations,
function (PreSaveRegistration $r) use ($campaignId) {
return $r->campaignId === $campaignId
&& $r->status->equals(RegistrationStatus::PENDING);
}
));
}
/** @return array<PreSaveRegistration> */
public function findRetryable(int $campaignId, int $maxRetries): array
{
return array_values(array_filter(
$this->registrations,
function (PreSaveRegistration $r) use ($campaignId, $maxRetries) {
return $r->campaignId === $campaignId
&& $r->status->equals(RegistrationStatus::FAILED)
&& $r->retryCount < $maxRetries;
}
));
}
/** @return array<string, int> */
public function getStatusCounts(int $campaignId): array
{
$registrations = array_filter(
$this->registrations,
fn(PreSaveRegistration $r) => $r->campaignId === $campaignId
);
$counts = [
'pending' => 0,
'completed' => 0,
'failed' => 0,
];
foreach ($registrations as $registration) {
if ($registration->status->equals(RegistrationStatus::PENDING)) {
$counts['pending']++;
} elseif ($registration->status->equals(RegistrationStatus::COMPLETED)) {
$counts['completed']++;
} elseif ($registration->status->equals(RegistrationStatus::FAILED)) {
$counts['failed']++;
}
}
return $counts;
}
public function delete(int $id): bool
{
if (isset($this->registrations[$id])) {
unset($this->registrations[$id]);
return true;
}
return false;
}
public function clear(): void
{
$this->registrations = [];
$this->nextId = 1;
}
}
describe('PreSaveCampaignRepository', function () {
beforeEach(function () {
$this->repository = new InMemoryPreSaveCampaignRepository();
});
afterEach(function () {
$this->repository->clear();
});
it('saves and finds campaigns by id', function () {
$releaseDate = Timestamp::fromDateTime(new DateTimeImmutable('2025-01-15 00:00:00'));
$campaign = PreSaveCampaign::create(
title: 'Test Album',
artistName: 'Test Artist',
coverImageUrl: 'https://example.com/cover.jpg',
releaseDate: $releaseDate,
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:123')]
);
$saved = $this->repository->save($campaign);
expect($saved->id)->not->toBeNull();
$found = $this->repository->findById($saved->id);
expect($found)->not->toBeNull();
expect($found->id)->toBe($saved->id);
expect($found->title)->toBe('Test Album');
expect($found->artistName)->toBe('Test Artist');
});
it('returns null when campaign not found', function () {
$found = $this->repository->findById(999);
expect($found)->toBeNull();
});
it('finds all campaigns', function () {
$releaseDate1 = Timestamp::fromDateTime(new DateTimeImmutable('2025-01-15'));
$releaseDate2 = Timestamp::fromDateTime(new DateTimeImmutable('2025-01-20'));
$campaign1 = PreSaveCampaign::create(
title: 'Album 1',
artistName: 'Artist 1',
coverImageUrl: 'https://example.com/cover1.jpg',
releaseDate: $releaseDate1,
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:1')]
);
$campaign2 = PreSaveCampaign::create(
title: 'Album 2',
artistName: 'Artist 2',
coverImageUrl: 'https://example.com/cover2.jpg',
releaseDate: $releaseDate2,
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:2')]
);
$this->repository->save($campaign1);
$this->repository->save($campaign2);
$all = $this->repository->findAll();
expect($all)->toHaveCount(2);
expect($all[0]->title)->toBe('Album 1');
expect($all[1]->title)->toBe('Album 2');
});
it('updates existing campaigns', function () {
$releaseDate = Timestamp::fromDateTime(new DateTimeImmutable('2025-01-15'));
$campaign = PreSaveCampaign::create(
title: 'Original Title',
artistName: 'Artist',
coverImageUrl: 'https://example.com/cover.jpg',
releaseDate: $releaseDate,
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:1')]
);
$saved = $this->repository->save($campaign);
$updated = $saved->markAsReleased();
$this->repository->save($updated);
$found = $this->repository->findById($saved->id);
expect($found->status->equals(CampaignStatus::RELEASED))->toBeTrue();
});
it('deletes campaigns', function () {
$releaseDate = Timestamp::fromDateTime(new DateTimeImmutable('2025-01-15'));
$campaign = PreSaveCampaign::create(
title: 'Test Album',
artistName: 'Test Artist',
coverImageUrl: 'https://example.com/cover.jpg',
releaseDate: $releaseDate,
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:123')]
);
$saved = $this->repository->save($campaign);
expect($this->repository->findById($saved->id))->not->toBeNull();
$deleted = $this->repository->delete($saved->id);
expect($deleted)->toBeTrue();
expect($this->repository->findById($saved->id))->toBeNull();
});
});
describe('PreSaveRegistrationRepository', function () {
beforeEach(function () {
$this->repository = new InMemoryPreSaveRegistrationRepository();
});
afterEach(function () {
$this->repository->clear();
});
it('saves and finds registrations', function () {
$registration = PreSaveRegistration::create(
campaignId: 1,
userId: 'user_456',
platform: StreamingPlatform::SPOTIFY
);
$saved = $this->repository->save($registration);
expect($saved->id)->not->toBeNull();
expect($saved->campaignId)->toBe(1);
expect($saved->userId)->toBe('user_456');
expect($saved->platform->equals(StreamingPlatform::SPOTIFY))->toBeTrue();
});
it('finds registrations for user and campaign', function () {
$reg1 = PreSaveRegistration::create(
campaignId: 1,
userId: 'user_1',
platform: StreamingPlatform::SPOTIFY
);
$reg2 = PreSaveRegistration::create(
campaignId: 2,
userId: 'user_1',
platform: StreamingPlatform::SPOTIFY
);
$saved1 = $this->repository->save($reg1);
$this->repository->save($reg2);
$found = $this->repository->findForUserAndCampaign(
'user_1',
1,
StreamingPlatform::SPOTIFY
);
expect($found)->not->toBeNull();
expect($found->id)->toBe($saved1->id);
expect($found->campaignId)->toBe(1);
});
it('finds registrations by user id', function () {
$reg1 = PreSaveRegistration::create(
campaignId: 1,
userId: 'user_123',
platform: StreamingPlatform::SPOTIFY
);
$reg2 = PreSaveRegistration::create(
campaignId: 2,
userId: 'user_123',
platform: StreamingPlatform::SPOTIFY
);
$reg3 = PreSaveRegistration::create(
campaignId: 1,
userId: 'user_456',
platform: StreamingPlatform::SPOTIFY
);
$this->repository->save($reg1);
$this->repository->save($reg2);
$this->repository->save($reg3);
$userRegistrations = $this->repository->findByUserId('user_123');
expect($userRegistrations)->toHaveCount(2);
expect($userRegistrations[0]->campaignId)->toBe(1);
expect($userRegistrations[1]->campaignId)->toBe(2);
});
it('finds pending registrations by campaign', function () {
$pending = PreSaveRegistration::create(
campaignId: 1,
userId: 'user_1',
platform: StreamingPlatform::SPOTIFY
);
$completed = PreSaveRegistration::create(
campaignId: 1,
userId: 'user_2',
platform: StreamingPlatform::SPOTIFY
);
$savedCompleted = $this->repository->save($completed);
$completed = $savedCompleted->markAsCompleted();
$this->repository->save($completed);
$savedPending = $this->repository->save($pending);
$pendingRegistrations = $this->repository->findPendingByCampaign(1);
expect($pendingRegistrations)->toHaveCount(1);
expect($pendingRegistrations[0]->id)->toBe($savedPending->id);
expect($pendingRegistrations[0]->status->equals(RegistrationStatus::PENDING))->toBeTrue();
});
it('finds retryable registrations', function () {
$retryable = PreSaveRegistration::create(
campaignId: 1,
userId: 'user_1',
platform: StreamingPlatform::SPOTIFY
);
$savedRetryable = $this->repository->save($retryable);
$retryable = $savedRetryable->markAsFailed('First failure');
$this->repository->save($retryable);
$maxedOut = PreSaveRegistration::create(
campaignId: 1,
userId: 'user_2',
platform: StreamingPlatform::SPOTIFY
);
$savedMaxedOut = $this->repository->save($maxedOut);
$maxedOut = $savedMaxedOut->markAsFailed('Failure 1');
$maxedOut = $this->repository->save($maxedOut);
$maxedOut = $maxedOut->resetForRetry()->markAsFailed('Failure 2');
$maxedOut = $this->repository->save($maxedOut);
$maxedOut = $maxedOut->resetForRetry()->markAsFailed('Failure 3');
$this->repository->save($maxedOut);
$retryableList = $this->repository->findRetryable(1, 3);
expect($retryableList)->toHaveCount(1);
expect($retryableList[0]->id)->toBe($savedRetryable->id);
expect($retryableList[0]->retryCount)->toBe(0);
});
it('deletes registrations', function () {
$registration = PreSaveRegistration::create(
campaignId: 1,
userId: 'user_456',
platform: StreamingPlatform::SPOTIFY
);
$saved = $this->repository->save($registration);
$deleted = $this->repository->delete($saved->id);
expect($deleted)->toBeTrue();
});
it('updates existing registrations', function () {
$registration = PreSaveRegistration::create(
campaignId: 1,
userId: 'user_456',
platform: StreamingPlatform::SPOTIFY
);
$saved = $this->repository->save($registration);
$updated = $saved->markAsCompleted();
$this->repository->save($updated);
$found = $this->repository->findForUserAndCampaign(
'user_456',
1,
StreamingPlatform::SPOTIFY
);
expect($found->status->equals(RegistrationStatus::COMPLETED))->toBeTrue();
});
});