master^2
parent
a36a2f0120
commit
2ce727b098
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Mobile;
|
||||
|
||||
/**
|
||||
* Explicit internal representation for a future authorized admin API.
|
||||
* Public mobile endpoints must never use this resource.
|
||||
*/
|
||||
class AdminCourseResource extends PublicCourseResource
|
||||
{
|
||||
public function toArray($request)
|
||||
{
|
||||
$data = parent::toArray($request);
|
||||
$data['admin_id'] = $this->admin_id;
|
||||
$data['department_id'] = $this->department_id;
|
||||
$data['teacher_id'] = $this->teacher_id;
|
||||
$data['is_virtual'] = (bool) $this->is_virtual;
|
||||
$data['show_txl'] = (bool) $this->show_txl;
|
||||
$data['show_mobile'] = (bool) $this->show_mobile;
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Mobile;
|
||||
|
||||
/**
|
||||
* Authenticated mobile representation.
|
||||
*
|
||||
* This is intentionally not used by the anonymous endpoints until optional
|
||||
* mobile authentication is explicitly enabled and tested. It documents the
|
||||
* boundary for future authenticated course APIs.
|
||||
*/
|
||||
class AuthenticatedCourseResource extends PublicCourseResource
|
||||
{
|
||||
public function toArray($request)
|
||||
{
|
||||
$data = parent::toArray($request);
|
||||
$data['my_user'] = $this->when(
|
||||
isset($this->my_user),
|
||||
(int) $this->my_user
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Mobile;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* Public representation of a course.
|
||||
*
|
||||
* Do not return Course models directly from public mobile endpoints. Course
|
||||
* has computed appends and relations that contain internal and personal data.
|
||||
*/
|
||||
class PublicCourseResource extends JsonResource
|
||||
{
|
||||
public function toArray($request)
|
||||
{
|
||||
$course = $this->resource;
|
||||
|
||||
return [
|
||||
'id' => (int) $this->id,
|
||||
'name' => $this->name,
|
||||
'start_date' => $this->start_date,
|
||||
'end_date' => $this->end_date,
|
||||
'content' => $this->content,
|
||||
'publicize_content' => $this->publicize_content,
|
||||
'type' => $this->type === null ? null : (int) $this->type,
|
||||
'is_fee' => $this->is_fee === null ? null : (int) $this->is_fee,
|
||||
'total' => $this->total === null ? null : (int) $this->total,
|
||||
'show_txl' => $this->show_txl === null ? null : (int) $this->show_txl,
|
||||
'show_mobile' => $this->show_mobile === null ? null : (int) $this->show_mobile,
|
||||
'url' => $this->url,
|
||||
'url_title' => $this->url_title,
|
||||
'status' => (int) $this->status,
|
||||
'course_status' => (int) $this->course_status,
|
||||
'sign_status' => (int) $this->sign_status,
|
||||
'my_user' => isset($this->my_user) ? (int) $this->my_user : 0,
|
||||
'type_detail' => $this->whenLoaded('typeDetail', function () {
|
||||
if (!$this->typeDetail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $this->typeDetail->id,
|
||||
'name' => $this->typeDetail->name,
|
||||
];
|
||||
}),
|
||||
'image' => $this->whenLoaded('image', function () {
|
||||
if (!$this->image) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $this->image->id,
|
||||
'url' => $this->image->url,
|
||||
];
|
||||
}),
|
||||
'qun_image_id' => $this->qun_image_id === null ? null : (int) $this->qun_image_id,
|
||||
'qun_image' => $this->whenLoaded('qunImage', function () {
|
||||
return $this->safeUpload($this->qunImage);
|
||||
}),
|
||||
'publicize' => $course->relationLoaded('publicAssets')
|
||||
? $course->getRelation('publicAssets')->map(fn ($upload) => $this->safeUpload($upload))->values()->all()
|
||||
: [],
|
||||
'teacher' => $this->whenLoaded('teacher', function () {
|
||||
if (!$this->teacher) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $this->teacher->id,
|
||||
'name' => $this->teacher->name,
|
||||
'introduce' => $this->teacher->introduce,
|
||||
];
|
||||
}),
|
||||
'course_forms' => $this->whenLoaded('courseForms', function () {
|
||||
return $this->courseForms->map(function ($form) {
|
||||
// Keep only fields needed to render the public sign-up form.
|
||||
return [
|
||||
'id' => (int) $form->id,
|
||||
'name' => $form->name,
|
||||
'field' => $form->field,
|
||||
'edit_input' => $form->edit_input,
|
||||
'rule' => $form->rule,
|
||||
'sort' => (int) $form->sort,
|
||||
'help' => $form->help,
|
||||
'select_item' => $form->select_item,
|
||||
'need_fill' => (bool) $form->need_fill,
|
||||
'belong_user' => (bool) $form->belong_user,
|
||||
];
|
||||
})->values()->all();
|
||||
}),
|
||||
'course_signs_count' => $this->when(
|
||||
isset($this->course_signs_count),
|
||||
(int) $this->course_signs_count
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private function safeUpload($upload): ?array
|
||||
{
|
||||
if (!$upload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $upload->id,
|
||||
'url' => $upload->url,
|
||||
];
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-https://wx.sstbc.com}"
|
||||
COURSE_ID="${COURSE_ID:-}"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
request() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local body="$TMP_DIR/$name.json"
|
||||
local status
|
||||
|
||||
status="$(curl -sS --max-time 20 -o "$body" -w '%{http_code}' "$url")"
|
||||
printf '%-28s HTTP %s\n' "$name" "$status"
|
||||
printf '%s\n' "$body"
|
||||
}
|
||||
|
||||
request course-list "$BASE_URL/api/mobile/course/course?page=1&page_size=1"
|
||||
request course-list-page-size-51 "$BASE_URL/api/mobile/course/course?page=1&page_size=51"
|
||||
request course-detail-invalid "$BASE_URL/api/mobile/course/course-detail?course_id=not-an-id"
|
||||
request course-detail-missing "$BASE_URL/api/mobile/course/course-detail?course_id=2147483647"
|
||||
|
||||
scan_sensitive_keys() {
|
||||
local name="$1"
|
||||
local body="$TMP_DIR/$name.json"
|
||||
local forbidden
|
||||
|
||||
forbidden="$(jq -r '.. | objects | keys[]' "$body" | sort -u | rg -i '^(mobile|idcard|id_card|identity_card|remark|admin_id|department_id|teacher_id|deleted_at|password|remember_token)$' || true)"
|
||||
if [[ -n "$forbidden" ]]; then
|
||||
printf '%s FORBIDDEN_KEYS\n%s\n' "$name" "$forbidden"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s SENSITIVE_SCAN PASS\n' "$name"
|
||||
}
|
||||
|
||||
list_body="$TMP_DIR/course-list.json"
|
||||
if jq -e '.data and (.data | type == "array")' "$list_body" >/dev/null; then
|
||||
scan_sensitive_keys course-list
|
||||
if [[ -z "$COURSE_ID" ]]; then
|
||||
COURSE_ID="$(jq -r '.data[0].id // empty' "$list_body")"
|
||||
fi
|
||||
|
||||
if [[ -n "$COURSE_ID" ]]; then
|
||||
request course-detail-valid "$BASE_URL/api/mobile/course/course-detail?course_id=$COURSE_ID"
|
||||
request course-detail-pc-valid "$BASE_URL/api/mobile/course/course-detail-pc?course_id=$COURSE_ID"
|
||||
scan_sensitive_keys course-detail-valid
|
||||
scan_sensitive_keys course-detail-pc-valid
|
||||
jq -e '.id and .name' "$TMP_DIR/course-detail-valid.json" >/dev/null
|
||||
jq -e '.id and .name' "$TMP_DIR/course-detail-pc-valid.json" >/dev/null
|
||||
printf 'DETAIL_CORE_FIELDS PASS\n'
|
||||
else
|
||||
printf 'DETAIL_SCAN SKIP: course list is empty\n'
|
||||
fi
|
||||
else
|
||||
printf 'SENSITIVE_SCAN SKIP: course list did not return a data array\n'
|
||||
fi
|
||||
|
||||
if jq -e '.errcode == 10001' "$TMP_DIR/course-list-page-size-51.json" >/dev/null; then
|
||||
printf 'PAGE_SIZE_VALIDATION PASS\n'
|
||||
else
|
||||
printf 'PAGE_SIZE_VALIDATION FAIL\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(jq -r 'type' "$TMP_DIR/course-detail-invalid.json")" == "object" ]] && jq -e '.errcode == 10001' "$TMP_DIR/course-detail-invalid.json" >/dev/null; then
|
||||
printf 'COURSE_ID_VALIDATION PASS\n'
|
||||
else
|
||||
printf 'COURSE_ID_VALIDATION FAIL\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(jq -r 'type' "$TMP_DIR/course-detail-missing.json")" == "object" ]] && jq -e 'has("message") and .message == "课程不存在"' "$TMP_DIR/course-detail-missing.json" >/dev/null; then
|
||||
printf 'MISSING_COURSE_VALIDATION PASS\n'
|
||||
else
|
||||
printf 'MISSING_COURSE_VALIDATION FAIL\n'
|
||||
exit 1
|
||||
fi
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class MobileCourseEndpointValidationTest extends TestCase
|
||||
{
|
||||
public function test_course_list_rejects_page_size_above_public_limit(): void
|
||||
{
|
||||
$response = $this->getJson('/api/mobile/course/course?page_size=51');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('errcode', 10001);
|
||||
}
|
||||
|
||||
public function test_course_list_rejects_non_public_status_filter(): void
|
||||
{
|
||||
$response = $this->getJson('/api/mobile/course/course?status=0');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('errcode', 10001);
|
||||
}
|
||||
|
||||
public function test_course_detail_rejects_non_integer_course_id(): void
|
||||
{
|
||||
$response = $this->getJson('/api/mobile/course/course-detail?course_id=not-an-id');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('errcode', 10001);
|
||||
}
|
||||
|
||||
public function test_pc_course_detail_rejects_non_integer_course_id(): void
|
||||
{
|
||||
$response = $this->getJson('/api/mobile/course/course-detail-pc?course_id=not-an-id');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('errcode', 10001);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MobileCoursePublicDataTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'database.default' => '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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Http\Resources\Mobile;
|
||||
|
||||
use App\Http\Resources\Mobile\PublicCourseResource;
|
||||
use App\Models\Course;
|
||||
use App\Models\CourseForm;
|
||||
use App\Models\CourseType;
|
||||
use App\Models\Teacher;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PublicCourseResourceTest extends TestCase
|
||||
{
|
||||
public function test_public_resource_returns_only_allowlisted_course_fields(): void
|
||||
{
|
||||
$course = new Course();
|
||||
$course->setRawAttributes([
|
||||
'id' => 150,
|
||||
'name' => '公开课程',
|
||||
'start_date' => '2026-08-01',
|
||||
'end_date' => '2026-08-02',
|
||||
'content' => '公开介绍',
|
||||
'publicize_content' => '宣传介绍',
|
||||
'status' => 1,
|
||||
'course_status' => 20,
|
||||
'sign_status' => 10,
|
||||
'admin_id' => 9,
|
||||
'department_id' => 10,
|
||||
'teacher_id' => 11,
|
||||
'show_mobile' => 1,
|
||||
]);
|
||||
|
||||
$teacher = new Teacher();
|
||||
$teacher->setRawAttributes([
|
||||
'id' => 11,
|
||||
'name' => '张老师',
|
||||
'mobile' => '13800138000',
|
||||
'remark' => '内部备注',
|
||||
]);
|
||||
|
||||
$form = new CourseForm();
|
||||
$form->setRawAttributes([
|
||||
'id' => 1,
|
||||
'name' => '姓名',
|
||||
'field' => 'name',
|
||||
'edit_input' => 'input',
|
||||
'rule' => 'required',
|
||||
'sort' => 1,
|
||||
'help' => '请填写姓名',
|
||||
'select_item' => null,
|
||||
'need_fill' => 1,
|
||||
'belong_user' => 1,
|
||||
'admin_id' => 9,
|
||||
'department_id' => 10,
|
||||
]);
|
||||
|
||||
$type = new CourseType();
|
||||
$type->setRawAttributes(['id' => 3, 'name' => '管理类', 'wait_tip' => '内部提示']);
|
||||
|
||||
$course->setRelation('teacher', $teacher);
|
||||
$course->setRelation('courseForms', new Collection([$form]));
|
||||
$course->setRelation('typeDetail', $type);
|
||||
|
||||
$payload = (new PublicCourseResource($course))->resolve();
|
||||
|
||||
$this->assertSame(150, $payload['id']);
|
||||
$this->assertSame('公开课程', $payload['name']);
|
||||
$this->assertSame('张老师', $payload['teacher']['name']);
|
||||
$this->assertArrayNotHasKey('mobile', $payload['teacher']);
|
||||
$this->assertArrayNotHasKey('remark', $payload['teacher']);
|
||||
$this->assertArrayNotHasKey('admin_id', $payload);
|
||||
$this->assertArrayNotHasKey('department_id', $payload);
|
||||
$this->assertArrayNotHasKey('teacher_id', $payload);
|
||||
$this->assertArrayNotHasKey('wait_tip', $payload['type_detail']);
|
||||
$this->assertArrayNotHasKey('admin_id', $payload['course_forms'][0]);
|
||||
$this->assertArrayNotHasKey('department_id', $payload['course_forms'][0]);
|
||||
$this->assertArrayNotHasKey('deleted_at', $payload['course_forms'][0]);
|
||||
}
|
||||
|
||||
public function test_public_resource_does_not_include_model_appends(): void
|
||||
{
|
||||
$course = new Course();
|
||||
$course->setRawAttributes([
|
||||
'id' => 1,
|
||||
'name' => '公开课程',
|
||||
'status' => 1,
|
||||
'course_status' => 20,
|
||||
'sign_status' => 10,
|
||||
]);
|
||||
|
||||
$payload = (new PublicCourseResource($course))->resolve();
|
||||
|
||||
$this->assertArrayNotHasKey('teacher_detail', $payload);
|
||||
$this->assertArrayNotHasKey('show_mobile_text', $payload);
|
||||
$this->assertSame([], $payload['publicize']);
|
||||
$this->assertArrayNotHasKey('qrcode', $payload);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue