test borked
This commit is contained in:
parent
49b528a66b
commit
2418edccfd
29 changed files with 2036 additions and 121 deletions
303
tests/Unit/MagicLoginTokenTest.php
Executable file
303
tests/Unit/MagicLoginTokenTest.php
Executable file
|
|
@ -0,0 +1,303 @@
|
|||
<?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\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MagicLoginTokenTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_word_token_generation_creates_exactly_4_words(): void
|
||||
{
|
||||
$token = MagicLoginToken::generateWordToken();
|
||||
|
||||
$words = explode('-', $token);
|
||||
|
||||
$this->assertCount(4, $words, 'Token should contain exactly 4 words');
|
||||
}
|
||||
|
||||
public function test_word_token_is_hyphen_separated(): void
|
||||
{
|
||||
$token = MagicLoginToken::generateWordToken();
|
||||
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/^[a-z]+-[a-z]+-[a-z]+-[a-z]+$/',
|
||||
$token,
|
||||
'Token should match pattern: word-word-word-word'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_words_are_from_word_list_file(): void
|
||||
{
|
||||
$wordList = explode("\n", trim(Storage::get('words.txt')));
|
||||
$wordList = array_map('trim', $wordList);
|
||||
$wordList = array_filter($wordList);
|
||||
|
||||
$token = MagicLoginToken::generateWordToken();
|
||||
$words = explode('-', $token);
|
||||
|
||||
foreach ($words as $word) {
|
||||
$this->assertContains(
|
||||
$word,
|
||||
$wordList,
|
||||
"Word '{$word}' should be from the word list file"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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('wrong-token-here-bad'),
|
||||
'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()->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_word_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);
|
||||
}
|
||||
}
|
||||
159
tests/Unit/UserHashEmailTest.php
Executable file
159
tests/Unit/UserHashEmailTest.php
Executable file
|
|
@ -0,0 +1,159 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserHashEmailTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_hash_uses_hmac_sha256_with_app_key(): void
|
||||
{
|
||||
$email = 'test@example.com';
|
||||
$hash = User::hashEmail($email);
|
||||
|
||||
$expectedHash = hash_hmac('sha256', strtolower(trim($email)), config('app.key'));
|
||||
$this->assertEquals($expectedHash, $hash);
|
||||
|
||||
$plainSha256Hash = hash('sha256', strtolower(trim($email)));
|
||||
$this->assertNotEquals($plainSha256Hash, $hash, 'HMAC-SHA256 should be different from plain SHA-256');
|
||||
}
|
||||
|
||||
public function test_hash_is_deterministic(): void
|
||||
{
|
||||
$email = 'test@example.com';
|
||||
|
||||
$hash1 = User::hashEmail($email);
|
||||
$hash2 = User::hashEmail($email);
|
||||
|
||||
$this->assertEquals($hash1, $hash2, 'Same email should produce the same hash');
|
||||
}
|
||||
|
||||
public function test_hash_is_case_insensitive(): void
|
||||
{
|
||||
$email1 = 'test@example.com';
|
||||
$email2 = 'TEST@EXAMPLE.COM';
|
||||
$email3 = 'TeSt@ExAmPlE.cOm';
|
||||
|
||||
$hash1 = User::hashEmail($email1);
|
||||
$hash2 = User::hashEmail($email2);
|
||||
$hash3 = User::hashEmail($email3);
|
||||
|
||||
$this->assertEquals($hash1, $hash2, 'Uppercase email should produce the same hash');
|
||||
$this->assertEquals($hash1, $hash3, 'Mixed case email should produce the same hash');
|
||||
}
|
||||
|
||||
public function test_hash_trims_whitespace(): void
|
||||
{
|
||||
$email1 = 'test@example.com';
|
||||
$email2 = ' test@example.com ';
|
||||
$email3 = "\ttest@example.com\n";
|
||||
|
||||
$hash1 = User::hashEmail($email1);
|
||||
$hash2 = User::hashEmail($email2);
|
||||
$hash3 = User::hashEmail($email3);
|
||||
|
||||
$this->assertEquals($hash1, $hash2, 'Email with leading/trailing spaces should produce the same hash');
|
||||
$this->assertEquals($hash1, $hash3, 'Email with tabs/newlines should produce the same hash');
|
||||
}
|
||||
|
||||
public function test_different_emails_produce_different_hashes(): void
|
||||
{
|
||||
$email1 = 'user1@example.com';
|
||||
$email2 = 'user2@example.com';
|
||||
$email3 = 'admin@example.com';
|
||||
|
||||
$hash1 = User::hashEmail($email1);
|
||||
$hash2 = User::hashEmail($email2);
|
||||
$hash3 = User::hashEmail($email3);
|
||||
|
||||
$this->assertNotEquals($hash1, $hash2, 'Different emails should produce different hashes');
|
||||
$this->assertNotEquals($hash1, $hash3, 'Different emails should produce different hashes');
|
||||
$this->assertNotEquals($hash2, $hash3, 'Different emails should produce different hashes');
|
||||
}
|
||||
|
||||
public function test_find_by_email_method_works_correctly(): void
|
||||
{
|
||||
$email = 'findme@example.com';
|
||||
$emailHash = User::hashEmail($email);
|
||||
|
||||
$this->assertNull(User::findByEmail($email), 'User should not exist yet');
|
||||
|
||||
$user = User::create([
|
||||
'email_hash' => $emailHash,
|
||||
]);
|
||||
|
||||
$foundUser = User::findByEmail($email);
|
||||
|
||||
$this->assertNotNull($foundUser, 'User should be found');
|
||||
$this->assertEquals($user->id, $foundUser->id, 'Found user should match created user');
|
||||
$this->assertEquals($emailHash, $foundUser->email_hash, 'Email hash should match');
|
||||
}
|
||||
|
||||
public function test_find_by_email_is_case_insensitive(): void
|
||||
{
|
||||
$email = 'case@example.com';
|
||||
$emailHash = User::hashEmail($email);
|
||||
|
||||
$user = User::create([
|
||||
'email_hash' => $emailHash,
|
||||
]);
|
||||
|
||||
$foundUser1 = User::findByEmail('case@example.com');
|
||||
$foundUser2 = User::findByEmail('CASE@EXAMPLE.COM');
|
||||
$foundUser3 = User::findByEmail('CaSe@ExAmPlE.cOm');
|
||||
|
||||
$this->assertNotNull($foundUser1);
|
||||
$this->assertNotNull($foundUser2);
|
||||
$this->assertNotNull($foundUser3);
|
||||
|
||||
$this->assertEquals($user->id, $foundUser1->id);
|
||||
$this->assertEquals($user->id, $foundUser2->id);
|
||||
$this->assertEquals($user->id, $foundUser3->id);
|
||||
}
|
||||
|
||||
public function test_find_by_email_trims_whitespace(): void
|
||||
{
|
||||
$email = 'trim@example.com';
|
||||
$emailHash = User::hashEmail($email);
|
||||
|
||||
$user = User::create([
|
||||
'email_hash' => $emailHash,
|
||||
]);
|
||||
|
||||
$foundUser = User::findByEmail(' trim@example.com ');
|
||||
|
||||
$this->assertNotNull($foundUser);
|
||||
$this->assertEquals($user->id, $foundUser->id);
|
||||
}
|
||||
|
||||
public function test_hash_length_is_consistent(): void
|
||||
{
|
||||
$emails = [
|
||||
'a@b.c',
|
||||
'test@example.com',
|
||||
'very.long.email.address@subdomain.example.com',
|
||||
];
|
||||
|
||||
$hashes = array_map(fn($email) => User::hashEmail($email), $emails);
|
||||
|
||||
foreach ($hashes as $hash) {
|
||||
$this->assertEquals(64, strlen($hash), 'SHA-256 hash should always be 64 characters');
|
||||
}
|
||||
}
|
||||
|
||||
public function test_hash_contains_only_hexadecimal_characters(): void
|
||||
{
|
||||
$email = 'test@example.com';
|
||||
$hash = User::hashEmail($email);
|
||||
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/^[a-f0-9]{64}$/',
|
||||
$hash,
|
||||
'Hash should contain only lowercase hexadecimal characters'
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue