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.
724 lines
26 KiB
724 lines
26 KiB
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Competition;
|
|
use App\Models\PublicSourceChannel;
|
|
use App\Models\SmsVerification;
|
|
use App\Support\SmsConfig;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Http\Client\Request;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Tests\TestCase;
|
|
|
|
class AuthSmsControllerTest 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_disabled_sender_creates_record_and_login_consumes_it(): void
|
|
{
|
|
$this->competition('main-event');
|
|
Http::fake();
|
|
|
|
$send = $this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138000',
|
|
])->assertOk();
|
|
Http::assertNothingSent();
|
|
|
|
$code = (string) $send->json('debug_code');
|
|
$this->assertMatchesRegularExpression('/^\d{6}$/', $code);
|
|
|
|
$record = SmsVerification::query()->firstOrFail();
|
|
$this->assertSame(SmsVerification::STATUS_SENT, $record->status);
|
|
$this->assertSame(SmsVerification::PROVIDER_DISABLED, $record->provider);
|
|
$this->assertSame('DISABLED_OK', $record->provider_code);
|
|
$this->assertSame('138****8000', $record->request_payload_json['phone_number']);
|
|
$this->assertArrayNotHasKey('code', $record->request_payload_json);
|
|
|
|
$this->postJson('/api/auth/sms/login', [
|
|
'mobile' => '13800138000',
|
|
'code' => $code,
|
|
'competition_slug' => 'main-event',
|
|
])->assertOk()
|
|
->assertJsonPath('token_type', 'Bearer');
|
|
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'id' => $record->id,
|
|
'status' => SmsVerification::STATUS_USED,
|
|
]);
|
|
$this->assertDatabaseHas('applications', [
|
|
'competition_id' => Competition::query()->where('slug', 'main-event')->value('id'),
|
|
'status' => 'draft',
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/login', [
|
|
'mobile' => '13800138000',
|
|
'code' => $code,
|
|
'competition_slug' => 'main-event',
|
|
])->assertUnprocessable();
|
|
}
|
|
|
|
public function test_public_source_resolve_and_registration_attribution(): void
|
|
{
|
|
$this->competition('main-event');
|
|
$sourceA = PublicSourceChannel::query()->create([
|
|
'source_code' => 'poster_A',
|
|
'source_name' => '海报 A',
|
|
'status' => PublicSourceChannel::STATUS_ENABLED,
|
|
]);
|
|
$sourceB = PublicSourceChannel::query()->create([
|
|
'source_code' => 'poster_B',
|
|
'source_name' => '海报 B',
|
|
'status' => PublicSourceChannel::STATUS_ENABLED,
|
|
]);
|
|
PublicSourceChannel::query()->create([
|
|
'source_code' => 'closed_src',
|
|
'source_name' => '停用渠道',
|
|
'status' => PublicSourceChannel::STATUS_DISABLED,
|
|
]);
|
|
|
|
$this->getJson('/api/v1/public/source-channels/resolve?source_code=poster_A')
|
|
->assertOk()
|
|
->assertJsonPath('valid', true)
|
|
->assertJsonPath('source_name', '海报 A');
|
|
|
|
$this->getJson('/api/v1/public/source-channels/resolve?source_code=closed_src')
|
|
->assertOk()
|
|
->assertJsonPath('valid', false);
|
|
$this->getJson('/api/v1/public/source-channels/resolve?source_code=bad%20code')
|
|
->assertOk()
|
|
->assertJsonPath('valid', false);
|
|
|
|
SmsVerification::query()->create([
|
|
'scene' => SmsVerification::SCENE_PARTICIPANT_LOGIN,
|
|
'mobile' => '13800138100',
|
|
'code' => '111111',
|
|
'provider' => SmsVerification::PROVIDER_DISABLED,
|
|
'status' => SmsVerification::STATUS_SENT,
|
|
'expires_at' => now()->addMinutes(5),
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/login', [
|
|
'mobile' => '13800138100',
|
|
'code' => '111111',
|
|
'competition_slug' => 'main-event',
|
|
'public_source_code' => 'poster_A',
|
|
])->assertOk();
|
|
|
|
$this->assertDatabaseHas('users', [
|
|
'mobile' => '13800138100',
|
|
'public_source_channel_id' => $sourceA->id,
|
|
'public_source_code' => 'poster_A',
|
|
]);
|
|
$this->assertNotNull(DB::table('users')
|
|
->where('mobile', '13800138100')
|
|
->value('public_source_attributed_at'));
|
|
|
|
SmsVerification::query()->create([
|
|
'scene' => SmsVerification::SCENE_PARTICIPANT_LOGIN,
|
|
'mobile' => '13800138100',
|
|
'code' => '222222',
|
|
'provider' => SmsVerification::PROVIDER_DISABLED,
|
|
'status' => SmsVerification::STATUS_SENT,
|
|
'expires_at' => now()->addMinutes(5),
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/login', [
|
|
'mobile' => '13800138100',
|
|
'code' => '222222',
|
|
'competition_slug' => 'main-event',
|
|
'public_source_code' => 'poster_B',
|
|
])->assertOk();
|
|
|
|
$this->assertDatabaseHas('users', [
|
|
'mobile' => '13800138100',
|
|
'public_source_channel_id' => $sourceA->id,
|
|
'public_source_code' => 'poster_A',
|
|
]);
|
|
$this->assertDatabaseMissing('users', [
|
|
'mobile' => '13800138100',
|
|
'public_source_channel_id' => $sourceB->id,
|
|
'public_source_code' => 'poster_B',
|
|
]);
|
|
|
|
SmsVerification::query()->create([
|
|
'scene' => SmsVerification::SCENE_PARTICIPANT_LOGIN,
|
|
'mobile' => '13800138101',
|
|
'code' => '333333',
|
|
'provider' => SmsVerification::PROVIDER_DISABLED,
|
|
'status' => SmsVerification::STATUS_SENT,
|
|
'expires_at' => now()->addMinutes(5),
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/login', [
|
|
'mobile' => '13800138101',
|
|
'code' => '333333',
|
|
'competition_slug' => 'main-event',
|
|
'public_source_code' => 'bad code',
|
|
])->assertOk();
|
|
|
|
$this->assertDatabaseHas('users', [
|
|
'mobile' => '13800138101',
|
|
'public_source_channel_id' => null,
|
|
'public_source_code' => null,
|
|
]);
|
|
}
|
|
|
|
public function test_enabled_sender_fails_fast_when_app_key_is_missing(): void
|
|
{
|
|
config([
|
|
'sms.enabled' => true,
|
|
'sms.tencentcloud.sdk_app_id' => '1400000000',
|
|
]);
|
|
Http::fake();
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138001',
|
|
])->assertStatus(503)
|
|
->assertJsonPath('message', '短信服务配置未完成');
|
|
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'mobile' => '13800138001',
|
|
'provider' => SmsVerification::PROVIDER_TENCENTCLOUD,
|
|
'status' => SmsVerification::STATUS_FAILED,
|
|
'provider_code' => 'CONFIG_INCOMPLETE',
|
|
]);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_failed_attempt_still_counts_for_rate_limit(): void
|
|
{
|
|
config(['sms.enabled' => true]);
|
|
Http::fake();
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138008',
|
|
])->assertStatus(503);
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138008',
|
|
])->assertUnprocessable()
|
|
->assertJsonValidationErrors('mobile');
|
|
|
|
$this->assertSame(1, SmsVerification::query()->where('mobile', '13800138008')->count());
|
|
}
|
|
|
|
public function test_send_is_rejected_without_record_or_http_call_when_mobile_lock_is_occupied(): void
|
|
{
|
|
$mobile = '13800138009';
|
|
$this->enableTencentCloudConfig();
|
|
Http::fake();
|
|
|
|
$lock = Cache::lock(
|
|
'sms-send-lock:'.hash('sha256', $mobile),
|
|
max(30, (int) config('sms.timeout', 5) + 10)
|
|
);
|
|
$this->assertTrue($lock->get());
|
|
|
|
try {
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => $mobile,
|
|
])->assertUnprocessable()
|
|
->assertJsonPath('errors.mobile.0', '请勿频繁发送');
|
|
} finally {
|
|
$lock->release();
|
|
}
|
|
|
|
$this->assertSame(0, SmsVerification::query()->where('mobile', $mobile)->count());
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_provider_failure_releases_lock_and_allows_send_after_rate_limit_window(): void
|
|
{
|
|
$mobile = '13800138010';
|
|
$startedAt = Carbon::parse('2026-06-06 10:00:00');
|
|
$this->enableTencentCloudConfig();
|
|
config(['sms.timeout' => 120]);
|
|
Http::fakeSequence()
|
|
->push([
|
|
'result' => 1014,
|
|
'errmsg' => 'template incorrect',
|
|
'sid' => 'request-failed-lock-release',
|
|
], 200)
|
|
->push([
|
|
'result' => 0,
|
|
'errmsg' => 'OK',
|
|
'sid' => 'request-after-window',
|
|
], 200);
|
|
|
|
Carbon::setTestNow($startedAt);
|
|
|
|
try {
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => $mobile,
|
|
])->assertStatus(503)
|
|
->assertJsonPath('message', '发送失败');
|
|
|
|
Carbon::setTestNow($startedAt->copy()->addSeconds(61));
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => $mobile,
|
|
])->assertOk();
|
|
} finally {
|
|
Carbon::setTestNow();
|
|
}
|
|
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'mobile' => $mobile,
|
|
'status' => SmsVerification::STATUS_FAILED,
|
|
'provider_request_id' => 'request-failed-lock-release',
|
|
]);
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'mobile' => $mobile,
|
|
'status' => SmsVerification::STATUS_SENT,
|
|
'provider_request_id' => 'request-after-window',
|
|
]);
|
|
Http::assertSentCount(2);
|
|
}
|
|
|
|
public function test_disabled_sender_does_not_expose_debug_code_in_production(): void
|
|
{
|
|
$this->app->detectEnvironment(fn () => 'production');
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138006',
|
|
])->assertOk()
|
|
->assertJsonMissingPath('debug_code');
|
|
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'mobile' => '13800138006',
|
|
'provider' => SmsVerification::PROVIDER_DISABLED,
|
|
'status' => SmsVerification::STATUS_SENT,
|
|
]);
|
|
}
|
|
|
|
public function test_enabled_sender_posts_tencent_cloud_template_parameters_and_marks_sent(): void
|
|
{
|
|
$this->enableTencentCloudConfig();
|
|
Http::fake(function (Request $request) {
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'mobile' => '13800138002',
|
|
'status' => SmsVerification::STATUS_PENDING,
|
|
]);
|
|
|
|
return Http::response([
|
|
'result' => 0,
|
|
'errmsg' => 'OK',
|
|
'ext' => '',
|
|
'fee' => 1,
|
|
'sid' => 'request-1',
|
|
], 200);
|
|
});
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138002',
|
|
])->assertOk()
|
|
->assertJsonMissingPath('debug_code');
|
|
|
|
Http::assertSent(function (Request $request): bool {
|
|
$payload = json_decode($request->body(), true);
|
|
parse_str((string) parse_url($request->url(), PHP_URL_QUERY), $query);
|
|
$expectedSig = hash(
|
|
'sha256',
|
|
'appkey=test-app-key'
|
|
.'&random='.$query['random']
|
|
.'&time='.$payload['time']
|
|
.'&mobile='.$payload['tel']['mobile']
|
|
);
|
|
|
|
return parse_url($request->url(), PHP_URL_SCHEME) === 'https'
|
|
&& parse_url($request->url(), PHP_URL_HOST) === 'yun.tim.qq.com'
|
|
&& parse_url($request->url(), PHP_URL_PATH) === '/v5/tlssmssvr/sendsms'
|
|
&& $query['sdkappid'] === '1400000000'
|
|
&& preg_match('/^\d+$/', (string) $query['random']) === 1
|
|
&& ! $request->hasHeader('Authorization')
|
|
&& ! $request->hasHeader('X-TC-Action')
|
|
&& $payload['ext'] === ''
|
|
&& $payload['extend'] === ''
|
|
&& count($payload['params']) === 2
|
|
&& preg_match('/^\d{6}$/', (string) $payload['params'][0]) === 1
|
|
&& $payload['params'][1] === '5'
|
|
&& $payload['sig'] === $expectedSig
|
|
&& $payload['sign'] === '元禾控股'
|
|
&& $payload['tel'] === ['mobile' => '13800138002', 'nationcode' => '86']
|
|
&& is_int($payload['time'])
|
|
&& $payload['tpl_id'] === 2175650;
|
|
});
|
|
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'mobile' => '13800138002',
|
|
'provider' => SmsVerification::PROVIDER_TENCENTCLOUD,
|
|
'status' => SmsVerification::STATUS_SENT,
|
|
'provider_request_id' => 'request-1',
|
|
'provider_code' => '0',
|
|
'provider_message' => 'OK',
|
|
]);
|
|
}
|
|
|
|
public function test_runtime_configuration_has_no_cam_dependency(): void
|
|
{
|
|
$requiredEnv = SmsConfig::realSendingRequiredEnv();
|
|
|
|
$this->assertSame('TENCENT_SMS_APP_KEY', $requiredEnv['sms.tencentcloud.app_key']);
|
|
$this->assertNotContains('TENCENTCLOUD_SECRET_ID', $requiredEnv);
|
|
$this->assertNotContains('TENCENTCLOUD_SECRET_KEY', $requiredEnv);
|
|
$this->assertArrayNotHasKey('sms.tencentcloud.region', $requiredEnv);
|
|
$this->assertArrayNotHasKey('sms.tencentcloud.secret_id', $requiredEnv);
|
|
$this->assertArrayNotHasKey('sms.tencentcloud.secret_key', $requiredEnv);
|
|
}
|
|
|
|
public function test_http_500_with_result_zero_is_recorded_as_failed(): void
|
|
{
|
|
$this->enableTencentCloudConfig();
|
|
Http::fake([
|
|
'https://yun.tim.qq.com/v5/tlssmssvr/sendsms*' => Http::response([
|
|
'result' => 0,
|
|
'errmsg' => 'upstream unavailable',
|
|
'sid' => 'http-500-sid',
|
|
], 500),
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138014',
|
|
])->assertStatus(503)
|
|
->assertJsonPath('message', '发送失败');
|
|
|
|
$record = SmsVerification::query()->where('mobile', '13800138014')->firstOrFail();
|
|
$this->assertSame(SmsVerification::STATUS_FAILED, $record->status);
|
|
$this->assertSame('HTTP_500', $record->provider_code);
|
|
$this->assertSame('http-500-sid', $record->provider_request_id);
|
|
$this->assertSame(500, $record->response_json['http_status']);
|
|
$this->assertSame(0, $record->response_json['result']);
|
|
$this->assertSame('upstream unavailable', $record->response_json['errmsg']);
|
|
$this->assertSame(0, SmsVerification::query()
|
|
->where('mobile', '13800138014')
|
|
->where('status', SmsVerification::STATUS_SENT)
|
|
->count());
|
|
}
|
|
|
|
public function test_network_error_is_recorded_with_locatable_code(): void
|
|
{
|
|
$this->enableTencentCloudConfig();
|
|
Http::fake(fn () => throw new \RuntimeException('connection refused'));
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138011',
|
|
])->assertStatus(503);
|
|
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'mobile' => '13800138011',
|
|
'status' => SmsVerification::STATUS_FAILED,
|
|
'provider_code' => 'NETWORK_ERROR',
|
|
]);
|
|
}
|
|
|
|
public function test_invalid_json_response_is_recorded_with_locatable_code(): void
|
|
{
|
|
$this->enableTencentCloudConfig();
|
|
Http::fake([
|
|
'https://yun.tim.qq.com/v5/tlssmssvr/sendsms*' => Http::response('not-json', 200),
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138012',
|
|
])->assertStatus(503);
|
|
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'mobile' => '13800138012',
|
|
'status' => SmsVerification::STATUS_FAILED,
|
|
'provider_code' => 'INVALID_JSON_RESPONSE',
|
|
]);
|
|
}
|
|
|
|
public function test_protocol_error_is_recorded_when_result_is_missing(): void
|
|
{
|
|
$this->enableTencentCloudConfig();
|
|
Http::fake([
|
|
'https://yun.tim.qq.com/v5/tlssmssvr/sendsms*' => Http::response([
|
|
'errmsg' => 'missing result',
|
|
'sid' => 'protocol-error-sid',
|
|
], 200),
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138013',
|
|
])->assertStatus(503);
|
|
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'mobile' => '13800138013',
|
|
'status' => SmsVerification::STATUS_FAILED,
|
|
'provider_request_id' => 'protocol-error-sid',
|
|
'provider_code' => 'PROTOCOL_ERROR',
|
|
]);
|
|
}
|
|
|
|
public function test_failed_new_send_keeps_previous_sent_code_usable(): void
|
|
{
|
|
$competition = $this->competition('main-event');
|
|
$old = SmsVerification::query()->create([
|
|
'scene' => SmsVerification::SCENE_PARTICIPANT_LOGIN,
|
|
'mobile' => '13800138003',
|
|
'code' => '111111',
|
|
'provider' => SmsVerification::PROVIDER_DISABLED,
|
|
'status' => SmsVerification::STATUS_SENT,
|
|
'expires_at' => now()->addMinutes(5),
|
|
]);
|
|
$old->forceFill([
|
|
'created_at' => now()->subMinutes(2),
|
|
'updated_at' => now()->subMinutes(2),
|
|
])->save();
|
|
|
|
$this->enableTencentCloudConfig();
|
|
Http::fake([
|
|
'https://yun.tim.qq.com/v5/tlssmssvr/sendsms*' => Http::response([
|
|
'result' => 1014,
|
|
'errmsg' => 'template incorrect',
|
|
'sid' => 'request-failed',
|
|
], 200),
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138003',
|
|
])->assertStatus(503)
|
|
->assertJsonPath('message', '发送失败');
|
|
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'id' => $old->id,
|
|
'status' => SmsVerification::STATUS_SENT,
|
|
]);
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'mobile' => '13800138003',
|
|
'status' => SmsVerification::STATUS_FAILED,
|
|
'provider_request_id' => 'request-failed',
|
|
'provider_code' => '1014',
|
|
'provider_message' => 'template incorrect',
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/login', [
|
|
'mobile' => '13800138003',
|
|
'code' => '111111',
|
|
'competition_slug' => $competition->slug,
|
|
])->assertOk();
|
|
}
|
|
|
|
public function test_successful_new_send_expires_previous_sent_code(): void
|
|
{
|
|
$this->competition('main-event');
|
|
$old = SmsVerification::query()->create([
|
|
'scene' => SmsVerification::SCENE_PARTICIPANT_LOGIN,
|
|
'mobile' => '13800138007',
|
|
'code' => '333333',
|
|
'provider' => SmsVerification::PROVIDER_DISABLED,
|
|
'status' => SmsVerification::STATUS_SENT,
|
|
'expires_at' => now()->addMinutes(5),
|
|
]);
|
|
$old->forceFill([
|
|
'created_at' => now()->subMinutes(2),
|
|
'updated_at' => now()->subMinutes(2),
|
|
])->save();
|
|
$oldPending = SmsVerification::query()->create([
|
|
'scene' => SmsVerification::SCENE_PARTICIPANT_LOGIN,
|
|
'mobile' => '13800138007',
|
|
'code' => '444444',
|
|
'provider' => SmsVerification::PROVIDER_TENCENTCLOUD,
|
|
'status' => SmsVerification::STATUS_PENDING,
|
|
'expires_at' => now()->addMinutes(5),
|
|
]);
|
|
$oldPending->forceFill([
|
|
'created_at' => now()->subMinutes(2),
|
|
'updated_at' => now()->subMinutes(2),
|
|
])->save();
|
|
|
|
$this->enableTencentCloudConfig();
|
|
Http::fake([
|
|
'https://yun.tim.qq.com/v5/tlssmssvr/sendsms*' => Http::response([
|
|
'result' => 0,
|
|
'errmsg' => 'OK',
|
|
'sid' => 'request-new',
|
|
], 200),
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138007',
|
|
])->assertOk();
|
|
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'id' => $old->id,
|
|
'status' => SmsVerification::STATUS_EXPIRED,
|
|
]);
|
|
$this->assertDatabaseHas('sms_verifications', [
|
|
'id' => $oldPending->id,
|
|
'status' => SmsVerification::STATUS_EXPIRED,
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/login', [
|
|
'mobile' => '13800138007',
|
|
'code' => '333333',
|
|
'competition_slug' => 'main-event',
|
|
])->assertUnprocessable()
|
|
->assertJsonValidationErrors('code');
|
|
}
|
|
|
|
public function test_rate_limit_is_kept_after_successful_send(): void
|
|
{
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138004',
|
|
])->assertOk();
|
|
|
|
$this->postJson('/api/auth/sms/send', [
|
|
'mobile' => '13800138004',
|
|
])->assertUnprocessable()
|
|
->assertJsonValidationErrors('mobile');
|
|
}
|
|
|
|
public function test_expired_sent_code_cannot_login(): void
|
|
{
|
|
$this->competition('main-event');
|
|
SmsVerification::query()->create([
|
|
'scene' => SmsVerification::SCENE_PARTICIPANT_LOGIN,
|
|
'mobile' => '13800138005',
|
|
'code' => '222222',
|
|
'provider' => SmsVerification::PROVIDER_DISABLED,
|
|
'status' => SmsVerification::STATUS_SENT,
|
|
'expires_at' => now()->subSecond(),
|
|
]);
|
|
|
|
$this->postJson('/api/auth/sms/login', [
|
|
'mobile' => '13800138005',
|
|
'code' => '222222',
|
|
'competition_slug' => 'main-event',
|
|
])->assertUnprocessable()
|
|
->assertJsonValidationErrors('code');
|
|
}
|
|
|
|
private function enableTencentCloudConfig(): void
|
|
{
|
|
config([
|
|
'sms.enabled' => true,
|
|
'sms.tencentcloud.sdk_app_id' => '1400000000',
|
|
'sms.tencentcloud.app_key' => 'test-app-key',
|
|
]);
|
|
}
|
|
|
|
private function competition(string $slug): Competition
|
|
{
|
|
return Competition::query()->create([
|
|
'slug' => $slug,
|
|
'name' => '测试赛事',
|
|
'status' => 'published',
|
|
'published' => true,
|
|
]);
|
|
}
|
|
|
|
private function createSchema(): void
|
|
{
|
|
Schema::create('users', function (Blueprint $table) {
|
|
$table->id();
|
|
$table->string('mobile')->unique();
|
|
$table->string('name')->nullable();
|
|
$table->string('email')->nullable();
|
|
$table->string('company')->nullable();
|
|
$table->string('password')->nullable();
|
|
$table->foreignId('public_source_channel_id')->nullable();
|
|
$table->string('public_source_code', 64)->nullable();
|
|
$table->timestamp('public_source_attributed_at')->nullable();
|
|
$table->timestamps();
|
|
});
|
|
|
|
Schema::create('public_source_channels', function (Blueprint $table) {
|
|
$table->id();
|
|
$table->string('source_code', 64)->unique();
|
|
$table->string('source_name', 100);
|
|
$table->string('status')->default(PublicSourceChannel::STATUS_ENABLED);
|
|
$table->timestamps();
|
|
});
|
|
|
|
Schema::create('competitions', function (Blueprint $table) {
|
|
$table->id();
|
|
$table->string('slug')->unique();
|
|
$table->string('name');
|
|
$table->string('status')->default('draft');
|
|
$table->boolean('published')->default(false);
|
|
$table->timestamps();
|
|
});
|
|
|
|
Schema::create('applications', function (Blueprint $table) {
|
|
$table->id();
|
|
$table->string('project_code', 32)->nullable();
|
|
$table->foreignId('user_id');
|
|
$table->foreignId('competition_id');
|
|
$table->string('status')->default('draft');
|
|
$table->softDeletes();
|
|
$table->timestamps();
|
|
$table->unique(['user_id', 'competition_id']);
|
|
});
|
|
|
|
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();
|
|
});
|
|
|
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
|
$table->id();
|
|
$table->morphs('tokenable');
|
|
$table->string('name');
|
|
$table->string('token', 64)->unique();
|
|
$table->text('abilities')->nullable();
|
|
$table->timestamp('last_used_at')->nullable();
|
|
$table->timestamp('expires_at')->nullable();
|
|
$table->timestamps();
|
|
});
|
|
}
|
|
}
|