You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

237 lines
8.9 KiB

<?php
namespace Tests\Feature;
use App\Models\SmsVerification;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class SendSmsTestCommandTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'app.url' => 'http://localhost',
'cache.default' => 'array',
'database.default' => 'sqlite',
'database.connections.sqlite' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
'foreign_key_constraints' => false,
],
'sms.driver' => 'tencentcloud',
'sms.enabled' => false,
'sms.resend_interval_seconds' => 60,
'sms.code_ttl_seconds' => 300,
'sms.tencentcloud.endpoint' => 'https://yun.tim.qq.com/v5/tlssmssvr/sendsms',
'sms.tencentcloud.sdk_app_id' => null,
'sms.tencentcloud.app_key' => null,
'sms.tencentcloud.sign_name' => '元禾控股',
'sms.tencentcloud.template_id' => '2175650',
'sms.tencentcloud.template_param_count' => 2,
]);
DB::purge();
DB::reconnect();
Cache::flush();
$this->createSchema();
}
public function test_command_is_registered(): void
{
$this->artisan('list')
->expectsOutputToContain('sms:send-test')
->assertOk();
}
public function test_disabled_sms_is_rejected_by_default_without_record_or_http_call(): void
{
Http::fake();
$exitCode = Artisan::call('sms:send-test', [
'mobile' => '13800138100',
]);
$this->assertSame(1, $exitCode);
$this->assertStringContainsString('SMS_ENABLED=false', Artisan::output());
$this->assertSame(0, SmsVerification::query()->count());
Http::assertNothingSent();
}
public function test_allow_disabled_dry_run_creates_record_without_exposing_debug_code_in_production(): void
{
$this->app->detectEnvironment(fn () => 'production');
Http::fake();
$exitCode = Artisan::call('sms:send-test', [
'mobile' => '13800138101',
'--allow-disabled' => true,
]);
$record = SmsVerification::query()->where('mobile', '13800138101')->firstOrFail();
$output = Artisan::output();
$this->assertSame(0, $exitCode);
$this->assertSame(SmsVerification::STATUS_SENT, $record->status);
$this->assertSame(SmsVerification::PROVIDER_DISABLED, $record->provider);
$this->assertStringContainsString('verification_id: '.$record->id, $output);
$this->assertStringContainsString('provider: disabled', $output);
$this->assertStringContainsString('provider_code: DISABLED_OK', $output);
$this->assertStringNotContainsString('debug_code', $output);
$this->assertStringNotContainsString($record->code, $output);
Http::assertNothingSent();
}
public function test_real_send_success_outputs_provider_sid_without_sensitive_payload(): void
{
$this->enableTencentCloudConfig();
Http::fake([
'https://yun.tim.qq.com/v5/tlssmssvr/sendsms*' => Http::response([
'result' => 0,
'errmsg' => 'OK',
'sid' => 'command-success-sid',
'fee' => 1,
], 200),
]);
$exitCode = Artisan::call('sms:send-test', [
'mobile' => '13800138102',
'--yes' => true,
]);
$output = Artisan::output();
$record = SmsVerification::query()->where('mobile', '13800138102')->firstOrFail();
$this->assertSame(0, $exitCode);
$this->assertSame(SmsVerification::STATUS_SENT, $record->status);
$this->assertStringContainsString('status: sent', $output);
$this->assertStringContainsString('mobile: 138****8102', $output);
$this->assertStringNotContainsString('13800138102', $output);
$this->assertStringContainsString('provider: tencentcloud', $output);
$this->assertStringContainsString('provider_code: 0', $output);
$this->assertStringContainsString('provider_request_id: command-success-sid', $output);
$this->assertStringNotContainsString('test-app-key', $output);
$this->assertStringNotContainsString('params', $output);
$this->assertStringNotContainsString('sig', $output);
$this->assertStringNotContainsString($record->code, $output);
Http::assertSent(function (Request $request): bool {
return parse_url($request->url(), PHP_URL_HOST) === 'yun.tim.qq.com';
});
}
public function test_real_send_failure_returns_non_zero_and_records_failed(): void
{
$this->enableTencentCloudConfig();
Http::fake([
'https://yun.tim.qq.com/v5/tlssmssvr/sendsms*' => Http::response([
'result' => 1014,
'errmsg' => 'template incorrect',
'sid' => 'command-failed-sid',
], 200),
]);
$exitCode = Artisan::call('sms:send-test', [
'mobile' => '13800138103',
'--yes' => true,
]);
$output = Artisan::output();
$this->assertSame(1, $exitCode);
$this->assertStringContainsString('status: failed', $output);
$this->assertStringContainsString('provider_code: 1014', $output);
$this->assertStringContainsString('provider_request_id: command-failed-sid', $output);
$this->assertDatabaseHas('sms_verifications', [
'mobile' => '13800138103',
'status' => SmsVerification::STATUS_FAILED,
'provider_request_id' => 'command-failed-sid',
'provider_code' => '1014',
]);
}
public function test_http_500_with_result_zero_still_returns_failure_and_records_failed(): void
{
$this->enableTencentCloudConfig();
Http::fake([
'https://yun.tim.qq.com/v5/tlssmssvr/sendsms*' => Http::response([
'result' => 0,
'errmsg' => 'upstream unavailable',
'sid' => 'command-http-500-sid',
], 500),
]);
$exitCode = Artisan::call('sms:send-test', [
'mobile' => '13800138104',
'--yes' => true,
]);
$record = SmsVerification::query()->where('mobile', '13800138104')->firstOrFail();
$this->assertSame(1, $exitCode);
$this->assertSame(SmsVerification::STATUS_FAILED, $record->status);
$this->assertSame('HTTP_500', $record->provider_code);
$this->assertSame('command-http-500-sid', $record->provider_request_id);
$this->assertSame(500, $record->response_json['http_status']);
$this->assertSame(0, $record->response_json['result']);
}
public function test_production_real_send_requires_yes_or_confirmation(): void
{
$this->app->detectEnvironment(fn () => 'production');
$this->enableTencentCloudConfig();
Http::fake();
$this->artisan('sms:send-test', [
'mobile' => '13800138105',
])->expectsConfirmation('production 环境将向真实手机号发送短信,是否继续?', 'no')
->expectsOutputToContain('已取消发送。')
->assertExitCode(1);
$this->assertSame(0, SmsVerification::query()->count());
Http::assertNothingSent();
}
private function enableTencentCloudConfig(): void
{
config([
'sms.enabled' => true,
'sms.tencentcloud.sdk_app_id' => '1400000000',
'sms.tencentcloud.app_key' => 'test-app-key',
]);
}
private function createSchema(): void
{
Schema::create('sms_verifications', function (Blueprint $table) {
$table->id();
$table->string('scene', 64)->default('participant_login');
$table->string('mobile', 20)->index();
$table->string('code', 10);
$table->string('provider', 32)->default('disabled');
$table->string('status', 16)->default('pending');
$table->string('template_id', 64)->nullable();
$table->string('sign_name', 64)->nullable();
$table->json('request_payload_json')->nullable();
$table->json('response_json')->nullable();
$table->string('provider_request_id', 128)->nullable();
$table->string('provider_code', 128)->nullable();
$table->string('provider_message', 500)->nullable();
$table->timestamp('expires_at');
$table->timestamp('sent_at')->nullable();
$table->timestamp('failed_at')->nullable();
$table->timestamp('used_at')->nullable();
$table->timestamps();
});
}
}