'sqlite', 'database.connections.sqlite.database' => ':memory:', 'audit.enabled' => false]); DB::purge('sqlite'); Schema::create('companies', function (Blueprint $t) { $t->id(); $t->string('company_name'); $t->string('credit_code')->nullable(); $t->timestamps(); $t->softDeletes(); }); Schema::create('company_qcc_accounts', function (Blueprint $t) { $t->id(); $t->unsignedBigInteger('company_id'); $t->string('status'); $t->timestamps(); $t->dateTime('first_success_at')->nullable(); $t->string('first_success_source')->nullable(); $t->string('first_success_ref')->nullable(); $t->text('unknown_note')->nullable(); }); } protected function tearDown(): void { DB::disconnect('sqlite'); parent::tearDown(); } private function company(string $status): void { DB::table('companies')->insert(['id' => 333, 'company_name' => '测试企业', 'credit_code' => 'test-credit']); DB::table('company_qcc_accounts')->insert([ 'company_id' => 333, 'status' => $status, 'first_success_ref' => 'original-ref', 'updated_at' => '2026-01-01 00:00:00', 'unknown_note' => '原有诊断备注', ]); } /** @dataProvider responseCases */ public function test_response_and_timeout_without_database_writes(string $status, array $response, array $options, int $timeout, int $exit): void { $this->company($status); $before = DB::table('company_qcc_accounts')->get()->toJson(); $companies = DB::table('companies')->get()->toJson(); $mock = Mockery::mock(YuanheRepository::class); $mock->shouldReceive('enterprisePackInfo')->once() ->with(['creditCode' => 'test-credit', 'enterpriseName' => '测试企业'], $timeout)->andReturn($response); $this->app->instance(YuanheRepository::class, $mock); $this->assertSame($exit, Artisan::call('qcc:verify-pack-info', ['company_id' => 333] + $options)); $this->assertSame($response, json_decode(Artisan::output(), true)); $this->assertSame($before, DB::table('company_qcc_accounts')->get()->toJson()); $this->assertSame($companies, DB::table('companies')->get()->toJson()); } public static function responseCases(): array { return [ ['selected', ['code' => 200, 'data' => ['news' => [['Title' => '标题']]]], [], 60, 0], ['occupied', ['code' => '200', 'data' => []], ['--timeout' => '120', '--raw' => true], 120, 0], ['unknown', ['code' => 200, 'data' => ['news' => []]], [], 60, 0], ['unknown', ['code' => 500], ['--timeout' => '120'], 120, 1], ['selected', ['code' => 0], [], 60, 1], ['occupied', ['code' => 500, 'msg' => '失败'], [], 60, 1], ['selected', ['data' => []], [], 60, 1], ]; } /** @dataProvider exceptionCases */ public function test_exception_leaves_account_unchanged(string $status, ?string $raw): void { $this->company($status); $before = DB::table('company_qcc_accounts')->get()->toJson(); $mock = Mockery::mock(YuanheRepository::class); $mock->shouldReceive('enterprisePackInfo')->once()->andThrow(new YuanhePackInfoException('请求超时', null, null, $raw)); $this->app->instance(YuanheRepository::class, $mock); $this->assertSame(1, Artisan::call('qcc:verify-pack-info', ['company_id' => 333])); $this->assertSame(($raw ?? '请求超时').PHP_EOL, Artisan::output()); $this->assertSame($before, DB::table('company_qcc_accounts')->get()->toJson()); } public static function exceptionCases(): array { return [['selected', null], ['selected', 'Bad Gateway'], ['unknown', null], ['unknown', 'Bad Gateway']]; } /** @dataProvider rejectedCases */ public function test_invalid_input_does_not_call_api(string $status, array $options): void { $this->company($status); $mock = Mockery::mock(YuanheRepository::class); $mock->shouldNotReceive('enterprisePackInfo'); $this->app->instance(YuanheRepository::class, $mock); $this->assertSame(1, Artisan::call('qcc:verify-pack-info', ['company_id' => 333] + $options)); $this->assertSame($status, DB::table('company_qcc_accounts')->value('status')); } public static function rejectedCases(): array { return [ ['none', []], ['non_occupying', []], ['selected', ['--timeout' => '0']], ['selected', ['--timeout' => '-1']], ['selected', ['--timeout' => '1.5']], ['selected', ['--timeout' => 'abc']], ]; } /** @dataProvider requestCases */ public function test_request_details_are_from_actual_call_even_on_failure(?string $failure): void { $this->company('unknown'); $before = DB::table('company_qcc_accounts')->get()->toJson(); $request = [ 'method' => 'POST', 'url' => 'https://example.test/gateway/openapi/businessCollege/enterprise/packInfo', 'headers' => ['Content-Type' => 'application/json', 'customerId' => 'test-customer', 'timestamp' => '1234567890000', 'token' => 'test-signature'], 'json' => ['creditCode' => 'test-credit', 'enterpriseName' => '测试企业'], ]; $mock = Mockery::mock(YuanheRepository::class); $mock->authKey = 'secret-not-for-output'; $mock->shouldReceive('enterprisePackInfo')->once()->with($request['json'], 120) ->andReturnUsing(function () use ($mock, $request, $failure) { $mock->lastRequest = $request; if ($failure !== null) { throw new YuanhePackInfoException('请求超时', null, null, $failure === 'timeout' ? null : $failure); } return ['code' => 200]; }); $this->app->instance(YuanheRepository::class, $mock); $exit = Artisan::call('qcc:verify-pack-info', ['company_id' => 333, '--timeout' => '120', '--raw' => true, '--request' => true]); $this->assertSame($failure === null ? 0 : 1, $exit); $expected = "请求信息(不含 authKey):\n".json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)."\n返回结果:\n"; $expected .= $failure === null ? json_encode(['code' => 200], JSON_PRETTY_PRINT) : ($failure === 'timeout' ? '请求超时' : $failure); $this->assertSame($expected.PHP_EOL, Artisan::output()); $this->assertStringNotContainsString('secret-not-for-output', Artisan::output()); $this->assertSame($before, DB::table('company_qcc_accounts')->get()->toJson()); } public static function requestCases(): array { return [[null], ['timeout'], ['504 Gateway Time-out']]; } }