clock = new SystemClock(); $this->shortCode = ShortCode::fromString('abc123'); $this->title = LinkTitle::fromString('My Smart Link'); }); describe('creation', function () { it('creates smart link with required fields', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title ); expect($link->shortCode->toString())->toBe('abc123'); expect($link->type)->toBe(LinkType::RELEASE); expect($link->title->toString())->toBe('My Smart Link'); expect($link->status)->toBe(LinkStatus::DRAFT); expect($link->userId)->toBeNull(); expect($link->coverImageUrl)->toBeNull(); }); it('creates smart link with user ID', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title, userId: 'user123' ); expect($link->userId)->toBe('user123'); expect($link->isOwnedBy('user123'))->toBeTrue(); }); it('creates smart link with cover image', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title, coverImageUrl: 'https://example.com/cover.jpg' ); expect($link->coverImageUrl)->toBe('https://example.com/cover.jpg'); }); it('starts in DRAFT status', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title ); expect($link->status)->toBe(LinkStatus::DRAFT); expect($link->isActive())->toBeFalse(); }); }); describe('status transitions', function () { it('transitions from DRAFT to ACTIVE', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title ); $activeLink = $link->withStatus(LinkStatus::ACTIVE); expect($activeLink->status)->toBe(LinkStatus::ACTIVE); expect($activeLink->isActive())->toBeTrue(); }); it('transitions from ACTIVE to PAUSED', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title )->withStatus(LinkStatus::ACTIVE); $pausedLink = $link->withStatus(LinkStatus::PAUSED); expect($pausedLink->status)->toBe(LinkStatus::PAUSED); }); it('throws exception for invalid transition', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title ); // Cannot transition directly from DRAFT to PAUSED expect(fn() => $link->withStatus(LinkStatus::PAUSED)) ->toThrow(\DomainException::class); }); it('throws exception for invalid transition from EXPIRED', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title )->withStatus(LinkStatus::ACTIVE)->withStatus(LinkStatus::EXPIRED); // Cannot transition from EXPIRED to ACTIVE expect(fn() => $link->withStatus(LinkStatus::ACTIVE)) ->toThrow(\DomainException::class); }); }); describe('title updates', function () { it('updates title', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title ); $newTitle = LinkTitle::fromString('Updated Title'); $updatedLink = $link->withTitle($newTitle); expect($updatedLink->title->toString())->toBe('Updated Title'); expect($link->title->toString())->toBe('My Smart Link'); // Original unchanged }); }); describe('access control', function () { it('identifies active links as active', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title )->withStatus(LinkStatus::ACTIVE); expect($link->isActive())->toBeTrue(); expect($link->canBeAccessed())->toBeTrue(); }); it('identifies draft links as not accessible', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title ); expect($link->canBeAccessed())->toBeFalse(); }); it('identifies paused links as not accessible', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title )->withStatus(LinkStatus::ACTIVE)->withStatus(LinkStatus::PAUSED); expect($link->canBeAccessed())->toBeFalse(); }); }); describe('ownership', function () { it('identifies owner correctly', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title, userId: 'user123' ); expect($link->isOwnedBy('user123'))->toBeTrue(); expect($link->isOwnedBy('user456'))->toBeFalse(); expect($link->isOwnedBy(null))->toBeFalse(); }); it('handles anonymous links', function () { $link = SmartLink::create( clock: $this->clock, shortCode: $this->shortCode, type: LinkType::RELEASE, title: $this->title ); expect($link->userId)->toBeNull(); expect($link->isOwnedBy('user123'))->toBeFalse(); }); }); });