374 lines
12 KiB
PHP
374 lines
12 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace HyperfTest\Cases\Integration\System;
|
||
|
|
|
||
|
|
use App\Model\FailedMessage;
|
||
|
|
use App\Model\Order;
|
||
|
|
use App\Model\Product;
|
||
|
|
use App\Model\Refund;
|
||
|
|
use HyperfTest\TestCase;
|
||
|
|
use HyperfTest\Traits\AuthenticatedTestTrait;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* DashboardController 集成测试
|
||
|
|
*
|
||
|
|
* 覆盖 overview/trend/breakdown 三个端点的正常返回、参数校验、401
|
||
|
|
*
|
||
|
|
* @internal
|
||
|
|
* @coversNothing
|
||
|
|
*/
|
||
|
|
class DashboardControllerTest extends TestCase
|
||
|
|
{
|
||
|
|
use AuthenticatedTestTrait;
|
||
|
|
|
||
|
|
private static array $testIds = [];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 确保有测试数据
|
||
|
|
*/
|
||
|
|
protected function ensureTestData(): void
|
||
|
|
{
|
||
|
|
if (!empty(self::$testIds)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
self::$testIds = $this->runInCoroutine(static function (): array {
|
||
|
|
$ids = [];
|
||
|
|
|
||
|
|
// 插入 order
|
||
|
|
$order = Order::query()->create([
|
||
|
|
'company_id' => 1,
|
||
|
|
'platform_id' => 1,
|
||
|
|
'store_id' => 1,
|
||
|
|
'order_status_id' => 1,
|
||
|
|
'platform_order_id' => 'DASH_TEST_' . uniqid(),
|
||
|
|
'buyer_user_id' => 'test_buyer',
|
||
|
|
'payment_method_id' => 1,
|
||
|
|
'presale' => false,
|
||
|
|
'total_amount' => '100.00',
|
||
|
|
'total_paid' => '100.00',
|
||
|
|
'total_discount' => '0.00',
|
||
|
|
'total_received' => '100.00',
|
||
|
|
'freight_fee' => '0.00',
|
||
|
|
'tax_fee' => '0.00',
|
||
|
|
'discount_fee' => '0.00',
|
||
|
|
'commission_fee' => '0.00',
|
||
|
|
'coupon_amount' => '0.00',
|
||
|
|
'voucher_amount' => '0.00',
|
||
|
|
'order_type_id' => 1,
|
||
|
|
'created_date' => '2026-03-17 10:00:00',
|
||
|
|
'hash' => md5('dash_test_order'),
|
||
|
|
'raw' => '{}',
|
||
|
|
'ext' => '{}',
|
||
|
|
]);
|
||
|
|
$ids['order'] = $order->id;
|
||
|
|
|
||
|
|
// 插入 product
|
||
|
|
$product = Product::query()->create([
|
||
|
|
'company_id' => 1,
|
||
|
|
'platform_id' => 1,
|
||
|
|
'store_id' => 1,
|
||
|
|
'status_id' => 1,
|
||
|
|
'type_id' => 1,
|
||
|
|
'platform_item_id' => 'DASH_PROD_' . uniqid(),
|
||
|
|
'price' => '50.00',
|
||
|
|
'currency' => 'CNY',
|
||
|
|
'num' => 10,
|
||
|
|
'hash' => md5('dash_test_product'),
|
||
|
|
'raw' => '{}',
|
||
|
|
]);
|
||
|
|
$ids['product'] = $product->id;
|
||
|
|
|
||
|
|
// 插入 refund
|
||
|
|
$refund = Refund::query()->create([
|
||
|
|
'company_id' => 1,
|
||
|
|
'platform_id' => 1,
|
||
|
|
'store_id' => 1,
|
||
|
|
'platform_order_id' => 'DASH_TEST_ORD',
|
||
|
|
'platform_refund_id' => 'DASH_RF_' . uniqid(),
|
||
|
|
'refund_status_id' => 1,
|
||
|
|
'refund_type_id' => 1,
|
||
|
|
'refund_amount' => '50.00',
|
||
|
|
'freight_refund' => '0.00',
|
||
|
|
'refund_total' => '50.00',
|
||
|
|
'currency' => 'CNY',
|
||
|
|
'buyer_user_id' => 'test_buyer',
|
||
|
|
'order_created_date' => '2026-03-17 09:00:00',
|
||
|
|
'order_paid_date' => '2026-03-17 09:30:00',
|
||
|
|
'created_date' => '2026-03-17 10:00:00',
|
||
|
|
'hash' => md5('dash_test_refund'),
|
||
|
|
'raw' => '{}',
|
||
|
|
]);
|
||
|
|
$ids['refund'] = $refund->id;
|
||
|
|
|
||
|
|
// 插入 failed_message
|
||
|
|
$failed = FailedMessage::query()->create([
|
||
|
|
'error_id' => 'err_dash_' . uniqid(),
|
||
|
|
'data_type' => 'order',
|
||
|
|
'platform' => 'JD',
|
||
|
|
'platform_id' => 1,
|
||
|
|
'company_id' => 1,
|
||
|
|
'store_id' => 1,
|
||
|
|
'error_type' => 'RuntimeException',
|
||
|
|
'error_message' => 'Dashboard test error',
|
||
|
|
'error_code' => 0,
|
||
|
|
'error_trace' => '#0 test',
|
||
|
|
'original_message' => ['test' => true],
|
||
|
|
'retry_count' => 0,
|
||
|
|
'failed_at' => '2026-03-17 10:00:00+08',
|
||
|
|
]);
|
||
|
|
$ids['failed'] = $failed->id;
|
||
|
|
|
||
|
|
return $ids;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function tearDownAfterClass(): void
|
||
|
|
{
|
||
|
|
if (!empty(self::$testIds)) {
|
||
|
|
$ids = self::$testIds;
|
||
|
|
$cleanup = static function () use ($ids): void {
|
||
|
|
if (isset($ids['order'])) {
|
||
|
|
Order::query()->where('id', $ids['order'])->delete();
|
||
|
|
}
|
||
|
|
if (isset($ids['product'])) {
|
||
|
|
Product::query()->where('id', $ids['product'])->delete();
|
||
|
|
}
|
||
|
|
if (isset($ids['refund'])) {
|
||
|
|
Refund::query()->where('id', $ids['refund'])->delete();
|
||
|
|
}
|
||
|
|
if (isset($ids['failed'])) {
|
||
|
|
FailedMessage::query()->where('id', $ids['failed'])->delete();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (\Swoole\Coroutine::getCid() > 0) {
|
||
|
|
$cleanup();
|
||
|
|
} else {
|
||
|
|
\Swoole\Coroutine\run($cleanup);
|
||
|
|
}
|
||
|
|
self::$testIds = [];
|
||
|
|
}
|
||
|
|
parent::tearDownAfterClass();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== Overview ==========
|
||
|
|
|
||
|
|
public function test_overview_returns_correct_structure(): void
|
||
|
|
{
|
||
|
|
$this->ensureTestData();
|
||
|
|
|
||
|
|
$response = $this->get('/api/v1/dashboard/overview', [], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(200);
|
||
|
|
$response->assertJsonPath('code', 0);
|
||
|
|
$response->assertJsonStructure([
|
||
|
|
'code',
|
||
|
|
'message',
|
||
|
|
'data' => [
|
||
|
|
'today' => ['success', 'failed'],
|
||
|
|
'this_week' => ['success', 'failed'],
|
||
|
|
'this_month' => ['success', 'failed'],
|
||
|
|
'by_type',
|
||
|
|
],
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_overview_by_type_contains_all_data_types(): void
|
||
|
|
{
|
||
|
|
$this->ensureTestData();
|
||
|
|
|
||
|
|
$response = $this->get('/api/v1/dashboard/overview', [], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(200);
|
||
|
|
$by_type = $response->json('data.by_type');
|
||
|
|
$types = array_column($by_type, 'data_type');
|
||
|
|
|
||
|
|
$this->assertContains('order', $types);
|
||
|
|
$this->assertContains('product', $types);
|
||
|
|
$this->assertContains('refund', $types);
|
||
|
|
$this->assertContains('inventory', $types);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== Trend ==========
|
||
|
|
|
||
|
|
public function test_trend_returns_array(): void
|
||
|
|
{
|
||
|
|
$this->ensureTestData();
|
||
|
|
|
||
|
|
$response = $this->get('/api/v1/dashboard/trend', [
|
||
|
|
'from' => '2026-03-01',
|
||
|
|
'to' => '2026-03-31',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(200);
|
||
|
|
$response->assertJsonPath('code', 0);
|
||
|
|
$this->assertIsArray($response->json('data'));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_trend_with_data_type_filter(): void
|
||
|
|
{
|
||
|
|
$this->ensureTestData();
|
||
|
|
|
||
|
|
$response = $this->get('/api/v1/dashboard/trend', [
|
||
|
|
'from' => '2026-03-01',
|
||
|
|
'to' => '2026-03-31',
|
||
|
|
'data_type' => 'order',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(200);
|
||
|
|
$response->assertJsonPath('code', 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_trend_invalid_group_by_returns_400(): void
|
||
|
|
{
|
||
|
|
$response = $this->get('/api/v1/dashboard/trend', [
|
||
|
|
'group_by' => 'invalid',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(400);
|
||
|
|
$this->assertSame(400, $response->json('code'));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_trend_invalid_data_type_returns_400(): void
|
||
|
|
{
|
||
|
|
$response = $this->get('/api/v1/dashboard/trend', [
|
||
|
|
'data_type' => 'invalid',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(400);
|
||
|
|
$this->assertSame(400, $response->json('code'));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_trend_invalid_date_format_returns_400(): void
|
||
|
|
{
|
||
|
|
$response = $this->get('/api/v1/dashboard/trend', [
|
||
|
|
'from' => '2026/03/01',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(400);
|
||
|
|
$this->assertSame(400, $response->json('code'));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_trend_from_after_to_returns_400(): void
|
||
|
|
{
|
||
|
|
$response = $this->get('/api/v1/dashboard/trend', [
|
||
|
|
'from' => '2026-03-31',
|
||
|
|
'to' => '2026-03-01',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(400);
|
||
|
|
$this->assertSame(400, $response->json('code'));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== Breakdown ==========
|
||
|
|
|
||
|
|
public function test_breakdown_by_company_returns_correct_structure(): void
|
||
|
|
{
|
||
|
|
$this->ensureTestData();
|
||
|
|
|
||
|
|
$response = $this->get('/api/v1/dashboard/breakdown', [
|
||
|
|
'dimension' => 'company',
|
||
|
|
'from' => '2026-03-01',
|
||
|
|
'to' => '2026-03-31',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(200);
|
||
|
|
$response->assertJsonPath('code', 0);
|
||
|
|
$data = $response->json('data');
|
||
|
|
$this->assertIsArray($data);
|
||
|
|
|
||
|
|
if (!empty($data)) {
|
||
|
|
$first = $data[0];
|
||
|
|
$this->assertArrayHasKey('id', $first);
|
||
|
|
$this->assertArrayHasKey('name', $first);
|
||
|
|
$this->assertArrayHasKey('success', $first);
|
||
|
|
$this->assertArrayHasKey('failed', $first);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_breakdown_by_platform(): void
|
||
|
|
{
|
||
|
|
$this->ensureTestData();
|
||
|
|
|
||
|
|
$response = $this->get('/api/v1/dashboard/breakdown', [
|
||
|
|
'dimension' => 'platform',
|
||
|
|
'from' => '2026-03-01',
|
||
|
|
'to' => '2026-03-31',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(200);
|
||
|
|
$response->assertJsonPath('code', 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_breakdown_by_store(): void
|
||
|
|
{
|
||
|
|
$this->ensureTestData();
|
||
|
|
|
||
|
|
$response = $this->get('/api/v1/dashboard/breakdown', [
|
||
|
|
'dimension' => 'store',
|
||
|
|
'from' => '2026-03-01',
|
||
|
|
'to' => '2026-03-31',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(200);
|
||
|
|
$response->assertJsonPath('code', 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_breakdown_missing_dimension_returns_400(): void
|
||
|
|
{
|
||
|
|
$response = $this->get('/api/v1/dashboard/breakdown', [], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(400);
|
||
|
|
$this->assertSame(400, $response->json('code'));
|
||
|
|
$this->assertStringContainsString('dimension', $response->json('message'));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_breakdown_invalid_dimension_returns_400(): void
|
||
|
|
{
|
||
|
|
$response = $this->get('/api/v1/dashboard/breakdown', [
|
||
|
|
'dimension' => 'invalid',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(400);
|
||
|
|
$this->assertSame(400, $response->json('code'));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_breakdown_with_data_type_filter(): void
|
||
|
|
{
|
||
|
|
$this->ensureTestData();
|
||
|
|
|
||
|
|
$response = $this->get('/api/v1/dashboard/breakdown', [
|
||
|
|
'dimension' => 'company',
|
||
|
|
'data_type' => 'order',
|
||
|
|
'from' => '2026-03-01',
|
||
|
|
'to' => '2026-03-31',
|
||
|
|
], $this->authHeaders());
|
||
|
|
|
||
|
|
$response->assertStatus(200);
|
||
|
|
$response->assertJsonPath('code', 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 认证检查 ==========
|
||
|
|
|
||
|
|
public function test_overview_without_token_returns_401(): void
|
||
|
|
{
|
||
|
|
$response = $this->get('/api/v1/dashboard/overview');
|
||
|
|
$response->assertStatus(401);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_trend_without_token_returns_401(): void
|
||
|
|
{
|
||
|
|
$response = $this->get('/api/v1/dashboard/trend');
|
||
|
|
$response->assertStatus(401);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_breakdown_without_token_returns_401(): void
|
||
|
|
{
|
||
|
|
$response = $this->get('/api/v1/dashboard/breakdown');
|
||
|
|
$response->assertStatus(401);
|
||
|
|
}
|
||
|
|
}
|