runInCoroutine(static function (): bool { return Product::query()->exists(); }); } protected function getFirstProductId(): ?int { return $this->runInCoroutine(static function (): ?int { return Product::query()->value('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 = $this->runInCoroutine(static function (): mixed { return 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); } }