mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 09:05:08 +00:00
91d34b204c
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.
48 lines
2.2 KiB
PHP
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']);
|
|
});
|