mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-16 16:45:07 +00:00
856c13b09c
Staff can now invite a specific address to register instead of typing a
password for somebody and finding a way to get it to them. The invited
person sets their own, the link is locked to the address it was sent to,
and an invitation always activates the account regardless of the
auto-approve setting -- naming an address is already the decision the
approval queue exists to make for one nobody named.
Two fixes ride along: outgoing mail now reads the installation's own site
name in its title, header and signature rather than the one baked into
config('app.name') at install time, and the CSRF cookie name is read per
request rather than captured once at load.
Follow-up work, tracked separately: an invitation cannot be cancelled --
there is no pending-invitations screen and no revoke, so letting one expire
is the only way to take it back, which the self-service resend button then
undoes. Redemption also needs the address-availability check every other
non-form caller of ClientProvisioning makes.
Thanks @mash2k3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPk8qAs38pudYGWwmGkYPe
131 lines
4.9 KiB
PHP
131 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Clients\Notifications\ClientWelcomeNotification;
|
|
use App\Modules\Platform\Attribution\Events\ResolvingAttribution;
|
|
use App\Modules\Platform\Notifications\ThemedMailChannel;
|
|
use App\Modules\Platform\Settings\Setting;
|
|
use App\Modules\Platform\Settings\Settings;
|
|
use App\Modules\Platform\Theming\EmailThemeService;
|
|
use Illuminate\Notifications\Channels\MailChannel;
|
|
use Illuminate\Support\Facades\Event;
|
|
|
|
/**
|
|
* Renders the actual markdown mail HTML through the real, container-bound
|
|
* mail channel — the same path every Notification's toMail() funnels
|
|
* through — rather than mocking, since the whole point of ThemedMailChannel
|
|
* is that no individual Notification class has to know about theming.
|
|
*/
|
|
function renderThemedNotificationHtml(): string
|
|
{
|
|
$client = new User(['name' => 'Client', 'email' => 'client@example.com']);
|
|
$client->exists = true;
|
|
$client->id = 1;
|
|
|
|
$mail = (new ClientWelcomeNotification)->toMail($client);
|
|
|
|
$channel = app(MailChannel::class);
|
|
$method = (new ReflectionClass($channel))->getMethod('buildMarkdownHtml');
|
|
$method->setAccessible(true);
|
|
|
|
return (string) ($method->invoke($channel, $mail))([]);
|
|
}
|
|
|
|
test('the mail channel is bound to ThemedMailChannel', function () {
|
|
expect(app(MailChannel::class))->toBeInstanceOf(ThemedMailChannel::class);
|
|
});
|
|
|
|
test('selecting the minimal email theme renders its distinct header markup and css', function () {
|
|
app(Settings::class)->set(Setting::EmailTheme, 'minimal');
|
|
|
|
$html = renderThemedNotificationHtml();
|
|
|
|
expect($html)->toContain('header-label')
|
|
->and($html)->toContain('letter-spacing: 0.08em');
|
|
});
|
|
|
|
test('selecting the drive email theme renders its distinct css', function () {
|
|
app(Settings::class)->set(Setting::EmailTheme, 'drive');
|
|
|
|
$html = renderThemedNotificationHtml();
|
|
|
|
expect($html)->toContain('#1a73e8')
|
|
->and($html)->not->toContain('header-label');
|
|
});
|
|
|
|
test('the default email theme omits the minimal-only header markup', function () {
|
|
app(Settings::class)->set(Setting::EmailTheme, 'default');
|
|
|
|
$html = renderThemedNotificationHtml();
|
|
|
|
expect($html)->not->toContain('header-label')
|
|
->and($html)->toContain('box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1)');
|
|
});
|
|
|
|
test('an unknown or unavailable stored email theme resolves to default rather than a broken send', function () {
|
|
app(Settings::class)->set(Setting::EmailTheme, 'does-not-exist');
|
|
|
|
expect(app(EmailThemeService::class)->currentThemeKey())->toBe('default');
|
|
|
|
$html = renderThemedNotificationHtml();
|
|
|
|
expect($html)->toContain('box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1)');
|
|
});
|
|
|
|
test('the branded email theme renders its own css and the stock logo', function () {
|
|
app(Settings::class)->set(Setting::EmailTheme, 'branded');
|
|
|
|
expect(app(EmailThemeService::class)->currentThemeKey())->toBe('branded');
|
|
|
|
$html = renderThemedNotificationHtml();
|
|
|
|
expect($html)->toContain('#3b0764')
|
|
->and($html)->toContain('<img')
|
|
->and($html)->toContain('apple-touch-icon.png');
|
|
});
|
|
|
|
test('every email theme carries the attribution line in its footer', function (string $theme) {
|
|
app(Settings::class)->set(Setting::EmailTheme, $theme);
|
|
|
|
$html = renderThemedNotificationHtml();
|
|
|
|
// Styled, not bare: the published footer partial reuses the `.footer p`
|
|
// and `.footer a` vocabulary all four themes already define, so a theme
|
|
// that forgot to define them would render this unstyled.
|
|
expect($html)->toContain('Powered by ProjectSend')
|
|
->and($html)->toContain(config('projectsend.links.website'));
|
|
})->with(['default', 'minimal', 'drive', 'branded']);
|
|
|
|
test('a listener that hides attribution strips the line from outgoing mail', function () {
|
|
app(Settings::class)->set(Setting::EmailTheme, 'default');
|
|
|
|
Event::listen(ResolvingAttribution::class, function (ResolvingAttribution $event): void {
|
|
$event->visible = false;
|
|
});
|
|
|
|
$html = renderThemedNotificationHtml();
|
|
|
|
// The rest of the footer — the site's own copyright line — stays.
|
|
expect($html)->not->toContain('Powered by ProjectSend')
|
|
->and($html)->toContain('All rights reserved');
|
|
});
|
|
|
|
test('the site name replaces the app name in the header and the salutation', function () {
|
|
// The default "Powered by ProjectSend" attribution line stays either
|
|
// way — this is about the header logo's alt text and the "Regards,"
|
|
// signature, not that fixed string, so it is disabled here to keep
|
|
// the assertion about one thing.
|
|
Event::listen(ResolvingAttribution::class, function (ResolvingAttribution $event): void {
|
|
$event->visible = false;
|
|
});
|
|
|
|
app(Settings::class)->set(Setting::SiteName, 'Renamed Installation');
|
|
|
|
$html = renderThemedNotificationHtml();
|
|
|
|
expect($html)->toContain('Renamed Installation')
|
|
->and($html)->not->toContain('ProjectSend');
|
|
});
|