'sqlite', 'database.connections.sqlite.database' => ':memory:', ]); DB::purge('sqlite'); $this->createSchema(); $this->seedCourseData(); } protected function tearDown(): void { Auth::guard('mobile')->forgetUser(); Auth::guard('admin')->forgetUser(); DB::disconnect('sqlite'); parent::tearDown(); } public function test_anonymous_user_sees_only_published_public_courses(): void { $response = $this->getJson('/api/mobile/course/course'); $response->assertOk(); $data = $response->json('data'); $this->assertNoSensitiveKeys($response->json()); $this->assertCount(1, $data); $this->assertSame(150, $data[0]['id']); $this->assertSame('张老师', $data[0]['teacher']['name']); $this->assertArrayNotHasKey('mobile', $data[0]['teacher']); $this->assertArrayNotHasKey('admin_id', $data[0]); } public function test_regular_mobile_user_cannot_read_unpublished_course_details(): void { $user = new User(); $user->forceFill(['id' => 200]); Auth::guard('mobile')->setUser($user); $response = $this->getJson('/api/mobile/course/course-detail?course_id=151'); $response->assertNotFound(); } public function test_registered_mobile_user_still_receives_safe_course_detail(): void { $user = new User(); $user->forceFill(['id' => 201]); Auth::guard('mobile')->setUser($user); DB::table('course_signs')->insert([ 'id' => 1, 'course_id' => 150, 'user_id' => 201, ]); $response = $this->getJson('/api/mobile/course/course-detail?course_id=150'); $response->assertOk(); $this->assertNoSensitiveKeys($response->json()); $this->assertSame('公开课程', $response->json('name')); $this->assertSame('张老师', $response->json('teacher.name')); $this->assertStringEndsWith('/course/public.jpg', $response->json('publicize.0.url')); $this->assertStringEndsWith('/course/group.jpg', $response->json('qun_image.url')); $this->assertArrayNotHasKey('folder', $response->json('publicize.0')); $this->assertArrayNotHasKey('name', $response->json('qun_image')); $this->assertArrayNotHasKey('mobile', $response->json('teacher')); $this->assertArrayNotHasKey('admin_id', $response->json()); $this->assertArrayNotHasKey('department_id', $response->json()); $this->assertArrayNotHasKey('deleted_at', $response->json()); $this->assertArrayNotHasKey('admin_id', $response->json('course_forms.0')); } public function test_admin_user_does_not_expand_public_mobile_response(): void { $admin = new Admin(); $admin->forceFill(['id' => 1]); Auth::guard('admin')->setUser($admin); $response = $this->getJson('/api/mobile/course/course-detail-pc?course_id=150'); $response->assertOk(); $this->assertNoSensitiveKeys($response->json()); $this->assertArrayNotHasKey('mobile', $response->json('teacher')); $this->assertArrayNotHasKey('admin_id', $response->json()); $this->assertArrayNotHasKey('teacher_id', $response->json()); } public function test_deleted_virtual_and_not_started_courses_are_not_publicly_addressable(): void { foreach ([152, 153, 154] as $courseId) { $response = $this->getJson('/api/mobile/course/course-detail?course_id=' . $courseId); $response->assertNotFound(); } } private function createSchema(): void { Schema::create('courses', function (Blueprint $table) { $table->increments('id'); $table->integer('admin_id')->nullable(); $table->integer('department_id')->nullable(); $table->string('name')->nullable(); $table->integer('image_id')->nullable(); $table->integer('qun_image_id')->nullable(); $table->date('start_date')->nullable(); $table->date('end_date')->nullable(); $table->integer('type')->nullable(); $table->text('content')->nullable(); $table->text('publicize_content')->nullable(); $table->json('publicize_ids')->nullable(); $table->tinyInteger('is_fee')->nullable(); $table->integer('total')->nullable(); $table->string('url')->nullable(); $table->string('url_title')->nullable(); $table->integer('teacher_id')->nullable(); $table->tinyInteger('status')->default(0); $table->tinyInteger('course_status')->default(0); $table->tinyInteger('sign_status')->default(0); $table->boolean('is_virtual')->default(0); $table->boolean('show_txl')->default(1); $table->boolean('show_mobile')->default(1); $table->timestamps(); $table->softDeletes(); }); Schema::create('teachers', function (Blueprint $table) { $table->increments('id'); $table->string('name')->nullable(); $table->string('mobile')->nullable(); $table->text('introduce')->nullable(); $table->string('remark')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('course_types', function (Blueprint $table) { $table->increments('id'); $table->string('name')->nullable(); $table->string('wait_tip')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('course_forms', function (Blueprint $table) { $table->increments('id'); $table->integer('course_id')->nullable(); $table->integer('admin_id')->nullable(); $table->integer('department_id')->nullable(); $table->string('name')->nullable(); $table->string('field')->nullable(); $table->string('edit_input')->nullable(); $table->string('rule')->nullable(); $table->integer('sort')->default(1); $table->string('help')->nullable(); $table->text('select_item')->nullable(); $table->boolean('need_fill')->default(0); $table->boolean('belong_user')->default(0); $table->timestamps(); $table->softDeletes(); }); Schema::create('uploads', function (Blueprint $table) { $table->increments('id'); $table->string('folder')->nullable(); $table->string('name')->nullable(); $table->string('original_name')->nullable(); $table->timestamps(); }); DB::table('uploads')->insert([ ['id' => 21, 'folder' => 'course', 'name' => 'public.jpg'], ['id' => 22, 'folder' => 'course', 'name' => 'group.jpg'], ]); Schema::create('course_signs', function (Blueprint $table) { $table->increments('id'); $table->integer('course_id')->nullable(); $table->integer('user_id')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('course_content_evaluations', function (Blueprint $table) { $table->increments('id'); $table->integer('course_id')->nullable(); $table->string('course_content_id')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('course_content_evaluation_asks', function (Blueprint $table) { $table->increments('id'); $table->integer('course_content_evaluation_id')->nullable(); $table->integer('course_content_id')->nullable(); $table->integer('sort')->default(1); $table->timestamps(); $table->softDeletes(); }); } private function seedCourseData(): void { DB::table('teachers')->insert([ 'id' => 11, 'name' => '张老师', 'mobile' => '13800138000', 'introduce' => '公开介绍', 'remark' => '内部备注', ]); DB::table('course_types')->insert(['id' => 3, 'name' => '管理类']); DB::table('course_forms')->insert([ 'id' => 1, 'course_id' => 150, 'admin_id' => 9, 'department_id' => 10, 'name' => '姓名', 'field' => 'name', 'edit_input' => 'input', 'rule' => 'required', 'sort' => 1, 'help' => '请填写姓名', 'need_fill' => 1, 'belong_user' => 1, ]); $base = [ 'name' => '公开课程', 'start_date' => '2026-08-01', 'end_date' => '2026-08-02', 'type' => 3, 'content' => '公开介绍', 'publicize_content' => '宣传介绍', 'teacher_id' => 11, 'status' => 1, 'course_status' => 20, 'sign_status' => 10, 'is_virtual' => 0, 'show_txl' => 1, 'show_mobile' => 1, 'is_fee' => 0, 'total' => 30, 'url' => 'https://example.test/review', 'url_title' => '精彩回顾', 'publicize_ids' => json_encode([21]), 'qun_image_id' => 22, 'deleted_at' => null, ]; DB::table('courses')->insert([ array_merge($base, ['id' => 150]), array_merge($base, ['id' => 151, 'status' => 0]), array_merge($base, ['id' => 152, 'is_virtual' => 1]), array_merge($base, ['id' => 153, 'sign_status' => 30]), array_merge($base, ['id' => 154, 'deleted_at' => now()]), ]); } private function assertNoSensitiveKeys($value): void { $forbidden = [ 'mobile', 'idcard', 'id_card', 'identity_card', 'remark', 'admin_id', 'department_id', 'teacher_id', 'deleted_at', 'password', 'remember_token', ]; if (is_array($value)) { foreach ($value as $key => $item) { $this->assertNotContains(strtolower((string) $key), $forbidden); $this->assertNoSensitiveKeys($item); } } } }