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 $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; } }