fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
417
tests/Framework/MagicLinks/MagicLinksValueObjectsTest.php
Normal file
417
tests/Framework/MagicLinks/MagicLinksValueObjectsTest.php
Normal file
@@ -0,0 +1,417 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\MagicLinks\ValueObjects\{
|
||||
TokenAction,
|
||||
MagicLinkToken,
|
||||
TokenConfig,
|
||||
ActionResult,
|
||||
MagicLinkData
|
||||
};
|
||||
|
||||
describe('TokenAction', function () {
|
||||
it('creates valid token action', function () {
|
||||
$action = new TokenAction('email_verification');
|
||||
|
||||
expect($action->value)->toBe('email_verification');
|
||||
});
|
||||
|
||||
it('validates action format', function () {
|
||||
new TokenAction('invalid-action'); // Hyphens not allowed
|
||||
})->throws(InvalidArgumentException::class, 'must contain only lowercase letters and underscores');
|
||||
|
||||
it('rejects empty action', function () {
|
||||
new TokenAction('');
|
||||
})->throws(InvalidArgumentException::class, 'cannot be empty');
|
||||
|
||||
it('compares actions correctly', function () {
|
||||
$action1 = new TokenAction('email_verification');
|
||||
$action2 = new TokenAction('email_verification');
|
||||
$action3 = new TokenAction('password_reset');
|
||||
|
||||
expect($action1->equals($action2))->toBeTrue();
|
||||
expect($action1->equals($action3))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MagicLinkToken', function () {
|
||||
it('creates valid token', function () {
|
||||
$token = new MagicLinkToken('abcdef0123456789'); // 16 chars
|
||||
|
||||
expect($token->value)->toBe('abcdef0123456789');
|
||||
});
|
||||
|
||||
it('rejects short tokens', function () {
|
||||
new MagicLinkToken('short'); // Less than 16 chars
|
||||
})->throws(InvalidArgumentException::class, 'must be at least 16 characters');
|
||||
|
||||
it('generates cryptographically secure tokens', function () {
|
||||
$token1 = MagicLinkToken::generate();
|
||||
$token2 = MagicLinkToken::generate();
|
||||
|
||||
expect($token1->value)->not->toBe($token2->value);
|
||||
expect(strlen($token1->value))->toBe(64); // 32 bytes * 2 (hex)
|
||||
});
|
||||
|
||||
it('compares tokens with constant time', function () {
|
||||
$token1 = new MagicLinkToken('abcdef0123456789');
|
||||
$token2 = new MagicLinkToken('abcdef0123456789');
|
||||
$token3 = new MagicLinkToken('different0123456');
|
||||
|
||||
expect($token1->equals($token2))->toBeTrue();
|
||||
expect($token1->equals($token3))->toBeFalse();
|
||||
});
|
||||
|
||||
it('generates tokens with custom length', function () {
|
||||
$token = MagicLinkToken::generate(16); // 16 bytes
|
||||
|
||||
expect(strlen($token->value))->toBe(32); // 16 bytes * 2 (hex)
|
||||
});
|
||||
});
|
||||
|
||||
describe('TokenConfig', function () {
|
||||
it('creates default config', function () {
|
||||
$config = new TokenConfig();
|
||||
|
||||
expect($config->ttlSeconds)->toBe(3600);
|
||||
expect($config->oneTimeUse)->toBeFalse();
|
||||
expect($config->maxUses)->toBeNull();
|
||||
expect($config->ipRestriction)->toBeFalse();
|
||||
});
|
||||
|
||||
it('creates custom config', function () {
|
||||
$config = new TokenConfig(
|
||||
ttlSeconds: 7200,
|
||||
oneTimeUse: true,
|
||||
maxUses: null,
|
||||
ipRestriction: true
|
||||
);
|
||||
|
||||
expect($config->ttlSeconds)->toBe(7200);
|
||||
expect($config->oneTimeUse)->toBeTrue();
|
||||
expect($config->ipRestriction)->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates positive ttl', function () {
|
||||
new TokenConfig(ttlSeconds: 0);
|
||||
})->throws(InvalidArgumentException::class, 'TTL must be positive');
|
||||
|
||||
it('validates positive max uses', function () {
|
||||
new TokenConfig(maxUses: 0);
|
||||
})->throws(InvalidArgumentException::class, 'Max uses must be positive');
|
||||
|
||||
it('validates one-time-use consistency', function () {
|
||||
new TokenConfig(oneTimeUse: true, maxUses: 5);
|
||||
})->throws(InvalidArgumentException::class, 'One-time-use tokens cannot have maxUses');
|
||||
|
||||
it('creates email verification config', function () {
|
||||
$config = TokenConfig::forEmailVerification();
|
||||
|
||||
expect($config->ttlSeconds)->toBe(86400); // 24 hours
|
||||
expect($config->oneTimeUse)->toBeTrue();
|
||||
});
|
||||
|
||||
it('creates password reset config', function () {
|
||||
$config = TokenConfig::forPasswordReset();
|
||||
|
||||
expect($config->ttlSeconds)->toBe(3600); // 1 hour
|
||||
expect($config->oneTimeUse)->toBeTrue();
|
||||
expect($config->ipRestriction)->toBeTrue();
|
||||
});
|
||||
|
||||
it('creates document access config', function () {
|
||||
$config = TokenConfig::forDocumentAccess(3);
|
||||
|
||||
expect($config->ttlSeconds)->toBe(3600);
|
||||
expect($config->maxUses)->toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ActionResult', function () {
|
||||
it('creates successful result', function () {
|
||||
$result = new ActionResult(
|
||||
success: true,
|
||||
message: 'Email verified',
|
||||
data: ['user_id' => 123]
|
||||
);
|
||||
|
||||
expect($result->isSuccess())->toBeTrue();
|
||||
expect($result->message)->toBe('Email verified');
|
||||
expect($result->data)->toBe(['user_id' => 123]);
|
||||
});
|
||||
|
||||
it('creates failure result', function () {
|
||||
$result = new ActionResult(
|
||||
success: false,
|
||||
message: 'Verification failed',
|
||||
errors: ['Token expired']
|
||||
);
|
||||
|
||||
expect($result->isSuccess())->toBeFalse();
|
||||
expect($result->hasErrors())->toBeTrue();
|
||||
expect($result->errors)->toBe(['Token expired']);
|
||||
});
|
||||
|
||||
it('detects redirect presence', function () {
|
||||
$result1 = new ActionResult(
|
||||
success: true,
|
||||
message: 'Success',
|
||||
redirectUrl: '/dashboard'
|
||||
);
|
||||
|
||||
$result2 = new ActionResult(
|
||||
success: true,
|
||||
message: 'Success'
|
||||
);
|
||||
|
||||
expect($result1->hasRedirect())->toBeTrue();
|
||||
expect($result2->hasRedirect())->toBeFalse();
|
||||
});
|
||||
|
||||
it('creates success via factory method', function () {
|
||||
$result = ActionResult::success(
|
||||
message: 'Operation completed',
|
||||
data: ['id' => 456],
|
||||
redirectUrl: '/success'
|
||||
);
|
||||
|
||||
expect($result->isSuccess())->toBeTrue();
|
||||
expect($result->message)->toBe('Operation completed');
|
||||
expect($result->data)->toBe(['id' => 456]);
|
||||
expect($result->redirectUrl)->toBe('/success');
|
||||
});
|
||||
|
||||
it('creates failure via factory method', function () {
|
||||
$result = ActionResult::failure(
|
||||
message: 'Operation failed',
|
||||
errors: ['Invalid input', 'Permission denied']
|
||||
);
|
||||
|
||||
expect($result->isSuccess())->toBeFalse();
|
||||
expect($result->errors)->toHaveCount(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MagicLinkData', function () {
|
||||
it('creates valid magic link data', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$expiresAt = $now->modify('+1 hour');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('email_verification'),
|
||||
payload: ['user_id' => 1, 'email' => 'test@example.com'],
|
||||
expiresAt: $expiresAt,
|
||||
createdAt: $now
|
||||
);
|
||||
|
||||
expect($data->id)->toBe('test-123');
|
||||
expect($data->action->value)->toBe('email_verification');
|
||||
expect($data->payload)->toBe(['user_id' => 1, 'email' => 'test@example.com']);
|
||||
});
|
||||
|
||||
it('detects expired tokens', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$past = $now->modify('-1 hour');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('test'),
|
||||
payload: [],
|
||||
expiresAt: $past,
|
||||
createdAt: $now->modify('-2 hours')
|
||||
);
|
||||
|
||||
expect($data->isExpired())->toBeTrue();
|
||||
expect($data->isValid())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates non-expired tokens', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$future = $now->modify('+1 hour');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('test'),
|
||||
payload: [],
|
||||
expiresAt: $future,
|
||||
createdAt: $now
|
||||
);
|
||||
|
||||
expect($data->isExpired())->toBeFalse();
|
||||
expect($data->isValid())->toBeTrue();
|
||||
});
|
||||
|
||||
it('invalidates one-time-use tokens after use', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$future = $now->modify('+1 hour');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('test'),
|
||||
payload: [],
|
||||
expiresAt: $future,
|
||||
createdAt: $now,
|
||||
oneTimeUse: true,
|
||||
isUsed: true
|
||||
);
|
||||
|
||||
expect($data->isValid())->toBeFalse();
|
||||
});
|
||||
|
||||
it('tracks use count correctly', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$future = $now->modify('+1 hour');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('test'),
|
||||
payload: [],
|
||||
expiresAt: $future,
|
||||
createdAt: $now,
|
||||
useCount: 2,
|
||||
maxUses: 5
|
||||
);
|
||||
|
||||
expect($data->hasRemainingUses())->toBeTrue();
|
||||
expect($data->isValid())->toBeTrue();
|
||||
});
|
||||
|
||||
it('invalidates tokens exceeding max uses', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$future = $now->modify('+1 hour');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('test'),
|
||||
payload: [],
|
||||
expiresAt: $future,
|
||||
createdAt: $now,
|
||||
useCount: 5,
|
||||
maxUses: 5
|
||||
);
|
||||
|
||||
expect($data->hasRemainingUses())->toBeFalse();
|
||||
expect($data->isValid())->toBeFalse();
|
||||
});
|
||||
|
||||
it('calculates remaining time correctly', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$future = $now->modify('+3600 seconds');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('test'),
|
||||
payload: [],
|
||||
expiresAt: $future,
|
||||
createdAt: $now
|
||||
);
|
||||
|
||||
$remaining = $data->getSecondsUntilExpiry();
|
||||
expect($remaining)->toBeGreaterThan(3590);
|
||||
expect($remaining)->toBeLessThanOrEqual(3600);
|
||||
});
|
||||
|
||||
it('returns zero for expired token remaining time', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$past = $now->modify('-1 hour');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('test'),
|
||||
payload: [],
|
||||
expiresAt: $past,
|
||||
createdAt: $now->modify('-2 hours')
|
||||
);
|
||||
|
||||
expect($data->getSecondsUntilExpiry())->toBe(0);
|
||||
});
|
||||
|
||||
it('marks token as used immutably', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$future = $now->modify('+1 hour');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('test'),
|
||||
payload: [],
|
||||
expiresAt: $future,
|
||||
createdAt: $now,
|
||||
oneTimeUse: true
|
||||
);
|
||||
|
||||
$usedData = $data->withUsed(new DateTimeImmutable());
|
||||
|
||||
expect($data->isUsed)->toBeFalse(); // Original unchanged
|
||||
expect($usedData->isUsed)->toBeTrue();
|
||||
expect($usedData->useCount)->toBe(1);
|
||||
});
|
||||
|
||||
it('increments use count immutably', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$future = $now->modify('+1 hour');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('test'),
|
||||
payload: [],
|
||||
expiresAt: $future,
|
||||
createdAt: $now,
|
||||
useCount: 2
|
||||
);
|
||||
|
||||
$incremented = $data->withIncrementedUseCount();
|
||||
|
||||
expect($data->useCount)->toBe(2); // Original unchanged
|
||||
expect($incremented->useCount)->toBe(3);
|
||||
});
|
||||
|
||||
it('serializes to array correctly', function () {
|
||||
$now = new DateTimeImmutable();
|
||||
$future = $now->modify('+1 hour');
|
||||
|
||||
$data = new MagicLinkData(
|
||||
id: 'test-123',
|
||||
action: new TokenAction('email_verification'),
|
||||
payload: ['user_id' => 1],
|
||||
expiresAt: $future,
|
||||
createdAt: $now,
|
||||
oneTimeUse: true,
|
||||
createdByIp: '127.0.0.1',
|
||||
userAgent: 'Mozilla/5.0'
|
||||
);
|
||||
|
||||
$array = $data->toArray();
|
||||
|
||||
expect($array)->toHaveKey('id');
|
||||
expect($array)->toHaveKey('action');
|
||||
expect($array)->toHaveKey('payload');
|
||||
expect($array)->toHaveKey('is_valid');
|
||||
expect($array['action'])->toBe('email_verification');
|
||||
expect($array['payload'])->toBe(['user_id' => 1]);
|
||||
});
|
||||
|
||||
it('deserializes from array correctly', function () {
|
||||
$array = [
|
||||
'id' => 'test-123',
|
||||
'action' => 'password_reset',
|
||||
'payload' => ['user_id' => 456],
|
||||
'expires_at' => '2025-12-31 23:59:59',
|
||||
'created_at' => '2025-12-31 12:00:00',
|
||||
'one_time_use' => true,
|
||||
'created_by_ip' => '192.168.1.1',
|
||||
'user_agent' => 'Test Agent',
|
||||
'is_used' => false,
|
||||
'use_count' => 0
|
||||
];
|
||||
|
||||
$data = MagicLinkData::fromArray($array);
|
||||
|
||||
expect($data->id)->toBe('test-123');
|
||||
expect($data->action->value)->toBe('password_reset');
|
||||
expect($data->payload)->toBe(['user_id' => 456]);
|
||||
expect($data->oneTimeUse)->toBeTrue();
|
||||
expect($data->createdByIp)->toBe('192.168.1.1');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user