733 lines
26 KiB
PHP
733 lines
26 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace HyperfTest\Cases\Integration\Sku;
|
||
|
||
use App\Model\SkuMapping;
|
||
use App\Model\SkuOrigin;
|
||
use HyperfTest\TestCase;
|
||
use HyperfTest\Traits\AuthenticatedTestTrait;
|
||
|
||
/**
|
||
* SkuMappingController 集成测试
|
||
*
|
||
* 覆盖列表筛选、origin_sku_id 过滤、创建校验(platform_product_id 可选)、CRUD、认证拦截、
|
||
* 三元组唯一性冲突、bundled 字段 CRUD 与筛选、组合商品映射(NC-6)、origin_sku 快照自动填充(NC-7)
|
||
*
|
||
* @internal
|
||
* @coversNothing
|
||
*/
|
||
class SkuMappingControllerTest extends TestCase
|
||
{
|
||
use AuthenticatedTestTrait;
|
||
|
||
protected function hasData(): bool
|
||
{
|
||
return $this->runInCoroutine(static function (): bool {
|
||
return SkuMapping::query()->exists();
|
||
});
|
||
}
|
||
|
||
protected function getFirstRecord(): ?array
|
||
{
|
||
return $this->runInCoroutine(static function (): ?array {
|
||
$record = SkuMapping::query()->first();
|
||
return $record ? $record->toArray() : null;
|
||
});
|
||
}
|
||
|
||
protected function getFirstOrigin(): ?array
|
||
{
|
||
return $this->runInCoroutine(static function (): ?array {
|
||
$record = SkuOrigin::query()->first();
|
||
return $record ? $record->toArray() : null;
|
||
});
|
||
}
|
||
|
||
protected function createTestOrigin(int $company_id): array
|
||
{
|
||
return $this->runInCoroutine(static function () use ($company_id): array {
|
||
// bin2hex(random_bytes) 比 uniqid() 子串更可靠,避免 barcode 微秒碰撞
|
||
$suffix = bin2hex(random_bytes(8));
|
||
$origin = SkuOrigin::query()->create([
|
||
'company_id' => $company_id,
|
||
'sku' => 'TESTSKU-' . $suffix,
|
||
'barcode' => 'BC-' . $suffix,
|
||
'name' => 'P21.3 Test Origin ' . $suffix,
|
||
]);
|
||
return $origin->toArray();
|
||
});
|
||
}
|
||
|
||
protected function deleteTestOrigins(array $ids): void
|
||
{
|
||
if (empty($ids)) {
|
||
return;
|
||
}
|
||
$this->runInCoroutine(static function () use ($ids): void {
|
||
SkuOrigin::query()->whereIn('id', $ids)->delete();
|
||
});
|
||
}
|
||
|
||
// ========== 列表接口 ==========
|
||
|
||
public function test_list_returns_paginated_data(): void
|
||
{
|
||
$response = $this->get('/api/v1/sku-mappings', [], $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_origin_sku_id(): void
|
||
{
|
||
if (!$this->hasData()) {
|
||
$this->markTestSkipped('没有 SKU Mapping 数据');
|
||
}
|
||
|
||
$first = $this->getFirstRecord();
|
||
if (!$first['origin_sku_id']) {
|
||
$this->markTestSkipped('首条记录无 origin_sku_id');
|
||
}
|
||
|
||
$response = $this->get('/api/v1/sku-mappings', [
|
||
'origin_sku_id' => $first['origin_sku_id'],
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(200);
|
||
$body = json_decode($response->getContent(), true);
|
||
|
||
foreach ($body['data']['items'] as $item) {
|
||
$this->assertSame($first['origin_sku_id'], $item['origin_sku_id']);
|
||
}
|
||
}
|
||
|
||
public function test_list_filter_by_company_and_platform(): void
|
||
{
|
||
if (!$this->hasData()) {
|
||
$this->markTestSkipped('没有 SKU Mapping 数据');
|
||
}
|
||
|
||
$first = $this->getFirstRecord();
|
||
|
||
$response = $this->get('/api/v1/sku-mappings', [
|
||
'company_id' => $first['company_id'],
|
||
'platform_id' => $first['platform_id'],
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(200);
|
||
$body = json_decode($response->getContent(), true);
|
||
|
||
foreach ($body['data']['items'] as $item) {
|
||
$this->assertSame($first['company_id'], $item['company_id']);
|
||
$this->assertSame($first['platform_id'], $item['platform_id']);
|
||
}
|
||
}
|
||
|
||
public function test_list_filter_by_enabled(): void
|
||
{
|
||
$response = $this->get('/api/v1/sku-mappings', ['enabled' => 'true'], $this->authHeaders());
|
||
|
||
$response->assertStatus(200);
|
||
$body = json_decode($response->getContent(), true);
|
||
|
||
foreach ($body['data']['items'] as $item) {
|
||
$this->assertTrue($item['enabled']);
|
||
}
|
||
}
|
||
|
||
// ========== 创建校验 ==========
|
||
|
||
public function test_create_without_platform_product_id_succeeds(): void
|
||
{
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku' => $origin['sku'],
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => 'TEST-' . uniqid(),
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(201);
|
||
$response->assertJsonPath('code', 0);
|
||
|
||
// 清理测试数据
|
||
$body = json_decode($response->getContent(), true);
|
||
if (isset($body['data']['id'])) {
|
||
$this->runInCoroutine(static function () use ($body): void {
|
||
SkuMapping::query()->where('id', $body['data']['id'])->delete();
|
||
});
|
||
}
|
||
}
|
||
|
||
public function test_create_with_platform_product_id_succeeds(): void
|
||
{
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku' => $origin['sku'],
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_product_id' => 'PROD-TEST-' . uniqid(),
|
||
'platform_outer_sku' => 'TEST-' . uniqid(),
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(201);
|
||
$response->assertJsonPath('code', 0);
|
||
|
||
// 清理测试数据
|
||
$body = json_decode($response->getContent(), true);
|
||
if (isset($body['data']['id'])) {
|
||
$this->runInCoroutine(static function () use ($body): void {
|
||
SkuMapping::query()->where('id', $body['data']['id'])->delete();
|
||
});
|
||
}
|
||
}
|
||
|
||
public function test_create_requires_company_id(): void
|
||
{
|
||
$response = $this->post('/api/v1/sku-mappings', [
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => 1,
|
||
'platform_outer_sku' => 'TEST-SKU',
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(422);
|
||
$response->assertJsonPath('message', '缺少必填字段: company_id');
|
||
}
|
||
|
||
public function test_create_requires_platform_id(): void
|
||
{
|
||
$response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => 1,
|
||
'origin_sku_id' => 1,
|
||
'platform_outer_sku' => 'TEST-SKU',
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(422);
|
||
$response->assertJsonPath('message', '缺少必填字段: platform_id');
|
||
}
|
||
|
||
public function test_create_requires_origin_sku_id(): void
|
||
{
|
||
$response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => 1,
|
||
'platform_id' => 1,
|
||
'platform_outer_sku' => 'TEST-SKU',
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(422);
|
||
$response->assertJsonPath('message', '缺少必填字段: origin_sku_id');
|
||
}
|
||
|
||
public function test_create_requires_platform_outer_sku(): void
|
||
{
|
||
$response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => 1,
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => 1,
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(422);
|
||
$response->assertJsonPath('message', '缺少必填字段: platform_outer_sku');
|
||
}
|
||
|
||
// ========== 详情接口 ==========
|
||
|
||
public function test_detail_not_found_returns_404(): void
|
||
{
|
||
$response = $this->get('/api/v1/sku-mappings/999999999', [], $this->authHeaders());
|
||
|
||
$response->assertStatus(404);
|
||
$response->assertJsonPath('code', 404);
|
||
}
|
||
|
||
// ========== 更新 & 删除 ==========
|
||
|
||
public function test_update_mapping(): void
|
||
{
|
||
if (!$this->hasData()) {
|
||
$this->markTestSkipped('没有 SKU Mapping 数据');
|
||
}
|
||
|
||
$first = $this->getFirstRecord();
|
||
|
||
$response = $this->put('/api/v1/sku-mappings/' . $first['id'], [
|
||
'note' => 'integration-test-update-' . uniqid(),
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(200);
|
||
$response->assertJsonPath('code', 0);
|
||
}
|
||
|
||
public function test_delete_mapping(): void
|
||
{
|
||
// 创建一条临时记录然后删除
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$createResponse = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku' => $origin['sku'],
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => 'DELETE-TEST-' . uniqid(),
|
||
], $this->authHeaders());
|
||
|
||
$createResponse->assertStatus(201);
|
||
$body = json_decode($createResponse->getContent(), true);
|
||
$newId = $body['data']['id'];
|
||
|
||
$deleteResponse = $this->delete('/api/v1/sku-mappings/' . $newId, [], $this->authHeaders());
|
||
|
||
$deleteResponse->assertStatus(200);
|
||
$deleteResponse->assertJsonPath('code', 0);
|
||
}
|
||
|
||
// ========== 三元组唯一性冲突(P21.3) ==========
|
||
|
||
public function test_store_duplicate_mapping_returns_422(): void
|
||
{
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$platform_outer_sku = 'DUP-TEST-' . uniqid();
|
||
$created_id = null;
|
||
|
||
try {
|
||
$first_response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => $platform_outer_sku,
|
||
], $this->authHeaders());
|
||
|
||
$first_response->assertStatus(201);
|
||
$first_body = json_decode($first_response->getContent(), true);
|
||
$created_id = $first_body['data']['id'];
|
||
|
||
$dup_response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => $platform_outer_sku,
|
||
], $this->authHeaders());
|
||
|
||
$dup_response->assertStatus(422);
|
||
$dup_response->assertJsonPath(
|
||
'message',
|
||
'该映射组合已存在(origin_sku_id + platform_id + platform_outer_sku)',
|
||
);
|
||
} finally {
|
||
if ($created_id !== null) {
|
||
$this->runInCoroutine(static function () use ($created_id): void {
|
||
SkuMapping::query()->where('id', $created_id)->delete();
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
public function test_update_to_duplicate_mapping_returns_422(): void
|
||
{
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$suffix = uniqid();
|
||
$a_sku = 'AAA-' . $suffix;
|
||
$b_sku = 'BBB-' . $suffix;
|
||
$a_id = null;
|
||
$b_id = null;
|
||
|
||
try {
|
||
$a_response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => $a_sku,
|
||
], $this->authHeaders());
|
||
$a_response->assertStatus(201);
|
||
$a_id = json_decode($a_response->getContent(), true)['data']['id'];
|
||
|
||
$b_response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => $b_sku,
|
||
], $this->authHeaders());
|
||
$b_response->assertStatus(201);
|
||
$b_id = json_decode($b_response->getContent(), true)['data']['id'];
|
||
|
||
// 把 B 改成 A 的 platform_outer_sku 触发三元组冲突
|
||
$update_response = $this->put('/api/v1/sku-mappings/' . $b_id, [
|
||
'platform_outer_sku' => $a_sku,
|
||
], $this->authHeaders());
|
||
|
||
$update_response->assertStatus(422);
|
||
$update_response->assertJsonPath(
|
||
'message',
|
||
'该映射组合已存在(origin_sku_id + platform_id + platform_outer_sku)',
|
||
);
|
||
} finally {
|
||
$this->runInCoroutine(static function () use ($a_id, $b_id): void {
|
||
if ($a_id !== null) {
|
||
SkuMapping::query()->where('id', $a_id)->delete();
|
||
}
|
||
if ($b_id !== null) {
|
||
SkuMapping::query()->where('id', $b_id)->delete();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// ========== bundled 字段 CRUD(P21.3) ==========
|
||
|
||
public function test_store_with_bundled_true(): void
|
||
{
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => 'BUNDLED-T-' . uniqid(),
|
||
'bundled' => true,
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(201);
|
||
$response->assertJsonPath('data.bundled', true);
|
||
|
||
$body = json_decode($response->getContent(), true);
|
||
$new_id = $body['data']['id'];
|
||
$this->runInCoroutine(static function () use ($new_id): void {
|
||
SkuMapping::query()->where('id', $new_id)->delete();
|
||
});
|
||
}
|
||
|
||
public function test_store_defaults_bundled_false(): void
|
||
{
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => 'BUNDLED-F-' . uniqid(),
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(201);
|
||
$response->assertJsonPath('data.bundled', false);
|
||
|
||
$body = json_decode($response->getContent(), true);
|
||
$new_id = $body['data']['id'];
|
||
$this->runInCoroutine(static function () use ($new_id): void {
|
||
SkuMapping::query()->where('id', $new_id)->delete();
|
||
});
|
||
}
|
||
|
||
public function test_update_bundled_field(): void
|
||
{
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$created_id = null;
|
||
try {
|
||
$create_response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => 'BUNDLED-UP-' . uniqid(),
|
||
], $this->authHeaders());
|
||
$create_response->assertStatus(201);
|
||
$created_id = json_decode($create_response->getContent(), true)['data']['id'];
|
||
|
||
$update_response = $this->put('/api/v1/sku-mappings/' . $created_id, [
|
||
'bundled' => true,
|
||
], $this->authHeaders());
|
||
$update_response->assertStatus(200);
|
||
|
||
$detail_response = $this->get('/api/v1/sku-mappings/' . $created_id, [], $this->authHeaders());
|
||
$detail_response->assertStatus(200);
|
||
$detail_response->assertJsonPath('data.bundled', true);
|
||
} finally {
|
||
if ($created_id !== null) {
|
||
$this->runInCoroutine(static function () use ($created_id): void {
|
||
SkuMapping::query()->where('id', $created_id)->delete();
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
public function test_index_filter_by_bundled(): void
|
||
{
|
||
// 自建 bundled=true / false 两条记录,避免依赖既有数据量导致空 items 静默通过
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$suffix = uniqid();
|
||
$true_id = null;
|
||
$false_id = null;
|
||
try {
|
||
$true_response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => 'FILTER-T-' . $suffix,
|
||
'bundled' => true,
|
||
], $this->authHeaders());
|
||
$true_response->assertStatus(201);
|
||
$true_id = json_decode($true_response->getContent(), true)['data']['id'];
|
||
|
||
$false_response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => 'FILTER-F-' . $suffix,
|
||
'bundled' => false,
|
||
], $this->authHeaders());
|
||
$false_response->assertStatus(201);
|
||
$false_id = json_decode($false_response->getContent(), true)['data']['id'];
|
||
|
||
$list_true = $this->get('/api/v1/sku-mappings', ['bundled' => 'true', 'per_page' => 100], $this->authHeaders());
|
||
$list_true->assertStatus(200);
|
||
$body_true = json_decode($list_true->getContent(), true);
|
||
$this->assertNotEmpty($body_true['data']['items'], 'bundled=true 筛选应至少命中新建的种子记录');
|
||
foreach ($body_true['data']['items'] as $item) {
|
||
$this->assertTrue($item['bundled'], "bundled=true 筛选却返回 bundled=false 的记录 id={$item['id']}");
|
||
}
|
||
|
||
$list_false = $this->get('/api/v1/sku-mappings', ['bundled' => 'false', 'per_page' => 100], $this->authHeaders());
|
||
$list_false->assertStatus(200);
|
||
$body_false = json_decode($list_false->getContent(), true);
|
||
$this->assertNotEmpty($body_false['data']['items'], 'bundled=false 筛选应至少命中新建的种子记录');
|
||
foreach ($body_false['data']['items'] as $item) {
|
||
$this->assertFalse($item['bundled'], "bundled=false 筛选却返回 bundled=true 的记录 id={$item['id']}");
|
||
}
|
||
} finally {
|
||
$this->runInCoroutine(static function () use ($true_id, $false_id): void {
|
||
foreach ([$true_id, $false_id] as $id) {
|
||
if ($id !== null) {
|
||
SkuMapping::query()->where('id', $id)->delete();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// ========== 组合商品场景(P21.3, NC-6) ==========
|
||
|
||
public function test_bundle_multiple_origin_skus_to_same_platform_outer_sku(): void
|
||
{
|
||
$base_origin = $this->getFirstOrigin();
|
||
if (!$base_origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 基线数据');
|
||
}
|
||
|
||
$company_id = (int) $base_origin['company_id'];
|
||
$extra_origin_ids = [];
|
||
$created_mapping_ids = [];
|
||
|
||
try {
|
||
// createTestOrigin 的任一次调用异常也能被 finally 清理已创建的 origin
|
||
$extra_1 = $this->createTestOrigin($company_id);
|
||
$extra_origin_ids[] = $extra_1['id'];
|
||
$extra_2 = $this->createTestOrigin($company_id);
|
||
$extra_origin_ids[] = $extra_2['id'];
|
||
|
||
$origins = [$base_origin, $extra_1, $extra_2];
|
||
$platform_outer_sku = 'BUNDLE-SHARED-' . uniqid();
|
||
|
||
foreach ($origins as $origin) {
|
||
$response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => $platform_outer_sku,
|
||
'bundled' => true,
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(201);
|
||
$body = json_decode($response->getContent(), true);
|
||
$this->assertSame($platform_outer_sku, $body['data']['platform_outer_sku']);
|
||
$this->assertTrue($body['data']['bundled']);
|
||
$created_mapping_ids[] = $body['data']['id'];
|
||
}
|
||
|
||
$this->assertCount(3, array_unique($created_mapping_ids));
|
||
} finally {
|
||
if (!empty($created_mapping_ids)) {
|
||
$this->runInCoroutine(static function () use ($created_mapping_ids): void {
|
||
SkuMapping::query()->whereIn('id', $created_mapping_ids)->delete();
|
||
});
|
||
}
|
||
$this->deleteTestOrigins($extra_origin_ids);
|
||
}
|
||
}
|
||
|
||
public function test_bundle_mapping_unique_constraint_per_origin_sku(): void
|
||
{
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$platform_outer_sku = 'BUNDLE-DUP-' . uniqid();
|
||
$created_id = null;
|
||
|
||
try {
|
||
$first_response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => $platform_outer_sku,
|
||
'bundled' => true,
|
||
], $this->authHeaders());
|
||
$first_response->assertStatus(201);
|
||
$first_body = json_decode($first_response->getContent(), true);
|
||
$created_id = $first_body['data']['id'];
|
||
// 额外断言首次 bundled=true 被正确持久化,与 test_store_duplicate_mapping 区分
|
||
$this->assertTrue($first_body['data']['bundled']);
|
||
|
||
// bundled=true 不应绕过三元组唯一索引
|
||
$dup_response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'platform_outer_sku' => $platform_outer_sku,
|
||
'bundled' => true,
|
||
], $this->authHeaders());
|
||
|
||
$dup_response->assertStatus(422);
|
||
$dup_response->assertJsonPath(
|
||
'message',
|
||
'该映射组合已存在(origin_sku_id + platform_id + platform_outer_sku)',
|
||
);
|
||
} finally {
|
||
if ($created_id !== null) {
|
||
$this->runInCoroutine(static function () use ($created_id): void {
|
||
SkuMapping::query()->where('id', $created_id)->delete();
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== origin_sku 快照自动填充(P21.3, NC-7) ==========
|
||
|
||
public function test_store_auto_fills_origin_sku_from_sku_origin(): void
|
||
{
|
||
$origin = $this->getFirstOrigin();
|
||
if (!$origin) {
|
||
$this->markTestSkipped('没有 SKU Origin 数据');
|
||
}
|
||
|
||
$created_id = null;
|
||
try {
|
||
// 客户端传入的 origin_sku 必须被服务端的快照覆盖,而非尊重请求体值
|
||
$response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin['id'],
|
||
'origin_sku' => 'CLIENT-SHOULD-NOT-WIN-' . uniqid(),
|
||
'platform_outer_sku' => 'SNAPSHOT-' . uniqid(),
|
||
], $this->authHeaders());
|
||
|
||
$response->assertStatus(201);
|
||
$response->assertJsonPath('data.origin_sku', $origin['sku']);
|
||
|
||
$created_id = json_decode($response->getContent(), true)['data']['id'];
|
||
} finally {
|
||
if ($created_id !== null) {
|
||
$this->runInCoroutine(static function () use ($created_id): void {
|
||
SkuMapping::query()->where('id', $created_id)->delete();
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
public function test_update_origin_sku_id_refreshes_origin_sku_snapshot(): void
|
||
{
|
||
$origin_a = $this->getFirstOrigin();
|
||
if (!$origin_a) {
|
||
$this->markTestSkipped('没有 SKU Origin 基线数据');
|
||
}
|
||
|
||
$origin_b = null;
|
||
$mapping_id = null;
|
||
try {
|
||
$origin_b = $this->createTestOrigin((int) $origin_a['company_id']);
|
||
|
||
$create_response = $this->post('/api/v1/sku-mappings', [
|
||
'company_id' => $origin_a['company_id'],
|
||
'platform_id' => 1,
|
||
'origin_sku_id' => $origin_a['id'],
|
||
'platform_outer_sku' => 'SNAPSHOT-UP-' . uniqid(),
|
||
], $this->authHeaders());
|
||
$create_response->assertStatus(201);
|
||
$create_body = json_decode($create_response->getContent(), true);
|
||
$mapping_id = $create_body['data']['id'];
|
||
$this->assertSame($origin_a['sku'], $create_body['data']['origin_sku']);
|
||
|
||
$update_response = $this->put('/api/v1/sku-mappings/' . $mapping_id, [
|
||
'origin_sku_id' => $origin_b['id'],
|
||
], $this->authHeaders());
|
||
|
||
$update_response->assertStatus(200);
|
||
$update_response->assertJsonPath('data.origin_sku', $origin_b['sku']);
|
||
$update_response->assertJsonPath('data.origin_sku_id', $origin_b['id']);
|
||
} finally {
|
||
if ($mapping_id !== null) {
|
||
$this->runInCoroutine(static function () use ($mapping_id): void {
|
||
SkuMapping::query()->where('id', $mapping_id)->delete();
|
||
});
|
||
}
|
||
if ($origin_b !== null) {
|
||
$this->deleteTestOrigins([$origin_b['id']]);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== 认证拦截 ==========
|
||
|
||
public function test_list_without_token_returns_401(): void
|
||
{
|
||
$response = $this->get('/api/v1/sku-mappings');
|
||
|
||
$response->assertStatus(401);
|
||
}
|
||
}
|