274 lines
10 KiB
PHP
274 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Paper;
|
|
use App\Models\QuestionBank;
|
|
use App\Models\SchoolClass;
|
|
use App\Models\User;
|
|
use App\Services\QuestionImportService;
|
|
use App\Services\QuizService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Tests\TestCase;
|
|
use Tymon\JWTAuth\Facades\JWTAuth;
|
|
|
|
final class QuizFlowTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_practice_answer_updates_wrong_question_state(): void
|
|
{
|
|
$user = User::factory()->create(['role' => 'user']);
|
|
$bank = QuestionBank::create([
|
|
'owner_id' => $user->id,
|
|
'name' => '测试题库',
|
|
'visibility' => 'public',
|
|
'is_active' => true,
|
|
]);
|
|
app(QuestionImportService::class)->importJsonText($bank, $user, file_get_contents(base_path('question.json')));
|
|
|
|
$service = app(QuizService::class);
|
|
$attempt = $service->startPractice($user, $bank, 'practice', ['limit' => 100]);
|
|
$item = $attempt->items()->with('question.options')->firstOrFail();
|
|
$wrongOption = $item->question->options->firstWhere('is_correct', false);
|
|
|
|
$service->answer($user, $attempt, $item->question_id, [$wrongOption->id]);
|
|
|
|
$this->assertDatabaseHas('wrong_questions', [
|
|
'user_id' => $user->id,
|
|
'question_id' => $item->question_id,
|
|
'wrong_count' => 1,
|
|
]);
|
|
}
|
|
|
|
public function test_paper_attempt_uses_override_score_and_enforces_attempt_limit(): void
|
|
{
|
|
$user = User::factory()->create(['role' => 'user']);
|
|
$bank = QuestionBank::create([
|
|
'owner_id' => $user->id,
|
|
'name' => '固定试卷题库',
|
|
'visibility' => 'public',
|
|
'is_active' => true,
|
|
]);
|
|
app(QuestionImportService::class)->importJsonText($bank, $user, file_get_contents(base_path('question.json')));
|
|
$question = $bank->questions()->with('options')->firstOrFail();
|
|
$paper = Paper::create([
|
|
'owner_id' => $user->id,
|
|
'question_bank_id' => $bank->id,
|
|
'title' => '限次测试',
|
|
'attempt_limit' => 1,
|
|
'is_active' => true,
|
|
]);
|
|
$paper->questions()->attach($question->id, ['score' => 5, 'sort' => 0]);
|
|
|
|
$service = app(QuizService::class);
|
|
$attempt = $service->startPaper($user, $paper);
|
|
$item = $attempt->items()->with('question.options')->firstOrFail();
|
|
$correctOptionIds = $item->question->options->where('is_correct', true)->pluck('id')->all();
|
|
|
|
$service->answer($user, $attempt, $item->question_id, $correctOptionIds);
|
|
$submitted = $service->submit($user, $attempt);
|
|
|
|
$this->assertSame('5.00', $submitted->score);
|
|
|
|
$this->expectException(ValidationException::class);
|
|
$service->startPaper($user, $paper);
|
|
}
|
|
|
|
public function test_wrong_question_is_marked_mastered_after_three_correct_answers(): void
|
|
{
|
|
$user = User::factory()->create(['role' => 'user']);
|
|
$bank = QuestionBank::create([
|
|
'owner_id' => $user->id,
|
|
'name' => '错题题库',
|
|
'visibility' => 'public',
|
|
'is_active' => true,
|
|
]);
|
|
app(QuestionImportService::class)->importJsonText($bank, $user, file_get_contents(base_path('question.json')));
|
|
|
|
$service = app(QuizService::class);
|
|
$attempt = $service->startPractice($user, $bank, 'practice', ['limit' => 1]);
|
|
$item = $attempt->items()->with('question.options')->firstOrFail();
|
|
$wrongOption = $item->question->options->firstWhere('is_correct', false);
|
|
$correctOptionIds = $item->question->options->where('is_correct', true)->pluck('id')->all();
|
|
|
|
$service->answer($user, $attempt, $item->question_id, [$wrongOption->id]);
|
|
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$nextAttempt = $service->startPractice($user, $bank, 'practice', ['limit' => 100]);
|
|
$service->answer($user, $nextAttempt, $item->question_id, $correctOptionIds);
|
|
}
|
|
|
|
$this->assertDatabaseMissing('wrong_questions', [
|
|
'user_id' => $user->id,
|
|
'question_id' => $item->question_id,
|
|
'mastered_at' => null,
|
|
]);
|
|
}
|
|
|
|
public function test_attempt_position_can_be_saved_and_resumed(): void
|
|
{
|
|
$user = User::factory()->create(['role' => 'user', 'password' => bcrypt('password')]);
|
|
$bank = QuestionBank::create([
|
|
'owner_id' => $user->id,
|
|
'name' => '续做题库',
|
|
'visibility' => 'public',
|
|
'is_active' => true,
|
|
]);
|
|
app(QuestionImportService::class)->importJsonText($bank, $user, file_get_contents(base_path('question.json')));
|
|
|
|
$attempt = app(QuizService::class)->startPractice($user, $bank, 'practice', ['limit' => 3]);
|
|
$token = $this->postJson('/api/auth/login', [
|
|
'email' => $user->email,
|
|
'password' => 'password',
|
|
])->assertOk()->json('data.token');
|
|
|
|
$this->assertSame($user->id, JWTAuth::setToken($token)->authenticate()->id);
|
|
$this->assertSame($user->id, $attempt->user_id);
|
|
|
|
$response = $this->withToken($token)
|
|
->putJson("/api/app/attempts/{$attempt->id}/position", [
|
|
'current_index' => 1,
|
|
]);
|
|
$response
|
|
->assertOk()
|
|
->assertJsonPath('data.current_index', 1);
|
|
|
|
$this->withToken($token)
|
|
->getJson("/api/app/attempts/{$attempt->id}")
|
|
->assertOk()
|
|
->assertJsonPath('data.current_index', 1);
|
|
}
|
|
|
|
public function test_private_bank_cannot_be_started_without_share(): void
|
|
{
|
|
$owner = User::factory()->create(['role' => 'teacher']);
|
|
$student = User::factory()->create(['role' => 'user']);
|
|
$bank = $this->bankWithQuestions($owner, ['visibility' => 'private']);
|
|
$token = JWTAuth::fromUser($student);
|
|
|
|
$this->withToken($token)
|
|
->postJson("/api/app/banks/{$bank->id}/attempts", [
|
|
'mode' => 'practice',
|
|
'limit' => 1,
|
|
])
|
|
->assertForbidden();
|
|
}
|
|
|
|
public function test_user_share_allows_starting_private_bank(): void
|
|
{
|
|
$owner = User::factory()->create(['role' => 'teacher']);
|
|
$student = User::factory()->create(['role' => 'user']);
|
|
$bank = $this->bankWithQuestions($owner, ['visibility' => 'assigned']);
|
|
DB::table('bank_shares')->insert([
|
|
'question_bank_id' => $bank->id,
|
|
'target_type' => 'user',
|
|
'target_id' => $student->id,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$this->withToken(JWTAuth::fromUser($student))
|
|
->postJson("/api/app/banks/{$bank->id}/attempts", [
|
|
'mode' => 'practice',
|
|
'limit' => 1,
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('data.question_bank_id', $bank->id);
|
|
}
|
|
|
|
public function test_class_share_allows_resources_and_paper_attempts(): void
|
|
{
|
|
$owner = User::factory()->create(['role' => 'teacher']);
|
|
$student = User::factory()->create(['role' => 'user']);
|
|
$bank = $this->bankWithQuestions($owner, ['visibility' => 'assigned']);
|
|
$class = SchoolClass::create([
|
|
'owner_id' => $owner->id,
|
|
'name' => '一班',
|
|
'join_code' => 'CLASS001',
|
|
'is_active' => true,
|
|
]);
|
|
$class->members()->attach($student->id, ['role' => 'student']);
|
|
DB::table('bank_shares')->insert([
|
|
'question_bank_id' => $bank->id,
|
|
'target_type' => 'class',
|
|
'target_id' => $class->id,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
$paper = Paper::create([
|
|
'owner_id' => $owner->id,
|
|
'question_bank_id' => $bank->id,
|
|
'title' => '班级测试',
|
|
'is_active' => true,
|
|
]);
|
|
$paper->questions()->attach($bank->questions()->firstOrFail()->id, ['sort' => 0]);
|
|
$token = JWTAuth::fromUser($student);
|
|
|
|
$this->withToken($token)
|
|
->getJson('/api/app/resources')
|
|
->assertOk()
|
|
->assertJsonPath('data.banks.0.id', $bank->id)
|
|
->assertJsonPath('data.papers.0.id', $paper->id);
|
|
|
|
$this->withToken($token)
|
|
->postJson("/api/app/papers/{$paper->id}/attempts")
|
|
->assertOk()
|
|
->assertJsonPath('data.paper_id', $paper->id);
|
|
}
|
|
|
|
public function test_public_bank_can_be_started_by_student(): void
|
|
{
|
|
$owner = User::factory()->create(['role' => 'teacher']);
|
|
$student = User::factory()->create(['role' => 'user']);
|
|
$bank = $this->bankWithQuestions($owner, ['visibility' => 'public']);
|
|
|
|
$this->withToken(JWTAuth::fromUser($student))
|
|
->postJson("/api/app/banks/{$bank->id}/attempts", [
|
|
'mode' => 'practice',
|
|
'limit' => 1,
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('data.question_bank_id', $bank->id);
|
|
}
|
|
|
|
public function test_private_unshared_paper_cannot_be_started(): void
|
|
{
|
|
$owner = User::factory()->create(['role' => 'teacher']);
|
|
$student = User::factory()->create(['role' => 'user']);
|
|
$bank = $this->bankWithQuestions($owner, ['visibility' => 'private']);
|
|
$paper = Paper::create([
|
|
'owner_id' => $owner->id,
|
|
'question_bank_id' => $bank->id,
|
|
'title' => '私有测试',
|
|
'is_active' => true,
|
|
]);
|
|
$paper->questions()->attach($bank->questions()->firstOrFail()->id, ['sort' => 0]);
|
|
|
|
$this->withToken(JWTAuth::fromUser($student))
|
|
->postJson("/api/app/papers/{$paper->id}/attempts")
|
|
->assertForbidden();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
*/
|
|
private function bankWithQuestions(User $owner, array $attributes = []): QuestionBank
|
|
{
|
|
$bank = QuestionBank::create($attributes + [
|
|
'owner_id' => $owner->id,
|
|
'name' => '权限题库',
|
|
'visibility' => 'public',
|
|
'is_active' => true,
|
|
]);
|
|
|
|
app(QuestionImportService::class)->importJsonText($bank, $owner, file_get_contents(base_path('question.json')));
|
|
|
|
return $bank;
|
|
}
|
|
}
|