mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-24 12:22:01 +00:00
f424fe5365
Two places asked "is this the API?" and got it wrong in opposite ways.
EnsureCapability asked $request->expectsJson(). Whether a feature exists
in this installation's edition is a property of the installation, not of
what the caller is willing to parse, so the same route answered
differently per header: `Accept: application/json` got the 403
`capability_unavailable` routes/api.php promises, `Accept: */*` -- curl's
default -- got a bare 404 `not_found`. The mirror image is worse: an
Inertia visit to a capability-gated *web* screen accepts JSON, so it got
403 with Laravel's default error body, naming the exception class, where
the point of the 404 is that an unavailable feature is absent rather than
teased.
ProblemDetails asked $request->is('api/*'). Two staff pages live under
that prefix -- the API dashboard at /api and the OpenAPI reference at
/api/docs, both registered in routes/web.php -- so a signed-out visitor to
either got 401 problem+json, "Send a valid API token in the Authorization
header as \"Bearer <token>\"", instead of the login redirect every other
page gives them.
Both now ask App\Support\ApiSurface: under the API prefix, and not part of
the `web` middleware group. The group is what actually separates the two
-- sessions and CSRF on one side, tokens on the other -- and it keeps
answering correctly for a future /api/v2 without being edited. An
unmatched path has no route to ask, which is the API's answer anyway: a
404 under its prefix is one it should describe in its own format, and the
existing test for that stays green.
Three tests, in the two files that already own these rules. Without the
fix all three go red.
83 lines
3.6 KiB
PHP
83 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Modules\Platform\Capabilities\Edition;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
beforeEach(function () {
|
|
// storage.configure rather than users.manage: this test needs a
|
|
// capability that is genuinely Community-only, and users.manage stopped
|
|
// being one in 2.2.0 when a platform's seats became a cap rather than a
|
|
// closed screen. Any Community-only key would do — this one is picked
|
|
// because a managed installation is given its storage, which is the
|
|
// clearest example of the edition line the middleware exists to draw.
|
|
Route::middleware('capability:storage.configure')->get('/test/community-only', fn () => 'ok');
|
|
Route::middleware('capability:branding.customize')->get('/test/cloud-only', fn () => 'ok');
|
|
|
|
// Under api/, because ProblemDetails is scoped to the API on purpose —
|
|
// a refusal on a web route is not supposed to be an RFC 7807 document.
|
|
// Not api/v1/, so OpenApiContractTest's documented-vs-registered
|
|
// comparison ignores it.
|
|
Route::middleware('capability:branding.customize')->get('api/test/cloud-only', fn () => 'ok');
|
|
});
|
|
|
|
test('a capability available in the current edition lets the request through', function () {
|
|
config()->set('projectsend.edition', Edition::Community);
|
|
|
|
$this->get('/test/community-only')->assertOk()->assertSee('ok');
|
|
});
|
|
|
|
test('a capability unavailable in the current edition returns 404 on web requests', function () {
|
|
config()->set('projectsend.edition', Edition::Community);
|
|
|
|
$this->get('/test/cloud-only')->assertNotFound();
|
|
});
|
|
|
|
// The machine-readable half survives, but as an RFC 7807 document like
|
|
// every other API error rather than a shape of its own — a caller that
|
|
// parses errors once should not have to special-case this one. `type` is
|
|
// the slug to branch on; `capability` and `edition` say which feature and
|
|
// where, which is the part worth giving up on rather than retrying.
|
|
test('a capability unavailable in the current edition returns a machine-readable 403 on API requests', function () {
|
|
config()->set('projectsend.edition', Edition::Community);
|
|
|
|
$this->getJson('/api/test/cloud-only')
|
|
->assertForbidden()
|
|
->assertHeader('Content-Type', 'application/problem+json')
|
|
->assertJson([
|
|
'type' => 'capability_unavailable',
|
|
'status' => 403,
|
|
'capability' => 'branding.customize',
|
|
'edition' => 'community',
|
|
]);
|
|
});
|
|
|
|
test('an API route answers 403 whatever the caller is willing to parse', function () {
|
|
// Accept is the caller's preference; whether a feature exists in this
|
|
// edition is not. routes/api.php promises the machine-readable 403,
|
|
// and a caller sending */* -- a curl default -- used to get a bare 404
|
|
// on the same route instead.
|
|
config()->set('projectsend.edition', Edition::Community);
|
|
|
|
$this->get('api/test/cloud-only', ['Accept' => '*/*'])
|
|
->assertForbidden()
|
|
->assertHeader('Content-Type', 'application/problem+json')
|
|
->assertJson(['type' => 'capability_unavailable']);
|
|
});
|
|
|
|
test('a web route still answers 404 even when the caller asks for JSON', function () {
|
|
// The mirror image: an Inertia request accepts JSON, and an
|
|
// unavailable feature must stay absent rather than announce itself.
|
|
config()->set('projectsend.edition', Edition::Community);
|
|
|
|
$this->getJson('/test/cloud-only')->assertNotFound();
|
|
});
|
|
|
|
test('the same routes flip availability when running as the cloud edition', function () {
|
|
config()->set('projectsend.edition', Edition::Cloud);
|
|
|
|
$this->get('/test/cloud-only')->assertOk();
|
|
$this->get('/test/community-only')->assertNotFound();
|
|
});
|