'sqlite', 'database.connections.sqlite.database' => ':memory:', 'audit.enabled' => false]); DB::purge('sqlite'); $this->withoutMiddleware(); foreach (['2025_06_30_105931_create_course_content_evaluation_asks_table.php', '2025_06_30_105932_create_course_content_evaluation_forms_table.php', '2025_07_03_093224_create_course_content_evaluations_table.php', '2026_01_13_094952_add_dimensions_to_course_content_evaluation_asks_table.php', '2026_10_09_000001_extend_survey_questions_and_snapshots.php'] as $migration) { (require database_path('migrations/' . $migration))->up(); } foreach (['users', 'course_contents', 'teachers'] as $name) { Schema::create($name, function (Blueprint $table) { $table->id(); $table->softDeletes(); }); } $user = new User(); $user->forceFill(['id' => 200]); Auth::guard('mobile')->setUser($user); $admin = new Admin(); $admin->forceFill(['id' => 1, 'department_id' => 1]); Auth::guard('admin')->setUser($admin); // The existing audit logger queries MySQL column comments; keep this integration test SQLite-only. $this->app->bind(CourseContentEvaluationAskController::class, fn () => new class extends CourseContentEvaluationAskController { protected function saveLogs($original, $model) {} }); $this->app->bind(CourseContentEvaluationsController::class, fn () => new class extends CourseContentEvaluationsController { protected function saveLogs($original, $model) {} }); DB::table('course_content_evaluations')->insert(['id' => 10, 'title' => '培训问卷', 'status' => 1]); } protected function tearDown(): void { Auth::guard('mobile')->forgetUser(); Auth::guard('admin')->forgetUser(); DB::disconnect('sqlite'); parent::tearDown(); } private function createQuestion(): int { $response = $this->postJson('/api/admin/course-content-evaluation-ask/save', [ 'course_content_evaluation_id' => 10, 'name' => '融资对接', 'field' => 'finance', 'edit_input' => 'multi_dimension', 'sort' => 2, 'dimensions' => [ ['field' => 'count', 'name' => '意向数量', 'edit_input' => 'number', 'need_fill' => true, 'config' => ['integer' => true, 'min' => 0]], ['field' => 'follow', 'name' => '需要跟进', 'edit_input' => 'radio', 'need_fill' => true, 'select_item' => [['key' => 'yes', 'value' => '是'], ['key' => 'no', 'value' => '否']]], ], ])->assertOk()->assertJsonPath('name', '融资对接'); return $response->json('id'); } public function testFullSubmissionStatisticsSnapshotAndStructureLock(): void { $id = $this->createQuestion(); $definition = $this->getJson('/api/mobile/course/evaluation-detail?course_content_evaluation_id=10')->assertOk(); $hash = $definition->json('schema_hash'); $this->assertNotEmpty($hash); $payload = ['course_content_evaluation_id' => 10, 'answer_version' => 2, 'schema_hash' => $hash, 'data' => [['id' => $id, 'name' => '伪造标题', 'value' => ['count' => 0, 'follow' => 'no']]]]; $reply = $this->postJson('/api/mobile/course/course-content-form', $payload)->assertOk() ->assertJsonPath('user_id', 200)->assertJsonPath('snapshot.version', 2) ->assertJsonPath('snapshot.questions.0.name', '融资对接'); $replyId = $reply->json('id'); $this->assertSame(1, DB::table('course_content_evaluation_forms')->count()); $this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('errmsg', '您已提交过评价'); $this->getJson('/api/admin/course-content-evaluation-form/statistics?course_content_evaluation_id=10') ->assertJsonPath('responses', 1)->assertJsonPath('questions.0.dimensions.0.stats.valid_count', 1) ->assertJsonPath('questions.0.dimensions.0.stats.sum', 0)->assertJsonPath('questions.0.dimensions.1.stats.options.1.count', 1); $this->getJson('/api/admin/course-content-evaluation-form/show?id=' . $replyId) ->assertJsonPath('questions.0.dimensions.0.display_value', '0') ->assertJsonPath('questions.0.dimensions.1.display_value', '否'); $this->postJson('/api/admin/course-content-evaluation-ask/save', ['id' => $id, 'name' => '改名'])->assertJsonPath('errmsg', '已有答卷,不能修改题目结构;请新建问卷'); $this->getJson('/api/admin/course-content-evaluation-ask/destroy?id=' . $id)->assertJsonPath('errmsg', '已有答卷,不能删除题目'); $this->postJson('/api/admin/course-content-evaluation-ask/reorder', ['course_content_evaluation_id' => 10, 'ids' => [$id]])->assertJsonPath('errmsg', '已有答卷,不能调整题目顺序'); $this->postJson('/api/admin/course-content-evaluations/save', ['id' => 10, 'title' => '改标题'])->assertJsonPath('errmsg', '已有答卷,仅可调整开放时间与发布状态;请新建问卷修改内容'); $this->postJson('/api/admin/course-content-evaluations/save', ['id' => 10, 'status' => 0])->assertJsonPath('status', 0); // Even an out-of-band definition change cannot rewrite an existing reply. DB::table('course_content_evaluation_asks')->where('id', $id)->update(['name' => '数据库修改']); $this->getJson('/api/admin/course-content-evaluation-form/show?id=' . $replyId)->assertJsonPath('questions.0.name', '融资对接'); } public function testMobileReentryReturnsOnlyOwnSnapshot(): void { $id = $this->createQuestion(); $url = '/api/mobile/course/evaluation-detail?course_content_evaluation_id=10'; $this->getJson($url)->assertJsonPath('my_response', null); $this->postJson('/api/mobile/course/course-content-form', [ 'course_content_evaluation_id' => 10, 'answer_version' => 2, 'data' => [['id' => $id, 'value' => ['count' => 0, 'follow' => 'no']]], ])->assertOk(); DB::table('course_content_evaluations')->where('id', 10)->update(['title' => '新标题', 'status' => 0]); DB::table('course_content_evaluation_asks')->where('id', $id)->update(['name' => '新题目']); $this->getJson($url)->assertJsonPath('my_response.title', '培训问卷') ->assertJsonPath('my_response.questions.0.name', '融资对接') ->assertJsonPath('my_response.questions.0.value.count', 0) ->assertJsonPath('my_response.questions.0.dimensions.1.selected_keys.0', 'no'); $other = new User(); $other->forceFill(['id' => 201]); Auth::guard('mobile')->setUser($other); $this->getJson($url)->assertJsonPath('my_response', null); } public function testInvalidQuantityAndStaleSchemaDoNotCreateReplies(): void { $id = $this->createQuestion(); $payload = ['course_content_evaluation_id' => 10, 'answer_version' => 2, 'data' => [['id' => $id, 'value' => ['count' => -1, 'follow' => 'no']]]]; $this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('errcode', 10001); $payload['schema_hash'] = 'stale'; $this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('errmsg', '问卷已更新,请刷新后重新填写'); $this->assertSame(0, DB::table('course_content_evaluation_forms')->count()); } public function testAtomicReorderRequiresCompleteList(): void { $id = $this->createQuestion(); $chapter = $this->postJson('/api/admin/course-content-evaluation-ask/save', ['course_content_evaluation_id' => 10, 'name' => '第五部分', 'field' => 'section', 'edit_input' => 'section', 'sort' => 1])->assertOk()->json('id'); $this->postJson('/api/admin/course-content-evaluation-ask/reorder', ['course_content_evaluation_id' => 10, 'ids' => [$id, $id]])->assertJsonPath('errmsg', '题目列表已变更,请刷新'); $this->postJson('/api/admin/course-content-evaluation-ask/reorder', ['course_content_evaluation_id' => 10, 'ids' => [$chapter, $id]])->assertJsonPath('msg', '排序已保存'); $this->assertSame([$chapter, $id], DB::table('course_content_evaluation_asks')->orderBy('sort')->pluck('id')->all()); } public function testTextAnswersArePaginatedAndZeroRemainsVisible(): void { for ($i = 0; $i < 22; $i++) DB::table('course_content_evaluation_forms')->insert(['course_content_evaluation_id' => 10, 'data' => json_encode([['id' => 5, 'value' => $i === 0 ? '0' : '建议' . $i]])]); $response = $this->getJson('/api/admin/course-content-evaluation-form/text-answers?course_content_evaluation_id=10&question_id=5&page=2&page_size=20')->assertJsonPath('total', 22); $this->assertCount(2, $response->json('data')); $this->assertSame('0', $response->json('data.1.text')); } public function testNewSurveyCanBeCreatedAndMigrationRolledBack(): void { $this->postJson('/api/admin/course-content-evaluations/save', ['title' => '新问卷', 'status' => 0])->assertJsonPath('title', '新问卷'); (require database_path('migrations/2026_10_09_000001_extend_survey_questions_and_snapshots.php'))->down(); $this->assertFalse(Schema::hasColumn('course_content_evaluation_forms', 'snapshot')); $this->assertFalse(Schema::hasColumn('course_content_evaluation_asks', 'config')); } }