seed(); Setting::set('pay_sandbox', '1'); $this->user = User::create([ 'name' => '测试会员', 'email' => 'member@test.com', 'password' => bcrypt('password'), ]); $this->plan = MembershipPlan::create([ 'name' => '月付会员', 'slug' => 'monthly', 'price' => 9900, 'duration_days' => 30, 'permissions' => ['read_members_only'], 'active' => true, ]); } public function test_membership_page_lists_plans(): void { $this->get('/membership')->assertOk()->assertSee('月付会员'); } public function test_subscribe_creates_payment_and_redirects(): void { $this->actingAs($this->user) ->post('/membership/'.$this->plan->id.'/subscribe') ->assertRedirect(); $payment = Payment::query()->where('user_id', $this->user->id)->first(); $this->assertNotNull($payment); $this->assertSame('pending', $payment->status); $this->assertTrue($payment->payable->is($this->plan)); $this->assertNull($payment->description); } public function test_sandbox_payment_activates_subscription(): void { $this->actingAs($this->user)->post('/membership/'.$this->plan->id.'/subscribe'); $payment = Payment::query()->where('user_id', $this->user->id)->first(); $this->actingAs($this->user)->get('/pay/checkout/'.$payment->id)->assertRedirect(); $this->actingAs($this->user)->post('/pay/sandbox/'.$payment->order_no.'/confirm')->assertRedirect(); $payment->refresh(); $this->assertSame('paid', $payment->status); $subscription = Subscription::query()->where('user_id', $this->user->id)->first(); $this->assertNotNull($subscription); $this->assertTrue($subscription->isActive()); } public function test_paid_content_hidden_for_guest_and_visible_for_member(): void { $post = Post::create([ 'title' => '付费文章', 'slug' => 'paid-post', 'content' => "公开内容\n\n[paid]\n付费隐藏内容\n[/paid]", 'content_format' => 'markdown', 'status' => 'published', 'published_at' => now(), ]); // 游客:付费部分被替换为 teaser,且不出现裸 [paid] 标签 $this->get('/posts/paid-post.shtml') ->assertOk() ->assertDontSee('付费隐藏内容') ->assertDontSee('[paid]') ->assertSee('本文包含付费内容') ->assertSee('登录后查看'); // 会员:可见,且 [paid] 标签被替换为付费内容块(不裸露) $this->actingAs($this->user)->post('/membership/'.$this->plan->id.'/subscribe'); $payment = Payment::query()->where('user_id', $this->user->id)->first(); $this->actingAs($this->user)->post('/pay/sandbox/'.$payment->order_no.'/confirm'); $this->actingAs($this->user)->get('/posts/paid-post.shtml') ->assertOk() ->assertSee('付费隐藏内容') ->assertSee('paid-content') ->assertDontSee('[paid]'); } public function test_unlock_post_creates_payment_with_payable_and_grants_access(): void { $post = Post::create([ 'title' => '单篇付费', 'slug' => 'single-paid', 'content' => '付费内容正文', 'content_format' => 'markdown', 'status' => 'published', 'published_at' => now(), // members_only:非会员被拦截,可单篇支付解锁 'meta' => ['price' => 500, 'members_only' => true], ]); $this->actingAs($this->user) ->post('/posts/'.$post->id.'/unlock') ->assertRedirect(); $payment = Payment::query()->where('user_id', $this->user->id)->first(); $this->assertNotNull($payment); $this->assertTrue($payment->payable->is($post)); $this->assertSame(500, $payment->amount); $this->assertStringContainsString('单篇付费', $payment->payable_label); $this->assertNotNull($payment->payable_url); // 沙箱支付后写入解锁用户 $this->actingAs($this->user)->post('/pay/sandbox/'.$payment->order_no.'/confirm'); $post->refresh(); $this->assertContains($this->user->id, $post->meta['unlocked_user_ids']); } public function test_legacy_order_without_payable_shows_placeholder(): void { $payment = Payment::create([ 'order_no' => 'LEGACY0001', 'user_id' => $this->user->id, 'subject' => '历史订单', 'amount' => 100, 'status' => 'paid', 'paid_at' => now(), ]); $this->assertSame('—', $payment->payable_label); $this->assertNull($payment->payable_url); } public function test_unlock_post_skips_payment_when_already_unlocked(): void { $post = Post::create([ 'title' => '已解锁', 'slug' => 'already-unlocked', 'content' => '内容', 'content_format' => 'markdown', 'status' => 'published', 'published_at' => now(), 'meta' => ['price' => 500, 'unlocked_user_ids' => [$this->user->id]], ]); $this->actingAs($this->user) ->post('/posts/'.$post->id.'/unlock') ->assertRedirect('/posts/already-unlocked.shtml'); $this->assertSame(0, Payment::query()->count()); } public function test_subscribe_skips_payment_when_plan_already_active(): void { Subscription::create([ 'user_id' => $this->user->id, 'membership_plan_id' => $this->plan->id, 'status' => 'active', 'starts_at' => now(), 'ends_at' => now()->addDays(30), ]); $this->actingAs($this->user) ->post('/membership/'.$this->plan->id.'/subscribe') ->assertRedirect(route('membership.mine')); $this->assertSame(0, Payment::query()->count()); } public function test_unlock_paid_block_creates_payment_for_non_member(): void { $post = Post::create([ 'title' => '付费块文章', 'slug' => 'paid-block-unlock', 'content' => "开头\n\n[paid]\n隐藏付费块\n[/paid]\n\n结尾", 'content_format' => 'markdown', 'status' => 'published', 'published_at' => now(), 'meta' => ['price' => 500], ]); // 付费块文章的整篇公开,但 [paid] 块不可读——解锁必须创建订单,不能误判为"已有权限" $this->actingAs($this->user) ->post('/posts/'.$post->id.'/unlock') ->assertRedirect(route('pay.checkout', Payment::first())); $payment = Payment::query()->where('user_id', $this->user->id)->first(); $this->assertNotNull($payment); $this->assertTrue($payment->payable->is($post)); $this->assertSame('pending', $payment->status); // 沙箱支付后 [paid] 块可见 $this->actingAs($this->user)->post('/pay/sandbox/'.$payment->order_no.'/confirm'); $this->actingAs($this->user)->get('/posts/paid-block-unlock.shtml') ->assertOk() ->assertSee('隐藏付费块'); } public function test_rendered_paid_content_never_leaks_raw_tags(): void { $post = Post::create([ 'title' => '渲染防泄漏', 'slug' => 'render-no-leak', 'content' => "公开\n\n[paid]\n隐藏内容\n[/paid]", 'content_format' => 'markdown', 'status' => 'published', 'published_at' => now(), ]); $html = app(\App\Blog\Services\PostContentRenderer::class)->render($post); $this->assertStringNotContainsString('[paid]', $html); $this->assertStringNotContainsString('[/paid]', $html); } public function test_members_only_post_shows_paywall_to_guest(): void { $post = Post::create([ 'title' => '会员专享文章', 'slug' => 'members-only-post', 'content' => '这是会员才能看到的正文内容', 'content_format' => 'markdown', 'status' => 'published', 'published_at' => now(), 'meta' => ['members_only' => true, 'price' => 500], ]); $this->get('/posts/members-only-post.shtml') ->assertOk() ->assertSee('登录后解锁') ->assertSee('单篇解锁') ->assertSee('会员专享文章') ->assertDontSee('这是会员才能看到的正文内容'); } public function test_members_only_post_paywall_shows_unlock_buttons_to_user(): void { $post = Post::create([ 'title' => '会员专享文章2', 'slug' => 'members-only-post-2', 'content' => '正文内容不可见', 'content_format' => 'markdown', 'status' => 'published', 'published_at' => now(), 'meta' => ['members_only' => true, 'price' => 9900], ]); $this->actingAs($this->user)->get('/posts/members-only-post-2.shtml') ->assertOk() ->assertSee('单篇解锁') ->assertSee('开通会员') ->assertDontSee('正文内容不可见'); } }