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.

378 lines
14 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\CompetitionPortalLocale;
use App\Models\FormSchemaDefinition;
use App\Models\SmsVerification;
use App\Models\User;
use App\Support\PortalLocale;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class PortalLocaleSignupTest extends TestCase
{
use RefreshDatabase;
public function test_unsubmitted_login_follows_requested_lang_instead_of_locking(): void
{
$competition = $this->publishedCompetition();
$this->issueSms('13800138111', '123456');
$this->postJson('/api/auth/sms/login', [
'mobile' => '13800138111',
'code' => '123456',
'competition_slug' => $competition->slug,
'lang' => 'en',
])->assertOk()
->assertJsonPath('signup_locale', PortalLocale::EN)
->assertJsonPath('signup_locale_locked', false);
$this->assertDatabaseHas('applications', [
'competition_id' => $competition->id,
'signup_locale' => PortalLocale::EN,
]);
$this->issueSms('13800138111', '654321');
$this->postJson('/api/auth/sms/login', [
'mobile' => '13800138111',
'code' => '654321',
'competition_slug' => $competition->slug,
])->assertOk()
->assertJsonPath('signup_locale', PortalLocale::ZH)
->assertJsonPath('signup_locale_locked', false);
$this->assertDatabaseHas('applications', [
'competition_id' => $competition->id,
'signup_locale' => PortalLocale::ZH,
]);
}
public function test_chinese_and_english_submissions_store_the_same_degree_code(): void
{
$competition = $this->publishedCompetition();
$zhUser = User::query()->create(['mobile' => '13800138121']);
$enUser = User::query()->create(['mobile' => '13800138122']);
$zhApp = Application::query()->create([
'user_id' => $zhUser->id,
'competition_id' => $competition->id,
'status' => 'draft',
'signup_locale' => PortalLocale::ZH,
]);
$enApp = Application::query()->create([
'user_id' => $enUser->id,
'competition_id' => $competition->id,
'status' => 'draft',
'signup_locale' => PortalLocale::EN,
]);
Sanctum::actingAs($zhUser);
$this->postJson('/api/applications/current/save?competition_slug='.$competition->slug, [
'degree' => 'bachelor',
'project_name' => '中文项目',
])->assertOk()
->assertJsonPath('degree', 'bachelor');
Sanctum::actingAs($enUser);
$this->postJson('/api/applications/current/save?competition_slug='.$competition->slug, [
'degree' => 'bachelor',
'project_name' => 'English project',
])->assertOk()
->assertJsonPath('degree', 'bachelor');
$this->assertSame('bachelor', $zhApp->fresh()->degree);
$this->assertSame('bachelor', $enApp->fresh()->degree);
Sanctum::actingAs(AdminUser::query()->create([
'username' => 'locale_admin',
'password_hash' => 'unused',
'name' => '管理员',
'status' => 'active',
]));
$zhApp->forceFill(['status' => 'submitted', 'submitted_at' => now()])->save();
$enApp->forceFill(['status' => 'submitted', 'submitted_at' => now()])->save();
$list = $this->getJson("/api/v1/admin/competitions/{$competition->id}/applications?status=submitted")
->assertOk()
->json('data');
$byId = collect($list)->keyBy('id');
$this->assertSame('bachelor', $byId[$zhApp->id]['degree']);
$this->assertSame('本科', $byId[$zhApp->id]['degree_label']);
$this->assertSame('中文填报', $byId[$zhApp->id]['signup_locale_label']);
$this->assertSame('bachelor', $byId[$enApp->id]['degree']);
$this->assertSame('Bachelor’s Degree', $byId[$enApp->id]['degree_label']);
$this->assertSame('英文填报', $byId[$enApp->id]['signup_locale_label']);
$enOnly = $this->getJson("/api/v1/admin/competitions/{$competition->id}/applications?status=submitted&signup_locale=en")
->assertOk()
->json('data');
$this->assertSame([$enApp->id], collect($enOnly)->pluck('id')->all());
}
public function test_audience_unsubmitted_login_follows_requested_lang(): void
{
$competition = $this->publishedCompetition();
$this->issueSms('13800138131', '111111', SmsVerification::SCENE_AUDIENCE_LOGIN);
$this->postJson('/api/auth/sms/audience/login', [
'mobile' => '13800138131',
'code' => '111111',
'competition_slug' => $competition->slug,
'lang' => 'en',
])->assertOk()
->assertJsonPath('signup_locale', PortalLocale::EN)
->assertJsonPath('signup_locale_locked', false);
$this->issueSms('13800138131', '222222', SmsVerification::SCENE_AUDIENCE_LOGIN);
$this->postJson('/api/auth/sms/audience/login', [
'mobile' => '13800138131',
'code' => '222222',
'competition_slug' => $competition->slug,
])->assertOk()
->assertJsonPath('signup_locale', PortalLocale::ZH)
->assertJsonPath('signup_locale_locked', false);
}
public function test_audience_registration_locks_locale_on_later_login(): void
{
$competition = $this->publishedCompetition();
$user = User::query()->create(['mobile' => '13800138132']);
$session = AudienceSession::query()->create([
'competition_id' => $competition->id,
'code' => 'locale-session',
'name' => '测试场次',
'event_date_text' => '2026年9月18日',
'address' => '上海市',
'capacity' => 10,
'signup_start_at' => now()->subDay(),
'signup_end_at' => now()->addDay(),
'is_final' => false,
'sort' => 1,
'enabled' => true,
]);
AudienceRegistration::query()->create([
'competition_id' => $competition->id,
'audience_session_id' => $session->id,
'user_id' => $user->id,
'name' => '已报名观众',
'phone' => '13800138132',
'company' => '朗业',
'purpose' => 'networking',
'registered_at' => now(),
]);
CompetitionPortalLocale::query()->create([
'user_id' => $user->id,
'competition_id' => $competition->id,
'portal' => PortalLocale::PORTAL_AUDIENCE,
'locale' => PortalLocale::EN,
]);
$this->issueSms('13800138132', '333333', SmsVerification::SCENE_AUDIENCE_LOGIN);
$this->postJson('/api/auth/sms/audience/login', [
'mobile' => '13800138132',
'code' => '333333',
'competition_slug' => $competition->slug,
])->assertOk()
->assertJsonPath('signup_locale', PortalLocale::EN)
->assertJsonPath('signup_locale_locked', true);
}
public function test_draft_application_follows_english_login_link(): void
{
$competition = $this->publishedCompetition();
$user = User::query()->create(['mobile' => '13800138141']);
Application::query()->create([
'user_id' => $user->id,
'competition_id' => $competition->id,
'status' => 'draft',
'signup_locale' => PortalLocale::ZH,
]);
$this->issueSms('13800138141', '333333');
$this->postJson('/api/auth/sms/login', [
'mobile' => '13800138141',
'code' => '333333',
'competition_slug' => $competition->slug,
'lang' => 'en',
])->assertOk()
->assertJsonPath('signup_locale', PortalLocale::EN)
->assertJsonPath('signup_locale_locked', false);
$this->assertDatabaseHas('applications', [
'user_id' => $user->id,
'competition_id' => $competition->id,
'signup_locale' => PortalLocale::EN,
]);
}
public function test_submitted_application_stays_on_submit_locale(): void
{
$competition = $this->publishedCompetition();
$user = User::query()->create(['mobile' => '13800138142']);
Application::query()->create([
'user_id' => $user->id,
'competition_id' => $competition->id,
'status' => 'submitted',
'submitted_at' => now(),
'signup_locale' => PortalLocale::ZH,
]);
CompetitionPortalLocale::query()->create([
'user_id' => $user->id,
'competition_id' => $competition->id,
'portal' => PortalLocale::PORTAL_PARTICIPANT,
'locale' => PortalLocale::ZH,
]);
$this->issueSms('13800138142', '444444');
$this->postJson('/api/auth/sms/login', [
'mobile' => '13800138142',
'code' => '444444',
'competition_slug' => $competition->slug,
'lang' => 'en',
])->assertOk()
->assertJsonPath('signup_locale', PortalLocale::ZH)
->assertJsonPath('signup_locale_locked', true);
}
public function test_draft_save_follows_request_lang(): void
{
$competition = $this->publishedCompetition();
$user = User::query()->create(['mobile' => '13800138143']);
Application::query()->create([
'user_id' => $user->id,
'competition_id' => $competition->id,
'status' => 'draft',
'signup_locale' => PortalLocale::ZH,
]);
Sanctum::actingAs($user);
$this->postJson('/api/applications/current/save?competition_slug='.$competition->slug.'&lang=en', [
'project_name' => 'English draft',
])->assertOk()
->assertJsonPath('signup_locale', PortalLocale::EN)
->assertJsonPath('signup_locale_locked', false);
}
public function test_admin_list_filters_schema_codes_and_shows_locale_labels(): void
{
$competition = $this->publishedCompetition();
$schema = FormSchemaDefinition::query()->create([
'competition_id' => $competition->id,
'purpose' => FormSchemaDefinition::PURPOSE_SIGNUP,
'name' => '双语报名表',
'version' => 1,
'is_published' => true,
'schema_json' => [
[
'key' => 'degree',
'type' => 'select',
'label' => '最高学历',
'list' => true,
'filter' => true,
'options' => [
['value' => 'bachelor', 'label' => '本科', 'label_i18n' => ['en' => "Bachelor’s Degree"]],
],
],
[
'key' => 'industry',
'type' => 'select',
'label' => '行业',
'list' => true,
'filter' => true,
'options' => [
['value' => 'ai', 'label' => '人工智能', 'label_i18n' => ['en' => 'Artificial Intelligence']],
],
],
],
]);
$competition->update(['form_schema_id' => $schema->id]);
$zhUser = User::query()->create(['mobile' => '13800138151']);
$enUser = User::query()->create(['mobile' => '13800138152']);
$zhApp = Application::query()->create([
'user_id' => $zhUser->id,
'competition_id' => $competition->id,
'status' => 'submitted',
'submitted_at' => now(),
'signup_locale' => PortalLocale::ZH,
'degree' => 'bachelor',
'answers_json' => ['industry' => 'ai'],
]);
$enApp = Application::query()->create([
'user_id' => $enUser->id,
'competition_id' => $competition->id,
'status' => 'draft',
'signup_locale' => PortalLocale::EN,
'degree' => 'bachelor',
]);
Sanctum::actingAs($enUser);
$this->postJson('/api/applications/current/save?competition_slug='.$competition->slug, [
'industry' => 'ai',
])->assertOk()
->assertJsonPath('industry', 'ai');
$this->assertSame('ai', $enApp->fresh()->answers_json['industry'] ?? null);
$enApp->forceFill(['status' => 'submitted', 'submitted_at' => now()])->save();
Sanctum::actingAs(AdminUser::query()->create([
'username' => 'schema_locale_admin',
'password_hash' => 'unused',
'name' => '管理员',
'status' => 'active',
]));
$list = $this->getJson("/api/v1/admin/competitions/{$competition->id}/applications?status=submitted")
->assertOk()
->assertJsonPath('schema_list_fields.1.key', 'industry')
->json('data');
$byId = collect($list)->keyBy('id');
$this->assertSame('人工智能', $byId[$zhApp->id]['schema_display']['industry']);
$this->assertSame('Artificial Intelligence', $byId[$enApp->id]['schema_display']['industry']);
$degreeOnly = $this->getJson("/api/v1/admin/competitions/{$competition->id}/applications?status=submitted&degree=bachelor")
->assertOk()
->json('data');
$this->assertCount(2, $degreeOnly);
$industryOnly = $this->getJson("/api/v1/admin/competitions/{$competition->id}/applications?status=submitted&industry=ai")
->assertOk()
->json('data');
$this->assertCount(2, $industryOnly);
}
private function publishedCompetition(): Competition
{
return Competition::query()->create([
'slug' => 'locale-event',
'name' => '双语测试赛',
'published' => true,
'status' => 'signup_open',
]);
}
private function issueSms(
string $mobile,
string $code,
string $scene = SmsVerification::SCENE_PARTICIPANT_LOGIN
): void
{
SmsVerification::query()->create([
'scene' => $scene,
'mobile' => $mobile,
'code' => $code,
'provider' => SmsVerification::PROVIDER_DISABLED,
'status' => SmsVerification::STATUS_SENT,
'expires_at' => now()->addMinutes(5),
]);
}
}