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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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