php-tenant/tests/Feature/EntitlementApiTest.php
Snider d0ad2737cb refactor: rename namespace from Core\Mod\Tenant to Core\Tenant
Simplifies the namespace hierarchy by removing the intermediate Mod
segment. Updates all 118 files including models, services, controllers,
middleware, tests, and composer.json autoload configuration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:30:46 +00:00

251 lines
8.8 KiB
PHP

<?php
use Core\Tenant\Models\Feature;
use Core\Tenant\Models\Package;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Support\Facades\Cache;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
beforeEach(function () {
Cache::flush();
// Create test user with API token capability
$this->user = User::factory()->create();
$this->workspace = Workspace::factory()->create();
$this->workspace->users()->attach($this->user->id, [
'role' => 'owner',
'is_default' => true,
]);
// Create features
$this->socialAccountsFeature = Feature::create([
'code' => 'social.accounts',
'name' => 'Social Accounts',
'description' => 'Connected social accounts',
'category' => 'social',
'type' => Feature::TYPE_LIMIT,
'reset_type' => Feature::RESET_NONE,
'is_active' => true,
]);
$this->socialPostsFeature = Feature::create([
'code' => 'social.posts.scheduled',
'name' => 'Scheduled Posts',
'description' => 'Monthly scheduled posts',
'category' => 'social',
'type' => Feature::TYPE_LIMIT,
'reset_type' => Feature::RESET_MONTHLY,
'is_active' => true,
]);
// Create package
$this->creatorPackage = Package::create([
'code' => 'social-creator',
'name' => 'SocialHost Creator',
'description' => 'For individual creators',
'is_stackable' => false,
'is_base_package' => true,
'is_active' => true,
]);
$this->creatorPackage->features()->attach($this->socialAccountsFeature->id, ['limit_value' => 5]);
$this->creatorPackage->features()->attach($this->socialPostsFeature->id, ['limit_value' => 30]);
$this->service = app(EntitlementService::class);
});
describe('Entitlement API', function () {
describe('GET /api/v1/entitlements/check', function () {
it('requires authentication', function () {
$response = $this->getJson('/api/v1/entitlements/check?email='.$this->user->email.'&feature=social.accounts');
$response->assertStatus(401);
});
it('returns 404 for non-existent user', function () {
$this->actingAs($this->user);
$response = $this->getJson('/api/v1/entitlements/check?email=nonexistent@example.com&feature=social.accounts');
$response->assertStatus(404)
->assertJson([
'allowed' => false,
'reason' => 'User not found',
]);
});
it('returns 404 when user has no workspace', function () {
$this->actingAs($this->user);
$this->workspace->users()->detach($this->user->id);
$response = $this->getJson('/api/v1/entitlements/check?email='.$this->user->email.'&feature=social.accounts');
$response->assertStatus(404)
->assertJson([
'allowed' => false,
'reason' => 'No workspace found for user',
]);
});
it('denies when user has no package', function () {
$this->actingAs($this->user);
$response = $this->getJson('/api/v1/entitlements/check?email='.$this->user->email.'&feature=social.accounts');
$response->assertStatus(200)
->assertJson([
'allowed' => false,
'feature_code' => 'social.accounts',
]);
});
it('allows when user has package with feature', function () {
$this->actingAs($this->user);
$this->service->provisionPackage($this->workspace, 'social-creator');
$response = $this->getJson('/api/v1/entitlements/check?email='.$this->user->email.'&feature=social.accounts');
$response->assertStatus(200)
->assertJson([
'allowed' => true,
'limit' => 5,
'used' => 0,
'remaining' => 5,
'unlimited' => false,
'feature_code' => 'social.accounts',
'workspace_id' => $this->workspace->id,
]);
});
it('respects quantity parameter', function () {
$this->actingAs($this->user);
$this->service->provisionPackage($this->workspace, 'social-creator');
// Use 4 of 5 allowed
$this->service->recordUsage($this->workspace, 'social.accounts', quantity: 4);
Cache::flush();
// Request 2 more (exceeds remaining)
$response = $this->getJson('/api/v1/entitlements/check?email='.$this->user->email.'&feature=social.accounts&quantity=2');
$response->assertStatus(200)
->assertJson([
'allowed' => false,
'remaining' => 1,
]);
});
});
describe('POST /api/v1/entitlements/usage', function () {
it('requires authentication', function () {
$response = $this->postJson('/api/v1/entitlements/usage', [
'email' => $this->user->email,
'feature' => 'social.posts.scheduled',
]);
$response->assertStatus(401);
});
it('records usage successfully', function () {
$this->actingAs($this->user);
$this->service->provisionPackage($this->workspace, 'social-creator');
$response = $this->postJson('/api/v1/entitlements/usage', [
'email' => $this->user->email,
'feature' => 'social.posts.scheduled',
'quantity' => 3,
]);
$response->assertStatus(201)
->assertJson([
'success' => true,
'feature_code' => 'social.posts.scheduled',
'quantity' => 3,
]);
// Verify usage was recorded
Cache::flush();
$result = $this->service->can($this->workspace, 'social.posts.scheduled');
expect($result->used)->toBe(3);
});
it('records usage with metadata', function () {
$this->actingAs($this->user);
$this->service->provisionPackage($this->workspace, 'social-creator');
$response = $this->postJson('/api/v1/entitlements/usage', [
'email' => $this->user->email,
'feature' => 'social.posts.scheduled',
'metadata' => ['source' => 'biohost', 'post_id' => 'abc123'],
]);
$response->assertStatus(201)
->assertJson([
'success' => true,
]);
});
});
describe('GET /api/v1/entitlements/summary', function () {
it('requires authentication', function () {
$response = $this->getJson('/api/v1/entitlements/summary');
$response->assertStatus(401);
});
it('returns summary for authenticated user', function () {
$this->actingAs($this->user);
$this->service->provisionPackage($this->workspace, 'social-creator');
$response = $this->getJson('/api/v1/entitlements/summary');
$response->assertStatus(200)
->assertJsonStructure([
'workspace_id',
'packages',
'features' => [
'social' => [
'*' => ['code', 'name', 'limit', 'used', 'remaining', 'unlimited', 'percentage'],
],
],
'boosts',
]);
});
it('includes package information', function () {
$this->actingAs($this->user);
$this->service->provisionPackage($this->workspace, 'social-creator');
$response = $this->getJson('/api/v1/entitlements/summary');
$response->assertStatus(200);
$packages = $response->json('packages');
expect($packages)->toHaveCount(1);
expect($packages[0]['code'])->toBe('social-creator');
});
});
describe('GET /api/v1/entitlements/summary/{workspace}', function () {
it('requires authentication', function () {
$response = $this->getJson('/api/v1/entitlements/summary/'.$this->workspace->id);
$response->assertStatus(401);
});
it('returns summary for specified workspace', function () {
$this->actingAs($this->user);
$this->service->provisionPackage($this->workspace, 'social-creator');
$response = $this->getJson('/api/v1/entitlements/summary/'.$this->workspace->id);
$response->assertStatus(200)
->assertJson([
'workspace_id' => $this->workspace->id,
]);
});
});
});