Files
projectsend/tests/Unit/CapabilityRegistryTest.php
T
ignacionelson 553f5fd2bf Declare the capability a managed installation's staff seats hang off
Cloud instances are sold seats rather than administering them, so the
tenant's own /users screens stay closed — capability:users.manage is
already Community-only — and a control plane creates, deactivates and
password-resets staff from outside. This is the key that plane gates on.

Only the declaration lives here, the same division StorageManaged and
Branding already use. Everything behind it is a module in the private
cloud-modules package.

Declared before that module exists, deliberately. A capability added
after a release is invisible to every image built from one, and that is
not hypothetical: StorageManaged landed 36 commits after v2.1.0 and has
never shipped, so a fleet with buckets provisioned, credentials scoped
and eight environment variables in place still writes every upload to
local disk — because the gate is here and the gate never left. Declaring
this one now is refusing to make the same mistake twice.

The seat *number* deliberately does not live here. There are no billing
or plan tiers in this application to key off, which is the reason
config/api.php gives for not inventing an installation-level rate limit,
and it holds for the same reason: the number lives where the plans do.
This capability says only who is in charge.

ModuleBoundaryTest grows the other half of its own rule. It filtered on
`api/v1/`, so a package claiming a route anywhere else passed — not
because that was sanctioned, but because nothing was looking, and
/platform/v1 is about to be somewhere else. What it polices now is
machine surfaces, the roots something other than a browser authenticates
to, with api/v1/modules and platform/v1 as the two sanctioned prefixes.

Written twice, because the first version was wrong in a useful way: it
policed every route and immediately caught community-modules' Custom
Assets screens. Those are a module doing exactly what a module is for,
through the host's session and capability middleware in plain sight, and
listing them would be the hardcoded URI list the test above it explains
it is avoiding. Web screens are not the boundary; trusted perimeters are.

Verified by making it fail: a package controller on platform/v2 is caught
and named.
2026-08-27 00:28:08 -03:00

57 lines
2.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Modules\Platform\Capabilities\Capability;
use App\Modules\Platform\Capabilities\CapabilityRegistry;
use App\Modules\Platform\Capabilities\Edition;
test('every capability declares at least one edition', function () {
foreach (Capability::cases() as $capability) {
expect($capability->editions())->not->toBeEmpty();
}
});
test('community edition has the self-management capabilities and no cloud exclusives', function () {
$registry = new CapabilityRegistry(Edition::Community);
expect($registry->has(Capability::UsersManage))->toBeTrue()
->and($registry->has(Capability::StorageConfigure))->toBeTrue()
->and($registry->has(Capability::EmailTransportConfigure))->toBeTrue()
->and($registry->has(Capability::SystemUpdates))->toBeTrue()
->and($registry->has(Capability::SchedulerMonitoring))->toBeTrue()
->and($registry->has(Capability::CustomAssets))->toBeTrue()
->and($registry->has(Capability::Branding))->toBeFalse()
// The counterpart of StorageConfigure above: a self-hosted install
// configures its own bucket and is never handed one.
->and($registry->has(Capability::StorageManaged))->toBeFalse();
});
test('cloud edition has cloud exclusives and none of the community-only capabilities', function () {
$registry = new CapabilityRegistry(Edition::Cloud);
expect($registry->has(Capability::Branding))->toBeTrue()
->and($registry->has(Capability::PlatformManaged))->toBeTrue()
->and($registry->has(Capability::StorageManaged))->toBeTrue()
->and($registry->has(Capability::UsersManage))->toBeFalse()
->and($registry->has(Capability::StorageConfigure))->toBeFalse()
->and($registry->has(Capability::EmailTransportConfigure))->toBeFalse()
->and($registry->has(Capability::SystemUpdates))->toBeFalse()
->and($registry->has(Capability::SchedulerMonitoring))->toBeFalse()
->and($registry->has(Capability::CustomAssets))->toBeFalse();
});
test('enabledKeys returns the string keys of enabled capabilities', function () {
$registry = new CapabilityRegistry(Edition::Cloud);
// Order follows the enum, which is the order the control plane reads
// them in — see GET /platform/v1/status in cloud-modules.
expect($registry->enabledKeys())->toBe([
'branding.customize',
'storage.managed',
'captcha.managed_keys',
'platform.managed',
'ai.connector',
]);
});