mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 04:23:34 +00:00
343 lines
13 KiB
PHP
343 lines
13 KiB
PHP
<?php
|
|
|
|
use App\Models\DiscordNotificationSettings;
|
|
use App\Models\EmailNotificationSettings;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\PushoverNotificationSettings;
|
|
use App\Models\SlackNotificationSettings;
|
|
use App\Models\Team;
|
|
use App\Models\TelegramNotificationSettings;
|
|
use App\Models\User;
|
|
use App\Models\WebhookNotificationSettings;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Once;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
config([
|
|
'app.maintenance.driver' => 'file',
|
|
'cache.default' => 'array',
|
|
'session.driver' => 'array',
|
|
]);
|
|
|
|
InstanceSettings::query()->whereKey(0)->delete();
|
|
$settings = new InstanceSettings(['is_api_enabled' => true]);
|
|
$settings->id = 0;
|
|
$settings->save();
|
|
Once::flush();
|
|
|
|
$this->team = Team::factory()->create();
|
|
$this->user = User::factory()->create();
|
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
|
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$this->token = $this->user->createToken('test-token', ['*']);
|
|
$this->bearerToken = $this->token->plainTextToken;
|
|
});
|
|
|
|
function authHeaders(string $token): array
|
|
{
|
|
return [
|
|
'Authorization' => 'Bearer '.$token,
|
|
'Content-Type' => 'application/json',
|
|
'Accept' => 'application/json',
|
|
];
|
|
}
|
|
|
|
describe('GET /api/v1/notifications/*', function () {
|
|
test('returns email notification settings for the current team', function () {
|
|
$response = $this->withHeaders(authHeaders($this->bearerToken))
|
|
->getJson('/api/v1/notifications/email');
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertJsonFragment([
|
|
'team_id' => $this->team->id,
|
|
'smtp_enabled' => false,
|
|
]);
|
|
$response->assertJsonStructure([
|
|
'team_id',
|
|
'smtp_enabled',
|
|
'deployment_failure_email_notifications',
|
|
'use_instance_email_settings',
|
|
]);
|
|
});
|
|
|
|
test('returns settings for every notification channel', function (string $channel) {
|
|
$response = $this->withHeaders(authHeaders($this->bearerToken))
|
|
->getJson("/api/v1/notifications/{$channel}");
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertJsonPath('team_id', $this->team->id);
|
|
})->with([
|
|
'email',
|
|
'discord',
|
|
'slack',
|
|
'telegram',
|
|
'pushover',
|
|
'webhook',
|
|
]);
|
|
|
|
test('hides encrypted secrets without read:sensitive ability', function () {
|
|
$this->team->discordNotificationSettings->update([
|
|
'discord_webhook_url' => 'https://discord.com/api/webhooks/hidden/secret',
|
|
'discord_enabled' => true,
|
|
]);
|
|
|
|
$readToken = $this->user->createToken('read-token', ['read'])->plainTextToken;
|
|
|
|
$response = $this->withHeaders(authHeaders($readToken))
|
|
->getJson('/api/v1/notifications/discord');
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertJsonMissingPath('discord_webhook_url');
|
|
expect($response->getContent())->not->toContain('hidden/secret');
|
|
});
|
|
|
|
test('includes encrypted secrets with read:sensitive ability for admins', function () {
|
|
$this->team->discordNotificationSettings->update([
|
|
'discord_webhook_url' => 'https://discord.com/api/webhooks/visible/secret-token',
|
|
'discord_enabled' => true,
|
|
]);
|
|
|
|
$sensitiveToken = $this->user->createToken('read-sensitive-token', ['read', 'read:sensitive'])->plainTextToken;
|
|
|
|
$response = $this->withHeaders(authHeaders($sensitiveToken))
|
|
->getJson('/api/v1/notifications/discord');
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertJsonFragment([
|
|
'discord_webhook_url' => 'https://discord.com/api/webhooks/visible/secret-token',
|
|
]);
|
|
});
|
|
|
|
test('member with read token can view settings but not secrets', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$this->team->emailNotificationSettings->update([
|
|
'smtp_password' => 'super-secret-password',
|
|
'smtp_enabled' => true,
|
|
]);
|
|
|
|
$memberToken = $member->createToken('member-read', ['read'])->plainTextToken;
|
|
|
|
$response = $this->withHeaders(authHeaders($memberToken))
|
|
->getJson('/api/v1/notifications/email');
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertJsonPath('smtp_enabled', true);
|
|
$response->assertJsonMissingPath('smtp_password');
|
|
expect($response->getContent())->not->toContain('super-secret-password');
|
|
});
|
|
|
|
test('member cannot use read:sensitive token ability', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$memberToken = $member->createToken('member-sensitive', ['read', 'read:sensitive'])->plainTextToken;
|
|
|
|
$this->withHeaders(authHeaders($memberToken))
|
|
->getJson('/api/v1/notifications/email')
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('rejects unauthenticated requests', function () {
|
|
$this->getJson('/api/v1/notifications/email')
|
|
->assertStatus(401);
|
|
});
|
|
|
|
test('firstOrCreate restores missing channel settings', function () {
|
|
DiscordNotificationSettings::query()->where('team_id', $this->team->id)->delete();
|
|
|
|
expect(DiscordNotificationSettings::query()->where('team_id', $this->team->id)->exists())->toBeFalse();
|
|
|
|
$response = $this->withHeaders(authHeaders($this->bearerToken))
|
|
->getJson('/api/v1/notifications/discord');
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertJsonPath('team_id', $this->team->id);
|
|
expect(DiscordNotificationSettings::query()->where('team_id', $this->team->id)->exists())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('PATCH /api/v1/notifications/*', function () {
|
|
test('updates email notification settings', function () {
|
|
$response = $this->withHeaders(authHeaders($this->bearerToken))
|
|
->patchJson('/api/v1/notifications/email', [
|
|
'smtp_enabled' => true,
|
|
'smtp_from_address' => 'alerts@example.com',
|
|
'smtp_host' => 'smtp.example.com',
|
|
'smtp_port' => 587,
|
|
'smtp_encryption' => 'starttls',
|
|
'deployment_failure_email_notifications' => false,
|
|
]);
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertJsonPath('smtp_enabled', true);
|
|
$response->assertJsonPath('deployment_failure_email_notifications', false);
|
|
|
|
$settings = EmailNotificationSettings::query()->where('team_id', $this->team->id)->first();
|
|
expect($settings->smtp_enabled)->toBeTrue()
|
|
->and($settings->smtp_from_address)->toBe('alerts@example.com')
|
|
->and($settings->smtp_host)->toBe('smtp.example.com')
|
|
->and($settings->smtp_port)->toBe(587)
|
|
->and($settings->deployment_failure_email_notifications)->toBeFalse();
|
|
});
|
|
|
|
test('updates discord notification settings', function () {
|
|
$response = $this->withHeaders(authHeaders($this->bearerToken))
|
|
->patchJson('/api/v1/notifications/discord', [
|
|
'discord_enabled' => true,
|
|
'discord_webhook_url' => 'https://discord.com/api/webhooks/123/abc',
|
|
'discord_ping_enabled' => false,
|
|
'deployment_success_discord_notifications' => true,
|
|
]);
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertJsonPath('discord_enabled', true);
|
|
$response->assertJsonPath('discord_ping_enabled', false);
|
|
|
|
$settings = DiscordNotificationSettings::query()->where('team_id', $this->team->id)->first();
|
|
expect($settings->discord_enabled)->toBeTrue()
|
|
->and($settings->discord_webhook_url)->toBe('https://discord.com/api/webhooks/123/abc')
|
|
->and($settings->discord_ping_enabled)->toBeFalse()
|
|
->and($settings->deployment_success_discord_notifications)->toBeTrue();
|
|
});
|
|
|
|
test('updates slack, telegram, pushover, and webhook channels', function (string $channel, array $payload, string $modelClass, string $enabledField) {
|
|
$response = $this->withHeaders(authHeaders($this->bearerToken))
|
|
->patchJson("/api/v1/notifications/{$channel}", $payload);
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertJsonPath($enabledField, true);
|
|
|
|
$settings = $modelClass::query()->where('team_id', $this->team->id)->first();
|
|
expect($settings->{$enabledField})->toBeTrue();
|
|
})->with([
|
|
'slack' => [
|
|
'slack',
|
|
[
|
|
'slack_enabled' => true,
|
|
'slack_webhook_url' => 'https://hooks.slack.com/services/T00/B00/xxx',
|
|
'deployment_failure_slack_notifications' => true,
|
|
],
|
|
SlackNotificationSettings::class,
|
|
'slack_enabled',
|
|
],
|
|
'telegram' => [
|
|
'telegram',
|
|
[
|
|
'telegram_enabled' => true,
|
|
'telegram_token' => '123456:ABC-DEF',
|
|
'telegram_chat_id' => '-100123',
|
|
'deployment_failure_telegram_notifications' => true,
|
|
],
|
|
TelegramNotificationSettings::class,
|
|
'telegram_enabled',
|
|
],
|
|
'pushover' => [
|
|
'pushover',
|
|
[
|
|
'pushover_enabled' => true,
|
|
'pushover_user_key' => 'user-key',
|
|
'pushover_api_token' => 'api-token',
|
|
'deployment_failure_pushover_notifications' => true,
|
|
],
|
|
PushoverNotificationSettings::class,
|
|
'pushover_enabled',
|
|
],
|
|
'webhook' => [
|
|
'webhook',
|
|
[
|
|
'webhook_enabled' => true,
|
|
'webhook_url' => 'https://example.com/hooks/coolify',
|
|
'deployment_failure_webhook_notifications' => true,
|
|
],
|
|
WebhookNotificationSettings::class,
|
|
'webhook_enabled',
|
|
],
|
|
]);
|
|
|
|
test('rejects unknown fields with 422', function () {
|
|
$response = $this->withHeaders(authHeaders($this->bearerToken))
|
|
->patchJson('/api/v1/notifications/email', [
|
|
'smtp_enabled' => true,
|
|
'not_a_real_field' => 'nope',
|
|
]);
|
|
|
|
$response->assertStatus(422);
|
|
$response->assertJsonPath('errors.not_a_real_field.0', 'This field is not allowed.');
|
|
});
|
|
|
|
test('rejects team_id mass assignment attempts', function () {
|
|
$otherTeam = Team::factory()->create();
|
|
|
|
$response = $this->withHeaders(authHeaders($this->bearerToken))
|
|
->patchJson('/api/v1/notifications/discord', [
|
|
'discord_enabled' => true,
|
|
'team_id' => $otherTeam->id,
|
|
]);
|
|
|
|
$response->assertStatus(422);
|
|
$response->assertJsonPath('errors.team_id.0', 'This field is not allowed.');
|
|
});
|
|
|
|
test('requires write ability', function () {
|
|
$readToken = $this->user->createToken('read-only', ['read'])->plainTextToken;
|
|
|
|
$response = $this->withHeaders(authHeaders($readToken))
|
|
->patchJson('/api/v1/notifications/email', [
|
|
'smtp_enabled' => true,
|
|
]);
|
|
|
|
$response->assertForbidden();
|
|
});
|
|
|
|
test('forbids members from updating notification settings', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
session(['currentTeam' => $this->team]);
|
|
$memberToken = $member->createToken('member-write', ['read', 'write'])->plainTextToken;
|
|
|
|
$response = $this->withHeaders(authHeaders($memberToken))
|
|
->patchJson('/api/v1/notifications/email', [
|
|
'smtp_enabled' => true,
|
|
]);
|
|
|
|
$response->assertForbidden();
|
|
});
|
|
|
|
test('rejects empty json body', function () {
|
|
$response = $this->withHeaders(authHeaders($this->bearerToken))
|
|
->patchJson('/api/v1/notifications/email', []);
|
|
|
|
$response->assertStatus(400);
|
|
});
|
|
|
|
test('does not leak updates across teams', function () {
|
|
$otherTeam = Team::factory()->create();
|
|
$otherUser = User::factory()->create();
|
|
$otherTeam->members()->attach($otherUser->id, ['role' => 'owner']);
|
|
session(['currentTeam' => $otherTeam]);
|
|
$otherToken = $otherUser->createToken('other-token', ['*'])->plainTextToken;
|
|
|
|
$this->withHeaders(authHeaders($otherToken))
|
|
->patchJson('/api/v1/notifications/email', [
|
|
'smtp_enabled' => true,
|
|
'smtp_from_address' => 'other@example.com',
|
|
])
|
|
->assertSuccessful();
|
|
|
|
$thisTeamSettings = EmailNotificationSettings::query()->where('team_id', $this->team->id)->first();
|
|
$otherSettings = EmailNotificationSettings::query()->where('team_id', $otherTeam->id)->first();
|
|
|
|
expect($thisTeamSettings->smtp_enabled)->toBeFalse()
|
|
->and($otherSettings->smtp_enabled)->toBeTrue()
|
|
->and($otherSettings->smtp_from_address)->toBe('other@example.com');
|
|
});
|
|
});
|