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
237 lines
8.9 KiB
|
4 months ago
|
<?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();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|