wip: article AI polish, category SEO fields, cover generator, membership plan seeder
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<?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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user