- 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.
155 lines
5.3 KiB
PHP
155 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\MagicLinks\MagicLinkData;
|
|
use App\Framework\MagicLinks\TokenAction;
|
|
use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload;
|
|
|
|
describe('MagicLinkData', function () {
|
|
beforeEach(function () {
|
|
$this->now = new DateTimeImmutable();
|
|
$this->futureExpiry = $this->now->modify('+1 hour');
|
|
$this->pastExpiry = $this->now->modify('-1 hour');
|
|
});
|
|
|
|
it('creates valid magiclink data', function () {
|
|
$action = new TokenAction('email_verification');
|
|
$payload = MagicLinkPayload::fromArray(['user_id' => 123, 'email' => 'test@example.com']);
|
|
|
|
$data = new MagicLinkData(
|
|
id: 'test-id-123',
|
|
action: $action,
|
|
payload: $payload,
|
|
expiresAt: $this->futureExpiry,
|
|
createdAt: $this->now
|
|
);
|
|
|
|
expect($data->id)->toBe('test-id-123');
|
|
expect($data->action)->toBe($action);
|
|
expect($data->payload)->toBe($payload);
|
|
expect($data->expiresAt)->toBe($this->futureExpiry);
|
|
expect($data->createdAt)->toBe($this->now);
|
|
expect($data->oneTimeUse)->toBeFalse();
|
|
expect($data->isUsed)->toBeFalse();
|
|
expect($data->usedAt)->toBeNull();
|
|
});
|
|
|
|
it('detects expired tokens', function () {
|
|
$data = new MagicLinkData(
|
|
id: 'expired-token',
|
|
action: new TokenAction('test_action'),
|
|
payload: MagicLinkPayload::fromArray(['test' => 'data']),
|
|
expiresAt: $this->pastExpiry,
|
|
createdAt: $this->now
|
|
);
|
|
|
|
expect($data->isExpired())->toBeTrue();
|
|
expect($data->isValid())->toBeFalse();
|
|
});
|
|
|
|
it('detects valid non-expired tokens', function () {
|
|
$data = new MagicLinkData(
|
|
id: 'valid-token',
|
|
action: new TokenAction('test_action'),
|
|
payload: MagicLinkPayload::fromArray(['test' => 'data']),
|
|
expiresAt: $this->futureExpiry,
|
|
createdAt: $this->now
|
|
);
|
|
|
|
expect($data->isExpired())->toBeFalse();
|
|
expect($data->isValid())->toBeTrue();
|
|
});
|
|
|
|
it('validates one-time use tokens', function () {
|
|
$data = new MagicLinkData(
|
|
id: 'one-time-token',
|
|
action: new TokenAction('test_action'),
|
|
payload: MagicLinkPayload::fromArray(['test' => 'data']),
|
|
expiresAt: $this->futureExpiry,
|
|
createdAt: $this->now,
|
|
oneTimeUse: true,
|
|
isUsed: false
|
|
);
|
|
|
|
expect($data->isValid())->toBeTrue();
|
|
|
|
// After marking as used
|
|
$usedData = $data->withUsed(new DateTimeImmutable());
|
|
expect($usedData->isValid())->toBeFalse();
|
|
expect($usedData->isUsed)->toBeTrue();
|
|
});
|
|
|
|
it('calculates remaining time correctly', function () {
|
|
$oneHourFuture = $this->now->modify('+1 hour');
|
|
$data = new MagicLinkData(
|
|
id: 'test-token',
|
|
action: new TokenAction('test_action'),
|
|
payload: MagicLinkPayload::fromArray(['test' => 'data']),
|
|
expiresAt: $oneHourFuture,
|
|
createdAt: $this->now
|
|
);
|
|
|
|
$remaining = $data->getRemainingTime();
|
|
|
|
// Should be approximately 1 hour (allowing for small execution time variance)
|
|
expect($remaining->h)->toBeGreaterThanOrEqual(0);
|
|
expect($remaining->i)->toBeGreaterThan(50); // At least 50 minutes
|
|
});
|
|
|
|
it('returns zero interval for expired tokens', function () {
|
|
$data = new MagicLinkData(
|
|
id: 'expired-token',
|
|
action: new TokenAction('test_action'),
|
|
payload: MagicLinkPayload::fromArray(['test' => 'data']),
|
|
expiresAt: $this->pastExpiry,
|
|
createdAt: $this->now
|
|
);
|
|
|
|
$remaining = $data->getRemainingTime();
|
|
|
|
expect($remaining->d)->toBe(0); // d = day component
|
|
expect($remaining->h)->toBe(0); // h = hour component
|
|
expect($remaining->i)->toBe(0); // i = minute component
|
|
});
|
|
|
|
it('marks token as used immutably', function () {
|
|
$originalData = new MagicLinkData(
|
|
id: 'test-token',
|
|
action: new TokenAction('test_action'),
|
|
payload: MagicLinkPayload::fromArray(['test' => 'data']),
|
|
expiresAt: $this->futureExpiry,
|
|
createdAt: $this->now,
|
|
oneTimeUse: true
|
|
);
|
|
|
|
$usedAt = new DateTimeImmutable('+10 seconds');
|
|
$usedData = $originalData->withUsed($usedAt);
|
|
|
|
// Original unchanged
|
|
expect($originalData->isUsed)->toBeFalse();
|
|
expect($originalData->usedAt)->toBeNull();
|
|
|
|
// New instance has updated values
|
|
expect($usedData->isUsed)->toBeTrue();
|
|
expect($usedData->usedAt)->toBe($usedAt);
|
|
expect($usedData->id)->toBe($originalData->id);
|
|
expect($usedData->action)->toBe($originalData->action);
|
|
});
|
|
|
|
it('stores IP address and user agent', function () {
|
|
$data = new MagicLinkData(
|
|
id: 'test-token',
|
|
action: new TokenAction('test_action'),
|
|
payload: MagicLinkPayload::fromArray(['test' => 'data']),
|
|
expiresAt: $this->futureExpiry,
|
|
createdAt: $this->now,
|
|
createdByIp: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0 Test Browser'
|
|
);
|
|
|
|
expect($data->createdByIp)->toBe('192.168.1.1');
|
|
expect($data->userAgent)->toBe('Mozilla/5.0 Test Browser');
|
|
});
|
|
});
|