Files
projectsend/tests/Feature/Platform/StatusCommandTest.php
T
ignacionelson 787e9ec189 Report version, edition, capabilities and seat usage as one probe
Asked for by the platform side, and the reason is better than
convenience. Their reconciler's rule is that it observes an end state and
never sends an instruction. `docker exec … php -r '…'` to reach a public
method is an instruction with the caller's argv in it, however harmless
the argv, and it would have been the first crack in that rule. A named
command is an observation, the same kind of thing as reading a directory
size.

`--json` for a machine, plain lines for a person. Nothing here is a
secret or a credential: every field is already visible to any signed-in
administrator, which is what makes it safe to read from outside the
container.

The counts come from SeatAllowance — the code that refuses the account
past the limit — rather than from a second query that agrees with it
today. Two counts that merely agree diverge eventually, over an inactive
account or a soft-deleted one, and the divergence reads as a billing
fault rather than a counting one.

Unlimited is emitted as null, with a test saying so, because the failure
if a reader takes it for zero is a customer on the most expensive plan
whose instance refuses to create a single client. The platform side
independently landed the same care on the emitting end, omitting the
variable rather than sending it empty.

It also answers the question that started all of this. Diagnosing why a
tenant ignored its bucket meant reaching into a container and calling
app() by hand; `projectsend:status` now says which capabilities the
edition grants, which is where that hunt began.
2026-08-27 02:40:28 -03:00

76 lines
2.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\User;
use App\Modules\Platform\Capabilities\Edition;
use Illuminate\Support\Facades\Artisan;
/**
* The probe a reconciler reads instead of being given a shell one-liner
* to run. Its contract is the JSON shape, so that is what these pin.
*/
beforeEach(function () {
$this->admin = User::factory()->create();
});
function statusJson(): array
{
// Capturing the command's own output rather than asserting on lines,
// because the contract here is the document and not the wording.
Artisan::call('projectsend:status', ['--json' => true]);
return json_decode(Artisan::output(), true, flags: JSON_THROW_ON_ERROR);
}
test('it reports the version, the edition and the capabilities that edition grants', function () {
config(['projectsend.edition' => Edition::Cloud]);
$status = statusJson();
expect($status['version'])->toBe(config('projectsend.version'))
->and($status['edition'])->toBe('cloud')
->and($status['capabilities'])->toContain('storage.managed', 'platform.managed', 'users.manage');
});
test('the counts are the ones the cap enforces on', function () {
config([
'projectsend.platform.max_staff_users' => 3,
'projectsend.platform.max_clients' => 25,
]);
User::factory()->client()->create();
User::factory()->client()->create(['account_requested' => true, 'active' => false]);
User::factory()->create(['active' => false]);
$status = statusJson();
// Two staff — the admin and the inactive one, which occupies a seat.
// One client — the pending request does not.
expect($status['seats']['staff'])->toBe(['used' => 2, 'limit' => 3])
->and($status['seats']['clients'])->toBe(['used' => 1, 'limit' => 25]);
});
test('unlimited is null rather than zero or a missing key', function () {
// A reader that mistook one for the other would report an installation
// selling unlimited accounts as one that may hold none.
config([
'projectsend.platform.max_staff_users' => null,
'projectsend.platform.max_clients' => null,
]);
$status = statusJson();
expect($status['seats']['staff'])->toHaveKey('limit')
->and($status['seats']['staff']['limit'])->toBeNull()
->and($status['seats']['clients']['limit'])->toBeNull();
});
test('the human form says unlimited in words', function () {
config(['projectsend.platform.max_staff_users' => null]);
$this->artisan('projectsend:status')
->expectsOutputToContain('of unlimited')
->assertSuccessful();
});