scan.fyi/tests/Unit/MagicLoginTokenTest.php

266 lines
8.5 KiB
PHP
Executable file

<?php
namespace Tests\Unit;
use App\Models\MagicLoginToken;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Tests\TestCase;
class MagicLoginTokenTest extends TestCase
{
use RefreshDatabase;
public function test_token_expiry_validation_works(): void
{
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
$this->assertTrue($token->isValid(), 'Newly created token should be valid');
$token->update(['expires_at' => now()->subMinutes(1)]);
$token->refresh();
$this->assertFalse($token->isValid(), 'Expired token should not be valid');
}
public function test_marking_token_as_used_works(): void
{
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
$this->assertNull($token->used_at, 'New token should not be marked as used');
$this->assertTrue($token->isValid(), 'New token should be valid');
$token->markAsUsed();
$token->refresh();
$this->assertNotNull($token->used_at, 'Token should be marked as used');
$this->assertFalse($token->isValid(), 'Used token should not be valid');
}
public function test_code_verification_works(): void
{
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
$code = $token->plain_code;
$this->assertTrue(
$token->verifyCode($code),
'Correct code should verify successfully'
);
$this->assertFalse(
$token->verifyCode('000000'),
'Incorrect code should not verify'
);
$this->assertFalse(
$token->verifyCode('999999'),
'Wrong code should not verify'
);
}
public function test_code_verification_uses_bcrypt(): void
{
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
$this->assertNotEquals(
$token->plain_code,
$token->code_hash,
'Code hash should not be plain text'
);
$this->assertTrue(
Hash::check($token->plain_code, $token->code_hash),
'Code hash should use bcrypt'
);
}
public function test_token_verification_works(): void
{
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
$plainToken = $token->plain_token;
$this->assertTrue(
$token->verifyToken($plainToken),
'Correct token should verify successfully'
);
$this->assertFalse(
$token->verifyToken(Str::random(64)),
'Incorrect token should not verify'
);
}
public function test_token_verification_uses_plain_text_comparison(): void
{
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
$this->assertEquals(
$token->plain_token,
$token->plain_token,
'Plain token should be stored as-is for comparison'
);
$this->assertTrue(
$token->verifyToken($token->plain_token),
'Token verification should use plain text comparison'
);
}
public function test_scope_valid_returns_only_valid_tokens(): void
{
$validToken = MagicLoginToken::generate('valid@example.com', '127.0.0.1', 'TestAgent');
$expiredToken = MagicLoginToken::generate('expired@example.com', '127.0.0.1', 'TestAgent');
$expiredToken->update(['expires_at' => now()->subMinutes(1)]);
$usedToken = MagicLoginToken::generate('used@example.com', '127.0.0.1', 'TestAgent');
$usedToken->markAsUsed();
$validTokens = MagicLoginToken::valid()->get();
$this->assertCount(1, $validTokens, 'Only one token should be valid');
$this->assertEquals($validToken->id, $validTokens->first()->id);
}
public function test_scope_expired_or_used_returns_correct_tokens(): void
{
$validToken = MagicLoginToken::generate('valid@example.com', '127.0.0.1', 'TestAgent');
$expiredToken = MagicLoginToken::generate('expired@example.com', '127.0.0.1', 'TestAgent');
$expiredToken->update(['expires_at' => now()->subMinutes(1)]);
$usedToken = MagicLoginToken::generate('used@example.com', '127.0.0.1', 'TestAgent');
$usedToken->markAsUsed();
$expiredOrUsedTokens = MagicLoginToken::expiredOrUsed()->get();
$this->assertCount(2, $expiredOrUsedTokens, 'Two tokens should be expired or used');
$ids = $expiredOrUsedTokens->pluck('id')->toArray();
$this->assertContains($expiredToken->id, $ids);
$this->assertContains($usedToken->id, $ids);
$this->assertNotContains($validToken->id, $ids);
}
public function test_generated_code_is_6_digits(): void
{
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
$this->assertMatchesRegularExpression(
'/^\d{6}$/',
$token->plain_code,
'Code should be exactly 6 digits'
);
}
public function test_generated_code_includes_leading_zeros(): void
{
for ($i = 0; $i < 20; $i++) {
$token = MagicLoginToken::generate('test' . $i . '@example.com', '127.0.0.1', 'TestAgent');
$this->assertEquals(
6,
strlen($token->plain_code),
'Code should always be 6 characters long, including leading zeros'
);
}
}
public function test_token_stores_ip_address_and_user_agent(): void
{
$ip = '192.168.1.1';
$userAgent = 'Mozilla/5.0 Test Browser';
$token = MagicLoginToken::generate('test@example.com', $ip, $userAgent);
$this->assertEquals($ip, $token->ip_address);
$this->assertEquals($userAgent, $token->user_agent);
}
public function test_token_expiry_is_15_minutes(): void
{
$beforeCreation = now()->startOfSecond()->addMinutes(15);
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
$afterCreation = now()->addMinutes(15);
$this->assertTrue(
$token->expires_at->between($beforeCreation, $afterCreation),
'Token should expire in 15 minutes'
);
}
public function test_token_hash_uses_bcrypt(): void
{
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
$this->assertNotEquals(
$token->plain_token,
$token->token_hash,
'Token hash should not be plain text'
);
$this->assertTrue(
Hash::check($token->plain_token, $token->token_hash),
'Token hash should use bcrypt'
);
}
public function test_email_hash_is_stored_correctly(): void
{
$email = 'test@example.com';
$expectedHash = User::hashEmail($email);
$token = MagicLoginToken::generate($email, '127.0.0.1', 'TestAgent');
$this->assertEquals($expectedHash, $token->email_hash);
}
public function test_multiple_tokens_can_exist_for_same_email(): void
{
$email = 'test@example.com';
$token1 = MagicLoginToken::generate($email, '127.0.0.1', 'TestAgent');
$token2 = MagicLoginToken::generate($email, '127.0.0.1', 'TestAgent');
$this->assertNotEquals($token1->id, $token2->id);
$this->assertNotEquals($token1->plain_token, $token2->plain_token);
$this->assertNotEquals($token1->plain_code, $token2->plain_code);
$this->assertEquals($token1->email_hash, $token2->email_hash);
}
public function test_tokens_are_unique(): void
{
$generatedTokens = [];
for ($i = 0; $i < 10; $i++) {
$token = MagicLoginToken::generate('test' . $i . '@example.com', '127.0.0.1', 'TestAgent');
$generatedTokens[] = $token->plain_token;
}
$uniqueTokens = array_unique($generatedTokens);
$this->assertCount(
10,
$uniqueTokens,
'All generated tokens should be unique'
);
}
public function test_token_casts_dates_correctly(): void
{
$token = MagicLoginToken::generate('test@example.com', '127.0.0.1', 'TestAgent');
$this->assertInstanceOf(\Illuminate\Support\Carbon::class, $token->expires_at);
$this->assertNull($token->used_at);
$token->markAsUsed();
$token->refresh();
$this->assertInstanceOf(\Illuminate\Support\Carbon::class, $token->used_at);
}
}