|
|
|
|
|
<?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°ree=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),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|