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); } }