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.

339 lines
12 KiB

1 month ago
<?php
namespace Tests\Feature;
use App\Models\AdminUser;
use App\Models\Application;
use App\Models\AudienceRegistration;
use App\Models\AudienceSession;
use App\Models\Competition;
use App\Models\SmsVerification;
use App\Models\User;
use App\Support\BizDateTime;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class AudienceRegistrationTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Carbon::setTestNow(Carbon::parse('2026-08-17 12:00:00', BizDateTime::TIMEZONE));
}
protected function tearDown(): void
{
Carbon::setTestNow();
parent::tearDown();
}
public function test_audience_login_does_not_create_application(): void
{
$competition = $this->publishedCompetition();
$this->issueSmsCode('13800138000', '123456');
$this->postJson('/api/auth/sms/audience/login', [
'mobile' => '13800138000',
'code' => '123456',
'competition_slug' => $competition->slug,
])->assertOk()
->assertJsonPath('token_type', 'Bearer');
$this->assertDatabaseHas('users', ['mobile' => '13800138000']);
$this->assertSame(0, Application::query()->count());
}
public function test_participant_token_cannot_access_audience_api(): void
{
$user = User::query()->create(['mobile' => '13800138001']);
$token = $user->createToken('web')->plainTextToken;
$this->withToken($token)
->getJson('/api/v1/audience/me')
->assertForbidden();
}
public function test_can_register_without_company_and_job(): void
{
$competition = $this->publishedCompetition();
$session = $this->openSession($competition);
$token = $this->audienceToken('13800138002');
$this->withToken($token)
->postJson('/api/v1/audience/registrations', [
'competition_slug' => $competition->slug,
'audience_session_id' => $session->id,
'name' => '王清萍',
'phone' => '13800138002',
'purpose' => '项目交流',
])
->assertCreated()
->assertJsonPath('session_name', '上海预赛路演')
->assertJsonPath('checked_in_at', null);
$this->assertNotEmpty(
AudienceRegistration::query()->where('name', '王清萍')->value('ticket_code'),
);
$this->assertDatabaseHas('audience_registrations', [
'audience_session_id' => $session->id,
'name' => '王清萍',
'company' => null,
'job' => null,
'purpose' => '项目交流',
]);
}
public function test_registration_phone_is_always_the_login_mobile(): void
{
$competition = $this->publishedCompetition();
$session = $this->openSession($competition);
$token = $this->audienceToken('13800138010');
$this->withToken($token)
->postJson('/api/v1/audience/registrations', [
'competition_slug' => $competition->slug,
'audience_session_id' => $session->id,
'name' => '锁号用户',
'phone' => '13900000000',
'purpose' => '项目交流',
])
->assertCreated()
->assertJsonPath('phone', '13800138010');
$this->assertDatabaseHas('audience_registrations', [
'audience_session_id' => $session->id,
'name' => '锁号用户',
'phone' => '13800138010',
]);
}
public function test_duplicate_registration_is_rejected(): void
{
$competition = $this->publishedCompetition();
$session = $this->openSession($competition);
$token = $this->audienceToken('13800138003');
$payload = [
'competition_slug' => $competition->slug,
'audience_session_id' => $session->id,
'name' => '重复用户',
'phone' => '13800138003',
'purpose' => '学习参访',
];
$this->withToken($token)->postJson('/api/v1/audience/registrations', $payload)->assertCreated();
$this->withToken($token)
->postJson('/api/v1/audience/registrations', $payload)
->assertUnprocessable()
->assertJsonValidationErrors('audience_session_id');
}
public function test_registration_outside_window_is_rejected(): void
{
$competition = $this->publishedCompetition();
$session = $this->makeSession($competition, [
'signup_start_at' => Carbon::parse('2026-09-01 00:00:00', BizDateTime::TIMEZONE),
'signup_end_at' => Carbon::parse('2026-09-10 23:59:59', BizDateTime::TIMEZONE),
]);
$token = $this->audienceToken('13800138004');
$this->withToken($token)
->postJson('/api/v1/audience/registrations', [
'competition_slug' => $competition->slug,
'audience_session_id' => $session->id,
'name' => '未开窗',
'phone' => '13800138004',
'purpose' => '其他',
])
->assertUnprocessable();
}
public function test_full_session_is_rejected(): void
{
$competition = $this->publishedCompetition();
$session = $this->openSession($competition, ['capacity' => 1]);
$first = User::query()->create(['mobile' => '13800138005']);
AudienceRegistration::query()->create([
'competition_id' => $competition->id,
'audience_session_id' => $session->id,
'user_id' => $first->id,
'name' => '先到',
'phone' => '13800138005',
'purpose' => '项目交流',
'registered_at' => now(),
]);
$token = $this->audienceToken('13800138006');
$this->withToken($token)
->postJson('/api/v1/audience/registrations', [
'competition_slug' => $competition->slug,
'audience_session_id' => $session->id,
'name' => '满额',
'phone' => '13800138006',
'purpose' => '项目交流',
])
->assertUnprocessable();
}
public function test_sessions_list_marks_registered_and_remaining(): void
{
$competition = $this->publishedCompetition();
$session = $this->openSession($competition, ['capacity' => 10]);
$token = $this->audienceToken('13800138007');
$this->withToken($token)
->postJson('/api/v1/audience/registrations', [
'competition_slug' => $competition->slug,
'audience_session_id' => $session->id,
'name' => '列表用户',
'phone' => '13800138007',
'company' => '组委会',
'job' => '观察员',
'purpose' => '媒体报道',
])
->assertCreated();
$this->withToken($token)
->getJson('/api/v1/audience/sessions?competition_slug='.$competition->slug)
->assertOk()
->assertJsonPath('data.0.is_registered', true)
->assertJsonPath('data.0.remaining', 9)
->assertJsonPath('data.0.status_label', '已报名');
}
public function test_admin_can_create_session_and_list_registrations(): void
{
Sanctum::actingAs($this->admin());
$competition = $this->publishedCompetition();
$this->postJson("/api/v1/admin/competitions/{$competition->id}/audience-sessions", [
'code' => 'demo-session',
'name' => '演示场次',
'event_date_text' => '2026年9月1日',
'address' => '上海',
'capacity' => 20,
'signup_start_at' => '2026-08-01T00:00:00+08:00',
'signup_end_at' => '2026-08-31T23:59:59+08:00',
'is_final' => false,
'sort' => 1,
'enabled' => true,
])->assertCreated()
->assertJsonPath('code', 'demo-session');
$session = AudienceSession::query()->where('code', 'demo-session')->firstOrFail();
$user = User::query()->create(['mobile' => '13800138008']);
AudienceRegistration::query()->create([
'competition_id' => $competition->id,
'audience_session_id' => $session->id,
'user_id' => $user->id,
'name' => '名单用户',
'phone' => '13800138008',
'purpose' => '投资对接',
'registered_at' => now(),
]);
$this->getJson("/api/v1/admin/competitions/{$competition->id}/audience-registrations?purpose=".urlencode('投资对接'))
->assertOk()
->assertJsonPath('total', 1)
->assertJsonPath('data.0.name', '名单用户');
$this->get("/api/v1/admin/competitions/{$competition->id}/audience-registrations/export")
->assertOk();
}
public function test_admin_cannot_delete_session_with_registrations(): void
{
Sanctum::actingAs($this->admin());
$competition = $this->publishedCompetition();
$session = $this->openSession($competition);
$user = User::query()->create(['mobile' => '13800138009']);
AudienceRegistration::query()->create([
'competition_id' => $competition->id,
'audience_session_id' => $session->id,
'user_id' => $user->id,
'name' => '占位',
'phone' => '13800138009',
'purpose' => '其他',
'registered_at' => now(),
]);
$this->deleteJson("/api/v1/admin/competitions/{$competition->id}/audience-sessions/{$session->id}")
->assertUnprocessable();
}
private function publishedCompetition(): Competition
{
return Competition::query()->create([
'slug' => 'husu-2026',
'name' => '沪苏协同新兴产业科创大赛',
'published' => true,
'status' => 'signup_open',
]);
}
/**
* @param array<string, mixed> $overrides
*/
private function openSession(Competition $competition, array $overrides = []): AudienceSession
{
return $this->makeSession($competition, array_merge([
'signup_start_at' => Carbon::parse('2026-08-10 00:00:00', BizDateTime::TIMEZONE),
'signup_end_at' => Carbon::parse('2026-09-10 23:59:59', BizDateTime::TIMEZONE),
], $overrides));
}
/**
* @param array<string, mixed> $overrides
*/
private function makeSession(Competition $competition, array $overrides = []): AudienceSession
{
return AudienceSession::query()->create(array_merge([
'competition_id' => $competition->id,
'code' => 'shanghai-preliminary',
'name' => '上海预赛路演',
'event_date_text' => '2026年9月18日(星期五)',
'address' => '上海市浦东新区张江科学会堂',
'capacity' => 300,
'signup_start_at' => Carbon::parse('2026-08-10 00:00:00', BizDateTime::TIMEZONE),
'signup_end_at' => Carbon::parse('2026-09-10 23:59:59', BizDateTime::TIMEZONE),
'is_final' => false,
'sort' => 1,
'enabled' => true,
], $overrides));
}
private function audienceToken(string $mobile): string
{
$user = User::query()->firstOrCreate(['mobile' => $mobile]);
return $user->createToken('audience')->plainTextToken;
}
private function issueSmsCode(string $mobile, string $code): void
{
SmsVerification::query()->create([
'scene' => SmsVerification::SCENE_PARTICIPANT_LOGIN,
'mobile' => $mobile,
'code' => $code,
'provider' => SmsVerification::PROVIDER_DISABLED,
'status' => SmsVerification::STATUS_SENT,
'expires_at' => now()->addMinutes(5),
]);
}
private function admin(): AdminUser
{
return AdminUser::query()->create([
'username' => 'audience_admin',
'password_hash' => 'unused',
'name' => '管理员',
'status' => 'active',
]);
}
}