From d5151ce31cf396da9d83c1b1ae7428f3d6d75e0e Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Mon, 9 Mar 2026 10:15:43 +0800 Subject: [PATCH] update test --- .../Integration/Auth/AuthControllerTest.php | 461 ++++++++++++++++++ backend/test/Cases/Unit/Model/ApiKeyTest.php | 156 ++++++ backend/test/Cases/Unit/Model/RoleTest.php | 69 +++ .../test/Cases/Unit/Model/UserRoleTest.php | 102 ++++ 4 files changed, 788 insertions(+) create mode 100644 backend/test/Cases/Integration/Auth/AuthControllerTest.php create mode 100644 backend/test/Cases/Unit/Model/ApiKeyTest.php create mode 100644 backend/test/Cases/Unit/Model/RoleTest.php create mode 100644 backend/test/Cases/Unit/Model/UserRoleTest.php diff --git a/backend/test/Cases/Integration/Auth/AuthControllerTest.php b/backend/test/Cases/Integration/Auth/AuthControllerTest.php new file mode 100644 index 0000000..5e2ba00 --- /dev/null +++ b/backend/test/Cases/Integration/Auth/AuthControllerTest.php @@ -0,0 +1,461 @@ +fetchUser(static function ($query): void { + $query->where('status', 1); + }); + } + if (!$user) { + $this->markTestSkipped('没有可用的活跃用户,无法测试'); + } + + $auth = make(AuthManager::class); + return $auth->guard('jwt')->login($user); + } + + protected function authHeaders(?User $user = null): array + { + return ['Authorization' => 'Bearer ' . $this->getAuthToken($user)]; + } + + protected function fetchUser(?callable $callback = null): ?User + { + if (\Swoole\Coroutine::getCid() > 0) { + $query = User::query(); + if ($callback !== null) { + $callback($query); + } + + return $query->first(); + } + + $user = null; + + \Swoole\Coroutine\run(static function () use ($callback, &$user): void { + $query = User::query(); + if ($callback !== null) { + $callback($query); + } + + $user = $query->first(); + }); + + return $user; + } + + protected function createTestUser(array $overrides = []): User + { + $suffix = bin2hex(random_bytes(4)); + + return User::query()->create(array_merge([ + 'username' => 'auth_test_' . $suffix, + 'password' => 'Pass_' . $suffix, + 'email' => 'auth_test_' . $suffix . '@example.com', + 'status' => 1, + ], $overrides)); + } + + // ========== 注册接口参数校验 ========== + + public function test_register_success(): void + { + $suffix = bin2hex(random_bytes(4)); + + $response = $this->post('/api/v1/register', [ + 'username' => 'reg_' . $suffix, + 'password' => 'Pass_' . $suffix, + 'email' => 'reg_' . $suffix . '@example.com', + ]); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + $response->assertJsonPath('data.username', 'reg_' . $suffix); + } + + public function test_register_missing_username_returns_400(): void + { + $response = $this->post('/api/v1/register', [ + 'password' => 'Pass_1234', + 'email' => 'test@example.com', + ]); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_register_short_username_returns_400(): void + { + $response = $this->post('/api/v1/register', [ + 'username' => 'ab', + 'password' => 'Pass_1234', + 'email' => 'test_short@example.com', + ]); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_register_missing_password_returns_400(): void + { + $suffix = bin2hex(random_bytes(4)); + + $response = $this->post('/api/v1/register', [ + 'username' => 'reg_' . $suffix, + 'email' => 'reg_' . $suffix . '@example.com', + ]); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_register_short_password_returns_400(): void + { + $suffix = bin2hex(random_bytes(4)); + + $response = $this->post('/api/v1/register', [ + 'username' => 'reg_' . $suffix, + 'password' => '12345', + 'email' => 'reg_' . $suffix . '@example.com', + ]); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_register_invalid_email_returns_400(): void + { + $suffix = bin2hex(random_bytes(4)); + + $response = $this->post('/api/v1/register', [ + 'username' => 'reg_' . $suffix, + 'password' => 'Pass_1234', + 'email' => 'not-an-email', + ]); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_register_duplicate_username_returns_400(): void + { + $user = $this->createTestUser(); + + $response = $this->post('/api/v1/register', [ + 'username' => $user->username, + 'password' => 'Pass_1234', + 'email' => 'dup_' . bin2hex(random_bytes(4)) . '@example.com', + ]); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + // ========== 登录接口参数校验 ========== + + public function test_login_success(): void + { + $password = 'Login_test_pass'; + $user = $this->createTestUser(['password' => $password]); + + $response = $this->post('/api/v1/login', [ + 'username' => $user->username, + 'password' => $password, + ]); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + $response->assertJsonStructure([ + 'data' => ['access_token', 'refresh_token', 'token_type', 'expires_in', 'user'], + ]); + } + + public function test_login_missing_username_returns_400(): void + { + $response = $this->post('/api/v1/login', [ + 'password' => 'Pass_1234', + ]); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_login_missing_password_returns_400(): void + { + $response = $this->post('/api/v1/login', [ + 'username' => 'some_user', + ]); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_login_wrong_password_returns_401(): void + { + $user = $this->createTestUser(); + + $response = $this->post('/api/v1/login', [ + 'username' => $user->username, + 'password' => 'wrong_password_here', + ]); + + $response->assertStatus(401); + $response->assertJsonPath('code', 401); + } + + public function test_login_disabled_user_returns_403(): void + { + $password = 'Disabled_pass'; + $user = $this->createTestUser(['password' => $password, 'status' => 0]); + + $response = $this->post('/api/v1/login', [ + 'username' => $user->username, + 'password' => $password, + ]); + + $response->assertStatus(403); + $response->assertJsonPath('code', 403); + } + + // ========== 密码修改接口 ========== + + public function test_change_password_success(): void + { + $old_password = 'OldPass_1234'; + $new_password = 'NewPass_5678'; + $user = $this->createTestUser(['password' => $old_password]); + + $response = $this->put('/api/v1/me/password', [ + 'old_password' => $old_password, + 'new_password' => $new_password, + ], $this->authHeaders($user)); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + + // 验证 refresh_token 已被清除 + $user->refresh(); + $this->assertNull($user->refresh_token); + $this->assertNull($user->refresh_token_expires_at); + } + + public function test_change_password_clears_refresh_token(): void + { + $old_password = 'OldPass_clear'; + $user = $this->createTestUser(['password' => $old_password]); + + // 先登录获取 refresh_token + $this->post('/api/v1/login', [ + 'username' => $user->username, + 'password' => $old_password, + ]); + + $user->refresh(); + $this->assertNotNull($user->refresh_token); + + // 修改密码 + $response = $this->put('/api/v1/me/password', [ + 'old_password' => $old_password, + 'new_password' => 'NewPass_clear', + ], $this->authHeaders($user)); + + $response->assertStatus(200); + + $user->refresh(); + $this->assertNull($user->refresh_token); + } + + public function test_change_password_wrong_old_password_returns_400(): void + { + $user = $this->createTestUser(['password' => 'CorrectOld_1']); + + $response = $this->put('/api/v1/me/password', [ + 'old_password' => 'WrongOld_pass', + 'new_password' => 'NewPass_5678', + ], $this->authHeaders($user)); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_change_password_short_new_password_returns_400(): void + { + $old_password = 'OldPass_short'; + $user = $this->createTestUser(['password' => $old_password]); + + $response = $this->put('/api/v1/me/password', [ + 'old_password' => $old_password, + 'new_password' => '12345', + ], $this->authHeaders($user)); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_change_password_missing_old_password_returns_400(): void + { + $user = $this->createTestUser(); + + $response = $this->put('/api/v1/me/password', [ + 'new_password' => 'NewPass_5678', + ], $this->authHeaders($user)); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_change_password_without_token_returns_401(): void + { + $response = $this->put('/api/v1/me/password', [ + 'old_password' => 'old', + 'new_password' => 'new_pass', + ]); + + $response->assertStatus(401); + } + + // ========== 个人信息更新接口 ========== + + public function test_update_profile_email(): void + { + $user = $this->createTestUser(); + $new_email = 'profile_' . bin2hex(random_bytes(4)) . '@example.com'; + + $response = $this->put('/api/v1/me/profile', [ + 'email' => $new_email, + ], $this->authHeaders($user)); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + $response->assertJsonPath('data.email', $new_email); + + $user->refresh(); + $this->assertSame($new_email, $user->email); + } + + public function test_update_profile_ext(): void + { + $user = $this->createTestUser(); + $ext = ['nickname' => 'TestNick', 'theme' => 'dark']; + + $response = $this->put('/api/v1/me/profile', [ + 'ext' => $ext, + ], $this->authHeaders($user)); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + $response->assertJsonPath('data.ext.nickname', 'TestNick'); + } + + public function test_update_profile_invalid_email_returns_400(): void + { + $user = $this->createTestUser(); + + $response = $this->put('/api/v1/me/profile', [ + 'email' => 'not-an-email', + ], $this->authHeaders($user)); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_update_profile_duplicate_email_returns_400(): void + { + $existing = $this->createTestUser(); + $user = $this->createTestUser(); + + $response = $this->put('/api/v1/me/profile', [ + 'email' => $existing->email, + ], $this->authHeaders($user)); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_update_profile_no_fields_returns_400(): void + { + $user = $this->createTestUser(); + + $response = $this->put('/api/v1/me/profile', [], $this->authHeaders($user)); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_update_profile_without_token_returns_401(): void + { + $response = $this->put('/api/v1/me/profile', [ + 'email' => 'test@example.com', + ]); + + $response->assertStatus(401); + } + + // ========== 现有接口测试 ========== + + public function test_me_returns_user_info(): void + { + $user = $this->createTestUser(); + + $response = $this->get('/api/v1/me', [], $this->authHeaders($user)); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + $response->assertJsonPath('data.id', $user->id); + $response->assertJsonPath('data.username', $user->username); + $response->assertJsonStructure([ + 'data' => ['id', 'username', 'email', 'status', 'ext', 'created_at'], + ]); + } + + public function test_me_without_token_returns_401(): void + { + $response = $this->get('/api/v1/me'); + + $response->assertStatus(401); + } + + public function test_logout_clears_refresh_token(): void + { + $password = 'LogoutTest_1'; + $user = $this->createTestUser(['password' => $password]); + + // 先登录获取 refresh_token + $this->post('/api/v1/login', [ + 'username' => $user->username, + 'password' => $password, + ]); + + $user->refresh(); + $this->assertNotNull($user->refresh_token); + + // 退出登录 + $response = $this->get('/api/v1/logout', [], $this->authHeaders($user)); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + + $user->refresh(); + $this->assertNull($user->refresh_token); + } +} diff --git a/backend/test/Cases/Unit/Model/ApiKeyTest.php b/backend/test/Cases/Unit/Model/ApiKeyTest.php new file mode 100644 index 0000000..e6e1623 --- /dev/null +++ b/backend/test/Cases/Unit/Model/ApiKeyTest.php @@ -0,0 +1,156 @@ +create([ + 'username' => 'apikey_test_' . $suffix, + 'password' => 'Pass_' . $suffix, + 'email' => 'apikey_test_' . $suffix . '@example.com', + 'status' => 1, + 'api_key_enabled' => true, + ]); + } + + public function test_generate_returns_plain_key_and_model(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createTestUser(); + $result = ApiKey::generate($user->id, 'Test Key'); + + $this->assertArrayHasKey('plain_key', $result); + $this->assertArrayHasKey('api_key', $result); + $this->assertInstanceOf(ApiKey::class, $result['api_key']); + $this->assertSame(64, strlen($result['plain_key'])); + }); + } + + public function test_generate_stores_sha256_hash(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createTestUser(); + $result = ApiKey::generate($user->id, 'Hash Test'); + + $expected_hash = hash('sha256', $result['plain_key']); + $this->assertSame($expected_hash, $result['api_key']->key_hash); + }); + } + + public function test_generate_stores_prefix(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createTestUser(); + $result = ApiKey::generate($user->id, 'Prefix Test'); + + $expected_prefix = substr($result['plain_key'], 0, 8); + $this->assertSame($expected_prefix, $result['api_key']->key_prefix); + }); + } + + public function test_find_by_plain_key_returns_valid_key(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createTestUser(); + $result = ApiKey::generate($user->id, 'Find Test'); + + $found = ApiKey::findByPlainKey($result['plain_key']); + $this->assertNotNull($found); + $this->assertSame($result['api_key']->id, $found->id); + }); + } + + public function test_find_by_plain_key_returns_null_for_invalid_key(): void + { + $this->runInCoroutine(function (): void { + $found = ApiKey::findByPlainKey('invalid_key_that_does_not_exist'); + $this->assertNull($found); + }); + } + + public function test_find_by_plain_key_excludes_disabled_key(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createTestUser(); + $result = ApiKey::generate($user->id, 'Disabled Test'); + + $result['api_key']->enabled = false; + $result['api_key']->save(); + + $found = ApiKey::findByPlainKey($result['plain_key']); + $this->assertNull($found); + }); + } + + public function test_find_by_plain_key_excludes_expired_key(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createTestUser(); + $result = ApiKey::generate($user->id, 'Expired Test', \Carbon\Carbon::now()->subDay()->toDateTimeString()); + + $found = ApiKey::findByPlainKey($result['plain_key']); + $this->assertNull($found); + }); + } + + public function test_key_hash_is_hidden_in_json(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createTestUser(); + $result = ApiKey::generate($user->id, 'Hidden Test'); + + $json = $result['api_key']->toArray(); + $this->assertArrayNotHasKey('key_hash', $json); + }); + } + + public function test_is_valid_returns_true_for_active_key(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createTestUser(); + $result = ApiKey::generate($user->id, 'Valid Test'); + + $this->assertTrue($result['api_key']->isValid()); + }); + } + + public function test_is_valid_returns_false_for_disabled_key(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createTestUser(); + $result = ApiKey::generate($user->id, 'Disabled Valid'); + + $result['api_key']->enabled = false; + $this->assertFalse($result['api_key']->isValid()); + }); + } +} diff --git a/backend/test/Cases/Unit/Model/RoleTest.php b/backend/test/Cases/Unit/Model/RoleTest.php new file mode 100644 index 0000000..1b364c7 --- /dev/null +++ b/backend/test/Cases/Unit/Model/RoleTest.php @@ -0,0 +1,69 @@ +assertEqualsCanonicalizing(['name', 'label', 'description'], $role->getFillable()); + } + + public function test_role_table_name(): void + { + $role = new Role(); + $this->assertSame('roles', $role->getTable()); + } + + public function test_administrator_role_exists(): void + { + $this->runInCoroutine(function (): void { + $role = Role::query()->where('name', 'administrator')->first(); + $this->assertNotNull($role); + $this->assertSame('超级管理员', $role->label); + }); + } + + public function test_developer_role_exists(): void + { + $this->runInCoroutine(function (): void { + $role = Role::query()->where('name', 'developer')->first(); + $this->assertNotNull($role); + $this->assertSame('开发者', $role->label); + }); + } + + public function test_accessor_role_exists(): void + { + $this->runInCoroutine(function (): void { + $role = Role::query()->where('name', 'accessor')->first(); + $this->assertNotNull($role); + $this->assertSame('数据访问者', $role->label); + }); + } +} diff --git a/backend/test/Cases/Unit/Model/UserRoleTest.php b/backend/test/Cases/Unit/Model/UserRoleTest.php new file mode 100644 index 0000000..615f302 --- /dev/null +++ b/backend/test/Cases/Unit/Model/UserRoleTest.php @@ -0,0 +1,102 @@ +where('name', $role_name)->firstOrFail(); + $suffix = bin2hex(random_bytes(4)); + + return User::query()->create([ + 'username' => 'role_test_' . $suffix, + 'password' => 'Pass_' . $suffix, + 'email' => 'role_test_' . $suffix . '@example.com', + 'status' => 1, + 'role_id' => $role->id, + ]); + } + + public function test_user_belongs_to_role(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createUserWithRole('administrator'); + $this->assertNotNull($user->role); + $this->assertSame('administrator', $user->role->name); + }); + } + + public function test_is_administrator(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createUserWithRole('administrator'); + $this->assertTrue($user->isAdministrator()); + $this->assertFalse($user->isDeveloper()); + $this->assertFalse($user->isAccessor()); + }); + } + + public function test_is_developer(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createUserWithRole('developer'); + $this->assertFalse($user->isAdministrator()); + $this->assertTrue($user->isDeveloper()); + $this->assertFalse($user->isAccessor()); + }); + } + + public function test_is_accessor(): void + { + $this->runInCoroutine(function (): void { + $user = $this->createUserWithRole('accessor'); + $this->assertFalse($user->isAdministrator()); + $this->assertFalse($user->isDeveloper()); + $this->assertTrue($user->isAccessor()); + }); + } + + public function test_user_without_role(): void + { + $this->runInCoroutine(function (): void { + $suffix = bin2hex(random_bytes(4)); + $user = User::query()->create([ + 'username' => 'no_role_' . $suffix, + 'password' => 'Pass_' . $suffix, + 'email' => 'no_role_' . $suffix . '@example.com', + 'status' => 1, + ]); + + $this->assertNull($user->role); + $this->assertFalse($user->isAdministrator()); + $this->assertFalse($user->isDeveloper()); + $this->assertFalse($user->isAccessor()); + }); + } +}