Files
CFDivePlatform/tests/Feature/AuthOAuthTest.php
T
a620906209 6f446f601f test(auth): 新增三角色登入/註冊/登出、P2 帳號鎖定、OAuth state 驗證測試
新增 34 個 Feature 測試(AuthLoginTest 19、AuthLockoutTest 11、
AuthOAuthTest 4)涵蓋 member/provider/admin 的註冊、登入、登出與跨角色
隔離;P2 帳號鎖定的 Fixed Window 計數、companion cache key 與 email
正規化;以及 P2 OAuth state 的 CSRF 驗證與一次性消耗。

同時新增 .gitea/workflows/test.yml 在 push/PR 時於 CI 自動執行測試
(含 GD 等擴充套件安裝),並補上對應的 OpenSpec 變更文件
(proposal/design/spec/tasks,34 項驗收情境與測試 1:1 對應)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 04:01:33 +08:00

95 lines
4.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\User as SocialiteUser;
use Tests\TestCase;
class AuthOAuthTest extends TestCase
{
use RefreshDatabase;
// ── helpers ─────────────────────────────────────────────
/**
* 組一個假的 Google 使用者物件,餵給 mock 過的 Socialite。
* 測試不應該真的打 Google API(會很慢、不穩定、還要真帳密),
* 所以用 Mockery 把 Socialite 換成假的,並回傳這個假資料。
*/
private function fakeGoogleUser(string $id, string $email, string $name = 'Google Test User'): SocialiteUser
{
$user = new SocialiteUser();
$user->id = $id;
$user->name = $name;
$user->email = $email;
$user->token = 'fake-access-token-' . $id;
return $user;
}
// ── state 缺失 / 不符 ────────────────────────────────────
//
// 「state」是 OAuth 流程裡用來防 CSRF 的一次性亂碼:後端發起登入時把它存進
// sessionGoogle callback 回來時要原樣帶回,兩邊對得上才繼續,對不上就視為
// 攻擊/偽造請求,直接擋下、不跟 Google 要使用者資料。
// 下面兩個測試就是在驗證「對不上的時候真的有擋下」這個安全底線。
public function test_oauth_callback_without_state_redirects_to_error(): void
{
// shouldReceive('driver')->never():斷言 Socialite::driver(...) 整個流程
// 都「不應該被呼叫」。這是防回歸的關鍵——萬一未來有人改壞了驗證順序,
// 導致系統在 state 比對「之前」就先去問 Google 拿使用者資料,這裡就會爆紅。
Socialite::shouldReceive('driver')->never();
$response = $this->get('/api/auth/google/callback');
$response->assertRedirectContains('error=oauth_failed');
}
public function test_oauth_callback_with_wrong_state_redirects_to_error(): void
{
Socialite::shouldReceive('driver')->never();
$response = $this->withSession(['oauth_state' => 'correct-state'])
->get('/api/auth/google/callback?state=wrong-state');
$response->assertRedirectContains('error=oauth_failed');
}
// ── state 正確 ───────────────────────────────────────────
public function test_oauth_callback_with_correct_state_completes_login(): void
{
// shouldReceive('driver->user'):模擬「Socialite::driver('google')->user()」這條
// 鏈式呼叫,讓它回傳上面準備好的假 Google 使用者,藉此驗證「state 對得上時,
// 系統會繼續走完登入流程並帶著 token 導回前端」。
Socialite::shouldReceive('driver->user')
->andReturn($this->fakeGoogleUser('google-state-ok', 'state-ok@example.com'));
$response = $this->withSession(['oauth_state' => 'matching-state'])
->get('/api/auth/google/callback?state=matching-state');
$response->assertRedirectContains('#token=');
}
public function test_oauth_state_is_consumed_after_successful_callback(): void
{
// 驗證「state 是一次性的」:用過一次就該從 session 移除,
// 避免同一組 state 被重放(replay attack)拿來再次完成登入。
Socialite::shouldReceive('driver->user')
->andReturn($this->fakeGoogleUser('google-one-time', 'one-time@example.com'));
// 第一次:state 正確,完成登入;session 中的 oauth_state 在驗證時被 pull() 消耗
$this->withSession(['oauth_state' => 'one-time-state'])
->get('/api/auth/google/callback?state=one-time-state')
->assertRedirectContains('#token=');
// 第二次:再帶相同 state 呼叫,但 session 已無 oauth_state,視同 state 缺失
$response = $this->get('/api/auth/google/callback?state=one-time-state');
$response->assertRedirectContains('error=oauth_failed');
}
}