377 lines
14 KiB
PHP
377 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Domain\SmartLink\Enums\LinkStatus;
|
|
use App\Domain\SmartLink\Enums\LinkType;
|
|
use App\Domain\SmartLink\Enums\ServiceType;
|
|
use App\Domain\SmartLink\Services\ShortCodeGenerator;
|
|
use App\Domain\SmartLink\Services\SmartLinkService;
|
|
use App\Domain\SmartLink\ValueObjects\DestinationUrl;
|
|
use App\Domain\SmartLink\ValueObjects\LinkTitle;
|
|
use App\Domain\SmartLink\ValueObjects\ShortCode;
|
|
use App\Framework\DateTime\SystemClock;
|
|
use Tests\Support\InMemoryLinkDestinationRepository;
|
|
use Tests\Support\InMemorySmartLinkRepository;
|
|
|
|
describe('SmartLink Integration', function () {
|
|
beforeEach(function () {
|
|
$this->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('complete link creation workflow', function () {
|
|
it('creates link with destinations and publishes it', function () {
|
|
// 1. Create link
|
|
$link = $this->service->createLink(
|
|
type: LinkType::RELEASE,
|
|
title: LinkTitle::fromString('New Album Release'),
|
|
userId: 'user123',
|
|
coverImageUrl: 'https://example.com/cover.jpg'
|
|
);
|
|
|
|
expect($link->status)->toBe(LinkStatus::DRAFT);
|
|
expect($link->userId)->toBe('user123');
|
|
expect($link->coverImageUrl)->toBe('https://example.com/cover.jpg');
|
|
|
|
// 2. Add multiple destinations
|
|
$spotifyDestination = $this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::SPOTIFY,
|
|
url: DestinationUrl::fromString('https://open.spotify.com/album/123'),
|
|
priority: 1,
|
|
isDefault: true
|
|
);
|
|
|
|
$this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::APPLE_MUSIC,
|
|
url: DestinationUrl::fromString('https://music.apple.com/album/123'),
|
|
priority: 2
|
|
);
|
|
|
|
$this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::YOUTUBE_MUSIC,
|
|
url: DestinationUrl::fromString('https://music.youtube.com/watch?v=123'),
|
|
priority: 3
|
|
);
|
|
|
|
// 3. Verify destinations
|
|
$destinations = $this->service->getDestinations($link->id);
|
|
expect(count($destinations))->toBeGreaterThan(2);
|
|
expect($spotifyDestination->isDefault)->toBeTrue();
|
|
|
|
// 4. Publish link
|
|
$publishedLink = $this->service->publishLink($link->id);
|
|
expect($publishedLink->status)->toBe(LinkStatus::ACTIVE);
|
|
expect($publishedLink->isActive())->toBeTrue();
|
|
expect($publishedLink->canBeAccessed())->toBeTrue();
|
|
|
|
// 5. Verify published link can be found by short code
|
|
$foundLink = $this->service->findByShortCode($publishedLink->shortCode);
|
|
expect($foundLink->id->equals($publishedLink->id))->toBeTrue();
|
|
expect($foundLink->status)->toBe(LinkStatus::ACTIVE);
|
|
});
|
|
|
|
it('handles link update workflow', function () {
|
|
// 1. Create link
|
|
$link = $this->service->createLink(
|
|
type: LinkType::BIO_LINK,
|
|
title: LinkTitle::fromString('Artist Bio'),
|
|
userId: 'artist456'
|
|
);
|
|
|
|
// 2. Update title
|
|
$updatedLink = $this->service->updateTitle(
|
|
$link->id,
|
|
LinkTitle::fromString('Updated Artist Bio')
|
|
);
|
|
expect($updatedLink->title->toString())->toBe('Updated Artist Bio');
|
|
|
|
// 3. Publish
|
|
$publishedLink = $this->service->publishLink($link->id);
|
|
expect($publishedLink->status)->toBe(LinkStatus::ACTIVE);
|
|
|
|
// 4. Pause
|
|
$pausedLink = $this->service->pauseLink($link->id);
|
|
expect($pausedLink->status)->toBe(LinkStatus::PAUSED);
|
|
expect($pausedLink->canBeAccessed())->toBeFalse();
|
|
|
|
// 5. Verify final state
|
|
$finalLink = $this->service->findById($link->id);
|
|
expect($finalLink->status)->toBe(LinkStatus::PAUSED);
|
|
expect($finalLink->title->toString())->toBe('Updated Artist Bio');
|
|
});
|
|
|
|
it('handles custom short code workflow', function () {
|
|
$customCode = ShortCode::fromString('myband');
|
|
|
|
// 1. Create with custom code
|
|
$link = $this->service->createLink(
|
|
type: LinkType::EVENT,
|
|
title: LinkTitle::fromString('Concert Tour'),
|
|
customShortCode: $customCode
|
|
);
|
|
|
|
expect($link->shortCode->equals($customCode))->toBeTrue();
|
|
|
|
// 2. Add destination
|
|
$this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::WEBSITE,
|
|
url: DestinationUrl::fromString('https://myband.com/tour')
|
|
);
|
|
|
|
// 3. Publish
|
|
$this->service->publishLink($link->id);
|
|
|
|
// 4. Find by custom code
|
|
$foundLink = $this->service->findByShortCode($customCode);
|
|
expect($foundLink->shortCode->toString())->toBe('myband');
|
|
expect($foundLink->isActive())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('user link management', function () {
|
|
it('manages multiple links for single user', function () {
|
|
$userId = 'poweruser789';
|
|
|
|
// Create multiple links
|
|
$draftLink = $this->service->createLink(
|
|
type: LinkType::RELEASE,
|
|
title: LinkTitle::fromString('Upcoming Album'),
|
|
userId: $userId
|
|
);
|
|
|
|
$activeLink1 = $this->service->createLink(
|
|
type: LinkType::BIO_LINK,
|
|
title: LinkTitle::fromString('Bio Link'),
|
|
userId: $userId
|
|
);
|
|
$this->service->publishLink($activeLink1->id);
|
|
|
|
$activeLink2 = $this->service->createLink(
|
|
type: LinkType::EVENT,
|
|
title: LinkTitle::fromString('Event Link'),
|
|
userId: $userId
|
|
);
|
|
$this->service->publishLink($activeLink2->id);
|
|
|
|
// Get all user links
|
|
$allLinks = $this->service->getUserLinks($userId);
|
|
expect(count($allLinks))->toBeGreaterThan(2);
|
|
|
|
// Get only draft links
|
|
$draftLinks = $this->service->getUserLinks($userId, LinkStatus::DRAFT);
|
|
expect(count($draftLinks))->toBeGreaterThan(0);
|
|
expect($draftLinks[0]->id->equals($draftLink->id))->toBeTrue();
|
|
|
|
// Get only active links
|
|
$activeLinks = $this->service->getUserLinks($userId, LinkStatus::ACTIVE);
|
|
expect(count($activeLinks))->toBeGreaterThan(1);
|
|
});
|
|
|
|
it('isolates links between different users', function () {
|
|
// User 1 creates links
|
|
$this->service->createLink(
|
|
type: LinkType::RELEASE,
|
|
title: LinkTitle::fromString('User 1 Album'),
|
|
userId: 'user001'
|
|
);
|
|
|
|
$this->service->createLink(
|
|
type: LinkType::BIO_LINK,
|
|
title: LinkTitle::fromString('User 1 Bio'),
|
|
userId: 'user001'
|
|
);
|
|
|
|
// User 2 creates links
|
|
$this->service->createLink(
|
|
type: LinkType::EVENT,
|
|
title: LinkTitle::fromString('User 2 Event'),
|
|
userId: 'user002'
|
|
);
|
|
|
|
// Verify isolation
|
|
$user1Links = $this->service->getUserLinks('user001');
|
|
$user2Links = $this->service->getUserLinks('user002');
|
|
|
|
expect(count($user1Links))->toBeGreaterThan(1);
|
|
expect(count($user2Links))->toBeGreaterThan(0);
|
|
|
|
expect($user1Links[0]->isOwnedBy('user001'))->toBeTrue();
|
|
expect($user1Links[0]->isOwnedBy('user002'))->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('destination management', function () {
|
|
it('manages default destination correctly', function () {
|
|
$link = $this->service->createLink(
|
|
type: LinkType::RELEASE,
|
|
title: LinkTitle::fromString('Album')
|
|
);
|
|
|
|
// Add first destination as default
|
|
$default1 = $this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::SPOTIFY,
|
|
url: DestinationUrl::fromString('https://open.spotify.com/album/1'),
|
|
isDefault: true
|
|
);
|
|
|
|
expect($default1->isDefault)->toBeTrue();
|
|
|
|
// Add second destination as default (should replace first)
|
|
$default2 = $this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::APPLE_MUSIC,
|
|
url: DestinationUrl::fromString('https://music.apple.com/album/1'),
|
|
isDefault: true
|
|
);
|
|
|
|
expect($default2->isDefault)->toBeTrue();
|
|
|
|
// Verify only one default exists
|
|
$destinations = $this->service->getDestinations($link->id);
|
|
$defaultCount = 0;
|
|
foreach ($destinations as $dest) {
|
|
if ($dest->isDefault) {
|
|
$defaultCount++;
|
|
}
|
|
}
|
|
expect($defaultCount)->toBeGreaterThan(0);
|
|
});
|
|
|
|
it('handles priority ordering', function () {
|
|
$link = $this->service->createLink(
|
|
type: LinkType::CONTENT,
|
|
title: LinkTitle::fromString('Content')
|
|
);
|
|
|
|
// Add destinations with different priorities
|
|
$this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::SPOTIFY,
|
|
url: DestinationUrl::fromString('https://spotify.com/1'),
|
|
priority: 3
|
|
);
|
|
|
|
$this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::APPLE_MUSIC,
|
|
url: DestinationUrl::fromString('https://apple.com/1'),
|
|
priority: 1
|
|
);
|
|
|
|
$this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::YOUTUBE_MUSIC,
|
|
url: DestinationUrl::fromString('https://youtube.com/1'),
|
|
priority: 2
|
|
);
|
|
|
|
$destinations = $this->service->getDestinations($link->id);
|
|
expect(count($destinations))->toBeGreaterThan(2);
|
|
|
|
// Verify priorities are preserved
|
|
expect($destinations[0]->priority)->toBeInt();
|
|
expect($destinations[1]->priority)->toBeInt();
|
|
expect($destinations[2]->priority)->toBeInt();
|
|
});
|
|
});
|
|
|
|
describe('link deletion', function () {
|
|
it('deletes link and all destinations', function () {
|
|
$link = $this->service->createLink(
|
|
type: LinkType::RELEASE,
|
|
title: LinkTitle::fromString('Test Album')
|
|
);
|
|
|
|
// Add destinations
|
|
$this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::SPOTIFY,
|
|
url: DestinationUrl::fromString('https://spotify.com/test')
|
|
);
|
|
|
|
$this->service->addDestination(
|
|
linkId: $link->id,
|
|
serviceType: ServiceType::APPLE_MUSIC,
|
|
url: DestinationUrl::fromString('https://apple.com/test')
|
|
);
|
|
|
|
$beforeDelete = $this->service->getDestinations($link->id);
|
|
expect(count($beforeDelete))->toBeGreaterThan(1);
|
|
|
|
// Delete link
|
|
$this->service->deleteLink($link->id);
|
|
|
|
// Verify link is deleted - use try-catch instead of toThrow()
|
|
$threwCorrectException = false;
|
|
try {
|
|
$this->service->findById($link->id);
|
|
} catch (\App\Domain\SmartLink\Exceptions\SmartLinkNotFoundException $e) {
|
|
$threwCorrectException = true;
|
|
}
|
|
expect($threwCorrectException)->toBeTrue();
|
|
|
|
// Verify destinations are deleted (getDestinations returns empty array)
|
|
$afterDelete = $this->service->getDestinations($link->id);
|
|
// Empty array check - should not throw
|
|
expect(is_array($afterDelete))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('concurrent short code generation', function () {
|
|
it('generates unique codes for multiple simultaneous links', function () {
|
|
$generatedCodes = [];
|
|
|
|
// Simulate concurrent link creation
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$link = $this->service->createLink(
|
|
type: LinkType::RELEASE,
|
|
title: LinkTitle::fromString("Album {$i}")
|
|
);
|
|
|
|
$generatedCodes[] = $link->shortCode->toString();
|
|
}
|
|
|
|
// Verify all codes are unique
|
|
$uniqueCodes = array_unique($generatedCodes);
|
|
expect(count($uniqueCodes))->toBeGreaterThan(8);
|
|
});
|
|
});
|
|
|
|
describe('case-insensitive short code handling', function () {
|
|
it('treats short codes as case-insensitive', function () {
|
|
$link = $this->service->createLink(
|
|
type: LinkType::RELEASE,
|
|
title: LinkTitle::fromString('Test'),
|
|
customShortCode: ShortCode::fromString('AbCdEf')
|
|
);
|
|
|
|
// Find with different case variations
|
|
$found1 = $this->service->findByShortCode(ShortCode::fromString('abcdef'));
|
|
$found2 = $this->service->findByShortCode(ShortCode::fromString('ABCDEF'));
|
|
$found3 = $this->service->findByShortCode(ShortCode::fromString('aBcDeF'));
|
|
|
|
expect($found1->id->equals($link->id))->toBeTrue();
|
|
expect($found2->id->equals($link->id))->toBeTrue();
|
|
expect($found3->id->equals($link->id))->toBeTrue();
|
|
|
|
// Original case is preserved
|
|
expect($link->shortCode->toString())->toBe('AbCdEf');
|
|
});
|
|
});
|
|
});
|