Files
projectsend/tests/Unit/CapabilityRegistryTest.php
T
ignacionelson 91d34b204c Let something other than a browser session identify itself to the audit log
An actor with no personal access token has always meant a browser, and
for as long as a session and a Sanctum token were the only two ways to
authenticate, that was true. It stops being true the moment anything else
can, and the failure is silent: the action gets recorded as a person
clicking, in the one table whose whole purpose is answering "did I do
that, or did something acting for me?"

Nothing misreports today — every call site that passes an explicit actor
is a browser request, an API request whose actor carries the token, or a
console command with no actor at all. This closes the trap before the AI
connector in cloud-modules walks into it.

ActivityOrigin is a closed enum, so core has to publish both the case and
the hook before a package can use either. ResolvingActivityOrigin is
asked only in the ambiguous case: a request carrying a token is the API
and a request with nobody signed in is public or system, and neither is
in any doubt, so neither is offered — one package must not be able to
quietly relabel how every integration's actions are attributed.

The person stays the actor. They authorised it, and a log naming the
assistant instead would lose the only fact that matters. What the
connector was called goes in api_token_name, beside a null token id,
because that column means a row in personal_access_tokens and this is not
one.

The new origin is kept out of the activity filter unless the edition can
actually produce it. A filter option that can only ever return nothing is
a feature dangled at an edition that does not have it, which is the one
thing the edition boundary exists not to do.
2026-08-25 14:19:32 -03:00

48 lines
2.2 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::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);
expect($registry->enabledKeys())->toBe(['branding.customize', 'storage.managed', 'captcha.managed_keys', 'ai.connector']);
});