mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 17:15:08 +00:00
da1f432d87
Every instance reached projectsend.org twice a day and an operator could stop neither. The news feed had no switch of any kind — FetchNewsCommand went straight to the request, touching Settings only to write results back. The update check had one, but its default is on, and a managed fleet had been setting PROJECTSEND_CHECK_FOR_UPDATES=false for months against code that reads no such variable: check_for_updates is a database setting, so the environment never touched it and updates were enabled fleet-wide the whole time. They look like one problem and are two, which is why they are fixed differently. **The news feed gets a Setting**, its own key, default on. A Cloud client with view_news sees that card today — DashboardController gates it on the permission alone, with a comment saying in as many words that it is both editions and carries no capability. So switching it off is an operator's choice rather than an edition's, and it must stay reachable everywhere. Its own key rather than riding on check_for_updates because they are two different wants: "do not tell me about releases" and "do not show me the project's news" are asked separately, and an installation with no outbound access at all wants both. **The update check gets a capability guard**, ahead of the setting it already had, and deliberately not a Setting of its own. On a managed installation the result is unreachable rather than unwanted: the dashboard's System card and the update UI are both gated on Capability::SystemUpdates, which is Community-only, and the image is chosen by whoever provisioned the instance. A Setting would encode a fact about the edition as a preference — leaving it switchable back on per tenant, buying a nightly call for a number no screen can draw, and putting the reason in a provisioning script rather than beside the code. A self-hosted install holds the capability and loses nothing: its own setting still decides. Both guards return success rather than failure. A scheduled task that was asked not to run has not failed, and reporting it as one would put a red line in the scheduler history every night for an installation behaving exactly as configured. The news switch is on the General settings screen, outside the can_manage_updates block that hides the update toggle where the capability is absent — a setting only reachable by editing a database row is a row, not a switch. Seven tests, and the two that matter go red when either guard is removed. Sixteen locales translated in the same commit rather than left for the pass, since a release is close.
161 lines
5.5 KiB
PHP
161 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Platform\Capabilities\Edition;
|
|
use App\Modules\Platform\Settings\Setting;
|
|
use App\Modules\Platform\Settings\Settings;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
beforeEach(function () {
|
|
$this->admin = User::factory()->create();
|
|
|
|
// Settings survive the per-test rollback, so nothing here may assume
|
|
// a default — see the note in CaptchaSettingsTest.
|
|
$settings = app(Settings::class);
|
|
$settings->set(Setting::CheckForUpdates, true);
|
|
$settings->set(Setting::FetchNews, true);
|
|
|
|
config()->set('projectsend.edition', Edition::Community);
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Two daily calls out of the container, and who may stop them
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| An operator could stop neither. The news feed had no switch of any kind,
|
|
| and the update check had one whose default is on — so a managed fleet
|
|
| believed it had disabled updates through an environment variable that
|
|
| nothing in this application reads.
|
|
|
|
|
| They are not the same case, and are not fixed the same way. Which
|
|
| mechanism each gets is the point of these tests.
|
|
*/
|
|
|
|
test('the news feed can be switched off, and says so rather than failing', function () {
|
|
Http::fake();
|
|
app(Settings::class)->set(Setting::FetchNews, false);
|
|
|
|
$this->artisan('projectsend:fetch-news')
|
|
->expectsOutputToContain('switched off')
|
|
->assertSuccessful();
|
|
|
|
// Not merely "no items stored" — the request never left.
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
test('the news feed is on by default, so nothing changes for an existing install', function () {
|
|
Http::fake(['*' => Http::response([])]);
|
|
|
|
$this->artisan('projectsend:fetch-news')->assertSuccessful();
|
|
|
|
Http::assertSentCount(1);
|
|
});
|
|
|
|
// The news is informational and both editions show it: a Cloud client
|
|
// with view_news sees that card today (DashboardController), so this must
|
|
// NOT be gated on a capability. Switching it off is an operator's choice,
|
|
// not an edition's.
|
|
test('the news feed is not an edition difference', function () {
|
|
Http::fake(['*' => Http::response([])]);
|
|
config()->set('projectsend.edition', Edition::Cloud);
|
|
|
|
$this->artisan('projectsend:fetch-news')->assertSuccessful();
|
|
|
|
Http::assertSentCount(1);
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| The update check is the other kind
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| On a managed installation the result is unreachable rather than
|
|
| unwanted: the dashboard's System card and the update UI are both gated
|
|
| on Capability::SystemUpdates, which is Community-only, and the image is
|
|
| chosen by whoever provisioned the instance. That is a fact about the
|
|
| edition, not a preference — so it is a capability, not a Setting.
|
|
*/
|
|
|
|
test('the update check does not run where its answer could never be seen', function () {
|
|
Http::fake();
|
|
config()->set('projectsend.edition', Edition::Cloud);
|
|
|
|
// On, and it still must not call out: the capability decides first.
|
|
app(Settings::class)->set(Setting::CheckForUpdates, true);
|
|
|
|
$this->artisan('projectsend:check-for-updates')
|
|
->expectsOutputToContain('do not apply')
|
|
->assertSuccessful();
|
|
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
test('a self-hosted install keeps its own switch, both ways', function () {
|
|
Http::fake(['*' => Http::response([])]);
|
|
|
|
app(Settings::class)->set(Setting::CheckForUpdates, false);
|
|
|
|
$this->artisan('projectsend:check-for-updates')
|
|
->expectsOutputToContain('disabled')
|
|
->assertSuccessful();
|
|
|
|
Http::assertNothingSent();
|
|
|
|
app(Settings::class)->set(Setting::CheckForUpdates, true);
|
|
|
|
$this->artisan('projectsend:check-for-updates')->assertSuccessful();
|
|
|
|
Http::assertSentCount(1);
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Reachable without a shell
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| A setting an operator cannot find is not a switch, it is a row. The
|
|
| update toggle beside it is hidden where the capability is absent; this
|
|
| one must not be, because the card it controls is shown in both editions.
|
|
*/
|
|
|
|
test('the news switch is on the settings page in both editions', function () {
|
|
foreach ([Edition::Community, Edition::Cloud] as $edition) {
|
|
config()->set('projectsend.edition', $edition);
|
|
|
|
$this->actingAs($this->admin)->get('/system/settings/general')->assertInertia(
|
|
fn (Inertia\Testing\AssertableInertia $page) => $page->where('fetch_news', true),
|
|
);
|
|
}
|
|
});
|
|
|
|
test('saving the settings page can turn the feed off and on', function () {
|
|
Http::fake();
|
|
|
|
$this->actingAs($this->admin)
|
|
->patch('/system/settings/general', generalPayload(['fetch_news' => false]))
|
|
->assertRedirect();
|
|
|
|
expect(app(Settings::class)->get(Setting::FetchNews))->toBeFalse();
|
|
|
|
$this->artisan('projectsend:fetch-news')->assertSuccessful();
|
|
Http::assertNothingSent();
|
|
|
|
$this->actingAs($this->admin)
|
|
->patch('/system/settings/general', generalPayload(['fetch_news' => true]))
|
|
->assertRedirect();
|
|
|
|
expect(app(Settings::class)->get(Setting::FetchNews))->toBeTrue();
|
|
});
|
|
|
|
/** The general form posts every field it owns; only the interesting one varies. */
|
|
function generalPayload(array $overrides = []): array
|
|
{
|
|
return array_merge([
|
|
'site_name' => 'ProjectSend',
|
|
'timezone' => 'UTC',
|
|
], $overrides);
|
|
}
|