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 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('') ->toContain('<script>') ->not->toContain('href="https://evil.example.com"') ->toContain('[Verify your account](https://evil.example.com)') ->not->toContain('

Big heading

') ->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)', '![tracking pixel](https://evil.example.com/pixel.png)', '', '', 'click me', '---', 'Co-authored-by: Jane Doe ', '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('not->toContain('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')); });