clock = new SystemClock(); $this->linkRepository = new InMemorySmartLinkRepository(); $this->destinationRepository = new InMemoryLinkDestinationRepository(); $this->shortCodeGenerator = new ShortCodeGenerator($this->linkRepository); $this->service = new SmartLinkService( linkRepository: $this->linkRepository, destinationRepository: $this->destinationRepository, shortCodeGenerator: $this->shortCodeGenerator, clock: $this->clock ); }); describe('createLink', function () { it('creates link with auto-generated short code', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); expect($link->type)->toBe(LinkType::RELEASE); expect($link->title->toString())->toBe('My Album'); expect($link->status)->toBe(LinkStatus::DRAFT); expect($link->userId)->toBeNull(); }); it('creates link with custom short code', function () { $customCode = ShortCode::fromString('custom'); $link = $this->service->createLink( type: LinkType::BIO_LINK, title: LinkTitle::fromString('My Bio'), customShortCode: $customCode ); expect($link->shortCode->equals($customCode))->toBeTrue(); }); it('creates link with user ID', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album'), userId: 'user123' ); expect($link->userId)->toBe('user123'); expect($link->isOwnedBy('user123'))->toBeTrue(); }); it('creates link with cover image', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album'), coverImageUrl: 'https://example.com/cover.jpg' ); expect($link->coverImageUrl)->toBe('https://example.com/cover.jpg'); }); it('throws exception when custom short code already exists', function () { $shortCode = ShortCode::fromString('exist1'); $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('First'), customShortCode: $shortCode ); expect(fn() => $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('Second'), customShortCode: $shortCode ))->toThrow(ShortCodeAlreadyExistsException::class); }); it('persists link to repository', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); $foundLink = $this->linkRepository->findById($link->id); expect($foundLink)->toBeInstanceOf(\App\Domain\SmartLink\Entities\SmartLink::class); expect($foundLink->id->equals($link->id))->toBeTrue(); }); }); describe('findById', function () { it('finds existing link by ID', function () { $createdLink = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); $foundLink = $this->service->findById($createdLink->id); expect($foundLink->id->equals($createdLink->id))->toBeTrue(); expect($foundLink->title->toString())->toBe('My Album'); }); it('throws exception when link not found', function () { $nonExistentId = SmartLinkId::generate($this->clock); expect(fn() => $this->service->findById($nonExistentId)) ->toThrow(SmartLinkNotFoundException::class); }); }); describe('findByShortCode', function () { it('finds existing link by short code', function () { $shortCode = ShortCode::fromString('abc123'); $createdLink = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album'), customShortCode: $shortCode ); $foundLink = $this->service->findByShortCode($shortCode); expect($foundLink->id->equals($createdLink->id))->toBeTrue(); expect($foundLink->shortCode->equals($shortCode))->toBeTrue(); }); it('throws exception when short code not found', function () { $nonExistentCode = ShortCode::fromString('notfound'); expect(fn() => $this->service->findByShortCode($nonExistentCode)) ->toThrow(SmartLinkNotFoundException::class); }); it('is case-insensitive for short code lookup', function () { $shortCode = ShortCode::fromString('AbCdEf'); $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album'), customShortCode: $shortCode ); $lowerCaseCode = ShortCode::fromString('abcdef'); $foundLink = $this->service->findByShortCode($lowerCaseCode); expect($foundLink)->toBeInstanceOf(\App\Domain\SmartLink\Entities\SmartLink::class); expect($foundLink->shortCode->toString())->toBe('AbCdEf'); }); }); describe('updateTitle', function () { it('updates link title', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('Original Title') ); $newTitle = LinkTitle::fromString('Updated Title'); $updatedLink = $this->service->updateTitle($link->id, $newTitle); expect($updatedLink->title->toString())->toBe('Updated Title'); expect($updatedLink->id->equals($link->id))->toBeTrue(); }); it('persists title update', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('Original Title') ); $newTitle = LinkTitle::fromString('Updated Title'); $this->service->updateTitle($link->id, $newTitle); $foundLink = $this->service->findById($link->id); expect($foundLink->title->toString())->toBe('Updated Title'); }); }); describe('publishLink', function () { it('publishes draft link', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); expect($link->status)->toBe(LinkStatus::DRAFT); $publishedLink = $this->service->publishLink($link->id); expect($publishedLink->status)->toBe(LinkStatus::ACTIVE); expect($publishedLink->isActive())->toBeTrue(); }); it('persists published status', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); $this->service->publishLink($link->id); $foundLink = $this->service->findById($link->id); expect($foundLink->status)->toBe(LinkStatus::ACTIVE); }); }); describe('pauseLink', function () { it('pauses active link', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); $publishedLink = $this->service->publishLink($link->id); expect($publishedLink->status)->toBe(LinkStatus::ACTIVE); $pausedLink = $this->service->pauseLink($link->id); expect($pausedLink->status)->toBe(LinkStatus::PAUSED); }); }); describe('addDestination', function () { it('adds destination to link', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); $destination = $this->service->addDestination( linkId: $link->id, serviceType: ServiceType::SPOTIFY, url: DestinationUrl::fromString('https://open.spotify.com/album/123') ); expect($destination->linkId->equals($link->id))->toBeTrue(); expect($destination->serviceType)->toBe(ServiceType::SPOTIFY); expect($destination->url->toString())->toBe('https://open.spotify.com/album/123'); }); it('adds destination with priority', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); $destination = $this->service->addDestination( linkId: $link->id, serviceType: ServiceType::SPOTIFY, url: DestinationUrl::fromString('https://open.spotify.com/album/123'), priority: 10 ); expect($destination->priority)->toBe(10); }); it('adds default destination', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); $destination = $this->service->addDestination( linkId: $link->id, serviceType: ServiceType::SPOTIFY, url: DestinationUrl::fromString('https://open.spotify.com/album/123'), isDefault: true ); expect($destination->isDefault)->toBeTrue(); }); }); describe('getDestinations', function () { it('retrieves all destinations for link', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); $this->service->addDestination( linkId: $link->id, serviceType: ServiceType::SPOTIFY, url: DestinationUrl::fromString('https://spotify.com/album/1') ); $this->service->addDestination( linkId: $link->id, serviceType: ServiceType::APPLE_MUSIC, url: DestinationUrl::fromString('https://music.apple.com/album/1') ); $destinations = $this->service->getDestinations($link->id); expect($destinations)->toHaveCount(2); }); it('returns empty array when no destinations', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); $destinations = $this->service->getDestinations($link->id); expect($destinations)->toBeEmpty(); }); }); describe('deleteLink', function () { it('deletes link from repository', function () { $link = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('My Album') ); $this->service->deleteLink($link->id); expect(fn() => $this->service->findById($link->id)) ->toThrow(SmartLinkNotFoundException::class); }); }); describe('getUserLinks', function () { it('retrieves all links for user', function () { $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('Album 1'), userId: 'user123' ); $this->service->createLink( type: LinkType::BIO_LINK, title: LinkTitle::fromString('Bio'), userId: 'user123' ); $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('Album 2'), userId: 'user456' ); $userLinks = $this->service->getUserLinks('user123'); expect($userLinks)->toHaveCount(2); }); it('filters links by status', function () { $link1 = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('Album 1'), userId: 'user123' ); $link2 = $this->service->createLink( type: LinkType::RELEASE, title: LinkTitle::fromString('Album 2'), userId: 'user123' ); $this->service->publishLink($link2->id); $draftLinks = $this->service->getUserLinks('user123', LinkStatus::DRAFT); $activeLinks = $this->service->getUserLinks('user123', LinkStatus::ACTIVE); expect($draftLinks)->toHaveCount(1); expect($activeLinks)->toHaveCount(1); }); it('returns empty array when user has no links', function () { $userLinks = $this->service->getUserLinks('nonexistent'); expect($userLinks)->toBeEmpty(); }); }); });