This commit is contained in:
wanghai
2026-06-26 19:20:24 +08:00
parent 2e248de433
commit 058673559c
375 changed files with 34935 additions and 1361 deletions

View File

@@ -0,0 +1,409 @@
<?php
declare(strict_types=1);
namespace Tests\ShuyunOpenPlatform;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;
use ShuyunOpenPlatformBundle\Entities\CompanyShuyunOpenPlatformConfig;
use ShuyunOpenPlatformBundle\Repositories\CompanyShuyunOpenPlatformConfigRepository;
use ShuyunOpenPlatformBundle\Services\DefaultShuyunOpenPlatformShopPlatCodeResolver;
use ShuyunOpenPlatformBundle\Services\ShuyunOpenPlatformGatewayClientFactory;
use ShuyunOpenPlatformBundle\Services\ShuyunOpenPlatformGatewayShopIdResolver;
use ShuyunOpenPlatformBundle\Services\ShuyunOpenPlatformShopSyncService;
use ShuyunOpenPlatformBundle\Services\ShuyunOpenPlatformTrafficAuditWriter;
/** @see .tasks/plans/shuyun-open-platform-shop-sync-body-and-logging.md A-BODY-*、A-LOG-01 */
class ShuyunOpenPlatformShopSyncServiceTest extends \TestCase
{
/** @see .tasks/plans/shuyun-open-platform-shop-sync.md A-SYNC-01 */
public function testSkipsWhenTenantNotEligible(): void
{
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn(null);
$http = $this->createMock(ClientInterface::class);
$http->expects($this->never())->method('request');
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $http, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$this->assertFalse($svc->syncShop(1, ['distributor_id' => 9, 'name' => 'N', 'shop_code' => 'S']));
}
/** auth_value 空 → 不合格 */
public function testNotEligibleWhenAuthValueEmpty(): void
{
$row = $this->eligibleRow();
$row->setAuthValue('');
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$http = $this->createMock(ClientInterface::class);
$http->expects($this->never())->method('request');
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $http, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$this->assertFalse($svc->syncShop(1, ['distributor_id' => 9, 'name' => 'N', 'shop_code' => 'S']));
}
/** @see .tasks/plans/shuyun-platform-shop-batch-register-api.md A5 / TC-A5 */
public function testNotEligibleWhenAppSecretEmpty(): void
{
$row = $this->eligibleRow();
$row->setAppSecret('');
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$http = $this->createMock(ClientInterface::class);
$http->expects($this->never())->method('request');
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $http, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$this->assertFalse($svc->syncShop(1, ['distributor_id' => 9, 'name' => 'N', 'shop_code' => 'S', 'is_valid' => 'true']));
}
/** @see .tasks/plans/shuyun-open-platform-shop-sync-body-and-logging.md A-BODY-01、A-BODY-02、A-BODY-04shop_id 取 distributor_id不用 shop_code */
public function testPostsJsonEnvelopeUsesDistributorIdAsShopId(): void
{
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
$row = $this->eligibleRow();
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$history = Middleware::history($container);
$mock = new MockHandler([
new Response(200, [], '{"code":10000,"data":null,"msg":""}'),
]);
$stack = HandlerStack::create($mock);
$stack->push($history);
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$this->assertTrue($svc->syncShop(1, ['distributor_id' => 99, 'name' => '店A', 'shop_code' => 'SC01']));
$this->assertNotEmpty($container);
$req = $container[0]['request'];
$this->assertGatewayShopBatchMethodAndBody($req, false);
$payload = json_decode((string) $req->getBody(), true, 512, JSON_THROW_ON_ERROR);
$this->assertSame('av', $payload['tenant_name'] ?? null);
$this->assertSame('aid', $payload['app_id'] ?? null);
$this->assertIsArray($payload['shops'] ?? null);
$shop = $payload['shops'][0];
$this->assertSame('99-off', $shop['shop_id'] ?? null);
$this->assertSame('店A', $shop['shop_name'] ?? null);
$this->assertSame('OFFLINE', $shop['plat_code'] ?? null);
$this->assertArrayNotHasKey('shopCode', $payload);
$this->assertSame('tok', $req->getHeaderLine('Gateway-Access-Token'));
$this->assertSame('offline', $req->getHeaderLine('platform'));
}
/** @see .tasks/plans/shuyun-open-platform-shop-sync-body-and-logging.md A-BODY-03 */
public function testShopIdUsesDistributorIdWhenShopCodeEmpty(): void
{
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
$row = $this->eligibleRow();
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$mock = new MockHandler([
new Response(200, [], '{"code":10000,"data":null,"msg":""}'),
]);
$stack = HandlerStack::create($mock);
$stack->push(Middleware::history($container));
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$this->assertTrue($svc->syncShop(1, ['distributor_id' => 42, 'name' => 'B', 'shop_code' => '']));
$this->assertGatewayShopBatchMethodAndBody($container[0]['request'], false);
$payload = json_decode((string) $container[0]['request']->getBody(), true, 512, JSON_THROW_ON_ERROR);
$this->assertSame('42-off', $payload['shops'][0]['shop_id'] ?? null);
}
/** @see .tasks/plans/shuyun-open-platform-shop-sync-body-and-logging.md A-LOG-01 */
public function testBusinessFailureReturnsFalseWithoutThrowing(): void
{
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
$row = $this->eligibleRow();
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$mock = new MockHandler([
new Response(200, [], '{"code":40001,"data":null,"msg":"bad"}'),
]);
$stack = HandlerStack::create($mock);
$stack->push(Middleware::history($container));
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$this->assertFalse($svc->syncShop(1, ['distributor_id' => 1, 'name' => 'N', 'shop_code' => 'x']));
$this->assertCount(1, $container);
$this->assertGatewayShopBatchMethodAndBody($container[0]['request'], false);
}
public function testBuildsOfflineShopWithEnabledStatusWhenLifecycleEnabled(): void
{
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
$row = $this->eligibleRow();
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$mock = new MockHandler([
new Response(200, [], '{"code":10000,"data":null,"msg":""}'),
]);
$stack = HandlerStack::create($mock);
$stack->push(Middleware::history($container));
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$ok = $svc->syncShop(1, ['distributor_id' => 88, 'name' => '门店A', 'shop_code' => 'S88', 'is_valid' => 'true']);
$this->assertTrue($ok);
$this->assertCount(1, $container);
$req = $container[0]['request'];
$this->assertSame('offline', $req->getHeaderLine('platform'));
$payload = json_decode((string) $req->getBody(), true, 512, JSON_THROW_ON_ERROR);
$this->assertSame('OFFLINE', $payload['shops'][0]['plat_code'] ?? null);
$this->assertSame('1', $payload['shops'][0]['status'] ?? null);
$this->assertSame('88-off', $payload['shops'][0]['shop_id'] ?? null);
$this->assertSame('门店A', $payload['shops'][0]['shop_name'] ?? null);
$this->assertGatewayShopBatchMethodAndBody($req, false);
}
public function testBuildsOfflineShopWithClosedStatusWhenIsValidClosed(): void
{
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
$row = $this->eligibleRow();
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$mock = new MockHandler([
new Response(200, [], '{"code":10000,"data":null,"msg":""}'),
]);
$stack = HandlerStack::create($mock);
$stack->push(Middleware::history($container));
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$ok = $svc->syncShop(1, ['distributor_id' => 91, 'name' => '闭店', 'shop_code' => 'S91', 'is_valid' => 'closed'], ['OFFLINE']);
$this->assertTrue($ok);
$this->assertCount(1, $container);
$payload = json_decode((string) $container[0]['request']->getBody(), true, 512, JSON_THROW_ON_ERROR);
$this->assertSame('2', $payload['shops'][0]['status'] ?? null);
$this->assertSame('闭店', $payload['shops'][0]['shop_name'] ?? null);
$this->assertSame('91-off', $payload['shops'][0]['shop_id'] ?? null);
$this->assertGatewayShopBatchMethodAndBody($container[0]['request'], false);
}
public function testBuildsOfflineShopWithDeletedStatusWhenIsValidDelete(): void
{
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
$row = $this->eligibleRow();
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$mock = new MockHandler([
new Response(200, [], '{"code":10000,"data":null,"msg":""}'),
]);
$stack = HandlerStack::create($mock);
$stack->push(Middleware::history($container));
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$ok = $svc->syncShop(1, ['distributor_id' => 92, 'name' => '废弃', 'shop_code' => 'S92', 'is_valid' => 'delete'], ['OFFLINE']);
$this->assertTrue($ok);
$this->assertCount(1, $container);
$payload = json_decode((string) $container[0]['request']->getBody(), true, 512, JSON_THROW_ON_ERROR);
$this->assertSame('0', $payload['shops'][0]['status'] ?? null);
$this->assertSame('废弃', $payload['shops'][0]['shop_name'] ?? null);
$this->assertSame('92-off', $payload['shops'][0]['shop_id'] ?? null);
$this->assertGatewayShopBatchMethodAndBody($container[0]['request'], false);
}
/** 禁用is_valid false仅一次 OFFLINE shop.batch.register不调 platform.shop.batch.register */
public function testBuildsOfflineOnlyWhenLifecycleDisabled(): void
{
#given
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
config(['shuyun_open_platform.default_plat_code' => 'ecshop']);
$row = $this->eligibleRow();
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$mock = new MockHandler([
new Response(200, [], '{"code":10000,"data":null,"msg":""}'),
]);
$stack = HandlerStack::create($mock);
$stack->push(Middleware::history($container));
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
#when
$ok = $svc->syncShop(1, ['distributor_id' => 89, 'name' => '门店B', 'shop_code' => 'S89', 'is_valid' => 'false']);
#then
$this->assertTrue($ok);
$this->assertCount(1, $container);
$req0 = $container[0]['request'];
$this->assertSame('offline', $req0->getHeaderLine('platform'));
$payload0 = json_decode((string) $req0->getBody(), true, 512, JSON_THROW_ON_ERROR);
$this->assertCount(1, $payload0['shops'] ?? []);
$this->assertSame('OFFLINE', $payload0['shops'][0]['plat_code'] ?? null);
$this->assertSame('1', $payload0['shops'][0]['status'] ?? null);
$this->assertSame('89-off', $payload0['shops'][0]['shop_id'] ?? null);
$this->assertSame('门店B', $payload0['shops'][0]['shop_name'] ?? null);
$this->assertGatewayShopBatchMethodAndBody($req0, false);
}
/** 禁用 + 显式 targetPlatCodes跳过非 OFFLINE仅一条 OFFLINE status=1 */
public function testDisabledWithTargetPlatCodesSkipsOnlinePlat(): void
{
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
$row = $this->eligibleRow();
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$mock = new MockHandler([
new Response(200, [], '{"code":10000,"data":null,"msg":""}'),
]);
$stack = HandlerStack::create($mock);
$stack->push(Middleware::history($container));
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$ok = $svc->syncShop(1, ['distributor_id' => 93, 'name' => '禁', 'shop_code' => 'S93', 'is_valid' => 'false'], ['OFFLINE', 'PL']);
$this->assertTrue($ok);
$this->assertCount(1, $container);
$payload = json_decode((string) $container[0]['request']->getBody(), true, 512, JSON_THROW_ON_ERROR);
$this->assertCount(1, $payload['shops'] ?? []);
$this->assertSame('OFFLINE', $payload['shops'][0]['plat_code'] ?? null);
$this->assertSame('1', $payload['shops'][0]['status'] ?? null);
$this->assertSame('禁', $payload['shops'][0]['shop_name'] ?? null);
$this->assertSame('93-off', $payload['shops'][0]['shop_id'] ?? null);
$this->assertGatewayShopBatchMethodAndBody($container[0]['request'], false);
}
/** 虚拟店默认 target仅 OFFLINE 一次请求 */
public function testVirtualShopSyncsOfflineOnlyByDefault(): void
{
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
$row = $this->eligibleRow();
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$mock = new MockHandler([
new Response(200, [], '{"code":10000,"data":null,"msg":""}'),
]);
$stack = HandlerStack::create($mock);
$stack->push(Middleware::history($container));
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$ok = $svc->syncShop(1, [
'distributor_id' => 101,
'name' => '虚拟店',
'shop_code' => 'V101',
'is_valid' => 'true',
'distributor_self' => '1',
]);
$this->assertTrue($ok);
$this->assertCount(1, $container);
$this->assertSame('offline', strtolower($container[0]['request']->getHeaderLine('platform')));
$payload = json_decode((string) $container[0]['request']->getBody(), true, 512, JSON_THROW_ON_ERROR);
$this->assertSame('OFFLINE', $payload['shops'][0]['plat_code'] ?? null);
$this->assertSame('1', $payload['shops'][0]['status'] ?? null);
$this->assertGatewayShopBatchMethodAndBody($container[0]['request'], false);
}
/** @see .tasks/plans/shuyun-open-platform-category-goods-sync.md A-PLAT-04 */
public function testEnabledUsesOfflineOnlyWhenCustomPlatUnresolved(): void
{
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
config(['shuyun_open_platform.default_plat_code' => '']);
$row = $this->eligibleRow();
$row->setPlatCode('');
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$mock = new MockHandler([
new Response(200, [], '{"code":10000,"data":null,"msg":""}'),
]);
$stack = HandlerStack::create($mock);
$stack->push(Middleware::history($container));
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$ok = $svc->syncShop(1, ['distributor_id' => 90, 'name' => '仅线下', 'shop_code' => 'S90', 'is_valid' => 'true']);
$this->assertTrue($ok);
$this->assertCount(1, $container);
$this->assertSame('offline', $container[0]['request']->getHeaderLine('platform'));
$payload = json_decode((string) $container[0]['request']->getBody(), true, 512, JSON_THROW_ON_ERROR);
$this->assertCount(1, $payload['shops'] ?? []);
$this->assertSame('OFFLINE', $payload['shops'][0]['plat_code'] ?? null);
$this->assertSame('90-off', $payload['shops'][0]['shop_id'] ?? null);
$this->assertSame('仅线下', $payload['shops'][0]['shop_name'] ?? null);
$this->assertGatewayShopBatchMethodAndBody($container[0]['request'], false);
}
/** OFFLINE 店铺名已带后缀时不重复拼接 */
public function testOfflineShopNameSuffixNotDuplicated(): void
{
config(['shuyun_open_platform.base_uri' => 'http://open-api.test']);
config(['shuyun_open_platform.timeout' => 5.0]);
config(['shuyun_open_platform.default_plat_code' => '']);
$row = $this->eligibleRow();
$row->setPlatCode('');
$repo = $this->createMock(CompanyShuyunOpenPlatformConfigRepository::class);
$repo->method('findOneByCompanyId')->willReturn($row);
$container = [];
$mock = new MockHandler([
new Response(200, [], '{"code":10000,"data":null,"msg":""}'),
]);
$stack = HandlerStack::create($mock);
$stack->push(Middleware::history($container));
$client = new Client(['handler' => $stack, 'base_uri' => 'http://open-api.test/']);
$svc = new ShuyunOpenPlatformShopSyncService($repo, new DefaultShuyunOpenPlatformShopPlatCodeResolver(), $client, new ShuyunOpenPlatformGatewayShopIdResolver(), new ShuyunOpenPlatformGatewayClientFactory(null));
$this->assertTrue($svc->syncShop(1, ['distributor_id' => 90, 'name' => '某店-线下', 'shop_code' => 'S90', 'is_valid' => 'true']));
$payload = json_decode((string) $container[0]['request']->getBody(), true, 512, JSON_THROW_ON_ERROR);
$this->assertSame('某店-线下', $payload['shops'][0]['shop_name'] ?? null);
$this->assertSame('90-off', $payload['shops'][0]['shop_id'] ?? null);
}
/**
* @param bool $expectPlatformShopBatchApi true → shuyun.base.platform.shop.batch.register + body.app_secret
*/
private function assertGatewayShopBatchMethodAndBody(RequestInterface $req, bool $expectPlatformShopBatchApi): void
{
$line = $req->getHeaderLine('Gateway-Action-Method');
$payload = json_decode((string) $req->getBody(), true, 512, JSON_THROW_ON_ERROR);
if ($expectPlatformShopBatchApi) {
$this->assertSame(ShuyunOpenPlatformShopSyncService::GATEWAY_ACTION_PLATFORM_SHOP_BATCH_REGISTER, $line);
$this->assertSame('sec', $payload['app_secret'] ?? null);
} else {
$this->assertSame(ShuyunOpenPlatformShopSyncService::GATEWAY_ACTION_SHOP_BATCH_REGISTER, $line);
$this->assertArrayNotHasKey('app_secret', $payload);
}
}
private function eligibleRow(): CompanyShuyunOpenPlatformConfig
{
$e = new CompanyShuyunOpenPlatformConfig();
$e->setCompanyId(1);
$e->setAuthValue('av');
$e->setPlatCode('pl');
$e->setAppId('aid');
$e->setAppSecret('sec');
$e->setAccessToken('tok');
$e->setIsEnabled(1);
return $e;
}
}