- 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.
597 lines
22 KiB
PHP
597 lines
22 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\Services\PreSaveProcessor;
|
|
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;
|
|
use App\Framework\Logging\Logger;
|
|
use App\Framework\OAuth\OAuthServiceInterface;
|
|
use App\Framework\OAuth\Providers\SupportsPreSaves;
|
|
use App\Framework\OAuth\ValueObjects\AccessToken;
|
|
use App\Framework\OAuth\ValueObjects\OAuthToken;
|
|
use App\Framework\OAuth\ValueObjects\RefreshToken;
|
|
use App\Framework\OAuth\ValueObjects\TokenType;
|
|
use App\Framework\OAuth\Storage\StoredOAuthToken;
|
|
|
|
// Helper function to create test OAuthToken
|
|
function createTestToken(string $accessToken = 'test_access', int $expiresIn = 3600): OAuthToken
|
|
{
|
|
return new OAuthToken(
|
|
accessToken: AccessToken::fromProviderResponse($accessToken, $expiresIn),
|
|
refreshToken: RefreshToken::create('test_refresh'),
|
|
tokenType: TokenType::BEARER,
|
|
scope: null
|
|
);
|
|
}
|
|
|
|
// Mock Music Provider for Testing (implements SupportsPreSaves)
|
|
class ProcessorMockMusicProvider implements SupportsPreSaves
|
|
{
|
|
public array $addedTracks = [];
|
|
public bool $shouldFail = false;
|
|
|
|
public function addTracksToLibrary(OAuthToken $token, array $trackIds): bool
|
|
{
|
|
if ($this->shouldFail) {
|
|
throw new \Exception('Music API error');
|
|
}
|
|
foreach ($trackIds as $trackId) {
|
|
$this->addedTracks[] = $trackId;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Mock Logger for Testing
|
|
class ProcessorMockLogger implements Logger
|
|
{
|
|
public array $logs = [];
|
|
|
|
public function debug(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
|
|
{
|
|
$this->logs[] = ['level' => 'debug', 'message' => $message, 'context' => $context];
|
|
}
|
|
|
|
public function info(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
|
|
{
|
|
$this->logs[] = ['level' => 'info', 'message' => $message, 'context' => $context];
|
|
}
|
|
|
|
public function notice(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
|
|
{
|
|
$this->logs[] = ['level' => 'notice', 'message' => $message, 'context' => $context];
|
|
}
|
|
|
|
public function warning(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
|
|
{
|
|
$this->logs[] = ['level' => 'warning', 'message' => $message, 'context' => $context];
|
|
}
|
|
|
|
public function error(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
|
|
{
|
|
$this->logs[] = ['level' => 'error', 'message' => $message, 'context' => $context];
|
|
}
|
|
|
|
public function critical(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
|
|
{
|
|
$this->logs[] = ['level' => 'critical', 'message' => $message, 'context' => $context];
|
|
}
|
|
|
|
public function alert(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
|
|
{
|
|
$this->logs[] = ['level' => 'alert', 'message' => $message, 'context' => $context];
|
|
}
|
|
|
|
public function emergency(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
|
|
{
|
|
$this->logs[] = ['level' => 'emergency', 'message' => $message, 'context' => $context];
|
|
}
|
|
|
|
public function log(\App\Framework\Logging\LogLevel $level, string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
|
|
{
|
|
$this->logs[] = ['level' => $level->value, 'message' => $message, 'context' => $context];
|
|
}
|
|
|
|
public function logToChannel(\App\Framework\Logging\LogChannel $channel, \App\Framework\Logging\LogLevel $level, string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
|
|
{
|
|
$this->logs[] = ['channel' => $channel->value, 'level' => $level->value, 'message' => $message, 'context' => $context];
|
|
}
|
|
|
|
public \App\Framework\Logging\ChannelLogger $security {
|
|
get {
|
|
static $securityLogger = null;
|
|
return $securityLogger ??= new \App\Framework\Logging\ChannelLogger($this, \App\Framework\Logging\LogChannel::SECURITY);
|
|
}
|
|
}
|
|
|
|
public \App\Framework\Logging\ChannelLogger $cache {
|
|
get {
|
|
static $cacheLogger = null;
|
|
return $cacheLogger ??= new \App\Framework\Logging\ChannelLogger($this, \App\Framework\Logging\LogChannel::CACHE);
|
|
}
|
|
}
|
|
|
|
public \App\Framework\Logging\ChannelLogger $database {
|
|
get {
|
|
static $dbLogger = null;
|
|
return $dbLogger ??= new \App\Framework\Logging\ChannelLogger($this, \App\Framework\Logging\LogChannel::DATABASE);
|
|
}
|
|
}
|
|
|
|
public \App\Framework\Logging\ChannelLogger $framework {
|
|
get {
|
|
static $frameworkLogger = null;
|
|
return $frameworkLogger ??= new \App\Framework\Logging\ChannelLogger($this, \App\Framework\Logging\LogChannel::FRAMEWORK);
|
|
}
|
|
}
|
|
|
|
public \App\Framework\Logging\ChannelLogger $error {
|
|
get {
|
|
static $errorLogger = null;
|
|
return $errorLogger ??= new \App\Framework\Logging\ChannelLogger($this, \App\Framework\Logging\LogChannel::ERROR);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extended Mock Campaign Repository for Processor
|
|
class ProcessorMockCampaignRepository implements PreSaveCampaignRepositoryInterface
|
|
{
|
|
private array $campaigns = [];
|
|
public array $readyForRelease = [];
|
|
public array $readyForProcessing = [];
|
|
|
|
public function save(PreSaveCampaign $campaign): PreSaveCampaign
|
|
{
|
|
$id = $campaign->id ?? count($this->campaigns) + 1;
|
|
$saved = new PreSaveCampaign(
|
|
id: $id,
|
|
status: $campaign->status,
|
|
title: $campaign->title,
|
|
artistName: $campaign->artistName,
|
|
coverImageUrl: $campaign->coverImageUrl,
|
|
releaseDate: $campaign->releaseDate,
|
|
trackUrls: $campaign->trackUrls,
|
|
description: $campaign->description,
|
|
startDate: $campaign->startDate,
|
|
createdAt: $campaign->createdAt,
|
|
updatedAt: $campaign->updatedAt
|
|
);
|
|
$this->campaigns[$id] = $saved;
|
|
return $saved;
|
|
}
|
|
|
|
public function findById(int $id): ?PreSaveCampaign
|
|
{
|
|
return $this->campaigns[$id] ?? null;
|
|
}
|
|
|
|
public function findAll(array $filters = []): array
|
|
{
|
|
return array_values($this->campaigns);
|
|
}
|
|
|
|
public function findReadyForRelease(): array
|
|
{
|
|
return $this->readyForRelease;
|
|
}
|
|
|
|
public function findReadyForProcessing(): array
|
|
{
|
|
return $this->readyForProcessing;
|
|
}
|
|
|
|
public function delete(int $id): bool
|
|
{
|
|
unset($this->campaigns[$id]);
|
|
return true;
|
|
}
|
|
|
|
public function getStatistics(int $campaignId): array
|
|
{
|
|
return [
|
|
'total_registrations' => 10,
|
|
'completed' => 5,
|
|
'pending' => 3,
|
|
'failed' => 2,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Extended Mock Registration Repository for Processor
|
|
class ProcessorMockRegistrationRepository implements PreSaveRegistrationRepositoryInterface
|
|
{
|
|
private array $registrations = [];
|
|
public array $pendingByCampaign = [];
|
|
public array $retryable = [];
|
|
|
|
public function save(PreSaveRegistration $registration): PreSaveRegistration
|
|
{
|
|
$id = $registration->id ?? count($this->registrations) + 1;
|
|
$saved = 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
|
|
);
|
|
$this->registrations[$id] = $saved;
|
|
return $saved;
|
|
}
|
|
|
|
public function findForUserAndCampaign(string $userId, int $campaignId, StreamingPlatform $platform): ?PreSaveRegistration
|
|
{
|
|
foreach ($this->registrations as $reg) {
|
|
if ($reg->userId === $userId && $reg->campaignId === $campaignId && $reg->platform === $platform) {
|
|
return $reg;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function findByUserId(string $userId): array
|
|
{
|
|
return array_values(array_filter($this->registrations, fn($r) => $r->userId === $userId));
|
|
}
|
|
|
|
public function findPendingByCampaign(int $campaignId): array
|
|
{
|
|
return $this->pendingByCampaign[$campaignId] ?? [];
|
|
}
|
|
|
|
public function findRetryable(int $campaignId, int $maxRetries): array
|
|
{
|
|
return $this->retryable[$campaignId] ?? [];
|
|
}
|
|
|
|
public function getStatusCounts(int $campaignId): array
|
|
{
|
|
return [
|
|
'pending' => 3,
|
|
'completed' => 5,
|
|
'failed' => 2,
|
|
];
|
|
}
|
|
|
|
public function delete(int $id): bool
|
|
{
|
|
unset($this->registrations[$id]);
|
|
return true;
|
|
}
|
|
|
|
public function hasRegistered(string $userId, int $campaignId, StreamingPlatform $platform): bool
|
|
{
|
|
return $this->findForUserAndCampaign($userId, $campaignId, $platform) !== null;
|
|
}
|
|
}
|
|
|
|
// Mock OAuth Service for Processor
|
|
class ProcessorMockOAuthService implements OAuthServiceInterface
|
|
{
|
|
private array $tokens = [];
|
|
|
|
public function addToken(string $userId, StreamingPlatform $platform, OAuthToken $token): void
|
|
{
|
|
$this->tokens[$userId . '_' . $platform->value] = $token;
|
|
}
|
|
|
|
public function getProvider(string $name): \App\Framework\OAuth\OAuthProvider
|
|
{
|
|
throw new \RuntimeException('Not implemented in test');
|
|
}
|
|
|
|
public function getAuthorizationUrl(string $provider, array $options = []): string
|
|
{
|
|
throw new \RuntimeException('Not implemented in test');
|
|
}
|
|
|
|
public function handleCallback(string $userId, string $provider, string $code, ?string $state = null): StoredOAuthToken
|
|
{
|
|
throw new \RuntimeException('Not implemented in test');
|
|
}
|
|
|
|
public function getTokenForUser(string $userId, string $provider): StoredOAuthToken
|
|
{
|
|
$key = $userId . '_' . $provider;
|
|
$token = $this->tokens[$key] ?? throw new \RuntimeException("No token for {$userId}/{$provider}");
|
|
|
|
return new StoredOAuthToken(
|
|
id: null,
|
|
userId: $userId,
|
|
provider: $provider,
|
|
token: $token,
|
|
createdAt: Timestamp::now(),
|
|
updatedAt: Timestamp::now()
|
|
);
|
|
}
|
|
|
|
public function refreshToken(StoredOAuthToken $storedToken): StoredOAuthToken
|
|
{
|
|
throw new \RuntimeException('Not implemented in test');
|
|
}
|
|
|
|
public function revokeToken(string $userId, string $provider): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function getUserProfile(string $userId, string $provider): array
|
|
{
|
|
throw new \RuntimeException('Not implemented in test');
|
|
}
|
|
|
|
public function hasProvider(string $userId, string $provider): bool
|
|
{
|
|
return isset($this->tokens[$userId . '_' . $provider]);
|
|
}
|
|
|
|
public function getUserProviders(string $userId): array
|
|
{
|
|
throw new \RuntimeException('Not implemented in test');
|
|
}
|
|
|
|
public function refreshExpiringTokens(int $withinSeconds = 300): int
|
|
{
|
|
throw new \RuntimeException('Not implemented in test');
|
|
}
|
|
|
|
public function cleanupExpiredTokens(): int
|
|
{
|
|
throw new \RuntimeException('Not implemented in test');
|
|
}
|
|
}
|
|
|
|
describe('PreSaveProcessor', function () {
|
|
beforeEach(function () {
|
|
$this->campaignRepo = new ProcessorMockCampaignRepository();
|
|
$this->registrationRepo = new ProcessorMockRegistrationRepository();
|
|
$this->oauthService = new ProcessorMockOAuthService();
|
|
$this->musicProvider = new ProcessorMockMusicProvider();
|
|
$this->logger = new ProcessorMockLogger();
|
|
|
|
$this->processor = new PreSaveProcessor(
|
|
$this->campaignRepo,
|
|
$this->registrationRepo,
|
|
$this->oauthService,
|
|
$this->musicProvider,
|
|
$this->logger
|
|
);
|
|
});
|
|
|
|
describe('processReleasedCampaigns', function () {
|
|
it('marks campaigns as released when release date reached', function () {
|
|
$campaign = PreSaveCampaign::create(
|
|
title: 'Released Album',
|
|
artistName: 'Test Artist',
|
|
coverImageUrl: 'https://example.com/cover.jpg',
|
|
releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('-1 day')),
|
|
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
|
|
);
|
|
$campaign = $campaign->publish();
|
|
$campaign = $this->campaignRepo->save($campaign);
|
|
|
|
$this->campaignRepo->readyForRelease = [$campaign];
|
|
|
|
$result = $this->processor->processReleasedCampaigns();
|
|
|
|
expect(isset($result['campaigns_marked_for_release']))->toBeTrue();
|
|
expect($result['campaigns_marked_for_release'])->toBe(1);
|
|
});
|
|
|
|
it('returns zero when no campaigns ready for release', function () {
|
|
$result = $this->processor->processReleasedCampaigns();
|
|
|
|
expect($result['campaigns_marked_for_release'])->toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('processPendingRegistrations', function () {
|
|
it('processes pending registrations for released campaigns', function () {
|
|
$campaign = PreSaveCampaign::create(
|
|
title: 'Processing Album',
|
|
artistName: 'Test Artist',
|
|
coverImageUrl: 'https://example.com/cover.jpg',
|
|
releaseDate: Timestamp::now(),
|
|
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
|
|
);
|
|
$campaign = $campaign->publish()->markAsReleased();
|
|
$campaign = $this->campaignRepo->save($campaign);
|
|
|
|
$registration = PreSaveRegistration::create($campaign->id, 'user123', StreamingPlatform::SPOTIFY);
|
|
$registration = $this->registrationRepo->save($registration);
|
|
|
|
$token = createTestToken('access_token_user123');
|
|
$this->oauthService->addToken('user123', StreamingPlatform::SPOTIFY, $token);
|
|
|
|
$this->campaignRepo->readyForProcessing = [$campaign];
|
|
$this->registrationRepo->pendingByCampaign[$campaign->id] = [$registration];
|
|
|
|
$result = $this->processor->processPendingRegistrations();
|
|
|
|
expect(isset($result['campaigns_processed']))->toBeTrue();
|
|
expect($result['campaigns_processed'])->toBe(1);
|
|
expect(isset($result['total_processed']))->toBeTrue();
|
|
expect(isset($result['total_successful']))->toBeTrue();
|
|
});
|
|
|
|
it('marks campaign as completed when no pending registrations remain', function () {
|
|
$campaign = PreSaveCampaign::create(
|
|
title: 'Complete Album',
|
|
artistName: 'Test Artist',
|
|
coverImageUrl: 'https://example.com/cover.jpg',
|
|
releaseDate: Timestamp::now(),
|
|
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
|
|
);
|
|
$campaign = $campaign->publish()->markAsReleased();
|
|
$campaign = $this->campaignRepo->save($campaign);
|
|
|
|
$this->campaignRepo->readyForProcessing = [$campaign];
|
|
$this->registrationRepo->pendingByCampaign[$campaign->id] = [];
|
|
|
|
$result = $this->processor->processPendingRegistrations();
|
|
|
|
$updatedCampaign = $this->campaignRepo->findById($campaign->id);
|
|
expect($updatedCampaign->status)->toBe(CampaignStatus::COMPLETED);
|
|
});
|
|
});
|
|
|
|
describe('processCampaignRegistrations', function () {
|
|
it('processes all pending registrations for a campaign', function () {
|
|
$campaign = PreSaveCampaign::create(
|
|
title: 'Batch Album',
|
|
artistName: 'Test Artist',
|
|
coverImageUrl: 'https://example.com/cover.jpg',
|
|
releaseDate: Timestamp::now(),
|
|
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
|
|
);
|
|
$campaign = $campaign->publish()->markAsReleased();
|
|
$campaign = $this->campaignRepo->save($campaign);
|
|
|
|
$reg1 = PreSaveRegistration::create($campaign->id, 'user1', StreamingPlatform::SPOTIFY);
|
|
$reg1 = $this->registrationRepo->save($reg1);
|
|
|
|
$reg2 = PreSaveRegistration::create($campaign->id, 'user2', StreamingPlatform::SPOTIFY);
|
|
$reg2 = $this->registrationRepo->save($reg2);
|
|
|
|
$token = createTestToken('access_token_users');
|
|
$this->oauthService->addToken('user1', StreamingPlatform::SPOTIFY, $token);
|
|
$this->oauthService->addToken('user2', StreamingPlatform::SPOTIFY, $token);
|
|
|
|
$this->registrationRepo->pendingByCampaign[$campaign->id] = [$reg1, $reg2];
|
|
|
|
$result = $this->processor->processCampaignRegistrations($campaign);
|
|
|
|
expect($result['processed'])->toBe(2);
|
|
expect($result['successful'])->toBe(2);
|
|
expect($result['failed'])->toBe(0);
|
|
});
|
|
|
|
it('handles registration processing failures gracefully', function () {
|
|
$campaign = PreSaveCampaign::create(
|
|
title: 'Failure Album',
|
|
artistName: 'Test Artist',
|
|
coverImageUrl: 'https://example.com/cover.jpg',
|
|
releaseDate: Timestamp::now(),
|
|
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
|
|
);
|
|
$campaign = $campaign->publish()->markAsReleased();
|
|
$campaign = $this->campaignRepo->save($campaign);
|
|
|
|
$registration = PreSaveRegistration::create($campaign->id, 'user123', StreamingPlatform::SPOTIFY);
|
|
$registration = $this->registrationRepo->save($registration);
|
|
|
|
$token = createTestToken('access_token_user123');
|
|
$this->oauthService->addToken('user123', StreamingPlatform::SPOTIFY, $token);
|
|
|
|
$this->musicProvider->shouldFail = true;
|
|
$this->registrationRepo->pendingByCampaign[$campaign->id] = [$registration];
|
|
|
|
$result = $this->processor->processCampaignRegistrations($campaign);
|
|
|
|
expect($result['processed'])->toBe(1);
|
|
expect($result['successful'])->toBe(0);
|
|
expect($result['failed'])->toBe(1);
|
|
|
|
$hasError = false;
|
|
foreach ($this->logger->logs as $log) {
|
|
if ($log['level'] === 'error' && str_contains($log['message'], 'Failed to process')) {
|
|
$hasError = true;
|
|
}
|
|
}
|
|
expect($hasError)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('retryFailedRegistrations', function () {
|
|
it('retries failed registrations within retry limit', function () {
|
|
$campaign = PreSaveCampaign::create(
|
|
title: 'Retry Album',
|
|
artistName: 'Test Artist',
|
|
coverImageUrl: 'https://example.com/cover.jpg',
|
|
releaseDate: Timestamp::now(),
|
|
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
|
|
);
|
|
$campaign = $campaign->publish()->markAsReleased();
|
|
$campaign = $this->campaignRepo->save($campaign);
|
|
|
|
$registration = PreSaveRegistration::create($campaign->id, 'user123', StreamingPlatform::SPOTIFY);
|
|
$registration = $registration->markAsFailed('Temporary error');
|
|
$registration = $this->registrationRepo->save($registration);
|
|
|
|
$token = createTestToken('access_token_user123');
|
|
$this->oauthService->addToken('user123', StreamingPlatform::SPOTIFY, $token);
|
|
|
|
$this->registrationRepo->retryable[$campaign->id] = [$registration];
|
|
|
|
$result = $this->processor->retryFailedRegistrations($campaign->id, 3);
|
|
|
|
expect($result['processed'])->toBe(1);
|
|
expect($result['successful'])->toBe(1);
|
|
});
|
|
|
|
it('does not retry registrations exceeding max retry count', function () {
|
|
$campaign = PreSaveCampaign::create(
|
|
title: 'Max Retry Album',
|
|
artistName: 'Test Artist',
|
|
coverImageUrl: 'https://example.com/cover.jpg',
|
|
releaseDate: Timestamp::now(),
|
|
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
|
|
);
|
|
$campaign = $campaign->publish()->markAsReleased();
|
|
$campaign = $this->campaignRepo->save($campaign);
|
|
|
|
$this->registrationRepo->retryable[$campaign->id] = [];
|
|
|
|
$result = $this->processor->retryFailedRegistrations($campaign->id, 3);
|
|
|
|
expect($result['processed'])->toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Music platform integration', function () {
|
|
it('successfully adds tracks to user library', function () {
|
|
$campaign = PreSaveCampaign::create(
|
|
title: 'Music Album',
|
|
artistName: 'Test Artist',
|
|
coverImageUrl: 'https://example.com/cover.jpg',
|
|
releaseDate: Timestamp::now(),
|
|
trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:test123')]
|
|
);
|
|
$campaign = $campaign->publish()->markAsReleased();
|
|
$campaign = $this->campaignRepo->save($campaign);
|
|
|
|
$registration = PreSaveRegistration::create($campaign->id, 'user123', StreamingPlatform::SPOTIFY);
|
|
$registration = $this->registrationRepo->save($registration);
|
|
|
|
$token = createTestToken('access_token_user123');
|
|
$this->oauthService->addToken('user123', StreamingPlatform::SPOTIFY, $token);
|
|
|
|
$this->registrationRepo->pendingByCampaign[$campaign->id] = [$registration];
|
|
|
|
$this->processor->processCampaignRegistrations($campaign);
|
|
|
|
expect($this->musicProvider->addedTracks)->toHaveCount(1);
|
|
expect($this->musicProvider->addedTracks[0])->toBe('spotify:track:test123');
|
|
|
|
$hasInfo = false;
|
|
foreach ($this->logger->logs as $log) {
|
|
if ($log['level'] === 'info' && str_contains($log['message'], 'successfully')) {
|
|
$hasInfo = true;
|
|
}
|
|
}
|
|
expect($hasInfo)->toBeTrue();
|
|
});
|
|
});
|
|
});
|