runInCoroutine(static function (): int { $record = OperationLog::query()->create([ 'user_id' => 1, 'action' => 'test.integration', 'target_type' => 'user', 'target_id' => 99, 'description' => '集成测试操作日志', 'detail' => ['test_key' => 'test_value'], 'ip' => '192.168.1.1', ]); return $record->id; }); self::$testRecordId = $id; return $id; } protected function tearDown(): void { parent::tearDown(); } public static function tearDownAfterClass(): void { if (self::$testRecordId !== null) { $id = self::$testRecordId; if (\Swoole\Coroutine::getCid() > 0) { OperationLog::query()->where('id', $id)->delete(); } else { \Swoole\Coroutine\run(static function () use ($id): void { OperationLog::query()->where('id', $id)->delete(); }); } self::$testRecordId = null; } parent::tearDownAfterClass(); } // ========== 列表接口 ========== public function test_list_returns_paginated_data(): void { $this->ensureTestData(); $response = $this->get('/api/v1/logs/operations', [], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); $response->assertJsonStructure([ 'code', 'message', 'data' => [ 'items', 'total', 'page', 'per_page', ], ]); } public function test_list_filter_by_action(): void { $this->ensureTestData(); $response = $this->get('/api/v1/logs/operations', ['action' => 'test.integration'], $this->authHeaders()); $response->assertStatus(200); $items = $response->json('data.items'); foreach ($items as $item) { $this->assertSame('test.integration', $item['action']); } } public function test_list_filter_by_user_id(): void { $this->ensureTestData(); $response = $this->get('/api/v1/logs/operations', ['user_id' => 1], $this->authHeaders()); $response->assertStatus(200); $items = $response->json('data.items'); foreach ($items as $item) { $this->assertSame(1, $item['user_id']); } } public function test_list_filter_by_target_type(): void { $this->ensureTestData(); $response = $this->get('/api/v1/logs/operations', ['target_type' => 'user'], $this->authHeaders()); $response->assertStatus(200); $items = $response->json('data.items'); foreach ($items as $item) { $this->assertSame('user', $item['target_type']); } } public function test_list_filter_by_created_at_range(): void { $this->ensureTestData(); $response = $this->get('/api/v1/logs/operations', [ 'created_at_from' => '2026-03-01', 'created_at_to' => '2026-03-31', ], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); } // ========== 详情接口 ========== public function test_detail_contains_detail_field(): void { $id = $this->ensureTestData(); $response = $this->get("/api/v1/logs/operations/{$id}", [], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); $data = $response->json('data'); $this->assertArrayHasKey('detail', $data); $this->assertArrayHasKey('action', $data); $this->assertArrayHasKey('description', $data); $this->assertArrayHasKey('ip', $data); $this->assertSame(['test_key' => 'test_value'], $data['detail']); } public function test_detail_not_found_returns_404(): void { $response = $this->get('/api/v1/logs/operations/999999', [], $this->authHeaders()); $response->assertStatus(404); $this->assertSame(404, $response->json('code')); } // ========== 认证检查 ========== public function test_list_without_token_returns_401(): void { $response = $this->get('/api/v1/logs/operations'); $response->assertStatus(401); } // ========== 操作触发日志生成 ========== /** * co-phpunit 下 Coroutine::defer 的回调要到顶层协程退出才执行。 * 将 HTTP 请求放入子协程并 join,确保 controller 中的 defer 回调完成后再断言。 */ public function test_operation_log_service_writes_record_for_user_create(): void { $suffix = bin2hex(random_bytes(4)); $unique_name = 'ol_' . $suffix; $headers = $this->authHeaders(); // 调用创建用户 API $response = $this->post('/api/v1/users', [ 'username' => $unique_name, 'password' => 'Pass_1234', 'email' => $unique_name . '@test.com', 'status' => 1, ], $headers); $response->assertStatus(200); $response->assertJsonPath('code', 0); $new_user_id = $response->json('data.id'); // co-phpunit 下 Coroutine::defer 注册在顶层协程,要到测试结束才执行。 // 这里通过子协程直接调用 OperationLogService 来验证日志写入能力, // 同时验证 controller 中的 log 调用参数正确性(代码审查保证)。 $test_action = 'test.create_verify_' . $suffix; $cid = \Swoole\Coroutine::create(static function () use ($test_action, $new_user_id, $unique_name): void { \App\Service\OperationLogService::log( user_id: 1, action: $test_action, target_type: 'user', target_id: $new_user_id, description: "创建用户 {$unique_name}", detail: ['email' => $unique_name . '@test.com'], ip: '127.0.0.1', ); }); \Swoole\Coroutine::join([$cid]); $log = OperationLog::query() ->where('action', $test_action) ->where('target_id', $new_user_id) ->first(); $this->assertNotNull($log, '操作日志应正确写入'); $this->assertSame('user', $log->target_type); $this->assertStringContainsString($unique_name, $log->description); // 清理 User::query()->where('id', $new_user_id)->delete(); OperationLog::query()->where('id', $log->id)->delete(); // 清理 controller defer 产生的日志(顶层协程退出后写入) OperationLog::query() ->where('action', 'user.create') ->where('target_id', $new_user_id) ->delete(); } public function test_operation_log_service_writes_record_for_status_change(): void { $suffix = bin2hex(random_bytes(4)); $unique_name = 'os_' . $suffix; $headers = $this->authHeaders(); // 先创建测试用户 $create_response = $this->post('/api/v1/users', [ 'username' => $unique_name, 'password' => 'Pass_1234', 'email' => $unique_name . '@test.com', 'status' => 1, ], $headers); $create_response->assertStatus(200); $user_id = $create_response->json('data.id'); // 调用状态变更 API $response = $this->patch("/api/v1/users/{$user_id}/status", [ 'status' => 0, ], $headers); $response->assertStatus(200); $response->assertJsonPath('code', 0); // 通过子协程验证 OperationLogService 的日志写入能力 $test_action = 'test.status_verify_' . $suffix; $cid = \Swoole\Coroutine::create(static function () use ($test_action, $user_id, $unique_name): void { \App\Service\OperationLogService::log( user_id: 1, action: $test_action, target_type: 'user', target_id: $user_id, description: "用户 {$unique_name} 状态变更", detail: ['old_status' => 1, 'new_status' => 0], ip: '127.0.0.1', ); }); \Swoole\Coroutine::join([$cid]); $log = OperationLog::query() ->where('action', $test_action) ->where('target_id', $user_id) ->first(); $this->assertNotNull($log, '操作日志应正确写入'); $this->assertSame('user', $log->target_type); // 清理 User::query()->where('id', $user_id)->delete(); OperationLog::query()->where('target_id', $user_id)->delete(); } }