Files
portal-api/tests/Feature/Support/TurnstileTest.php
baodev0andClaude Opus 5.5 e888a6ed5d feat: Turnstile captcha, menu khách + nút tắt topbar cấu hình từ admin, ẩn setting nhạy cảm
- Turnstile (App\Support\Turnstile, middleware turnstile) cho login admin và login/register/forgot/reset của khách; cấu hình ở setting key=turnstile, secret mã hoá SignRSA.
- Menu sidebar khách (setting client_menu) và nút tắt topbar (setting client_quick_links) do admin sửa qua administrator/client-menu[/quick-links], khách đọc v1/setting/menu, v1/setting/quick-links.
- v1/setting và settings/public chỉ trả whitelist PublicSetting::FIELDS (trước đây lộ bot_token, inet_api_key).
- Cài đặt tài khoản khách (preferences), rate limit theo IP cho register/resend-verify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:53:43 +07:00

73 lines
2.5 KiB
PHP

<?php
namespace Tests\Feature\Support;
use App\Http\Facade\SignRSA;
use App\Models\Setting;
use App\Support\Turnstile;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Http;
use InvalidArgumentException;
use Tests\TestCase;
class TurnstileTest extends TestCase
{
use DatabaseTransactions;
private function configure(bool $enabled): void
{
Setting::updateOrCreate(['key' => Turnstile::SETTING_KEY], ['value' => json_encode([
'enabled' => $enabled,
'site_key' => 'site',
'secret_key' => SignRSA::EncryptFunc('secret'),
])]);
}
public function test_disabled_passes_without_token(): void
{
$this->configure(false);
Http::fake();
$this->assertNull(Turnstile::siteKey());
$this->assertTrue(Turnstile::verify(null, null));
Http::assertNothingSent();
}
public function test_enabled_requires_valid_token(): void
{
$this->configure(true);
Http::fake(['challenges.cloudflare.com/*' => Http::sequence()
->push(['success' => true])
->push(['success' => false, 'error-codes' => ['timeout-or-duplicate']])]);
$this->assertSame('site', Turnstile::siteKey());
$this->assertFalse(Turnstile::verify('', '1.1.1.1'));
$this->assertTrue(Turnstile::verify('ok', '1.1.1.1'));
$this->assertFalse(Turnstile::verify('reused', '1.1.1.1'));
Http::assertSent(fn ($request) => $request['secret'] === 'secret' && $request['response'] === 'ok');
}
public function test_save_keeps_masked_secret_and_rejects_enabling_without_keys(): void
{
$this->configure(false);
Turnstile::save(['enabled' => true, 'site_key' => 'new-site', 'secret_key' => Turnstile::SECRET_MASK]);
$this->assertSame('secret', SignRSA::DecryptFunc(Turnstile::stored()['secret_key']));
$this->assertSame(['enabled' => true, 'site_key' => 'new-site', 'secret_key' => Turnstile::SECRET_MASK], Turnstile::forAdmin());
$this->expectException(InvalidArgumentException::class);
Turnstile::save(['secret_key' => '']);
}
public function test_admin_login_rejects_missing_token_when_enabled(): void
{
$this->configure(true);
Http::fake();
$this->postJson('/api/v1/administrator/login', ['username' => 'x', 'password' => 'y'])
->assertStatus(422)
->assertJsonPath('error', true);
Http::assertNothingSent();
}
}