mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-29 01:54:37 +08:00
Co-authored-by: Aditya Tripathi <aditya@climactic.co> Co-authored-by: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: rosslh <ross@rosshill.ca> Co-authored-by: Abdulfetah Suudi <suudiabdulfetah@gmail.com> Co-authored-by: Dharmveer <51308779+dharmveer97@users.noreply.github.com> Co-authored-by: Maxwell <136101+mxswd@users.noreply.github.com>
320 lines
13 KiB
PHP
320 lines
13 KiB
PHP
<?php
|
|
|
|
use App\Jobs\ApplicationDeploymentJob;
|
|
use App\Livewire\Notifications\Email as NotificationEmail;
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationDeploymentQueue;
|
|
use App\Models\ApplicationPreview;
|
|
use App\Models\Environment;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use App\Notifications\Application\DeploymentFailed;
|
|
use App\Notifications\Application\DeploymentSuccess;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Notification;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
const DEPLOYMENT_EMAIL_COMMIT_SHA = '3f9c2a7b1d4e5f60718293a4b5c6d7e8f9012345';
|
|
|
|
beforeEach(function () {
|
|
InstanceSettings::forceCreate(['id' => 0]);
|
|
|
|
$this->team = Team::factory()->create();
|
|
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
|
$destination = StandaloneDocker::factory()->create([
|
|
'server_id' => $server->id,
|
|
'network' => 'commit-details-'.fake()->unique()->word(),
|
|
]);
|
|
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
|
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
|
|
|
$this->application = Application::factory()->create([
|
|
'environment_id' => $environment->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => StandaloneDocker::class,
|
|
'git_repository' => 'https://github.com/acme/shop.git',
|
|
'git_branch' => 'main',
|
|
'fqdn' => 'https://shop.example.com',
|
|
]);
|
|
});
|
|
|
|
function createDeploymentRow(Application $application, array $attributes = []): ApplicationDeploymentQueue
|
|
{
|
|
return ApplicationDeploymentQueue::create([
|
|
'application_id' => $application->id,
|
|
'deployment_uuid' => 'deploy-'.fake()->unique()->uuid(),
|
|
'commit' => DEPLOYMENT_EMAIL_COMMIT_SHA,
|
|
'commit_message' => "Add checkout page\n\nHandles card and wallet payments.",
|
|
'status' => 'finished',
|
|
...$attributes,
|
|
]);
|
|
}
|
|
|
|
function enableCommitDetails(Team $team, bool $enabled = true): void
|
|
{
|
|
$team->emailNotificationSettings->update(['deployment_commit_details_email_notifications' => $enabled]);
|
|
$team->refresh();
|
|
}
|
|
|
|
function renderDeploymentMail(string $notificationClass, Application $application, ApplicationDeploymentQueue $deployment, Team $team, ?ApplicationPreview $preview = null): string
|
|
{
|
|
return (string) (new $notificationClass($application, $deployment->deployment_uuid, $preview, $deployment))->toMail($team)->render();
|
|
}
|
|
|
|
it('defaults the commit details setting to off', function () {
|
|
expect($this->team->emailNotificationSettings->fresh()->deployment_commit_details_email_notifications)->toBeFalse();
|
|
});
|
|
|
|
it('includes commit details in deployment emails when enabled', function (string $notificationClass) {
|
|
enableCommitDetails($this->team);
|
|
$deployment = createDeploymentRow($this->application);
|
|
|
|
$html = renderDeploymentMail($notificationClass, $this->application, $deployment, $this->team);
|
|
|
|
expect($html)
|
|
->toContain('3f9c2a7')
|
|
->toContain('https://github.com/acme/shop/commit/'.DEPLOYMENT_EMAIL_COMMIT_SHA)
|
|
->toContain('main')
|
|
->toContain('Add checkout page')
|
|
->toContain('Handles card and wallet payments.');
|
|
})->with([DeploymentSuccess::class, DeploymentFailed::class]);
|
|
|
|
it('leaves deployment emails unchanged when the setting is off', function (string $notificationClass) {
|
|
enableCommitDetails($this->team, false);
|
|
$deployment = createDeploymentRow($this->application);
|
|
|
|
$html = renderDeploymentMail($notificationClass, $this->application, $deployment, $this->team);
|
|
|
|
expect($html)
|
|
->not->toContain('3f9c2a7')
|
|
->not->toContain('Add checkout page')
|
|
->toContain('View Deployment Logs');
|
|
})->with([DeploymentSuccess::class, DeploymentFailed::class]);
|
|
|
|
it('hides the commit block when the deployment has no commit information', function () {
|
|
enableCommitDetails($this->team);
|
|
$deployment = createDeploymentRow($this->application, ['commit' => 'HEAD', 'commit_message' => null]);
|
|
|
|
$html = renderDeploymentMail(DeploymentSuccess::class, $this->application, $deployment, $this->team);
|
|
|
|
expect($html)
|
|
->not->toContain('Commit:')
|
|
->not->toContain('Commit message:')
|
|
->toContain('View Deployment Logs');
|
|
});
|
|
|
|
it('shows the commit for preview deployments without the base branch', function () {
|
|
enableCommitDetails($this->team);
|
|
$preview = ApplicationPreview::create([
|
|
'application_id' => $this->application->id,
|
|
'pull_request_id' => 42,
|
|
'pull_request_html_url' => 'https://github.com/acme/shop/pull/42',
|
|
'fqdn' => 'https://pr-42.shop.example.com',
|
|
]);
|
|
$deployment = createDeploymentRow($this->application, ['pull_request_id' => 42]);
|
|
|
|
$html = renderDeploymentMail(DeploymentSuccess::class, $this->application, $deployment, $this->team, $preview);
|
|
|
|
expect($html)
|
|
->toContain('Pull request #42')
|
|
->toContain('3f9c2a7')
|
|
->toContain('Add checkout page')
|
|
->not->toContain(' on main');
|
|
});
|
|
|
|
it('escapes html and markdown in commit messages', function () {
|
|
enableCommitDetails($this->team);
|
|
$deployment = createDeploymentRow($this->application, [
|
|
'commit_message' => "Fix <script>alert(1)</script> bug\n[Verify your account](https://evil.example.com)\n# Big heading",
|
|
]);
|
|
|
|
$html = renderDeploymentMail(DeploymentSuccess::class, $this->application, $deployment, $this->team);
|
|
|
|
expect($html)
|
|
->not->toContain('<script>alert(1)</script>')
|
|
->toContain('<script>')
|
|
->not->toContain('href="https://evil.example.com"')
|
|
->toContain('[Verify your account](https://evil.example.com)')
|
|
->not->toContain('<h1>Big heading</h1>')
|
|
->toContain('# Big heading');
|
|
});
|
|
|
|
it('lets team admins toggle commit details from email notification settings', function () {
|
|
$admin = User::factory()->create();
|
|
$this->team->members()->attach($admin->id, ['role' => 'admin']);
|
|
$this->actingAs($admin);
|
|
session(['currentTeam' => $this->team]);
|
|
Cache::flush();
|
|
|
|
Livewire::test(NotificationEmail::class)
|
|
->assertSet('deploymentCommitDetailsEmailNotifications', false)
|
|
->set('deploymentCommitDetailsEmailNotifications', true)
|
|
->call('instantSaveDeploymentCommitDetails')
|
|
->assertHasNoErrors();
|
|
|
|
expect($this->team->emailNotificationSettings->fresh()->deployment_commit_details_email_notifications)->toBeTrue();
|
|
});
|
|
|
|
it('does not let team members change the commit details setting', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
$this->actingAs($member);
|
|
session(['currentTeam' => $this->team]);
|
|
Cache::flush();
|
|
|
|
Livewire::test(NotificationEmail::class)
|
|
->set('deploymentCommitDetailsEmailNotifications', true)
|
|
->call('instantSaveDeploymentCommitDetails')
|
|
->assertSet('deploymentCommitDetailsEmailNotifications', false);
|
|
|
|
expect($this->team->emailNotificationSettings->fresh()->deployment_commit_details_email_notifications)->toBeFalse();
|
|
});
|
|
|
|
it('does not let admins of another team change the commit details setting', function () {
|
|
$otherTeam = Team::factory()->create();
|
|
$outsider = User::factory()->create();
|
|
$otherTeam->members()->attach($outsider->id, ['role' => 'admin']);
|
|
$this->actingAs($outsider);
|
|
session(['currentTeam' => $otherTeam]);
|
|
Cache::flush();
|
|
|
|
Livewire::test(NotificationEmail::class)
|
|
->set('deploymentCommitDetailsEmailNotifications', true)
|
|
->call('instantSaveDeploymentCommitDetails');
|
|
|
|
expect($this->team->emailNotificationSettings->fresh()->deployment_commit_details_email_notifications)->toBeFalse()
|
|
->and($otherTeam->emailNotificationSettings->fresh()->deployment_commit_details_email_notifications)->toBeTrue();
|
|
});
|
|
|
|
function bigMarkdownCommitMessage(): string
|
|
{
|
|
return implode("\n", [
|
|
'feat(checkout): rebuild payment flow',
|
|
'',
|
|
'# Heading one',
|
|
'Setext heading',
|
|
'==============',
|
|
'## Heading two',
|
|
'- bullet one',
|
|
'* bullet two',
|
|
'+ bullet three',
|
|
'1. first step',
|
|
'2. second step',
|
|
'> quoted text',
|
|
' indented code line',
|
|
'```php',
|
|
'echo "hello";',
|
|
'```',
|
|
'| Column A | Column B |',
|
|
'| -------- | -------- |',
|
|
'| cell 1 | cell 2 |',
|
|
'**bold** _italic_ ~~strike~~ `inline code`',
|
|
'[Reset your password](https://evil.example.com/reset)',
|
|
'',
|
|
'<https://evil.example.com/autolink>',
|
|
'<img src="https://evil.example.com/x.png" onerror="alert(1)">',
|
|
'<a href="https://evil.example.com">click me</a>',
|
|
'---',
|
|
'Co-authored-by: Jane Doe <jane@example.com>',
|
|
'Emoji check 🚀 ✅',
|
|
'',
|
|
str_repeat('Long body sentence for truncation. ', 60),
|
|
'THIS_TAIL_SHOULD_BE_TRUNCATED',
|
|
]);
|
|
}
|
|
|
|
it('renders a big markdown commit message as plain safe text', function () {
|
|
enableCommitDetails($this->team);
|
|
$deployment = createDeploymentRow($this->application, ['commit_message' => bigMarkdownCommitMessage()]);
|
|
|
|
$html = renderDeploymentMail(DeploymentSuccess::class, $this->application, $deployment, $this->team);
|
|
|
|
expect($html)
|
|
->toContain('feat(checkout): rebuild payment flow')
|
|
->toContain('# Heading one')
|
|
->toContain('==============')
|
|
->toContain('- bullet one')
|
|
->toContain('1. first step')
|
|
->toContain('```php')
|
|
->toContain('| Column A | Column B |')
|
|
->toContain('**bold** _italic_ ~~strike~~ `inline code`')
|
|
->toContain('Co-authored-by: Jane Doe <jane@example.com>')
|
|
->toContain('Emoji check 🚀 ✅')
|
|
->not->toContain("\u{FFFD}")
|
|
->toContain('Long body sentence for truncation.')
|
|
->not->toContain('THIS_TAIL_SHOULD_BE_TRUNCATED')
|
|
->not->toContain('href="https://evil.example.com')
|
|
->not->toContain('src="https://evil.example.com')
|
|
->not->toContain('<img')
|
|
->not->toContain('<a href="https://evil')
|
|
->not->toContain('<h1>')
|
|
->not->toContain('<h2>')
|
|
->not->toContain('<ul>')
|
|
->not->toContain('<ol>')
|
|
->not->toContain('<blockquote>')
|
|
->not->toContain('<pre>')
|
|
->not->toContain('<table>')
|
|
->not->toContain('<strong>bold</strong>')
|
|
->toContain('---<br />');
|
|
});
|
|
|
|
it('keeps the email working when a commit message has invalid utf-8 bytes', function () {
|
|
enableCommitDetails($this->team);
|
|
$deployment = createDeploymentRow($this->application, ['commit_message' => "Fix caf\xE9 menu (latin-1 byte)"]);
|
|
|
|
$html = renderDeploymentMail(DeploymentSuccess::class, $this->application, $deployment, $this->team);
|
|
|
|
expect($html)
|
|
->toContain('Fix caf')
|
|
->toContain('menu (latin-1 byte)')
|
|
->toContain('View Deployment Logs');
|
|
});
|
|
|
|
it('builds commit details without querying the deployment queue', function (string $notificationClass) {
|
|
enableCommitDetails($this->team);
|
|
$deployment = createDeploymentRow($this->application);
|
|
|
|
DB::flushQueryLog();
|
|
DB::enableQueryLog();
|
|
$notification = new $notificationClass($this->application, $deployment->deployment_uuid, null, $deployment);
|
|
$queries = collect(DB::getQueryLog())->pluck('query');
|
|
DB::disableQueryLog();
|
|
|
|
expect($queries->filter(fn (string $query): bool => str_contains($query, 'application_deployment_queues')))->toBeEmpty()
|
|
->and($notification->commit_sha)->toBe(DEPLOYMENT_EMAIL_COMMIT_SHA);
|
|
})->with([DeploymentSuccess::class, DeploymentFailed::class]);
|
|
|
|
it('keeps the old constructor working without a deployment record', function () {
|
|
enableCommitDetails($this->team);
|
|
|
|
$html = (string) (new DeploymentSuccess($this->application, 'legacy-call'))->toMail($this->team)->render();
|
|
|
|
expect($html)
|
|
->toContain('View Deployment Logs')
|
|
->not->toContain('Commit message:');
|
|
});
|
|
|
|
it('passes the in-memory deployment record from the deployment job', function () {
|
|
Notification::fake();
|
|
$this->team->emailNotificationSettings->update(['smtp_enabled' => true, 'deployment_success_email_notifications' => true]);
|
|
$deployment = createDeploymentRow($this->application);
|
|
$reflection = new ReflectionClass(ApplicationDeploymentJob::class);
|
|
$job = $reflection->newInstanceWithoutConstructor();
|
|
foreach (['application' => $this->application, 'deployment_uuid' => $deployment->deployment_uuid, 'preview' => null, 'application_deployment_queue' => $deployment] as $name => $value) {
|
|
$reflection->getProperty($name)->setValue($job, $value);
|
|
}
|
|
|
|
$reflection->getMethod('sendDeploymentNotification')->invoke($job, DeploymentSuccess::class);
|
|
|
|
Notification::assertSentTo($this->team, DeploymentSuccess::class, fn (DeploymentSuccess $notification) => $notification->commit_sha === DEPLOYMENT_EMAIL_COMMIT_SHA
|
|
&& str_starts_with($notification->commit_message, 'Add checkout page'));
|
|
});
|