244 lines
9.1 KiB
PHP
244 lines
9.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Domain\Blog\AccessDecision;
|
|
use App\Domain\Blog\ArticleAccess;
|
|
use App\Domain\Plugin\Hook;
|
|
use App\Domain\Plugin\PluginManager;
|
|
use App\Models\Article;
|
|
use App\Models\Category;
|
|
use App\Models\Plugin;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Plugins\Larablog\Membership\Database\Seeders\MembershipPlanSeeder;
|
|
use Plugins\Larablog\Membership\Domain\MembershipService;
|
|
use Plugins\Larablog\Membership\Models\ArticleMembership;
|
|
use Plugins\Larablog\Membership\Models\MembershipPlan;
|
|
use Plugins\Larablog\Membership\PluginServiceProvider as MembershipProvider;
|
|
use Plugins\Larablog\PaidContent\PluginServiceProvider as PaidContentProvider;
|
|
use Plugins\Larablog\Payment\Domain\OrderService;
|
|
use Plugins\Larablog\Payment\Domain\ProductType;
|
|
use Plugins\Larablog\Payment\Models\Entitlement;
|
|
use Plugins\Larablog\Payment\PluginServiceProvider as PaymentProvider;
|
|
use RuntimeException;
|
|
use Tests\TestCase;
|
|
|
|
class MembershipCommerceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Hook::flush();
|
|
|
|
$this->artisan('migrate', ['--path' => 'plugins/larablog/payment/database/migrations']);
|
|
$this->artisan('migrate', ['--path' => 'plugins/larablog/paid-content/database/migrations']);
|
|
$this->artisan('migrate', ['--path' => 'plugins/larablog/membership/database/migrations']);
|
|
|
|
app()->register(PaymentProvider::class);
|
|
app()->register(PaidContentProvider::class);
|
|
app()->register(MembershipProvider::class);
|
|
|
|
(new MembershipPlanSeeder)->run();
|
|
}
|
|
|
|
public function test_membership_requires_payment_plugin(): void
|
|
{
|
|
$manager = app(PluginManager::class);
|
|
$manager->syncDiscoveredPlugins();
|
|
|
|
Plugin::query()->where('name', 'larablog/payment')->update(['enabled' => false]);
|
|
Plugin::query()->where('name', 'larablog/membership')->update(['enabled' => false]);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$manager->enable('larablog/membership');
|
|
}
|
|
|
|
public function test_subscribe_monthly_sets_expires_at_and_unlocks_article(): void
|
|
{
|
|
$manager = app(PluginManager::class);
|
|
$manager->syncDiscoveredPlugins();
|
|
$manager->enable('larablog/payment');
|
|
$manager->enable('larablog/membership');
|
|
|
|
$user = User::factory()->create();
|
|
[$article] = $this->makeArticle([
|
|
'content' => str_repeat('secret ', 40).'MEMBER_FULL_TOKEN',
|
|
]);
|
|
|
|
ArticleMembership::query()->create([
|
|
'article_id' => $article->id,
|
|
'enabled' => true,
|
|
'required_plan_id' => null,
|
|
]);
|
|
|
|
$this->get('/show-'.$article->id.'.shtml')
|
|
->assertOk()
|
|
->assertDontSee('MEMBER_FULL_TOKEN');
|
|
|
|
$plan = MembershipPlan::query()->where('slug', 'monthly')->firstOrFail();
|
|
$orders = app(OrderService::class);
|
|
$order = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $plan->id, $plan->name, (string) $plan->price);
|
|
$orders->markPaid($order);
|
|
|
|
$status = app(MembershipService::class)->statusFor($user);
|
|
$this->assertTrue($status['active']);
|
|
$this->assertSame('monthly', $status['plan_slug']);
|
|
$this->assertNotNull($status['expires_at']);
|
|
|
|
$this->actingAs($user)
|
|
->get('/show-'.$article->id.'.shtml')
|
|
->assertOk()
|
|
->assertSee('MEMBER_FULL_TOKEN');
|
|
|
|
$this->actingAs($user)
|
|
->getJson('/plugins/membership/status')
|
|
->assertOk()
|
|
->assertJsonPath('active', true)
|
|
->assertJsonPath('plan_slug', 'monthly');
|
|
}
|
|
|
|
public function test_expired_membership_can_renew(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$plan = MembershipPlan::query()->where('slug', 'monthly')->firstOrFail();
|
|
$orders = app(OrderService::class);
|
|
|
|
$first = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $plan->id, $plan->name, (string) $plan->price);
|
|
$orders->markPaid($first);
|
|
|
|
Entitlement::query()
|
|
->where('user_id', $user->id)
|
|
->where('product_type', ProductType::MEMBERSHIP)
|
|
->where('product_id', $plan->id)
|
|
->update(['expires_at' => now()->subDay()]);
|
|
|
|
$this->assertFalse($orders->hasEntitlement((int) $user->id, ProductType::MEMBERSHIP, (int) $plan->id));
|
|
|
|
$second = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $plan->id, $plan->name, (string) $plan->price);
|
|
$orders->markPaid($second);
|
|
|
|
$this->assertTrue($orders->hasEntitlement((int) $user->id, ProductType::MEMBERSHIP, (int) $plan->id));
|
|
$entitlement = Entitlement::query()
|
|
->where('user_id', $user->id)
|
|
->where('product_type', ProductType::MEMBERSHIP)
|
|
->where('product_id', $plan->id)
|
|
->firstOrFail();
|
|
$this->assertTrue($entitlement->expires_at?->isFuture());
|
|
}
|
|
|
|
public function test_required_plan_gate_rejects_other_plan(): void
|
|
{
|
|
$manager = app(PluginManager::class);
|
|
$manager->syncDiscoveredPlugins();
|
|
$manager->enable('larablog/payment');
|
|
$manager->enable('larablog/membership');
|
|
|
|
$user = User::factory()->create();
|
|
[$article] = $this->makeArticle(['content' => 'NEED_LIFETIME_TOKEN '.str_repeat('x ', 30)]);
|
|
$monthly = MembershipPlan::query()->where('slug', 'monthly')->firstOrFail();
|
|
$lifetime = MembershipPlan::query()->where('slug', 'lifetime')->firstOrFail();
|
|
|
|
ArticleMembership::query()->create([
|
|
'article_id' => $article->id,
|
|
'enabled' => true,
|
|
'required_plan_id' => $lifetime->id,
|
|
]);
|
|
|
|
$orders = app(OrderService::class);
|
|
$order = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $monthly->id, $monthly->name, (string) $monthly->price);
|
|
$orders->markPaid($order);
|
|
|
|
$decision = app(ArticleAccess::class)->resolve($article, $user);
|
|
$this->assertSame(AccessDecision::NEED_PURCHASE, $decision->status);
|
|
|
|
$lifeOrder = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $lifetime->id, $lifetime->name, (string) $lifetime->price);
|
|
$orders->markPaid($lifeOrder);
|
|
|
|
$this->assertTrue(app(ArticleAccess::class)->resolve($article, $user)->isAllow());
|
|
}
|
|
|
|
public function test_forge_membership_amount_query_is_rejected(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$plan = MembershipPlan::query()->where('slug', 'monthly')->firstOrFail();
|
|
|
|
$this->actingAs($user)
|
|
->get('/plugins/payment/checkout?'.http_build_query([
|
|
'product_type' => ProductType::MEMBERSHIP,
|
|
'product_id' => $plan->id,
|
|
'amount' => '0.01',
|
|
'title' => 'hack',
|
|
]))
|
|
->assertRedirect();
|
|
|
|
// Server-side price must be used; after redirect order amount is plan price.
|
|
$this->assertDatabaseHas('orders', [
|
|
'user_id' => $user->id,
|
|
'amount' => '9.90',
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get('/plugins/payment/checkout?'.http_build_query([
|
|
'product_type' => 'theme',
|
|
'product_id' => 1,
|
|
'amount' => '0.01',
|
|
'title' => 'hack',
|
|
]))
|
|
->assertStatus(422);
|
|
}
|
|
|
|
public function test_mutex_validate_hook_rejects_paid_and_membership(): void
|
|
{
|
|
$data = [
|
|
'read_password' => null,
|
|
'paid_content' => ['enabled' => true, 'price' => 1],
|
|
'membership' => ['enabled' => true],
|
|
];
|
|
|
|
$this->expectException(ValidationException::class);
|
|
Hook::filter('filament.article.validate_access_restrictions', $data, null);
|
|
}
|
|
|
|
public function test_cannot_delete_plan_with_entitlement(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$plan = MembershipPlan::query()->where('slug', 'monthly')->firstOrFail();
|
|
$orders = app(OrderService::class);
|
|
$order = $orders->createOrder($user, ProductType::MEMBERSHIP, (int) $plan->id, $plan->name, (string) $plan->price);
|
|
$orders->markPaid($order);
|
|
|
|
$this->assertTrue($plan->fresh()->hasEntitlements());
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
* @return array{0: Article, 1: User, 2: Category}
|
|
*/
|
|
protected function makeArticle(array $overrides = []): array
|
|
{
|
|
$user = isset($overrides['user_id'])
|
|
? User::query()->findOrFail($overrides['user_id'])
|
|
: User::factory()->create();
|
|
$category = Category::query()->create(['name' => 'Mem', 'display_order' => 0]);
|
|
|
|
$article = Article::query()->create(array_merge([
|
|
'category_id' => $category->id,
|
|
'user_id' => $user->id,
|
|
'title' => 'Members post',
|
|
'content' => 'Hello',
|
|
'content_format' => 'markdown',
|
|
'published_at' => now()->subMinute(),
|
|
'visible' => true,
|
|
], $overrides));
|
|
|
|
return [$article, $user, $category];
|
|
}
|
|
}
|