php-tenant/tests/Feature/UsageAlertServiceTest.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

261 lines
8.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Tenant\Tests\Feature;
use Core\Tenant\Models\Feature;
use Core\Tenant\Models\Package;
use Core\Tenant\Models\UsageAlertHistory;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Notifications\UsageAlertNotification;
use Core\Tenant\Services\EntitlementService;
use Core\Tenant\Services\UsageAlertService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
class UsageAlertServiceTest extends TestCase
{
use RefreshDatabase;
protected UsageAlertService $alertService;
protected EntitlementService $entitlementService;
protected function setUp(): void
{
parent::setUp();
$this->entitlementService = app(EntitlementService::class);
$this->alertService = app(UsageAlertService::class);
}
public function test_it_sends_warning_alert_at_80_percent(): void
{
Notification::fake();
// Create feature with limit
$feature = Feature::factory()->create([
'code' => 'test.feature',
'name' => 'Test Feature',
'type' => Feature::TYPE_LIMIT,
]);
// Create package with limit of 10
$package = Package::factory()->create(['code' => 'test-package', 'is_base_package' => true]);
$package->features()->attach($feature->id, ['limit_value' => 10]);
// Create workspace with owner
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, ['role' => 'owner']);
// Provision package
$this->entitlementService->provisionPackage($workspace, 'test-package');
// Record 8 uses (80%)
for ($i = 0; $i < 8; $i++) {
$this->entitlementService->recordUsage($workspace, 'test.feature', 1);
}
// Check for alerts
$result = $this->alertService->checkWorkspace($workspace);
// Should send one alert
$this->assertEquals(1, $result['alerts_sent']);
// Notification should be sent to owner
Notification::assertSentTo(
$user,
UsageAlertNotification::class,
fn ($notification) => $notification->threshold === UsageAlertHistory::THRESHOLD_WARNING
);
// Alert should be recorded
$this->assertDatabaseHas('entitlement_usage_alert_history', [
'workspace_id' => $workspace->id,
'feature_code' => 'test.feature',
'threshold' => 80,
]);
}
public function test_it_does_not_send_duplicate_alerts(): void
{
Notification::fake();
$feature = Feature::factory()->create([
'code' => 'test.feature',
'name' => 'Test Feature',
'type' => Feature::TYPE_LIMIT,
]);
$package = Package::factory()->create(['code' => 'test-package', 'is_base_package' => true]);
$package->features()->attach($feature->id, ['limit_value' => 10]);
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, ['role' => 'owner']);
$this->entitlementService->provisionPackage($workspace, 'test-package');
// Record 8 uses (80%)
for ($i = 0; $i < 8; $i++) {
$this->entitlementService->recordUsage($workspace, 'test.feature', 1);
}
// First check - should send alert
$result1 = $this->alertService->checkWorkspace($workspace);
$this->assertEquals(1, $result1['alerts_sent']);
// Second check - should NOT send duplicate
$result2 = $this->alertService->checkWorkspace($workspace);
$this->assertEquals(0, $result2['alerts_sent']);
// Only one notification should be sent
Notification::assertSentToTimes($user, UsageAlertNotification::class, 1);
}
public function test_it_sends_escalating_alerts_at_different_thresholds(): void
{
Notification::fake();
$feature = Feature::factory()->create([
'code' => 'test.feature',
'name' => 'Test Feature',
'type' => Feature::TYPE_LIMIT,
]);
$package = Package::factory()->create(['code' => 'test-package', 'is_base_package' => true]);
$package->features()->attach($feature->id, ['limit_value' => 10]);
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, ['role' => 'owner']);
$this->entitlementService->provisionPackage($workspace, 'test-package');
// Record 8 uses (80%) - warning
for ($i = 0; $i < 8; $i++) {
$this->entitlementService->recordUsage($workspace, 'test.feature', 1);
}
$this->alertService->checkWorkspace($workspace);
// Record 1 more (90%) - critical
$this->entitlementService->recordUsage($workspace, 'test.feature', 1);
$result = $this->alertService->checkWorkspace($workspace);
$this->assertEquals(1, $result['alerts_sent']);
// Record 1 more (100%) - limit reached
$this->entitlementService->recordUsage($workspace, 'test.feature', 1);
$result = $this->alertService->checkWorkspace($workspace);
$this->assertEquals(1, $result['alerts_sent']);
// Should have 3 notifications total
Notification::assertSentToTimes($user, UsageAlertNotification::class, 3);
}
public function test_it_resolves_alerts_when_usage_drops(): void
{
$feature = Feature::factory()->create([
'code' => 'test.feature',
'name' => 'Test Feature',
'type' => Feature::TYPE_LIMIT,
'reset_type' => Feature::RESET_NONE,
]);
$workspace = Workspace::factory()->create();
// Create an unresolved alert
UsageAlertHistory::record(
workspaceId: $workspace->id,
featureCode: 'test.feature',
threshold: 80,
metadata: ['used' => 8, 'limit' => 10]
);
$this->assertDatabaseHas('entitlement_usage_alert_history', [
'workspace_id' => $workspace->id,
'feature_code' => 'test.feature',
'resolved_at' => null,
]);
// Resolve alerts
$resolved = UsageAlertHistory::resolveAllForFeature($workspace->id, 'test.feature');
$this->assertEquals(1, $resolved);
$this->assertDatabaseMissing('entitlement_usage_alert_history', [
'workspace_id' => $workspace->id,
'feature_code' => 'test.feature',
'resolved_at' => null,
]);
}
public function test_it_skips_unlimited_features(): void
{
Notification::fake();
$feature = Feature::factory()->create([
'code' => 'unlimited.feature',
'name' => 'Unlimited Feature',
'type' => Feature::TYPE_UNLIMITED,
]);
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, ['role' => 'owner']);
$result = $this->alertService->checkFeatureUsage($workspace, $feature);
$this->assertFalse($result['alert_sent']);
Notification::assertNothingSent();
}
public function test_it_skips_boolean_features(): void
{
Notification::fake();
$feature = Feature::factory()->create([
'code' => 'boolean.feature',
'name' => 'Boolean Feature',
'type' => Feature::TYPE_BOOLEAN,
]);
$user = User::factory()->create();
$workspace = Workspace::factory()->create();
$workspace->users()->attach($user->id, ['role' => 'owner']);
// Boolean features should be skipped by the service
// since they don't have limits to check against
$result = $this->alertService->checkFeatureUsage($workspace, $feature);
$this->assertFalse($result['alert_sent']);
Notification::assertNothingSent();
}
public function test_get_active_alerts_returns_unresolved_only(): void
{
$workspace = Workspace::factory()->create();
// Create resolved alert
$resolved = UsageAlertHistory::record(
workspaceId: $workspace->id,
featureCode: 'feature.a',
threshold: 80
);
$resolved->resolve();
// Create unresolved alert
UsageAlertHistory::record(
workspaceId: $workspace->id,
featureCode: 'feature.b',
threshold: 90
);
$activeAlerts = $this->alertService->getActiveAlertsForWorkspace($workspace);
$this->assertCount(1, $activeAlerts);
$this->assertEquals('feature.b', $activeAlerts->first()->feature_code);
}
}