fetchAdminRole(); $user = $this->fetchUser(static function ($query) use ($admin_role): void { $query->where('status', 1)->where('role_id', $admin_role->id); }); if (!$user) { $this->markTestSkipped('没有可用的 administrator 用户'); } $auth = make(AuthManager::class); return $auth->guard('jwt')->login($user); } protected function fetchAdminRole(): Role { if (\Swoole\Coroutine::getCid() > 0) { return Role::query()->where('name', 'administrator')->firstOrFail(); } $role = null; \Swoole\Coroutine\run(static function () use (&$role): void { $role = Role::query()->where('name', 'administrator')->firstOrFail(); }); return $role; } 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 authHeaders(): array { return ['Authorization' => 'Bearer ' . $this->getAdminToken()]; } protected function hasProductData(): bool { if (\Swoole\Coroutine::getCid() > 0) { return Product::query()->exists(); } $exists = false; \Swoole\Coroutine\run(static function () use (&$exists): void { $exists = Product::query()->exists(); }); return $exists; } protected function getFirstProductId(): ?int { if (\Swoole\Coroutine::getCid() > 0) { return Product::query()->value('id'); } $id = null; \Swoole\Coroutine\run(static function () use (&$id): void { $id = Product::query()->value('id'); }); return $id; } // ========== Normal 列表接口 ========== public function test_normal_list_returns_paginated_data(): void { $response = $this->get('/api/v1/products', [], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); $response->assertJsonStructure([ 'code', 'message', 'data' => [ 'items', 'total', 'page', 'per_page', ], ]); } public function test_normal_list_respects_per_page(): void { $response = $this->get('/api/v1/products', ['per_page' => 5], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('data.per_page', 5); } public function test_normal_list_per_page_max_100(): void { $response = $this->get('/api/v1/products', ['per_page' => 999], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('data.per_page', 100); } public function test_normal_list_excludes_raw_and_hash(): void { if (!$this->hasProductData()) { $this->markTestSkipped('没有产品数据'); } $response = $this->get('/api/v1/products', [], $this->authHeaders()); $response->assertStatus(200); $body = json_decode($response->getContent(), true); $first_item = $body['data']['items'][0] ?? []; $this->assertArrayNotHasKey('raw', $first_item); $this->assertArrayNotHasKey('hash', $first_item); } public function test_normal_list_contains_expected_fields(): void { if (!$this->hasProductData()) { $this->markTestSkipped('没有产品数据'); } $response = $this->get('/api/v1/products', [], $this->authHeaders()); $response->assertStatus(200); $body = json_decode($response->getContent(), true); $first_item = $body['data']['items'][0]; $expected_keys = ['id', 'company_id', 'platform_id', 'store_id', 'status_id', 'platform_item_id', 'platform_model_id', 'name', 'price', 'currency', 'num', 'created_date', 'updated_date']; foreach ($expected_keys as $key) { $this->assertArrayHasKey($key, $first_item, "列表缺少字段: {$key}"); } } public function test_normal_list_filter_by_company_id(): void { if (!$this->hasProductData()) { $this->markTestSkipped('没有产品数据'); } // 获取一个已知的 company_id $company_id = null; if (\Swoole\Coroutine::getCid() > 0) { $company_id = Product::query()->value('company_id'); } else { \Swoole\Coroutine\run(static function () use (&$company_id): void { $company_id = Product::query()->value('company_id'); }); } $response = $this->get('/api/v1/products', ['company_id' => $company_id], $this->authHeaders()); $response->assertStatus(200); $body = json_decode($response->getContent(), true); foreach ($body['data']['items'] as $item) { $this->assertSame($company_id, $item['company_id']); } } // ========== Normal 详情接口 ========== public function test_normal_detail_returns_product(): void { $id = $this->getFirstProductId(); if (!$id) { $this->markTestSkipped('没有产品数据'); } $response = $this->get('/api/v1/products/' . $id, [], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); $response->assertJsonPath('data.id', $id); } public function test_normal_detail_excludes_raw_and_hash(): void { $id = $this->getFirstProductId(); if (!$id) { $this->markTestSkipped('没有产品数据'); } $response = $this->get('/api/v1/products/' . $id, [], $this->authHeaders()); $response->assertStatus(200); $body = json_decode($response->getContent(), true); $this->assertArrayNotHasKey('raw', $body['data']); $this->assertArrayNotHasKey('hash', $body['data']); } public function test_normal_detail_contains_ext(): void { $id = $this->getFirstProductId(); if (!$id) { $this->markTestSkipped('没有产品数据'); } $response = $this->get('/api/v1/products/' . $id, [], $this->authHeaders()); $response->assertStatus(200); $body = json_decode($response->getContent(), true); $this->assertArrayHasKey('ext', $body['data']); } public function test_normal_detail_not_found_returns_404(): void { $response = $this->get('/api/v1/products/999999999', [], $this->authHeaders()); $response->assertStatus(404); $response->assertJsonPath('code', 404); } // ========== Raw 列表接口 ========== public function test_raw_list_returns_paginated_data(): void { $response = $this->get('/api/v1/raw/products', [], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); $response->assertJsonStructure([ 'code', 'message', 'data' => [ 'items', 'total', 'page', 'per_page', ], ]); } public function test_raw_list_contains_hash_but_not_raw(): void { if (!$this->hasProductData()) { $this->markTestSkipped('没有产品数据'); } $response = $this->get('/api/v1/raw/products', [], $this->authHeaders()); $response->assertStatus(200); $body = json_decode($response->getContent(), true); $first_item = $body['data']['items'][0] ?? []; $this->assertArrayHasKey('hash', $first_item, 'Raw 列表应包含 hash 字段'); $this->assertArrayNotHasKey('raw', $first_item, 'Raw 列表不应包含 raw 字段(太大)'); } public function test_raw_list_excludes_business_fields(): void { if (!$this->hasProductData()) { $this->markTestSkipped('没有产品数据'); } $response = $this->get('/api/v1/raw/products', [], $this->authHeaders()); $response->assertStatus(200); $body = json_decode($response->getContent(), true); $first_item = $body['data']['items'][0] ?? []; // Raw 列表不应包含业务字段 $this->assertArrayNotHasKey('price', $first_item); $this->assertArrayNotHasKey('currency', $first_item); $this->assertArrayNotHasKey('num', $first_item); $this->assertArrayNotHasKey('status_id', $first_item); } // ========== Raw 详情接口 ========== public function test_raw_detail_contains_raw_and_hash(): void { $id = $this->getFirstProductId(); if (!$id) { $this->markTestSkipped('没有产品数据'); } $response = $this->get('/api/v1/raw/products/' . $id, [], $this->authHeaders()); $response->assertStatus(200); $body = json_decode($response->getContent(), true); $this->assertArrayHasKey('raw', $body['data'], 'Raw 详情应包含 raw 字段'); $this->assertArrayHasKey('hash', $body['data'], 'Raw 详情应包含 hash 字段'); $this->assertArrayHasKey('ext', $body['data'], 'Raw 详情应包含 ext 字段'); } public function test_raw_detail_excludes_business_fields(): void { $id = $this->getFirstProductId(); if (!$id) { $this->markTestSkipped('没有产品数据'); } $response = $this->get('/api/v1/raw/products/' . $id, [], $this->authHeaders()); $response->assertStatus(200); $body = json_decode($response->getContent(), true); // Raw 详情不应包含业务字段 $this->assertArrayNotHasKey('price', $body['data']); $this->assertArrayNotHasKey('currency', $body['data']); $this->assertArrayNotHasKey('status_id', $body['data']); } public function test_raw_detail_not_found_returns_404(): void { $response = $this->get('/api/v1/raw/products/999999999', [], $this->authHeaders()); $response->assertStatus(404); $response->assertJsonPath('code', 404); } // ========== 认证拦截 ========== public function test_normal_list_without_token_returns_401(): void { $response = $this->get('/api/v1/products'); $response->assertStatus(401); } public function test_normal_detail_without_token_returns_401(): void { $response = $this->get('/api/v1/products/1'); $response->assertStatus(401); } public function test_raw_list_without_token_returns_401(): void { $response = $this->get('/api/v1/raw/products'); $response->assertStatus(401); } public function test_raw_detail_without_token_returns_401(): void { $response = $this->get('/api/v1/raw/products/1'); $response->assertStatus(401); } }