'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']], ]; } }