mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 14:17:18 +00:00
Merge remote-tracking branch 'origin/next' into fix/url-validator-underscore-hostnames
This commit is contained in:
commit
b50839d451
505 files changed
+21391
-5998
No files matched your search
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Server;
|
||||
|
||||
it('normalizes requested log line counts', function () {
|
||||
if (! function_exists('normalizeLogLines')) {
|
||||
expect(function_exists('normalizeLogLines'))->toBeTrue();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
expect(normalizeLogLines(null))->toBe(100)
|
||||
->and(normalizeLogLines(''))->toBe(100)
|
||||
->and(normalizeLogLines('abc'))->toBe(100)
|
||||
->and(normalizeLogLines('0'))->toBe(100)
|
||||
->and(normalizeLogLines('-5'))->toBe(100)
|
||||
->and(normalizeLogLines('50'))->toBe(50)
|
||||
->and(normalizeLogLines('50000'))->toBe(10000);
|
||||
});
|
||||
|
||||
it('parses show_timestamps query values as booleans', function () {
|
||||
if (! function_exists('parseLogTimestampFlag')) {
|
||||
expect(function_exists('parseLogTimestampFlag'))->toBeTrue();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
expect(parseLogTimestampFlag('true'))->toBeTrue()
|
||||
->and(parseLogTimestampFlag('1'))->toBeTrue()
|
||||
->and(parseLogTimestampFlag(true))->toBeTrue()
|
||||
->and(parseLogTimestampFlag('false'))->toBeFalse()
|
||||
->and(parseLogTimestampFlag('0'))->toBeFalse()
|
||||
->and(parseLogTimestampFlag(false))->toBeFalse()
|
||||
->and(parseLogTimestampFlag('not-a-bool'))->toBeFalse()
|
||||
->and(parseLogTimestampFlag(null))->toBeFalse();
|
||||
});
|
||||
|
||||
it('builds docker log commands with options before an escaped container id', function () {
|
||||
if (! function_exists('buildContainerLogsCommand')) {
|
||||
expect(function_exists('buildContainerLogsCommand'))->toBeTrue();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$server = new Server;
|
||||
$server->settings = ['is_swarm_manager' => false];
|
||||
|
||||
expect(buildContainerLogsCommand($server, 'container-1', 25, true))
|
||||
->toBe("docker logs -n 25 --timestamps 'container-1' 2>&1");
|
||||
});
|
||||
|
||||
it('builds swarm service log commands with options before an escaped service id', function () {
|
||||
if (! function_exists('buildContainerLogsCommand')) {
|
||||
expect(function_exists('buildContainerLogsCommand'))->toBeTrue();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$server = new Server;
|
||||
$server->settings = ['is_swarm_manager' => true];
|
||||
|
||||
expect(buildContainerLogsCommand($server, "service'name", 25, true))
|
||||
->toBe("docker service logs -n 25 --timestamps 'service'\\''name' 2>&1");
|
||||
});
|
||||
|
||||
it('filters service sub containers in PHP instead of using user input in shell filters', function () {
|
||||
if (! function_exists('filterServiceSubContainersByName')) {
|
||||
expect(function_exists('filterServiceSubContainersByName'))->toBeTrue();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$containers = collect([
|
||||
['ID' => 'first', 'Labels' => 'coolify.serviceId=10,coolify.name=app-service-uuid,coolify.type=service'],
|
||||
['ID' => 'second', 'Labels' => 'coolify.serviceId=10,coolify.name=db-service-uuid,coolify.type=service'],
|
||||
['ID' => 'third', 'Labels' => ['coolify.name' => 'app-service-uuid']],
|
||||
]);
|
||||
|
||||
expect(filterServiceSubContainersByName($containers, 'app-service-uuid')->pluck('ID')->all())
|
||||
->toBe(['first', 'third']);
|
||||
});
|
||||
|
||||
it('does not interpolate the requested service name into the docker ps shell command', function () {
|
||||
$source = file_get_contents(__DIR__.'/../../../bootstrap/helpers/docker.php');
|
||||
|
||||
expect($source)->not->toContain('coolify.name={$name}');
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
|
||||
function setDeploymentJobProperty(object $job, string $property, mixed $value): void
|
||||
{
|
||||
$reflectionProperty = new ReflectionProperty(ApplicationDeploymentJob::class, $property);
|
||||
$reflectionProperty->setValue($job, $value);
|
||||
}
|
||||
|
||||
function getDeploymentJobProperty(object $job, string $property): mixed
|
||||
{
|
||||
$reflectionProperty = new ReflectionProperty(ApplicationDeploymentJob::class, $property);
|
||||
|
||||
return $reflectionProperty->getValue($job);
|
||||
}
|
||||
|
||||
function invokeCheckGitIfBuildNeeded(object $job): void
|
||||
{
|
||||
$method = new ReflectionMethod(ApplicationDeploymentJob::class, 'check_git_if_build_needed');
|
||||
$method->invoke($job);
|
||||
}
|
||||
|
||||
function makeDeploymentJobForCommitCheck(string $pinnedSha, string $branchHeadSha): object
|
||||
{
|
||||
$job = new class extends ApplicationDeploymentJob
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function execute_remote_command(...$commands) {}
|
||||
};
|
||||
|
||||
$application = new class extends Application
|
||||
{
|
||||
public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null)
|
||||
{
|
||||
return [
|
||||
'commands' => collect([]),
|
||||
'branch' => 'main',
|
||||
'fullRepoUrl' => 'https://github.com/coollabsio/coolify.git',
|
||||
];
|
||||
}
|
||||
};
|
||||
$application->forceFill([
|
||||
'uuid' => 'application-uuid',
|
||||
'git_branch' => 'main',
|
||||
]);
|
||||
$application->setRelation('settings', (object) [
|
||||
'include_source_commit_in_build' => false,
|
||||
'use_build_secrets' => false,
|
||||
]);
|
||||
$application->setRelation('private_key', null);
|
||||
|
||||
$deploymentQueue = new class extends ApplicationDeploymentQueue
|
||||
{
|
||||
public bool $saved = false;
|
||||
|
||||
public function save(array $options = []): bool
|
||||
{
|
||||
$this->saved = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
$deploymentQueue->commit = $pinnedSha;
|
||||
|
||||
setDeploymentJobProperty($job, 'application', $application);
|
||||
setDeploymentJobProperty($job, 'application_deployment_queue', $deploymentQueue);
|
||||
setDeploymentJobProperty($job, 'deployment_uuid', 'deployment-uuid');
|
||||
setDeploymentJobProperty($job, 'pull_request_id', 0);
|
||||
setDeploymentJobProperty($job, 'commit', $pinnedSha);
|
||||
setDeploymentJobProperty($job, 'rollback', false);
|
||||
setDeploymentJobProperty($job, 'git_type', 'github');
|
||||
setDeploymentJobProperty($job, 'saved_outputs', collect([
|
||||
'git_commit_sha' => str("{$branchHeadSha}\trefs/heads/main"),
|
||||
]));
|
||||
|
||||
return $job;
|
||||
}
|
||||
|
||||
function shouldResolveBranchHeadForCommit(?string $commit): bool
|
||||
{
|
||||
$job = (new ReflectionClass(ApplicationDeploymentJob::class))->newInstanceWithoutConstructor();
|
||||
|
||||
$commitProperty = new ReflectionProperty($job, 'commit');
|
||||
$commitProperty->setValue($job, $commit ?? '');
|
||||
|
||||
$method = new ReflectionMethod($job, 'shouldResolveBranchHeadCommit');
|
||||
|
||||
return $method->invoke($job);
|
||||
}
|
||||
|
||||
describe('ApplicationDeploymentJob commit resolution', function () {
|
||||
test('resolves branch head for HEAD deployments', function () {
|
||||
expect(shouldResolveBranchHeadForCommit('HEAD'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('resolves branch head for blank deployments', function () {
|
||||
expect(shouldResolveBranchHeadForCommit(''))->toBeTrue();
|
||||
});
|
||||
|
||||
test('keeps pinned deployment commits instead of replacing them with branch head', function () {
|
||||
expect(shouldResolveBranchHeadForCommit('abc123def456abc123def456abc123def456abc1'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('check git does not overwrite pinned deployment commit with branch head', function () {
|
||||
$pinnedSha = 'abc123def456abc123def456abc123def456abc1';
|
||||
$branchHeadSha = '111222333444555666777888999000aaabbbccc1';
|
||||
$job = makeDeploymentJobForCommitCheck($pinnedSha, $branchHeadSha);
|
||||
|
||||
invokeCheckGitIfBuildNeeded($job);
|
||||
|
||||
$deploymentQueue = getDeploymentJobProperty($job, 'application_deployment_queue');
|
||||
|
||||
expect(getDeploymentJobProperty($job, 'commit'))->toBe($pinnedSha)
|
||||
->and($deploymentQueue->commit)->toBe($pinnedSha)
|
||||
->and($deploymentQueue->saved)->toBeFalse();
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,8 @@
|
||||
use App\Exceptions\DeploymentException;
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -216,6 +218,75 @@ it('fails fast when docker buildx is unavailable for railpack builds', function
|
||||
->toThrow(DeploymentException::class, 'Railpack deployments require the Docker buildx CLI plugin');
|
||||
});
|
||||
|
||||
it('checks buildx inside the helper container before railpack builds', function () {
|
||||
[$job, $reflection] = makeRailpackDeploymentJob([], [
|
||||
'railpack_helper_buildx_available' => 'available',
|
||||
]);
|
||||
|
||||
invokeRailpackMethod($job, $reflection, 'ensure_helper_docker_buildx_available_for_railpack');
|
||||
|
||||
expect($job->recordedCommands[0][0][0])
|
||||
->toContain('DOCKER_CONFIG=/root/.docker docker buildx version');
|
||||
});
|
||||
|
||||
it('fails clearly when buildx is missing inside the helper container', function () {
|
||||
[$job, $reflection] = makeRailpackDeploymentJob([], [
|
||||
'railpack_helper_buildx_available' => 'not-available',
|
||||
]);
|
||||
|
||||
expect(fn () => invokeRailpackMethod($job, $reflection, 'ensure_helper_docker_buildx_available_for_railpack'))
|
||||
->toThrow(DeploymentException::class, 'helper container');
|
||||
});
|
||||
|
||||
it('pins docker config while running railpack buildx commands', function () {
|
||||
[$job, $reflection] = makeRailpackDeploymentJob([
|
||||
'uuid' => 'application-uuid',
|
||||
]);
|
||||
|
||||
$command = invokeRailpackMethod(
|
||||
$job,
|
||||
$reflection,
|
||||
'railpack_build_command',
|
||||
[
|
||||
'coollabsio/coolify:test',
|
||||
collect([]),
|
||||
],
|
||||
);
|
||||
|
||||
expect($command)
|
||||
->toContain('DOCKER_CONFIG=/root/.docker docker buildx create --name coolify-railpack')
|
||||
->toContain('DOCKER_CONFIG=/root/.docker docker buildx build --builder coolify-railpack');
|
||||
});
|
||||
|
||||
it('filters reserved docker client variables from railpack build secrets', function () {
|
||||
[$job, $reflection] = makeRailpackDeploymentJob([
|
||||
'uuid' => 'application-uuid',
|
||||
]);
|
||||
|
||||
$command = invokeRailpackMethod(
|
||||
$job,
|
||||
$reflection,
|
||||
'railpack_build_command',
|
||||
[
|
||||
'coollabsio/coolify:test',
|
||||
collect([
|
||||
'DOCKER_CONFIG' => '/tmp/no-buildx',
|
||||
'DOCKER_HOST' => 'tcp://invalid:2375',
|
||||
'RAILPACK_NODE_VERSION' => '22',
|
||||
'APP_ENV' => 'production',
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
expect($command)
|
||||
->toContain("--secret 'id=RAILPACK_NODE_VERSION,env=RAILPACK_NODE_VERSION'")
|
||||
->toContain("--secret 'id=APP_ENV,env=APP_ENV'")
|
||||
->not->toContain('id=DOCKER_CONFIG')
|
||||
->not->toContain('id=DOCKER_HOST')
|
||||
->not->toContain("env 'DOCKER_CONFIG=")
|
||||
->not->toContain("env 'DOCKER_HOST=");
|
||||
});
|
||||
|
||||
it('builds railpack docker command with matching env and secret flags for all railpack variables', function () {
|
||||
[$job, $reflection] = makeRailpackDeploymentJob([
|
||||
'uuid' => 'application-uuid',
|
||||
@@ -236,10 +307,15 @@ it('builds railpack docker command with matching env and secret flags for all ra
|
||||
],
|
||||
);
|
||||
|
||||
// Build-time variables are interpolated by sourcing the build-time .env file before
|
||||
// the build, so user/Coolify variables must NOT be forwarded inline as literals.
|
||||
expect($command)->toContain('set -a && source /artifacts/build-time.env && set +a');
|
||||
expect($command)->toContain("env 'RAILPACK_NODE_VERSION=22'");
|
||||
expect($command)->toContain("'RAILPACK_INSTALL_CMD=npm ci && npm run postinstall'");
|
||||
expect($command)->toContain("'RAILPACK_DEPLOY_APT_PACKAGES=curl wget'");
|
||||
expect($command)->toContain("'SECRET_JSON={\"token\":\"abc\"}'");
|
||||
// SECRET_JSON is not a buildpack control variable, so it is provided via the sourced
|
||||
// build-time .env file (which supports $VAR interpolation) rather than inline `env`.
|
||||
expect($command)->not->toContain("'SECRET_JSON={\"token\":\"abc\"}'");
|
||||
expect($command)->toContain("--secret 'id=RAILPACK_NODE_VERSION,env=RAILPACK_NODE_VERSION'");
|
||||
expect($command)->toContain("--secret 'id=RAILPACK_INSTALL_CMD,env=RAILPACK_INSTALL_CMD'");
|
||||
expect($command)->toContain("--secret 'id=RAILPACK_DEPLOY_APT_PACKAGES,env=RAILPACK_DEPLOY_APT_PACKAGES'");
|
||||
@@ -247,3 +323,69 @@ it('builds railpack docker command with matching env and secret flags for all ra
|
||||
expect($command)->toContain(' --build-arg secrets-hash=');
|
||||
expect($command)->toContain('--build-arg BUILDKIT_SYNTAX="ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version').'"');
|
||||
});
|
||||
|
||||
it('interpolates build-time variable references for railpack by sourcing the build-time env file', function () {
|
||||
[$job, $reflection] = makeRailpackDeploymentJob([
|
||||
'uuid' => 'application-uuid',
|
||||
]);
|
||||
|
||||
// Mirrors the issue: BETTER_AUTH_URL=$COOLIFY_URL must be interpolated at build time.
|
||||
$command = invokeRailpackMethod(
|
||||
$job,
|
||||
$reflection,
|
||||
'railpack_build_command',
|
||||
[
|
||||
'coollabsio/coolify:test',
|
||||
collect([
|
||||
'BETTER_AUTH_URL' => '$COOLIFY_URL',
|
||||
'COOLIFY_URL' => 'https://sapere-10.bobman.dev',
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
// The literal `$COOLIFY_URL` must NOT be forwarded inline; it is resolved by the shell
|
||||
// after sourcing the build-time .env file, then read through the build secret.
|
||||
expect($command)->toContain('set -a && source /artifacts/build-time.env && set +a');
|
||||
expect($command)->not->toContain("'BETTER_AUTH_URL=\$COOLIFY_URL'");
|
||||
expect($command)->not->toContain("env 'BETTER_AUTH_URL");
|
||||
expect($command)->toContain("--secret 'id=BETTER_AUTH_URL,env=BETTER_AUTH_URL'");
|
||||
expect($command)->toContain("--secret 'id=COOLIFY_URL,env=COOLIFY_URL'");
|
||||
});
|
||||
|
||||
it('creates an empty build-time env file for railpack when there are no generated build-time variables', function () {
|
||||
[$job, $reflection] = makeRailpackDeploymentJob([
|
||||
'build_pack' => 'railpack',
|
||||
'compose_parsing_version' => '3',
|
||||
]);
|
||||
|
||||
$applicationProperty = $reflection->getProperty('application');
|
||||
$applicationProperty->setAccessible(true);
|
||||
$application = $applicationProperty->getValue($job);
|
||||
$application->setRelation('settings', new ApplicationSetting([
|
||||
'include_source_commit_in_build' => false,
|
||||
'is_env_sorting_enabled' => false,
|
||||
]));
|
||||
$application->setRelation('environment_variables', collect([
|
||||
new EnvironmentVariable(['key' => 'COOLIFY_FQDN']),
|
||||
new EnvironmentVariable(['key' => 'COOLIFY_URL']),
|
||||
new EnvironmentVariable(['key' => 'COOLIFY_BRANCH']),
|
||||
new EnvironmentVariable(['key' => 'COOLIFY_RESOURCE_UUID']),
|
||||
]));
|
||||
|
||||
foreach ([
|
||||
'application_deployment_queue' => new class
|
||||
{
|
||||
public function addLogEntry(string $message, string $type = 'info', bool $hidden = false): void {}
|
||||
},
|
||||
'build_pack' => 'railpack',
|
||||
'pull_request_id' => 0,
|
||||
] as $property => $value) {
|
||||
$reflectionProperty = $reflection->getProperty($property);
|
||||
$reflectionProperty->setAccessible(true);
|
||||
$reflectionProperty->setValue($job, $value);
|
||||
}
|
||||
|
||||
invokeRailpackMethod($job, $reflection, 'save_buildtime_environment_variables');
|
||||
|
||||
expect(collect($job->recordedCommands)->flatten()->implode(' '))->toContain('touch /artifacts/build-time.env');
|
||||
});
|
||||
@@ -182,7 +182,7 @@ test('escapeshellarg neutralizes command injection in mariadb password', functio
|
||||
$escaped = escapeshellarg($maliciousPassword);
|
||||
|
||||
// Single quotes in the value get escaped as '\''
|
||||
expect($escaped)->toBe("'pass'\\'''; whoami; echo '\\'''");
|
||||
expect($escaped)->toBe("'pass'\\''; whoami; echo '\\'''");
|
||||
$command = "docker exec container mariadb-dump -u root -p$escaped db";
|
||||
// Verify the command doesn't contain an unescaped semicolon outside quotes
|
||||
expect($command)->toContain("-p'pass'");
|
||||
|
||||
@@ -4,11 +4,50 @@ use App\Models\Application;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\PrivateKey;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
function commandStrings(array|Collection|string $commands): Collection
|
||||
{
|
||||
if (is_string($commands)) {
|
||||
return collect([$commands]);
|
||||
}
|
||||
|
||||
return collect($commands)->map(fn ($command) => data_get($command, 'command') ?? $command[0] ?? $command);
|
||||
}
|
||||
|
||||
function privateKeyMaterializationCommands(array|Collection|string $commands): Collection
|
||||
{
|
||||
if (is_string($commands)) {
|
||||
$commands = [$commands];
|
||||
}
|
||||
|
||||
return collect($commands)->filter(fn ($command) => str(commandStrings([$command])->first())->contains('base64 -d | tee /root/.ssh/id_rsa_coolify_'));
|
||||
}
|
||||
|
||||
function expectCommandListToContain(array|Collection|string $commands, string $expected): void
|
||||
{
|
||||
expect(commandStrings($commands)->implode(' && '))->toContain($expected);
|
||||
}
|
||||
|
||||
function expectCommandListNotToContain(array|Collection|string $commands, string $expected): void
|
||||
{
|
||||
expect(commandStrings($commands)->implode(' && '))->not->toContain($expected);
|
||||
}
|
||||
|
||||
function expectPrivateKeyMaterializationCommandsSkipLogging(array|Collection|string $commands): void
|
||||
{
|
||||
$keyCommands = privateKeyMaterializationCommands($commands);
|
||||
|
||||
expect($keyCommands)->not->toBeEmpty();
|
||||
$keyCommands->each(function ($command): void {
|
||||
expect(data_get($command, 'skip_command_log'))->toBeTrue();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Git operations authenticate with the SSH key assigned in the UI. Coolify writes that key to a
|
||||
* per-deployment path (/root/.ssh/id_rsa_coolify_<deployment_uuid>) instead of the shared
|
||||
@@ -19,6 +58,24 @@ afterEach(function () {
|
||||
*/
|
||||
$keyPath = '/root/.ssh/id_rsa_coolify_test-deployment-uuid';
|
||||
|
||||
it('skips logging the docker deploy key materialization command before ls-remote', function () {
|
||||
$source = file_get_contents(__DIR__.'/../../app/Jobs/ApplicationDeploymentJob.php');
|
||||
$commandPosition = strpos($source, 'base64 -d | tee {$customSshKeyLocation}');
|
||||
|
||||
expect($commandPosition)->not->toBeFalse()
|
||||
->and(substr($source, $commandPosition, 200))->toContain("'skip_command_log' => true");
|
||||
});
|
||||
|
||||
it('supports skipping command log entries without adding a hidden command entry', function () {
|
||||
$source = file_get_contents(__DIR__.'/../../app/Traits/ExecuteRemoteCommand.php');
|
||||
|
||||
expect($source)
|
||||
->toContain('$skip_command_log = data_get($single_command, \'skip_command_log\', false);')
|
||||
->toContain('if ($command_hidden && ! $skip_command_log && isset($this->application_deployment_queue))')
|
||||
->toContain('use ($command, $hidden, $customType, $append, $command_hidden, $skip_command_log)')
|
||||
->toContain('\'command\' => $skip_command_log || $command_hidden ? null : $this->redact_sensitive_info($command),');
|
||||
});
|
||||
|
||||
it('writes a deploy key to a per-deployment path and cleans it up for ls-remote on the host', function () use ($keyPath) {
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->shouldReceive('getAttribute')->with('private_key')->andReturn('fake-private-key');
|
||||
@@ -31,11 +88,11 @@ it('writes a deploy key to a per-deployment path and cleans it up for ls-remote
|
||||
|
||||
$result = $application->generateGitLsRemoteCommands('test-deployment-uuid', false);
|
||||
|
||||
expect($result['commands'])
|
||||
->toContain("tee {$keyPath}")
|
||||
->toContain("-i {$keyPath} -o IdentitiesOnly=yes")
|
||||
->toContain("trap 'rm -f {$keyPath}' EXIT") // removed when the shell exits
|
||||
->not->toContain('tee /root/.ssh/id_rsa >'); // never overwrites the host root's own key
|
||||
expectCommandListToContain($result['commands'], "tee {$keyPath}");
|
||||
expectCommandListToContain($result['commands'], "-i {$keyPath} -o IdentitiesOnly=yes");
|
||||
expectCommandListToContain($result['commands'], "trap 'rm -f {$keyPath}' EXIT"); // removed when the shell exits
|
||||
expectCommandListNotToContain($result['commands'], 'tee /root/.ssh/id_rsa >'); // never overwrites the host root's own key
|
||||
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
|
||||
});
|
||||
|
||||
it('writes a deploy key to a per-deployment path for ls-remote inside docker without a trap', function () use ($keyPath) {
|
||||
@@ -50,11 +107,11 @@ it('writes a deploy key to a per-deployment path for ls-remote inside docker wit
|
||||
|
||||
$result = $application->generateGitLsRemoteCommands('test-deployment-uuid', true);
|
||||
|
||||
expect($result['commands'])
|
||||
->toContain("tee {$keyPath}")
|
||||
->toContain("-i {$keyPath} -o IdentitiesOnly=yes")
|
||||
->not->toContain('trap ') // ephemeral container, no cleanup needed
|
||||
->not->toContain('tee /root/.ssh/id_rsa >');
|
||||
expectCommandListToContain($result['commands'], "tee {$keyPath}");
|
||||
expectCommandListToContain($result['commands'], "-i {$keyPath} -o IdentitiesOnly=yes");
|
||||
expectCommandListNotToContain($result['commands'], 'trap '); // ephemeral container, no cleanup needed
|
||||
expectCommandListNotToContain($result['commands'], 'tee /root/.ssh/id_rsa >');
|
||||
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
|
||||
});
|
||||
|
||||
it('writes a GitLab source private key to a per-deployment path with cleanup on the host', function () use ($keyPath) {
|
||||
@@ -77,11 +134,11 @@ it('writes a GitLab source private key to a per-deployment path with cleanup on
|
||||
|
||||
$result = $application->generateGitLsRemoteCommands('test-deployment-uuid', false);
|
||||
|
||||
expect($result['commands'])
|
||||
->toContain("tee {$keyPath}")
|
||||
->toContain("-i {$keyPath} -o IdentitiesOnly=yes")
|
||||
->toContain("trap 'rm -f {$keyPath}' EXIT")
|
||||
->not->toContain('tee /root/.ssh/id_rsa >');
|
||||
expectCommandListToContain($result['commands'], "tee {$keyPath}");
|
||||
expectCommandListToContain($result['commands'], "-i {$keyPath} -o IdentitiesOnly=yes");
|
||||
expectCommandListToContain($result['commands'], "trap 'rm -f {$keyPath}' EXIT");
|
||||
expectCommandListNotToContain($result['commands'], 'tee /root/.ssh/id_rsa >');
|
||||
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
|
||||
});
|
||||
|
||||
it('writes a deploy key to a per-deployment path and cleans it up when cloning on the host', function () use ($keyPath) {
|
||||
@@ -104,11 +161,11 @@ it('writes a deploy key to a per-deployment path and cleans it up when cloning o
|
||||
// exec_in_docker = false → the loadComposeFile / host clone path
|
||||
$result = $application->generateGitImportCommands('test-deployment-uuid', 0, null, false);
|
||||
|
||||
expect($result['commands'])
|
||||
->toContain("tee {$keyPath}")
|
||||
->toContain("-i {$keyPath} -o IdentitiesOnly=yes")
|
||||
->toContain("trap 'rm -f {$keyPath}' EXIT")
|
||||
->not->toContain('tee /root/.ssh/id_rsa >');
|
||||
expectCommandListToContain($result['commands'], "tee {$keyPath}");
|
||||
expectCommandListToContain($result['commands'], "-i {$keyPath} -o IdentitiesOnly=yes");
|
||||
expectCommandListToContain($result['commands'], "trap 'rm -f {$keyPath}' EXIT");
|
||||
expectCommandListNotToContain($result['commands'], 'tee /root/.ssh/id_rsa >');
|
||||
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
|
||||
});
|
||||
|
||||
it('writes a GitLab source private key to a per-deployment path and cleans it up when cloning on the host', function () use ($keyPath) {
|
||||
@@ -137,11 +194,11 @@ it('writes a GitLab source private key to a per-deployment path and cleans it up
|
||||
|
||||
$result = $application->generateGitImportCommands('test-deployment-uuid', 0, null, false);
|
||||
|
||||
expect($result['commands'])
|
||||
->toContain("tee {$keyPath}")
|
||||
->toContain("-i {$keyPath} -o IdentitiesOnly=yes")
|
||||
->toContain("trap 'rm -f {$keyPath}' EXIT")
|
||||
->not->toContain('tee /root/.ssh/id_rsa >');
|
||||
expectCommandListToContain($result['commands'], "tee {$keyPath}");
|
||||
expectCommandListToContain($result['commands'], "-i {$keyPath} -o IdentitiesOnly=yes");
|
||||
expectCommandListToContain($result['commands'], "trap 'rm -f {$keyPath}' EXIT");
|
||||
expectCommandListNotToContain($result['commands'], 'tee /root/.ssh/id_rsa >');
|
||||
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
|
||||
});
|
||||
|
||||
it('uses the per-deployment deploy key for pull request fetches', function () use ($keyPath) {
|
||||
@@ -163,9 +220,9 @@ it('uses the per-deployment deploy key for pull request fetches', function () us
|
||||
|
||||
$result = $application->generateGitImportCommands('test-deployment-uuid', 123, 'github', false);
|
||||
|
||||
expect($result['commands'])
|
||||
->toContain("GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$keyPath} -o IdentitiesOnly=yes\" git fetch origin pull/123/head:pr-123-coolify")
|
||||
->not->toContain('GIT_SSH_COMMAND="ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa" git fetch origin pull/123/head:pr-123-coolify');
|
||||
expectCommandListToContain($result['commands'], "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$keyPath} -o IdentitiesOnly=yes\" git fetch origin pull/123/head:pr-123-coolify");
|
||||
expectCommandListNotToContain($result['commands'], 'GIT_SSH_COMMAND="ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa" git fetch origin pull/123/head:pr-123-coolify');
|
||||
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
|
||||
});
|
||||
|
||||
it('does not force a missing per-deployment key for other repository pull request fetches', function () use ($keyPath) {
|
||||
@@ -183,7 +240,6 @@ it('does not force a missing per-deployment key for other repository pull reques
|
||||
|
||||
$result = $application->generateGitImportCommands('test-deployment-uuid', 123, 'github', false);
|
||||
|
||||
expect($result['commands'])
|
||||
->toContain('GIT_SSH_COMMAND="ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa" git fetch origin pull/123/head:pr-123-coolify')
|
||||
->not->toContain($keyPath);
|
||||
expectCommandListToContain($result['commands'], 'GIT_SSH_COMMAND="ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa" git fetch origin pull/123/head:pr-123-coolify');
|
||||
expectCommandListNotToContain($result['commands'], $keyPath);
|
||||
});
|
||||
@@ -95,6 +95,6 @@ it('ensures docker_compose_raw update is wrapped in try-catch for error handling
|
||||
->toContain('// This keeps the original user input clean while preventing content reapplication')
|
||||
->toContain('try {')
|
||||
->toContain('$originalYaml = Yaml::parse($originalCompose);')
|
||||
->toContain('} catch (\Exception $e) {')
|
||||
->toContain("ray('Failed to update docker_compose_raw");
|
||||
->toContain('} catch (Exception) {')
|
||||
->toContain('// If parsing fails, keep the original docker_compose_raw unchanged');
|
||||
});
|
||||
@@ -141,3 +141,77 @@ test('file storage accepts relative dot-prefixed paths', function () {
|
||||
expect(fn () => validateShellSafePath('./data', 'storage path'))
|
||||
->not->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('file mount path validator rejects parent segments and unsafe separators', function (string $path) {
|
||||
expect(fn () => validateFileMountPath($path, 'file storage path'))
|
||||
->toThrow(Exception::class);
|
||||
})->with([
|
||||
'parent segment to etc' => ['/../../etc/passwd'],
|
||||
'embedded parent segment' => ['/foo/../bar'],
|
||||
'parent segment' => ['/..'],
|
||||
'double slash before parent segment' => ['/foo//../bar'],
|
||||
'current directory segment' => ['/foo/./bar'],
|
||||
'backslash parent segment' => ['\\..\\etc\\passwd'],
|
||||
'null byte' => ["/app/config\0/../../etc/passwd"],
|
||||
]);
|
||||
|
||||
test('file mount path validator accepts safe absolute container file paths', function (string $path, string $expected) {
|
||||
expect(validateFileMountPath($path, 'file storage path'))->toBe($expected);
|
||||
})->with([
|
||||
'nginx config' => ['/etc/nginx/nginx.conf', '/etc/nginx/nginx.conf'],
|
||||
'app env filename' => ['/app/.env', '/app/.env'],
|
||||
'relative input becomes absolute' => ['config/app.yaml', '/config/app.yaml'],
|
||||
'duplicate slashes collapse' => ['/opt//app///config.json', '/opt/app/config.json'],
|
||||
]);
|
||||
|
||||
test('host file mount path validator accepts absolute host file paths', function () {
|
||||
expect(validateHostFileMountPath('/etc/nginx/nginx.conf', 'host file path'))
|
||||
->toBe('/etc/nginx/nginx.conf');
|
||||
});
|
||||
|
||||
test('host file mount path validator rejects ambiguous or directory paths', function (string $path) {
|
||||
expect(fn () => validateHostFileMountPath($path, 'host file path'))
|
||||
->toThrow(Exception::class);
|
||||
})->with([
|
||||
'relative path' => ['etc/nginx/nginx.conf'],
|
||||
'root directory' => ['/'],
|
||||
'trailing slash' => ['/etc/nginx/'],
|
||||
'parent segment' => ['/etc/../shadow'],
|
||||
'current segment' => ['/etc/./nginx.conf'],
|
||||
'backslash' => ['\\etc\\nginx.conf'],
|
||||
]);
|
||||
|
||||
test('confined path resolver keeps file mounts inside their resource configuration root', function () {
|
||||
expect(confineFileMountPath('/data/coolify/applications/app-uuid', '/etc/nginx/nginx.conf', 'file storage path'))
|
||||
->toBe('/data/coolify/applications/app-uuid/etc/nginx/nginx.conf');
|
||||
|
||||
expect(confineFileMountPath('/data/coolify/databases/db-uuid/', 'postgres/postgresql.conf', 'file storage path'))
|
||||
->toBe('/data/coolify/databases/db-uuid/postgres/postgresql.conf');
|
||||
|
||||
expect(confineFileMountPath('/data/coolify/services/service-uuid', '/config.yaml', 'file storage path'))
|
||||
->toBe('/data/coolify/services/service-uuid/config.yaml');
|
||||
});
|
||||
|
||||
test('confined path resolver rejects paths that escape the resource configuration root', function (string $base, string $path) {
|
||||
expect(fn () => confineFileMountPath($base, $path, 'file storage path'))
|
||||
->toThrow(Exception::class);
|
||||
})->with([
|
||||
'application parent segment' => ['/data/coolify/applications/app-uuid', '/../../etc/passwd'],
|
||||
'database parent segment' => ['/data/coolify/databases/db-uuid', '/postgres/../../../etc/shadow'],
|
||||
'service dot segment' => ['/data/coolify/services/service-uuid', '/./config.yaml'],
|
||||
]);
|
||||
|
||||
test('local file volume write sink keeps saved managed file paths for compatibility', function () {
|
||||
$source = file_get_contents(__DIR__.'/../../app/Models/LocalFileVolume.php');
|
||||
|
||||
expect($source)->not->toContain('confinePathToBase($workdir, $path->value(), \'storage path\')')
|
||||
->and($source)->toContain('tee {$escapedPath}');
|
||||
});
|
||||
|
||||
test('host file mounts are bind-only and skipped by server storage writes', function () {
|
||||
$source = file_get_contents(__DIR__.'/../../app/Models/LocalFileVolume.php');
|
||||
|
||||
expect($source)->toContain('if ($this->is_host_file) {')
|
||||
->and($source)->toContain('return;')
|
||||
->and($source)->toContain('tee {$escapedPath}');
|
||||
});
|
||||
@@ -3,11 +3,45 @@
|
||||
use App\Models\Application;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\PrivateKey;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
function gitlabCommandStrings(array|Collection|string $commands): Collection
|
||||
{
|
||||
if (is_string($commands)) {
|
||||
return collect([$commands]);
|
||||
}
|
||||
|
||||
return collect($commands)->map(fn ($command) => data_get($command, 'command') ?? $command[0] ?? $command);
|
||||
}
|
||||
|
||||
function expectGitlabCommandListToContain(array|Collection|string $commands, string $expected): void
|
||||
{
|
||||
expect(gitlabCommandStrings($commands)->implode(' && '))->toContain($expected);
|
||||
}
|
||||
|
||||
function expectGitlabCommandListNotToContain(array|Collection|string $commands, string $expected): void
|
||||
{
|
||||
expect(gitlabCommandStrings($commands)->implode(' && '))->not->toContain($expected);
|
||||
}
|
||||
|
||||
function expectGitlabPrivateKeyMaterializationCommandsSkipLogging(array|Collection|string $commands): void
|
||||
{
|
||||
if (is_string($commands)) {
|
||||
$commands = [$commands];
|
||||
}
|
||||
|
||||
$keyCommands = collect($commands)->filter(fn ($command) => str(data_get($command, 'command') ?? $command[0] ?? $command)->contains('base64 -d | tee /root/.ssh/id_rsa_coolify_'));
|
||||
|
||||
expect($keyCommands)->not->toBeEmpty();
|
||||
$keyCommands->each(function ($command): void {
|
||||
expect(data_get($command, 'skip_command_log'))->toBeTrue();
|
||||
});
|
||||
}
|
||||
|
||||
it('generates ls-remote commands for GitLab source with private key', function () {
|
||||
$deploymentUuid = 'test-deployment-uuid';
|
||||
|
||||
@@ -15,7 +49,8 @@ it('generates ls-remote commands for GitLab source with private key', function (
|
||||
$privateKey->shouldReceive('getAttribute')->with('private_key')->andReturn('fake-private-key');
|
||||
|
||||
$gitlabSource = Mockery::mock(GitlabApp::class)->makePartial();
|
||||
$gitlabSource->shouldReceive('getMorphClass')->andReturn(\App\Models\GitlabApp::class);
|
||||
$gitlabSource->shouldReceive('getMorphClass')->andReturn(GitlabApp::class);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('html_url')->andReturn('https://gitlab.com');
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('privateKey')->andReturn($privateKey);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('private_key_id')->andReturn(1);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('custom_port')->andReturn(22);
|
||||
@@ -34,16 +69,18 @@ it('generates ls-remote commands for GitLab source with private key', function (
|
||||
|
||||
expect($result)->toBeArray();
|
||||
expect($result)->toHaveKey('commands');
|
||||
expect($result['commands'])->toContain('git ls-remote');
|
||||
expect($result['commands'])->toContain('id_rsa');
|
||||
expect($result['commands'])->toContain('mkdir -p /root/.ssh');
|
||||
expectGitlabCommandListToContain($result['commands'], 'git ls-remote');
|
||||
expectGitlabCommandListToContain($result['commands'], 'id_rsa');
|
||||
expectGitlabCommandListToContain($result['commands'], 'mkdir -p /root/.ssh');
|
||||
expectGitlabPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
|
||||
});
|
||||
|
||||
it('generates ls-remote commands for GitLab source without private key', function () {
|
||||
$deploymentUuid = 'test-deployment-uuid';
|
||||
|
||||
$gitlabSource = Mockery::mock(GitlabApp::class)->makePartial();
|
||||
$gitlabSource->shouldReceive('getMorphClass')->andReturn(\App\Models\GitlabApp::class);
|
||||
$gitlabSource->shouldReceive('getMorphClass')->andReturn(GitlabApp::class);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('html_url')->andReturn('https://gitlab.com');
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('privateKey')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('private_key_id')->andReturn(null);
|
||||
|
||||
@@ -61,17 +98,18 @@ it('generates ls-remote commands for GitLab source without private key', functio
|
||||
|
||||
expect($result)->toBeArray();
|
||||
expect($result)->toHaveKey('commands');
|
||||
expect($result['commands'])->toContain('git ls-remote');
|
||||
expect($result['commands'])->toContain('https://gitlab.com/user/repo.git');
|
||||
expectGitlabCommandListToContain($result['commands'], 'git ls-remote');
|
||||
expectGitlabCommandListToContain($result['commands'], 'https://gitlab.com/user/repo.git');
|
||||
// Should NOT contain SSH key setup
|
||||
expect($result['commands'])->not->toContain('id_rsa');
|
||||
expectGitlabCommandListNotToContain($result['commands'], 'id_rsa');
|
||||
});
|
||||
|
||||
it('does not return null for GitLab source type', function () {
|
||||
$deploymentUuid = 'test-deployment-uuid';
|
||||
|
||||
$gitlabSource = Mockery::mock(GitlabApp::class)->makePartial();
|
||||
$gitlabSource->shouldReceive('getMorphClass')->andReturn(\App\Models\GitlabApp::class);
|
||||
$gitlabSource->shouldReceive('getMorphClass')->andReturn(GitlabApp::class);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('html_url')->andReturn('https://gitlab.com');
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('privateKey')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('private_key_id')->andReturn(null);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ test('buildRestoreCommand handles PostgreSQL with dumpAll', function () {
|
||||
|
||||
$result = $component->buildRestoreCommand('/tmp/test.dump');
|
||||
|
||||
expect($result)->toContain('gunzip -cf /tmp/test.dump');
|
||||
expect($result)->toContain("gunzip -cf '/tmp/test.dump'");
|
||||
expect($result)->toContain('psql -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}');
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ test('buildRestoreCommand handles MySQL without dumpAll', function () {
|
||||
$result = $component->buildRestoreCommand('/tmp/test.dump');
|
||||
|
||||
expect($result)->toContain('mysql -u $MYSQL_USER');
|
||||
expect($result)->toContain('< /tmp/test.dump');
|
||||
expect($result)->toContain("< '/tmp/test.dump'");
|
||||
});
|
||||
|
||||
test('buildRestoreCommand handles MariaDB without dumpAll', function () {
|
||||
@@ -57,7 +57,7 @@ test('buildRestoreCommand handles MariaDB without dumpAll', function () {
|
||||
$result = $component->buildRestoreCommand('/tmp/test.dump');
|
||||
|
||||
expect($result)->toContain('mariadb -u $MARIADB_USER');
|
||||
expect($result)->toContain('< /tmp/test.dump');
|
||||
expect($result)->toContain("< '/tmp/test.dump'");
|
||||
});
|
||||
|
||||
test('buildRestoreCommand always appends the MongoDB archive path', function (bool $dumpAll) {
|
||||
@@ -68,5 +68,5 @@ test('buildRestoreCommand always appends the MongoDB archive path', function (bo
|
||||
$result = $component->buildRestoreCommand('/tmp/test.dump');
|
||||
|
||||
expect($result)->toContain('mongorestore');
|
||||
expect($result)->toContain('--archive=/tmp/test.dump');
|
||||
expect($result)->toContain("--archive='/tmp/test.dump'");
|
||||
})->with([false, true]);
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\SendMessageToDiscordJob;
|
||||
use App\Jobs\SendMessageToSlackJob;
|
||||
use App\Notifications\Dto\DiscordMessage;
|
||||
use App\Notifications\Dto\SlackMessage;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
it('blocks queued Slack notifications to IPv4-mapped link-local URLs', function () {
|
||||
Http::fake();
|
||||
|
||||
$job = new SendMessageToSlackJob(
|
||||
new SlackMessage('Test', 'Description'),
|
||||
'http://[::ffff:169.254.169.254]/'
|
||||
);
|
||||
|
||||
$job->handle();
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('blocks queued Discord notifications to IPv4-mapped link-local URLs', function () {
|
||||
Http::fake();
|
||||
|
||||
$job = new SendMessageToDiscordJob(
|
||||
new DiscordMessage('Test', 'Description', DiscordMessage::infoColor()),
|
||||
'http://[::ffff:169.254.169.254]/'
|
||||
);
|
||||
|
||||
$job->handle();
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Policies\ApiTokenPolicy;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
it('allows any user to view any api tokens', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows any user to create api tokens', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows any user to manage api tokens', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->manage($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows owner to view their own api token', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->id = 1;
|
||||
|
||||
$token = Mockery::mock(PersonalAccessToken::class)->makePartial();
|
||||
$token->tokenable_id = 1;
|
||||
$token->tokenable_type = User::class;
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->view($user, $token))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-owner from viewing api token', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->id = 2;
|
||||
|
||||
$token = Mockery::mock(PersonalAccessToken::class)->makePartial();
|
||||
$token->tokenable_id = 1;
|
||||
$token->tokenable_type = User::class;
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->view($user, $token))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows owner to update their own api token', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->id = 1;
|
||||
|
||||
$token = Mockery::mock(PersonalAccessToken::class)->makePartial();
|
||||
$token->tokenable_id = 1;
|
||||
$token->tokenable_type = User::class;
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->update($user, $token))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-owner from updating api token', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->id = 2;
|
||||
|
||||
$token = Mockery::mock(PersonalAccessToken::class)->makePartial();
|
||||
$token->tokenable_id = 1;
|
||||
$token->tokenable_type = User::class;
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->update($user, $token))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows owner to delete their own api token', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->id = 1;
|
||||
|
||||
$token = Mockery::mock(PersonalAccessToken::class)->makePartial();
|
||||
$token->tokenable_id = 1;
|
||||
$token->tokenable_type = User::class;
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->delete($user, $token))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-owner from deleting api token', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->id = 2;
|
||||
|
||||
$token = Mockery::mock(PersonalAccessToken::class)->makePartial();
|
||||
$token->tokenable_id = 1;
|
||||
$token->tokenable_type = User::class;
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->delete($user, $token))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to use root permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useRootPermissions($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows owner to use root permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
$user->shouldReceive('isOwner')->andReturn(true);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useRootPermissions($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member from using root permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
$user->shouldReceive('isOwner')->andReturn(false);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useRootPermissions($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to use write permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useWritePermissions($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member from using write permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
$user->shouldReceive('isOwner')->andReturn(false);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useWritePermissions($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to use deploy permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useDeployPermissions($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows owner to use deploy permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
$user->shouldReceive('isOwner')->andReturn(true);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useDeployPermissions($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member from using deploy permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
$user->shouldReceive('isOwner')->andReturn(false);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useDeployPermissions($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to use sensitive permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useSensitivePermissions($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows owner to use sensitive permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
$user->shouldReceive('isOwner')->andReturn(true);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useSensitivePermissions($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member from using sensitive permissions', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
$user->shouldReceive('isOwner')->andReturn(false);
|
||||
|
||||
$policy = new ApiTokenPolicy;
|
||||
expect($policy->useSensitivePermissions($user))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\User;
|
||||
use App\Policies\ApplicationPolicy;
|
||||
|
||||
it('allows any user to view any applications', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view their own team application', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->view($user, $application))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-member to view another team application', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 2]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->view($user, $application))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies view when application has no team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn(null);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->view($user, $application))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to create an application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member to create an application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update their own team application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->update($user, $application)->allowed())->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to update their own team application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->update($user, $application)->allowed())->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies update when application has no team', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn(null);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->update($user, $application)->allowed())->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete their own team application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->delete($user, $application))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to delete their own team application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->delete($user, $application))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies delete when application has no team', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn(null);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->delete($user, $application))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to deploy their own team application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->deploy($user, $application))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to deploy their own team application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->deploy($user, $application))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage deployments', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->manageDeployments($user, $application))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to manage deployments', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->manageDeployments($user, $application))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage environment variables', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->manageEnvironment($user, $application))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to manage environment variables', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->manageEnvironment($user, $application))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to cleanup deployment queue', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->cleanupDeploymentQueue($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member to cleanup deployment queue', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->cleanupDeploymentQueue($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->restore($user, $application))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
|
||||
$policy = new ApplicationPolicy;
|
||||
expect($policy->forceDelete($user, $application))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\User;
|
||||
use App\Policies\ApplicationPreviewPolicy;
|
||||
|
||||
it('allows any user to view any application previews', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view application preview', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->view($user, $preview))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-team member from viewing application preview', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 2, 'pivot' => (object) ['role' => 'member']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->view($user, $preview))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies viewing application preview with null application', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn(null);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->view($user, $preview))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin user to create application preview', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin user from creating application preview', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update application preview', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->update($user, $preview)->allowed())->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member from updating application preview', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->update($user, $preview)->allowed())->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies updating application preview with null application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn(null);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
$response = $policy->update($user, $preview);
|
||||
expect($response->allowed())->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete application preview', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->delete($user, $preview))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member from deleting application preview', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->delete($user, $preview))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies deleting application preview with null application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn(null);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->delete($user, $preview))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to deploy application preview', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->deploy($user, $preview))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member from deploying application preview', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->deploy($user, $preview))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage preview deployments', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->manageDeployments($user, $preview))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member from managing preview deployments', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
$preview->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->manageDeployments($user, $preview))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restoring application preview', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->restore($user, $preview))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force deleting application preview', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$preview = Mockery::mock(ApplicationPreview::class)->makePartial();
|
||||
|
||||
$policy = new ApplicationPreviewPolicy;
|
||||
expect($policy->forceDelete($user, $preview))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\User;
|
||||
use App\Policies\ApplicationSettingPolicy;
|
||||
|
||||
it('allows any user to view any application settings', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view application setting', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
$setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->view($user, $setting))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-team member from viewing application setting', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 2, 'pivot' => (object) ['role' => 'member']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
$setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->view($user, $setting))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies viewing application setting with null application', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
$setting->shouldReceive('getAttribute')->with('application')->andReturn(null);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->view($user, $setting))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin user to create application setting', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin user from creating application setting', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update application setting', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
$setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->update($user, $setting))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member from updating application setting', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
$setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->update($user, $setting))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies updating application setting with null application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
$setting->shouldReceive('getAttribute')->with('application')->andReturn(null);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->update($user, $setting))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete application setting', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
$setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->delete($user, $setting))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member from deleting application setting', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$team = (object) ['id' => 1];
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->shouldReceive('team')->andReturn($team);
|
||||
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
$setting->shouldReceive('getAttribute')->with('application')->andReturn($application);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->delete($user, $setting))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies deleting application setting with null application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
$setting->shouldReceive('getAttribute')->with('application')->andReturn(null);
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->delete($user, $setting))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restoring application setting', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->restore($user, $setting))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force deleting application setting', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$setting = Mockery::mock(ApplicationSetting::class)->makePartial();
|
||||
|
||||
$policy = new ApplicationSettingPolicy;
|
||||
expect($policy->forceDelete($user, $setting))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\User;
|
||||
use App\Policies\DatabasePolicy;
|
||||
|
||||
it('allows any user to view any databases', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view their own team database', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->view($user, $database))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-member to view another team database', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 2]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->view($user, $database))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies view when database has no team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn(null);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->view($user, $database))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to create a database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member to create a database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update their own team database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->update($user, $database)->allowed())->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to update their own team database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->update($user, $database)->allowed())->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies update when database has no team', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn(null);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->update($user, $database)->allowed())->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete their own team database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->delete($user, $database))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to delete their own team database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->delete($user, $database))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies delete when database has no team', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn(null);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->delete($user, $database))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage their own team database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->manage($user, $database))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to manage their own team database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->manage($user, $database))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage backups', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->manageBackups($user, $database))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to manage backups', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->manageBackups($user, $database))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage database environment variables', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->manageEnvironment($user, $database))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to manage database environment variables', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
$database->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->manageEnvironment($user, $database))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->restore($user, $database))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$database = Mockery::mock(StandalonePostgresql::class)->makePartial();
|
||||
|
||||
$policy = new DatabasePolicy;
|
||||
expect($policy->forceDelete($user, $database))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Environment;
|
||||
use App\Models\User;
|
||||
use App\Policies\EnvironmentPolicy;
|
||||
|
||||
it('allows any user to view any environments', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view their own team environment', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
$environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->view($user, $environment))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-member to view another team environment', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
$environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 2]);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->view($user, $environment))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies view when environment has no project', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
$environment->shouldReceive('getAttribute')->with('project')->andReturn(null);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->view($user, $environment))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to create an environment', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member to create an environment', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update their own team environment', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
$environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->update($user, $environment))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to update their own team environment', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
$environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->update($user, $environment))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies update when environment has no project', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
$environment->shouldReceive('getAttribute')->with('project')->andReturn(null);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->update($user, $environment))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete their own team environment', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
$environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->delete($user, $environment))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to delete their own team environment', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
$environment->shouldReceive('getAttribute')->with('project')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->delete($user, $environment))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies delete when environment has no project', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
$environment->shouldReceive('getAttribute')->with('project')->andReturn(null);
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->delete($user, $environment))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->restore($user, $environment))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$environment = Mockery::mock(Environment::class)->makePartial();
|
||||
|
||||
$policy = new EnvironmentPolicy;
|
||||
expect($policy->forceDelete($user, $environment))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\User;
|
||||
use App\Policies\EnvironmentVariablePolicy;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
it('allows any user to view any environment variables', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->teams = new Collection([(object) ['id' => 1]]);
|
||||
|
||||
$resource = Mockery::mock(Application::class)->makePartial();
|
||||
$resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->view($user, $envVar))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-team member from viewing environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->teams = new Collection([(object) ['id' => 2]]);
|
||||
|
||||
$resource = Mockery::mock(Application::class)->makePartial();
|
||||
$resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->view($user, $envVar))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies view when resourceable is null', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->teams = new Collection([(object) ['id' => 1]]);
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn(null);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->view($user, $envVar))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to create environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member from creating environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$resource = Mockery::mock(Application::class)->makePartial();
|
||||
$resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->update($user, $envVar))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin from updating environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$resource = Mockery::mock(Application::class)->makePartial();
|
||||
$resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->update($user, $envVar))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies update when resourceable is null', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn(null);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->update($user, $envVar))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$resource = Mockery::mock(Application::class)->makePartial();
|
||||
$resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->delete($user, $envVar))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin from deleting environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$resource = Mockery::mock(Application::class)->makePartial();
|
||||
$resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->delete($user, $envVar))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies delete when resourceable is null', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn(null);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->delete($user, $envVar))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->restore($user, $envVar))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->forceDelete($user, $envVar))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage environment', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$resource = Mockery::mock(Application::class)->makePartial();
|
||||
$resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->manageEnvironment($user, $envVar))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin from managing environment', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$resource = Mockery::mock(Application::class)->makePartial();
|
||||
$resource->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn($resource);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->manageEnvironment($user, $envVar))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies manage environment when resourceable is null', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$envVar = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$envVar->shouldReceive('getAttribute')->with('resourceable')->andReturn(null);
|
||||
|
||||
$policy = new EnvironmentVariablePolicy;
|
||||
expect($policy->manageEnvironment($user, $envVar))->toBeFalse();
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\User;
|
||||
use App\Policies\GithubAppPolicy;
|
||||
|
||||
@@ -13,12 +14,7 @@ it('allows any user to view any github apps', function () {
|
||||
it('allows any user to view system-wide github app', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = true;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: true);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->view($user, $model))->toBeTrue();
|
||||
@@ -32,12 +28,7 @@ it('allows team member to view non-system-wide github app', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = false;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: false);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->view($user, $model))->toBeTrue();
|
||||
@@ -51,12 +42,7 @@ it('denies non-team member to view non-system-wide github app', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = false;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: false);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->view($user, $model))->toBeFalse();
|
||||
@@ -82,12 +68,7 @@ it('allows user with system access to update system-wide github app', function (
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('canAccessSystemResources')->andReturn(true);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = true;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: true);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->update($user, $model))->toBeTrue();
|
||||
@@ -97,12 +78,7 @@ it('denies user without system access to update system-wide github app', functio
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('canAccessSystemResources')->andReturn(false);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = true;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: true);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->update($user, $model))->toBeFalse();
|
||||
@@ -112,12 +88,7 @@ it('allows team admin to update non-system-wide github app', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = false;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: false);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->update($user, $model))->toBeTrue();
|
||||
@@ -127,12 +98,7 @@ it('denies team member to update non-system-wide github app', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = false;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: false);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->update($user, $model))->toBeFalse();
|
||||
@@ -142,12 +108,7 @@ it('allows user with system access to delete system-wide github app', function (
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('canAccessSystemResources')->andReturn(true);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = true;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: true);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->delete($user, $model))->toBeTrue();
|
||||
@@ -157,12 +118,7 @@ it('denies user without system access to delete system-wide github app', functio
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('canAccessSystemResources')->andReturn(false);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = true;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: true);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->delete($user, $model))->toBeFalse();
|
||||
@@ -172,12 +128,7 @@ it('allows team admin to delete non-system-wide github app', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = false;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: false);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->delete($user, $model))->toBeTrue();
|
||||
@@ -187,12 +138,7 @@ it('denies team member to delete non-system-wide github app', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = false;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: false);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->delete($user, $model))->toBeFalse();
|
||||
@@ -201,12 +147,7 @@ it('denies team member to delete non-system-wide github app', function () {
|
||||
it('denies restore of github app', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = false;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: false);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->restore($user, $model))->toBeFalse();
|
||||
@@ -215,13 +156,17 @@ it('denies restore of github app', function () {
|
||||
it('denies force delete of github app', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
|
||||
public $is_system_wide = false;
|
||||
};
|
||||
$model = mockGithubApp(teamId: 1, isSystemWide: false);
|
||||
|
||||
$policy = new GithubAppPolicy;
|
||||
expect($policy->forceDelete($user, $model))->toBeFalse();
|
||||
});
|
||||
|
||||
function mockGithubApp(int $teamId, bool $isSystemWide): GithubApp
|
||||
{
|
||||
$githubApp = Mockery::mock(GithubApp::class)->makePartial();
|
||||
$githubApp->team_id = $teamId;
|
||||
$githubApp->is_system_wide = $isSystemWide;
|
||||
|
||||
return $githubApp;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Policies\NotificationPolicy;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
it('allows team member to view notification settings', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->view($user, $notification))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-team member from viewing notification settings', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 2, 'pivot' => (object) ['role' => 'member']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->view($user, $notification))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies viewing notification settings with no team', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn(null);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->view($user, $notification))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update notification settings', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->update($user, $notification))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member from updating notification settings', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->update($user, $notification))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies updating notification settings with no team', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn(null);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->update($user, $notification))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage notification settings', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->manage($user, $notification))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member from managing notification settings', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->manage($user, $notification))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies managing notification settings with no team', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn(null);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->manage($user, $notification))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to send test notification', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->sendTest($user, $notification))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member from sending test notification', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->sendTest($user, $notification))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies sending test notification with no team', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn(null);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->sendTest($user, $notification))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team member to view but not update notification settings', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->view($user, $notification))->toBeTrue();
|
||||
expect($policy->update($user, $notification))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to view and update notification settings', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$notification = Mockery::mock(Model::class)->makePartial();
|
||||
$notification->shouldReceive('getAttribute')->with('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new NotificationPolicy;
|
||||
expect($policy->view($user, $notification))->toBeTrue();
|
||||
expect($policy->update($user, $notification))->toBeTrue();
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\User;
|
||||
use App\Policies\PrivateKeyPolicy;
|
||||
|
||||
@@ -11,10 +12,8 @@ it('allows root team admin to view system private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 0;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 0;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->view($user, $privateKey))->toBeTrue();
|
||||
@@ -28,10 +27,8 @@ it('allows root team owner to view system private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 0;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 0;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->view($user, $privateKey))->toBeTrue();
|
||||
@@ -45,10 +42,8 @@ it('denies regular member of root team to view system private key', function ()
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 0;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 0;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->view($user, $privateKey))->toBeFalse();
|
||||
@@ -62,10 +57,8 @@ it('denies non-root team member to view system private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 0;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 0;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->view($user, $privateKey))->toBeFalse();
|
||||
@@ -79,10 +72,8 @@ it('allows team member to view their own team private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 1;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->view($user, $privateKey))->toBeTrue();
|
||||
@@ -96,10 +87,8 @@ it('denies team member to view another team private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 2;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 2;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->view($user, $privateKey))->toBeFalse();
|
||||
@@ -113,10 +102,8 @@ it('allows root team admin to update system private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 0;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 0;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->update($user, $privateKey))->toBeTrue();
|
||||
@@ -130,10 +117,8 @@ it('denies root team member to update system private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 0;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 0;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->update($user, $privateKey))->toBeFalse();
|
||||
@@ -147,10 +132,8 @@ it('allows team admin to update their own team private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 1;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->update($user, $privateKey))->toBeTrue();
|
||||
@@ -164,10 +147,8 @@ it('denies team member to update their own team private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 1;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->update($user, $privateKey))->toBeFalse();
|
||||
@@ -181,10 +162,8 @@ it('allows root team admin to delete system private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 0;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 0;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->delete($user, $privateKey))->toBeTrue();
|
||||
@@ -198,10 +177,8 @@ it('denies root team member to delete system private key', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$privateKey = new class
|
||||
{
|
||||
public $team_id = 0;
|
||||
};
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->team_id = 0;
|
||||
|
||||
$policy = new PrivateKeyPolicy;
|
||||
expect($policy->delete($user, $privateKey))->toBeFalse();
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use App\Policies\ProjectPolicy;
|
||||
|
||||
it('allows any user to view any projects', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view their own team project', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$project = Mockery::mock(Project::class)->makePartial();
|
||||
$project->team_id = 1;
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->view($user, $project))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-member to view another team project', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$project = Mockery::mock(Project::class)->makePartial();
|
||||
$project->team_id = 2;
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->view($user, $project))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to create a project', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member to create a project', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update their own team project', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$project = Mockery::mock(Project::class)->makePartial();
|
||||
$project->team_id = 1;
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->update($user, $project))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to update their own team project', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$project = Mockery::mock(Project::class)->makePartial();
|
||||
$project->team_id = 1;
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->update($user, $project))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete their own team project', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$project = Mockery::mock(Project::class)->makePartial();
|
||||
$project->team_id = 1;
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->delete($user, $project))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to delete their own team project', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$project = Mockery::mock(Project::class)->makePartial();
|
||||
$project->team_id = 1;
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->delete($user, $project))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$project = Mockery::mock(Project::class)->makePartial();
|
||||
$project->team_id = 1;
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->restore($user, $project))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$project = Mockery::mock(Project::class)->makePartial();
|
||||
$project->team_id = 1;
|
||||
|
||||
$policy = new ProjectPolicy;
|
||||
expect($policy->forceDelete($user, $project))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\User;
|
||||
use App\Policies\ResourceCreatePolicy;
|
||||
|
||||
it('allows admin to create any resource', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ResourceCreatePolicy;
|
||||
expect($policy->createAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member from creating any resource', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ResourceCreatePolicy;
|
||||
expect($policy->createAny($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to create a valid resource class', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ResourceCreatePolicy;
|
||||
expect($policy->create($user, Application::class))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member from creating a valid resource class', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ResourceCreatePolicy;
|
||||
expect($policy->create($user, Application::class))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies admin from creating an invalid resource class', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ResourceCreatePolicy;
|
||||
expect($policy->create($user, 'App\Models\NonExistent'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to authorize all resource creation', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ResourceCreatePolicy;
|
||||
expect($policy->authorizeAllResourceCreation($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member from authorizing all resource creation', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ResourceCreatePolicy;
|
||||
expect($policy->authorizeAllResourceCreation($user))->toBeFalse();
|
||||
});
|
||||
@@ -52,7 +52,23 @@ it('allows team admin to update S3 storage from their team', function () {
|
||||
expect($policy->update($user, $storage))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to update S3 storage from another team', function () {
|
||||
it('denies team member to update S3 storage from their team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$storage = Mockery::mock(S3Storage::class)->makePartial();
|
||||
$storage->shouldReceive('getAttribute')->with('team_id')->andReturn(1);
|
||||
$storage->team_id = 1;
|
||||
|
||||
$policy = new S3StoragePolicy;
|
||||
expect($policy->update($user, $storage))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies team admin to update S3 storage from another team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
|
||||
]);
|
||||
@@ -68,9 +84,9 @@ it('denies team member to update S3 storage from another team', function () {
|
||||
expect($policy->update($user, $storage))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team member to delete S3 storage from their team', function () {
|
||||
it('allows team admin to delete S3 storage from their team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
@@ -84,7 +100,23 @@ it('allows team member to delete S3 storage from their team', function () {
|
||||
expect($policy->delete($user, $storage))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to delete S3 storage from another team', function () {
|
||||
it('denies team member to delete S3 storage from their team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$storage = Mockery::mock(S3Storage::class)->makePartial();
|
||||
$storage->shouldReceive('getAttribute')->with('team_id')->andReturn(1);
|
||||
$storage->team_id = 1;
|
||||
|
||||
$policy = new S3StoragePolicy;
|
||||
expect($policy->delete($user, $storage))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies team admin to delete S3 storage from another team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'owner']],
|
||||
]);
|
||||
@@ -116,9 +148,9 @@ it('denies non-admin to create S3 storage', function () {
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team member to validate connection of S3 storage from their team', function () {
|
||||
it('allows team admin to validate connection of S3 storage from their team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
@@ -132,7 +164,23 @@ it('allows team member to validate connection of S3 storage from their team', fu
|
||||
expect($policy->validateConnection($user, $storage))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to validate connection of S3 storage from another team', function () {
|
||||
it('denies team member to validate connection of S3 storage from their team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$storage = Mockery::mock(S3Storage::class)->makePartial();
|
||||
$storage->shouldReceive('getAttribute')->with('team_id')->andReturn(1);
|
||||
$storage->team_id = 1;
|
||||
|
||||
$policy = new S3StoragePolicy;
|
||||
expect($policy->validateConnection($user, $storage))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies team admin to validate connection of S3 storage from another team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']],
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Policies\ServerPolicy;
|
||||
|
||||
it('allows any user to view any servers', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view their own team server', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->view($user, $server))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-member to view another team server', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 2;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->view($user, $server))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to create a server', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member to create a server', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update their own team server', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->update($user, $server))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to update their own team server', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->update($user, $server))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete their own team server', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->delete($user, $server))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to delete their own team server', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->delete($user, $server))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->restore($user, $server))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->forceDelete($user, $server))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage proxy', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->manageProxy($user, $server))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to manage proxy', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->manageProxy($user, $server))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage sentinel, ca certificate, and view security', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->manageSentinel($user, $server))->toBeTrue();
|
||||
expect($policy->viewSentinel($user, $server))->toBeTrue();
|
||||
expect($policy->manageCaCertificate($user, $server))->toBeTrue();
|
||||
expect($policy->viewSecurity($user, $server))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to view sentinel', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->team_id = 1;
|
||||
|
||||
$policy = new ServerPolicy;
|
||||
expect($policy->viewSentinel($user, $server))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\User;
|
||||
use App\Policies\ServiceApplicationPolicy;
|
||||
|
||||
it('allows admin to create service application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ServiceApplicationPolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member from creating service application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ServiceApplicationPolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for service application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$serviceApp = Mockery::mock(ServiceApplication::class)->makePartial();
|
||||
|
||||
$policy = new ServiceApplicationPolicy;
|
||||
expect($policy->restore($user, $serviceApp))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for service application', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$serviceApp = Mockery::mock(ServiceApplication::class)->makePartial();
|
||||
|
||||
$policy = new ServiceApplicationPolicy;
|
||||
expect($policy->forceDelete($user, $serviceApp))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\User;
|
||||
use App\Policies\ServiceDatabasePolicy;
|
||||
|
||||
it('allows admin to create service database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ServiceDatabasePolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member from creating service database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ServiceDatabasePolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for service database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$serviceDb = Mockery::mock(ServiceDatabase::class)->makePartial();
|
||||
|
||||
$policy = new ServiceDatabasePolicy;
|
||||
expect($policy->restore($user, $serviceDb))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for service database', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$serviceDb = Mockery::mock(ServiceDatabase::class)->makePartial();
|
||||
|
||||
$policy = new ServiceDatabasePolicy;
|
||||
expect($policy->forceDelete($user, $serviceDb))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Service;
|
||||
use App\Models\User;
|
||||
use App\Policies\ServicePolicy;
|
||||
|
||||
it('allows any user to view any services', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view their own team service', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->view($user, $service))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-member to view another team service', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 2]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->view($user, $service))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies view when service has no team', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn(null);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->view($user, $service))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to create a service', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies member to create a service', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update their own team service', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->update($user, $service))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to update their own team service', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->update($user, $service))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete their own team service', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->delete($user, $service))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to delete their own team service', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->delete($user, $service))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies delete when service has no team', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn(null);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->delete($user, $service))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to deploy their own team service', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->deploy($user, $service))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to deploy their own team service', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->deploy($user, $service))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to stop their own team service', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->stop($user, $service))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to stop their own team service', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->stop($user, $service))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to manage service environment variables', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->manageEnvironment($user, $service))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to manage service environment variables', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->manageEnvironment($user, $service))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to access terminal', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->accessTerminal($user, $service))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies team member to access terminal', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('team')->andReturn((object) ['id' => 1]);
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->accessTerminal($user, $service))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->restore($user, $service))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for any user', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
|
||||
$policy = new ServicePolicy;
|
||||
expect($policy->forceDelete($user, $service))->toBeFalse();
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Models\User;
|
||||
use App\Policies\SharedEnvironmentVariablePolicy;
|
||||
|
||||
@@ -18,10 +19,7 @@ it('allows team member to view their team shared environment variable', function
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$model = mockSharedEnvironmentVariable(teamId: 1);
|
||||
|
||||
$policy = new SharedEnvironmentVariablePolicy;
|
||||
expect($policy->view($user, $model))->toBeTrue();
|
||||
@@ -35,10 +33,7 @@ it('denies non-team member to view shared environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 2;
|
||||
};
|
||||
$model = mockSharedEnvironmentVariable(teamId: 2);
|
||||
|
||||
$policy = new SharedEnvironmentVariablePolicy;
|
||||
expect($policy->view($user, $model))->toBeFalse();
|
||||
@@ -64,10 +59,7 @@ it('allows team admin to update shared environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$model = mockSharedEnvironmentVariable(teamId: 1);
|
||||
|
||||
$policy = new SharedEnvironmentVariablePolicy;
|
||||
expect($policy->update($user, $model))->toBeTrue();
|
||||
@@ -77,10 +69,7 @@ it('denies team member to update shared environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$model = mockSharedEnvironmentVariable(teamId: 1);
|
||||
|
||||
$policy = new SharedEnvironmentVariablePolicy;
|
||||
expect($policy->update($user, $model))->toBeFalse();
|
||||
@@ -90,10 +79,7 @@ it('allows team admin to delete shared environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$model = mockSharedEnvironmentVariable(teamId: 1);
|
||||
|
||||
$policy = new SharedEnvironmentVariablePolicy;
|
||||
expect($policy->delete($user, $model))->toBeTrue();
|
||||
@@ -103,10 +89,7 @@ it('denies team member to delete shared environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$model = mockSharedEnvironmentVariable(teamId: 1);
|
||||
|
||||
$policy = new SharedEnvironmentVariablePolicy;
|
||||
expect($policy->delete($user, $model))->toBeFalse();
|
||||
@@ -115,10 +98,7 @@ it('denies team member to delete shared environment variable', function () {
|
||||
it('denies restore of shared environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$model = mockSharedEnvironmentVariable(teamId: 1);
|
||||
|
||||
$policy = new SharedEnvironmentVariablePolicy;
|
||||
expect($policy->restore($user, $model))->toBeFalse();
|
||||
@@ -127,10 +107,7 @@ it('denies restore of shared environment variable', function () {
|
||||
it('denies force delete of shared environment variable', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$model = mockSharedEnvironmentVariable(teamId: 1);
|
||||
|
||||
$policy = new SharedEnvironmentVariablePolicy;
|
||||
expect($policy->forceDelete($user, $model))->toBeFalse();
|
||||
@@ -140,10 +117,7 @@ it('allows team admin to manage environment', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$model = mockSharedEnvironmentVariable(teamId: 1);
|
||||
|
||||
$policy = new SharedEnvironmentVariablePolicy;
|
||||
expect($policy->manageEnvironment($user, $model))->toBeTrue();
|
||||
@@ -153,11 +127,16 @@ it('denies team member to manage environment', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$model = new class
|
||||
{
|
||||
public $team_id = 1;
|
||||
};
|
||||
$model = mockSharedEnvironmentVariable(teamId: 1);
|
||||
|
||||
$policy = new SharedEnvironmentVariablePolicy;
|
||||
expect($policy->manageEnvironment($user, $model))->toBeFalse();
|
||||
});
|
||||
|
||||
function mockSharedEnvironmentVariable(int $teamId): SharedEnvironmentVariable
|
||||
{
|
||||
$sharedEnvironmentVariable = Mockery::mock(SharedEnvironmentVariable::class)->makePartial();
|
||||
$sharedEnvironmentVariable->team_id = $teamId;
|
||||
|
||||
return $sharedEnvironmentVariable;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\User;
|
||||
use App\Policies\StandaloneDockerPolicy;
|
||||
|
||||
it('allows any user to view any standalone docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view their team standalone docker', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
|
||||
$standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->view($user, $standaloneDocker))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies user from viewing another team standalone docker', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
|
||||
$standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 2]);
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->view($user, $standaloneDocker))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to create standalone docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin from creating standalone docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update standalone docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
|
||||
$standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->update($user, $standaloneDocker))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin from updating standalone docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
|
||||
$standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->update($user, $standaloneDocker))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete standalone docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
|
||||
$standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->delete($user, $standaloneDocker))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin from deleting standalone docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
|
||||
$standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->delete($user, $standaloneDocker))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for standalone docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
|
||||
$standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->restore($user, $standaloneDocker))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for standalone docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$standaloneDocker = Mockery::mock(StandaloneDocker::class)->makePartial();
|
||||
$standaloneDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new StandaloneDockerPolicy;
|
||||
expect($policy->forceDelete($user, $standaloneDocker))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Models\User;
|
||||
use App\Policies\SwarmDockerPolicy;
|
||||
|
||||
it('allows any user to view any swarm docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows team member to view their team swarm docker', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
|
||||
$swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->view($user, $swarmDocker))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies user from viewing another team swarm docker', function () {
|
||||
$teams = collect([
|
||||
(object) ['id' => 1],
|
||||
]);
|
||||
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
|
||||
|
||||
$swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
|
||||
$swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 2]);
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->view($user, $swarmDocker))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows admin to create swarm docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin from creating swarm docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdmin')->andReturn(false);
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->create($user))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to update swarm docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
|
||||
$swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->update($user, $swarmDocker))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin from updating swarm docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
|
||||
$swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->update($user, $swarmDocker))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows team admin to delete swarm docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
|
||||
$swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
|
||||
$swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->delete($user, $swarmDocker))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-admin from deleting swarm docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
|
||||
$swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
|
||||
$swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->delete($user, $swarmDocker))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies restore for swarm docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
|
||||
$swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->restore($user, $swarmDocker))->toBeFalse();
|
||||
});
|
||||
|
||||
it('denies force delete for swarm docker', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
$swarmDocker = Mockery::mock(SwarmDocker::class)->makePartial();
|
||||
$swarmDocker->shouldReceive('getAttribute')->with('server')->andReturn((object) ['team_id' => 1]);
|
||||
|
||||
$policy = new SwarmDockerPolicy;
|
||||
expect($policy->forceDelete($user, $swarmDocker))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Policies\TeamPolicy;
|
||||
|
||||
function teamPolicyUserWithTeams(array $teamIds): User
|
||||
{
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
$user->shouldReceive('getAttribute')->with('teams')->andReturn(collect(
|
||||
array_map(fn (int $teamId): object => (object) ['id' => $teamId], $teamIds)
|
||||
));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function teamPolicyTeam(int $teamId): Team
|
||||
{
|
||||
$team = Mockery::mock(Team::class)->makePartial();
|
||||
$team->shouldReceive('getAttribute')->with('id')->andReturn($teamId);
|
||||
|
||||
return $team;
|
||||
}
|
||||
|
||||
it('allows any authenticated user to view any teams list', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
expect((new TeamPolicy)->viewAny($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows authenticated users to create teams', function () {
|
||||
$user = Mockery::mock(User::class)->makePartial();
|
||||
|
||||
expect((new TeamPolicy)->create($user))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows target team members to view the team', function () {
|
||||
$user = teamPolicyUserWithTeams([1]);
|
||||
$team = teamPolicyTeam(1);
|
||||
|
||||
expect((new TeamPolicy)->view($user, $team))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies non-members from viewing the team', function () {
|
||||
$user = teamPolicyUserWithTeams([2]);
|
||||
$team = teamPolicyTeam(1);
|
||||
|
||||
expect((new TeamPolicy)->view($user, $team))->toBeFalse();
|
||||
});
|
||||
|
||||
it('allows target team admins to perform privileged team actions', function (string $ability) {
|
||||
$user = teamPolicyUserWithTeams([1]);
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
|
||||
$team = teamPolicyTeam(1);
|
||||
|
||||
expect((new TeamPolicy)->{$ability}($user, $team))->toBeTrue();
|
||||
})->with([
|
||||
'update',
|
||||
'delete',
|
||||
'manageMembers',
|
||||
'viewAdmin',
|
||||
'manageInvitations',
|
||||
]);
|
||||
|
||||
it('denies target team members even when their current session role is admin elsewhere', function (string $ability) {
|
||||
$user = teamPolicyUserWithTeams([1, 2]);
|
||||
$user->shouldReceive('isAdmin')->andReturn(true);
|
||||
$user->shouldReceive('isOwner')->andReturn(false);
|
||||
$user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
|
||||
$team = teamPolicyTeam(1);
|
||||
|
||||
expect((new TeamPolicy)->{$ability}($user, $team))->toBeFalse();
|
||||
})->with([
|
||||
'update',
|
||||
'delete',
|
||||
'manageMembers',
|
||||
'viewAdmin',
|
||||
'manageInvitations',
|
||||
]);
|
||||
|
||||
it('denies non-members from privileged team actions', function (string $ability) {
|
||||
$user = teamPolicyUserWithTeams([2]);
|
||||
$team = teamPolicyTeam(1);
|
||||
|
||||
expect((new TeamPolicy)->{$ability}($user, $team))->toBeFalse();
|
||||
})->with([
|
||||
'update',
|
||||
'delete',
|
||||
'manageMembers',
|
||||
'viewAdmin',
|
||||
'manageInvitations',
|
||||
]);
|
||||
@@ -37,10 +37,8 @@ test('validateBucketName accepts valid bucket names', function () {
|
||||
|
||||
// Valid bucket names
|
||||
expect($method->invoke($component, 'my-bucket'))->toBeTrue();
|
||||
expect($method->invoke($component, 'my_bucket'))->toBeTrue();
|
||||
expect($method->invoke($component, 'mybucket123'))->toBeTrue();
|
||||
expect($method->invoke($component, 'my.bucket.name'))->toBeTrue();
|
||||
expect($method->invoke($component, 'Bucket-Name_123'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('validateBucketName rejects invalid bucket names', function () {
|
||||
@@ -55,6 +53,9 @@ test('validateBucketName rejects invalid bucket names', function () {
|
||||
expect($method->invoke($component, 'bucket&ls'))->toBeFalse();
|
||||
expect($method->invoke($component, "bucket\nid"))->toBeFalse();
|
||||
expect($method->invoke($component, 'bucket name'))->toBeFalse(); // Space not allowed in bucket
|
||||
expect($method->invoke($component, 'my_bucket'))->toBeFalse();
|
||||
expect($method->invoke($component, 'Bucket-Name'))->toBeFalse();
|
||||
expect($method->invoke($component, '192.168.1.1'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('validateS3Path accepts valid S3 paths', function () {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
it('generates lowercase URL-safe public ids without Cuid2', function () {
|
||||
$ids = collect(range(1, 100))->map(fn () => new_public_id());
|
||||
|
||||
expect($ids)->each
|
||||
->toBeString()
|
||||
->toHaveLength(24)
|
||||
->toMatch('/^[a-z0-9]+$/');
|
||||
|
||||
expect($ids->unique())->toHaveCount(100);
|
||||
});
|
||||
|
||||
it('honors custom public id lengths', function () {
|
||||
expect(new_public_id(32))
|
||||
->toBeString()
|
||||
->toHaveLength(32)
|
||||
->toMatch('/^[a-z0-9]+$/');
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
function rayRemovalFiles(string $directory): array
|
||||
{
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
|
||||
$files = [];
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
|
||||
sort($files);
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
function rayRemovalBasePath(string $path = ''): string
|
||||
{
|
||||
return dirname(__DIR__, 2).($path === '' ? '' : DIRECTORY_SEPARATOR.$path);
|
||||
}
|
||||
|
||||
it('does not include Ray as a dependency', function () {
|
||||
$composer = json_decode(file_get_contents(rayRemovalBasePath('composer.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$lock = json_decode(file_get_contents(rayRemovalBasePath('composer.lock')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$lockedPackageNames = collect([
|
||||
...($lock['packages'] ?? []),
|
||||
...($lock['packages-dev'] ?? []),
|
||||
])->pluck('name');
|
||||
|
||||
expect($composer['require'] ?? [])->not->toHaveKey('spatie/laravel-ray')
|
||||
->and($composer['require-dev'] ?? [])->not->toHaveKey('spatie/laravel-ray')
|
||||
->and($lockedPackageNames)->not->toContain('spatie/laravel-ray', 'spatie/ray');
|
||||
});
|
||||
|
||||
it('does not ship Ray configuration', function () {
|
||||
expect(file_exists(rayRemovalBasePath('config/ray.php')))->toBeFalse();
|
||||
});
|
||||
|
||||
it('does not call ray from application code', function () {
|
||||
$files = [
|
||||
...rayRemovalFiles(rayRemovalBasePath('app')),
|
||||
...rayRemovalFiles(rayRemovalBasePath('bootstrap/helpers')),
|
||||
];
|
||||
|
||||
$rayCalls = collect($files)
|
||||
->filter(fn (string $file): bool => preg_match('/\bray\s*\(/', file_get_contents($file)) === 1)
|
||||
->map(fn (string $file): string => str_replace(rayRemovalBasePath().'/', '', $file))
|
||||
->values();
|
||||
|
||||
expect($rayCalls)->toBeEmpty();
|
||||
});
|
||||
@@ -1,21 +1,23 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\S3Storage;
|
||||
use App\Rules\SafeWebhookUrl;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* Regression tests for SSRF via S3 Storage endpoint.
|
||||
* Regression tests for S3 Storage endpoint validation.
|
||||
*
|
||||
* The Livewire forms (Create.php, Form.php) and the model-level defense in
|
||||
* S3Storage::testConnection() share the same SafeWebhookUrl rule. These tests
|
||||
* assert the rule rejects the concrete payloads and that the model refuses to
|
||||
* build an S3 client for an unsafe endpoint.
|
||||
*/
|
||||
it('rejects SSRF payloads on the S3 endpoint', function (string $endpoint) {
|
||||
it('rejects disallowed targets on the S3 endpoint', function (string $endpoint) {
|
||||
$validator = Validator::make(
|
||||
['endpoint' => $endpoint],
|
||||
['endpoint' => ['required', 'max:255', new SafeWebhookUrl]],
|
||||
@@ -23,8 +25,9 @@ it('rejects SSRF payloads on the S3 endpoint', function (string $endpoint) {
|
||||
|
||||
expect($validator->fails())->toBeTrue("Expected rejection: {$endpoint}");
|
||||
})->with([
|
||||
'AWS IMDS' => 'http://169.254.169.254/latest/meta-data/',
|
||||
'AWS IMDS bare' => 'http://169.254.169.254',
|
||||
'link-local address' => 'http://169.254.169.254/',
|
||||
'link-local address bare' => 'http://169.254.169.254',
|
||||
'link-local address IPv4-mapped IPv6' => 'http://[::ffff:169.254.169.254]/',
|
||||
'GCP metadata via link-local' => 'http://169.254.0.1',
|
||||
'loopback v4' => 'http://127.0.0.1',
|
||||
'loopback Redis' => 'http://127.0.0.1:6379',
|
||||
@@ -42,19 +45,15 @@ it('rejects SSRF payloads on the S3 endpoint', function (string $endpoint) {
|
||||
it('accepts real-world S3 endpoints', function (string $endpoint) {
|
||||
$validator = Validator::make(
|
||||
['endpoint' => $endpoint],
|
||||
['endpoint' => ['required', 'max:255', new SafeWebhookUrl]],
|
||||
['endpoint' => ['required', 'max:255', new SafeWebhookUrl(fn (string $host): array => ['93.184.216.34'])]],
|
||||
);
|
||||
|
||||
expect($validator->passes())->toBeTrue("Expected accepted: {$endpoint}");
|
||||
})->with([
|
||||
'AWS S3' => 'https://s3.us-east-1.amazonaws.com',
|
||||
'Cloudflare R2' => 'https://fake.r2.cloudflarestorage.com',
|
||||
'DigitalOcean Spaces' => 'https://nyc3.digitaloceanspaces.com',
|
||||
'Backblaze B2' => 'https://s3.us-west-001.backblazeb2.com',
|
||||
'Self-hosted MinIO on 10.x' => 'http://10.0.0.5:9000',
|
||||
'Self-hosted MinIO on 172.16.x' => 'http://172.16.0.10:9000',
|
||||
'Self-hosted MinIO on 192.168.x' => 'http://192.168.1.50:9000',
|
||||
'Custom domain MinIO' => 'https://minio.example.com',
|
||||
'Custom public domain S3-compatible endpoint' => 'https://example.com',
|
||||
]);
|
||||
|
||||
it('blocks testConnection() on an unsafe endpoint without issuing HTTP', function () {
|
||||
@@ -87,5 +86,21 @@ it('blocks testConnection() for loopback endpoints', function (string $endpoint)
|
||||
'http loopback' => 'http://127.0.0.1:6379',
|
||||
'localhost' => 'http://localhost:9000',
|
||||
'IPv6 loopback' => 'http://[::1]',
|
||||
'IPv4-mapped IPv6 link-local' => 'http://[::ffff:169.254.169.254]',
|
||||
'internal TLD' => 'http://backend.internal',
|
||||
]);
|
||||
|
||||
it('accepts explicitly allowlisted intranet S3 endpoints', function (string $endpoint, array $allowlist) {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], ['webhook_allowed_internal_hosts' => $allowlist]));
|
||||
|
||||
$validator = Validator::make(
|
||||
['endpoint' => $endpoint],
|
||||
['endpoint' => ['required', 'max:255', new SafeWebhookUrl]],
|
||||
);
|
||||
|
||||
expect($validator->passes())->toBeTrue("Expected allowlisted intranet S3 endpoint: {$endpoint}");
|
||||
})->with([
|
||||
'Self-hosted MinIO on 10.x CIDR' => ['http://10.0.0.5:9000', ['10.0.0.0/8']],
|
||||
'Self-hosted MinIO on 172.16.x CIDR' => ['http://172.16.0.10:9000', ['172.16.0.0/12']],
|
||||
'Self-hosted MinIO on 192.168.x exact IP' => ['http://192.168.1.50:9000', ['192.168.1.50']],
|
||||
]);
|
||||
@@ -53,6 +53,7 @@ test('S3Storage model fillable attributes are configured correctly', function ()
|
||||
$s3Storage = new S3Storage;
|
||||
|
||||
expect($s3Storage->getFillable())->toBe([
|
||||
'team_id',
|
||||
'name',
|
||||
'description',
|
||||
'region',
|
||||
@@ -74,6 +75,7 @@ test('S3Storage connection validation uses short s3 client timeouts', function (
|
||||
->with(Mockery::on(function (array $config) {
|
||||
expect($config['http']['connect_timeout'])->toBe(15);
|
||||
expect($config['http']['timeout'])->toBe(15);
|
||||
expect($config['http']['allow_redirects'])->toBeFalse();
|
||||
|
||||
return true;
|
||||
}))
|
||||
@@ -118,3 +120,27 @@ test('S3Storage connection validation returns friendly timeout error', function
|
||||
|
||||
expect($s3Storage->is_usable)->toBeFalse();
|
||||
});
|
||||
|
||||
test('S3Storage testConnection rejects invalid bucket before building client', function (string $bucket) {
|
||||
Storage::shouldReceive('build')->never();
|
||||
|
||||
$s3Storage = new S3Storage;
|
||||
$s3Storage->setRawAttributes([
|
||||
'name' => 'Test S3',
|
||||
'region' => 'us-east-1',
|
||||
'key' => 'AKIAEXAMPLE',
|
||||
'secret' => 'secret',
|
||||
'bucket' => $bucket,
|
||||
'endpoint' => 'https://s3.amazonaws.com',
|
||||
]);
|
||||
|
||||
expect(fn () => $s3Storage->testConnection())
|
||||
->toThrow(RuntimeException::class, 'S3 bucket name is not allowed');
|
||||
})->with([
|
||||
'semicolon injection' => ['lab; id; #'],
|
||||
'command substitution' => ['lab$(id)'],
|
||||
'backticks' => ['lab`id`'],
|
||||
'newline' => ["lab\nid"],
|
||||
'underscore' => ['lab_bucket'],
|
||||
'uppercase' => ['LabBucket'],
|
||||
]);
|
||||
@@ -11,7 +11,7 @@ it('accepts valid public URLs', function () {
|
||||
|
||||
$validUrls = [
|
||||
'https://api.github.com',
|
||||
'https://github.example.com/api/v3',
|
||||
'https://github.com/api/v3',
|
||||
'https://example.com',
|
||||
'http://example.com',
|
||||
];
|
||||
@@ -22,6 +22,14 @@ it('accepts valid public URLs', function () {
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts custom external hostnames that resolve to public IPs', function () {
|
||||
$rule = new SafeExternalUrl(fn (string $host): array => ['93.184.216.34']);
|
||||
|
||||
$validator = Validator::make(['url' => 'https://github.example.com/api/v3'], ['url' => $rule]);
|
||||
|
||||
expect($validator->passes())->toBeTrue('Expected valid custom external hostname');
|
||||
});
|
||||
|
||||
it('rejects private IPv4 addresses', function (string $url) {
|
||||
$rule = new SafeExternalUrl;
|
||||
|
||||
@@ -42,6 +50,34 @@ it('rejects cloud metadata IP', function () {
|
||||
expect($validator->fails())->toBeTrue('Expected rejection: cloud metadata IP');
|
||||
});
|
||||
|
||||
it('rejects hostnames that resolve to private or reserved addresses', function (string $url, array $resolvedIps) {
|
||||
$rule = new SafeExternalUrl(fn (string $host): array => $resolvedIps);
|
||||
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue("Expected rejection after DNS resolution: {$url}");
|
||||
})->with([
|
||||
'hostname to link-local IP' => ['http://169.254.169.254.nip.io/', ['169.254.169.254']],
|
||||
'hostname to loopback' => ['http://loopback.example.test/', ['127.0.0.1']],
|
||||
'hostname to private IPv4' => ['http://private.example.test/', ['10.0.0.1']],
|
||||
'hostname to IPv6 loopback' => ['http://ipv6-loopback.example.test/', ['::1']],
|
||||
'hostname to IPv6 link-local' => ['http://ipv6-link-local.example.test/', ['fe80::1']],
|
||||
'hostname to IPv6 ULA' => ['http://ipv6-ula.example.test/', ['fc00::1']],
|
||||
'hostname to mapped private IPv4' => ['http://mapped-private.example.test/', ['::ffff:10.0.0.1']],
|
||||
]);
|
||||
|
||||
it('rejects IPv4-mapped IPv6 literals for private or reserved IPv4 ranges', function (string $url) {
|
||||
$rule = new SafeExternalUrl;
|
||||
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue("Expected rejection: {$url}");
|
||||
})->with([
|
||||
'mapped link-local IP' => 'http://[::ffff:169.254.169.254]/',
|
||||
'mapped loopback' => 'http://[::ffff:127.0.0.1]/',
|
||||
'mapped private' => 'http://[::ffff:10.0.0.1]/',
|
||||
]);
|
||||
|
||||
it('rejects localhost and internal hostnames', function (string $url) {
|
||||
$rule = new SafeExternalUrl;
|
||||
|
||||
@@ -50,9 +86,12 @@ it('rejects localhost and internal hostnames', function (string $url) {
|
||||
})->with([
|
||||
'localhost' => 'http://localhost',
|
||||
'localhost with port' => 'http://localhost:8080',
|
||||
'localhost with trailing dot' => 'http://localhost.',
|
||||
'zero address' => 'http://0.0.0.0',
|
||||
'.local domain' => 'http://myservice.local',
|
||||
'.local domain with trailing dot' => 'http://myservice.local.',
|
||||
'.internal domain' => 'http://myservice.internal',
|
||||
'.internal domain with trailing dot' => 'http://myservice.internal.',
|
||||
]);
|
||||
|
||||
it('rejects non-URL strings', function (string $value) {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Rules\SafeWebhookUrl;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
it('accepts valid public URLs', function () {
|
||||
$rule = new SafeWebhookUrl;
|
||||
$rule = new SafeWebhookUrl(fn (string $host): array => ['93.184.216.34']);
|
||||
|
||||
$validUrls = [
|
||||
'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXX',
|
||||
@@ -22,17 +24,6 @@ it('accepts valid public URLs', function () {
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts private network IPs for self-hosted deployments', function (string $url) {
|
||||
$rule = new SafeWebhookUrl;
|
||||
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
expect($validator->passes())->toBeTrue("Expected valid (private IP): {$url}");
|
||||
})->with([
|
||||
'10.x range' => 'http://10.0.0.5/webhook',
|
||||
'172.16.x range' => 'http://172.16.0.1:8080/hook',
|
||||
'192.168.x range' => 'http://192.168.1.50:8080/webhook',
|
||||
]);
|
||||
|
||||
it('rejects loopback addresses', function (string $url) {
|
||||
$rule = new SafeWebhookUrl;
|
||||
|
||||
@@ -59,6 +50,33 @@ it('rejects link-local range', function () {
|
||||
expect($validator->fails())->toBeTrue('Expected rejection: link-local IP');
|
||||
});
|
||||
|
||||
it('rejects hostnames that resolve to blocked addresses', function (string $url, array $resolvedIps) {
|
||||
$rule = new SafeWebhookUrl(fn (string $host): array => $resolvedIps);
|
||||
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue("Expected rejection after DNS resolution: {$url}");
|
||||
})->with([
|
||||
'hostname to link-local IP' => ['http://169.254.169.254.nip.io/', ['169.254.169.254']],
|
||||
'hostname to loopback' => ['http://loopback.example.test/', ['127.0.0.1']],
|
||||
'hostname to IPv6 loopback' => ['http://ipv6-loopback.example.test/', ['::1']],
|
||||
'hostname to IPv6 link-local' => ['http://ipv6-link-local.example.test/', ['fe80::1']],
|
||||
'hostname to IPv6 ULA' => ['http://ipv6-ula.example.test/', ['fc00::1']],
|
||||
'hostname to mapped link-local IP' => ['http://mapped-link-local.example.test/', ['::ffff:169.254.169.254']],
|
||||
]);
|
||||
|
||||
it('rejects IPv4-mapped IPv6 literals for blocked IPv4 ranges', function (string $url) {
|
||||
$rule = new SafeWebhookUrl;
|
||||
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue("Expected rejection: {$url}");
|
||||
})->with([
|
||||
'mapped link-local IP' => 'http://[::ffff:169.254.169.254]/',
|
||||
'mapped loopback' => 'http://[::ffff:127.0.0.1]/',
|
||||
'mapped zero' => 'http://[::ffff:0.0.0.0]/',
|
||||
]);
|
||||
|
||||
it('rejects localhost and internal hostnames', function (string $url) {
|
||||
$rule = new SafeWebhookUrl;
|
||||
|
||||
@@ -67,7 +85,9 @@ it('rejects localhost and internal hostnames', function (string $url) {
|
||||
})->with([
|
||||
'localhost' => 'http://localhost',
|
||||
'localhost with port' => 'http://localhost:8080',
|
||||
'localhost with trailing dot' => 'http://localhost.',
|
||||
'.internal domain' => 'http://myservice.internal',
|
||||
'.internal domain with trailing dot' => 'http://myservice.internal.',
|
||||
]);
|
||||
|
||||
it('rejects non-http schemes', function (string $value) {
|
||||
@@ -88,3 +108,173 @@ it('rejects IPv6 loopback', function () {
|
||||
$validator = Validator::make(['url' => 'http://[::1]'], ['url' => $rule]);
|
||||
expect($validator->fails())->toBeTrue('Expected rejection: IPv6 loopback');
|
||||
});
|
||||
|
||||
it('rejects private and reserved network targets by default', function (string $url) {
|
||||
$rule = new SafeWebhookUrl;
|
||||
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue("Expected default rejection: {$url}");
|
||||
})->with([
|
||||
'private 10/8' => 'http://10.0.0.5/webhook',
|
||||
'private 172.16/12' => 'http://172.16.0.1:8080/hook',
|
||||
'private 192.168/16' => 'http://192.168.1.50:8080/webhook',
|
||||
'shared address space' => 'http://100.64.0.1/webhook',
|
||||
'zero network peer alias' => 'http://0.0.0.1/webhook',
|
||||
'multicast' => 'http://224.0.0.1/webhook',
|
||||
'benchmark range' => 'http://198.18.0.1/webhook',
|
||||
'documentation range' => 'http://192.0.2.10/webhook',
|
||||
]);
|
||||
|
||||
it('rejects hostname forms that resolve to loopback', function (string $url) {
|
||||
$rule = new SafeWebhookUrl;
|
||||
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue("Expected loopback hostname-form rejection: {$url}");
|
||||
})->with([
|
||||
'decimal IPv4' => 'http://2130706433:8888/exfil',
|
||||
'hex IPv4' => 'http://0x7f000001:8888/exfil',
|
||||
'octal IPv4' => 'http://017700000001:8888/exfil',
|
||||
'short dotted IPv4' => 'http://127.1:8888/exfil',
|
||||
'IPv4-mapped IPv6 hex loopback' => 'http://[::ffff:7f00:1]:8888/exfil',
|
||||
]);
|
||||
|
||||
it('rejects internal DNS suffixes by default', function (string $url) {
|
||||
$rule = new SafeWebhookUrl(fn (string $host): array => ['93.184.216.34']);
|
||||
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue("Expected default rejection: {$url}");
|
||||
})->with([
|
||||
'.local host' => 'http://receiver.local/webhook',
|
||||
'.cluster.local host' => 'http://service.cluster.local/webhook',
|
||||
]);
|
||||
|
||||
it('rejects unresolvable hostnames by default', function () {
|
||||
$rule = new SafeWebhookUrl(fn (string $host): array => []);
|
||||
|
||||
$validator = Validator::make(['url' => 'http://does-not-resolve.example.test/webhook'], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue('Expected default rejection for unresolvable host');
|
||||
});
|
||||
|
||||
it('keeps webhook DNS resolution enabled when general DNS validation is disabled', function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], ['is_dns_validation_enabled' => false]));
|
||||
|
||||
$rule = new SafeWebhookUrl(fn (string $host): array => ['127.0.0.1']);
|
||||
|
||||
$validator = Validator::make(['url' => 'http://rebinding.example.test/webhook'], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue('Expected webhook SSRF DNS checks to remain enabled');
|
||||
});
|
||||
|
||||
it('reads configured custom DNS servers for webhook hostname resolution', function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], ['custom_dns_servers' => '1.1.1.1, invalid, 2606:4700:4700::1111']));
|
||||
|
||||
$method = new ReflectionMethod(SafeWebhookUrl::class, 'customDnsServers');
|
||||
$method->setAccessible(true);
|
||||
|
||||
expect($method->invoke(new SafeWebhookUrl))
|
||||
->toBe(['1.1.1.1', '2606:4700:4700::1111']);
|
||||
});
|
||||
|
||||
it('allows explicitly configured intranet webhook targets', function (string $url, array $resolvedIps, array $allowlist) {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], ['webhook_allowed_internal_hosts' => $allowlist]));
|
||||
|
||||
$rule = new SafeWebhookUrl(fn (string $host): array => $resolvedIps);
|
||||
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
|
||||
expect($validator->passes())->toBeTrue("Expected configured intranet target to pass: {$url}");
|
||||
})->with([
|
||||
'exact .local hostname' => ['http://receiver.local/webhook', ['192.168.10.20'], ['receiver.local']],
|
||||
'private CIDR' => ['http://hooks.example.test/webhook', ['10.50.10.20'], ['10.50.0.0/16']],
|
||||
]);
|
||||
|
||||
it('requires explicit localhost opt in in addition to allowlist', function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], ['webhook_allowed_internal_hosts' => ['localhost']]));
|
||||
|
||||
$rule = new SafeWebhookUrl;
|
||||
|
||||
$validator = Validator::make(['url' => 'http://localhost:8080/webhook'], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue('Expected localhost to remain blocked without explicit localhost opt in');
|
||||
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], ['webhook_allow_localhost' => true]));
|
||||
|
||||
$validator = Validator::make(['url' => 'http://localhost:8080/webhook'], ['url' => $rule]);
|
||||
|
||||
expect($validator->passes())->toBeTrue('Expected localhost to pass only after explicit localhost opt in');
|
||||
});
|
||||
|
||||
it('builds HTTP client options that pin resolved DNS for the request', function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], [
|
||||
'webhook_allowed_internal_hosts' => ['localhost'],
|
||||
'webhook_allow_localhost' => true,
|
||||
]));
|
||||
|
||||
$options = SafeWebhookUrl::httpClientOptions('http://localhost:8080/webhook');
|
||||
|
||||
expect($options['allow_redirects'])->toBeFalse();
|
||||
|
||||
if (defined('CURLOPT_RESOLVE')) {
|
||||
expect($options['curl'][CURLOPT_RESOLVE])->toContain('localhost:8080:127.0.0.1');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails closed while building HTTP options when the send-time resolution is unsafe', function () {
|
||||
expect(fn () => SafeWebhookUrl::httpClientOptions('http://localhost:8080/webhook'))
|
||||
->toThrow(RuntimeException::class, 'unsafe IP address');
|
||||
});
|
||||
|
||||
it('builds MinIO client resolve options for S3 backup uploads', function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate(['id' => 0], [
|
||||
'webhook_allowed_internal_hosts' => ['localhost'],
|
||||
'webhook_allow_localhost' => true,
|
||||
]));
|
||||
|
||||
$options = SafeWebhookUrl::minioClientResolveOptions('http://localhost:9000');
|
||||
|
||||
expect($options)->toContain('localhost:9000=127.0.0.1');
|
||||
});
|
||||
|
||||
it('rejects trailing-dot hostnames to avoid DNS pinning mismatch', function () {
|
||||
$rule = new SafeWebhookUrl(fn (string $host): array => ['93.184.216.34']);
|
||||
|
||||
$validator = Validator::make(['url' => 'http://example.com./webhook'], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue('Expected trailing-dot hostname rejection');
|
||||
|
||||
expect(fn () => SafeWebhookUrl::httpClientOptions('http://example.com./webhook'))
|
||||
->toThrow(RuntimeException::class, 'trailing dot');
|
||||
});
|
||||
|
||||
it('rejects reserved IPv6 ranges by default', function (string $url) {
|
||||
$rule = new SafeWebhookUrl;
|
||||
|
||||
$validator = Validator::make(['url' => $url], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue("Expected reserved IPv6 rejection: {$url}");
|
||||
})->with([
|
||||
'documentation IPv6' => 'http://[2001:db8::1]/webhook',
|
||||
'IPv4/IPv6 translation prefix' => 'http://[64:ff9b::1]/webhook',
|
||||
'6to4' => 'http://[2002::1]/webhook',
|
||||
]);
|
||||
|
||||
it('rejects hostnames that resolve to reserved IPv6 ranges by default', function (string $resolvedIp) {
|
||||
$rule = new SafeWebhookUrl(fn (string $host): array => [$resolvedIp]);
|
||||
|
||||
$validator = Validator::make(['url' => 'http://ipv6-reserved.example.test/webhook'], ['url' => $rule]);
|
||||
|
||||
expect($validator->fails())->toBeTrue("Expected reserved IPv6 resolution rejection: {$resolvedIp}");
|
||||
})->with([
|
||||
'2001:db8::1',
|
||||
'64:ff9b::1',
|
||||
'2002::1',
|
||||
]);
|
||||
|
||||
it('redacts webhook URLs for logs', function () {
|
||||
expect(SafeWebhookUrl::redactedUrlForLog('https://hooks.slack.com/services/T000/B000/secret-token?foo=bar'))
|
||||
->toBe('https://hooks.slack.com');
|
||||
});
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
use App\Jobs\SendWebhookJob;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
@@ -24,11 +23,6 @@ it('sends webhook to valid URLs', function () {
|
||||
|
||||
it('blocks webhook to loopback address', function () {
|
||||
Http::fake();
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->withArgs(function ($message) {
|
||||
return str_contains($message, 'blocked unsafe webhook URL');
|
||||
});
|
||||
|
||||
$job = new SendWebhookJob(
|
||||
payload: ['event' => 'test'],
|
||||
@@ -42,15 +36,23 @@ it('blocks webhook to loopback address', function () {
|
||||
|
||||
it('blocks webhook to cloud metadata endpoint', function () {
|
||||
Http::fake();
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->withArgs(function ($message) {
|
||||
return str_contains($message, 'blocked unsafe webhook URL');
|
||||
});
|
||||
|
||||
$job = new SendWebhookJob(
|
||||
payload: ['event' => 'test'],
|
||||
webhookUrl: 'http://169.254.169.254/latest/meta-data/'
|
||||
webhookUrl: 'http://169.254.169.254/'
|
||||
);
|
||||
|
||||
$job->handle();
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('blocks webhook to IPv4-mapped IPv6 link-local endpoint', function () {
|
||||
Http::fake();
|
||||
|
||||
$job = new SendWebhookJob(
|
||||
payload: ['event' => 'test'],
|
||||
webhookUrl: 'http://[::ffff:169.254.169.254]/'
|
||||
);
|
||||
|
||||
$job->handle();
|
||||
@@ -60,11 +62,6 @@ it('blocks webhook to cloud metadata endpoint', function () {
|
||||
|
||||
it('blocks webhook to localhost', function () {
|
||||
Http::fake();
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->withArgs(function ($message) {
|
||||
return str_contains($message, 'blocked unsafe webhook URL');
|
||||
});
|
||||
|
||||
$job = new SendWebhookJob(
|
||||
payload: ['event' => 'test'],
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\ServicesController;
|
||||
use App\Models\Service;
|
||||
|
||||
it('does not treat service URLs with different path casing as duplicates', function () {
|
||||
$controller = new ServicesController;
|
||||
$method = new ReflectionMethod($controller, 'applyServiceUrls');
|
||||
$service = new class extends Service
|
||||
{
|
||||
public function applications()
|
||||
{
|
||||
return new class
|
||||
{
|
||||
public function where(string $column, mixed $value): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function first(): null
|
||||
{
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$result = $method->invoke($controller, $service, [
|
||||
['name' => 'web', 'url' => 'https://example.com/Route'],
|
||||
['name' => 'api', 'url' => 'HTTPS://EXAMPLE.COM/route'],
|
||||
], '1');
|
||||
|
||||
expect($result['errors'] ?? [])->toBe([
|
||||
"Service container with 'web' not found.",
|
||||
"Service container with 'api' not found.",
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Storage\Create;
|
||||
use App\Livewire\Storage\Form;
|
||||
use App\Rules\ValidS3BucketName;
|
||||
|
||||
function storageRulesFor(string $componentClass): array
|
||||
{
|
||||
$component = new $componentClass;
|
||||
$method = new ReflectionMethod($component, 'rules');
|
||||
$method->setAccessible(true);
|
||||
|
||||
return $method->invoke($component);
|
||||
}
|
||||
|
||||
it('uses the shared S3 bucket rule in storage create and edit forms', function (string $componentClass) {
|
||||
$bucketRules = storageRulesFor($componentClass)['bucket'];
|
||||
|
||||
expect($bucketRules)->toContain('required')
|
||||
->and(collect($bucketRules)->contains(fn ($rule) => $rule instanceof ValidS3BucketName))->toBeTrue();
|
||||
})->with([
|
||||
'create form' => [Create::class],
|
||||
'edit form' => [Form::class],
|
||||
]);
|
||||
@@ -1,21 +1,43 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Server\UpdateCoolify;
|
||||
use App\Livewire\Settings\Updates;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Once;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Tests\TestCase;
|
||||
|
||||
beforeEach(function () {
|
||||
// Mock Server
|
||||
$this->mockServer = Mockery::mock(Server::class)->makePartial();
|
||||
$this->mockServer->id = 0;
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
// Mock InstanceSettings
|
||||
$this->settings = Mockery::mock(InstanceSettings::class);
|
||||
$this->settings->is_auto_update_enabled = true;
|
||||
$this->settings->shouldReceive('save')->andReturn(true);
|
||||
});
|
||||
function updateCoolifyTestCreateRootServerAndSettings(array $settings = []): void
|
||||
{
|
||||
Team::factory()->create(['id' => 0]);
|
||||
Server::forceCreate([
|
||||
'id' => 0,
|
||||
'name' => 'localhost',
|
||||
'ip' => '127.0.0.1',
|
||||
'user' => 'root',
|
||||
'team_id' => 0,
|
||||
'private_key_id' => 1,
|
||||
]);
|
||||
InstanceSettings::forceCreate(array_merge([
|
||||
'id' => 0,
|
||||
'is_auto_update_enabled' => true,
|
||||
'auto_update_frequency' => '0 0 * * *',
|
||||
'update_check_frequency' => '0 * * * *',
|
||||
], $settings));
|
||||
Once::flush();
|
||||
}
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
@@ -26,15 +48,7 @@ it('has UpdateCoolify action class', function () {
|
||||
});
|
||||
|
||||
it('validates cache against running version before fallback', function () {
|
||||
// Mock Server::find to return our mock server
|
||||
Server::shouldReceive('find')
|
||||
->with(0)
|
||||
->andReturn($this->mockServer);
|
||||
|
||||
// Mock instanceSettings
|
||||
$this->app->instance('App\Models\InstanceSettings', function () {
|
||||
return $this->settings;
|
||||
});
|
||||
updateCoolifyTestCreateRootServerAndSettings();
|
||||
|
||||
// CDN fails
|
||||
Http::fake(['*' => Http::response(null, 500)]);
|
||||
@@ -51,7 +65,7 @@ it('validates cache against running version before fallback', function () {
|
||||
try {
|
||||
$action->handle(manual_update: false);
|
||||
expect(false)->toBeTrue('Expected exception was not thrown');
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
expect($e->getMessage())->toContain('cache version');
|
||||
expect($e->getMessage())->toContain('4.0.5');
|
||||
expect($e->getMessage())->toContain('4.0.10');
|
||||
@@ -59,15 +73,9 @@ it('validates cache against running version before fallback', function () {
|
||||
});
|
||||
|
||||
it('uses validated cache when CDN fails and cache is newer', function () {
|
||||
// Mock Server::find
|
||||
Server::shouldReceive('find')
|
||||
->with(0)
|
||||
->andReturn($this->mockServer);
|
||||
|
||||
// Mock instanceSettings
|
||||
$this->app->instance('App\Models\InstanceSettings', function () {
|
||||
return $this->settings;
|
||||
});
|
||||
updateCoolifyTestCreateRootServerAndSettings();
|
||||
Queue::fake();
|
||||
config(['constants.ssh.mux_enabled' => false]);
|
||||
|
||||
// CDN fails
|
||||
Http::fake(['*' => Http::response(null, 500)]);
|
||||
@@ -78,12 +86,9 @@ it('uses validated cache when CDN fails and cache is newer', function () {
|
||||
|
||||
config(['constants.coolify.version' => '4.0.5']);
|
||||
|
||||
// Mock the update method to prevent actual update
|
||||
$action = Mockery::mock(UpdateCoolify::class)->makePartial();
|
||||
$action->shouldReceive('update')->once();
|
||||
$action->server = $this->mockServer;
|
||||
$action = new UpdateCoolify;
|
||||
|
||||
\Illuminate\Support\Facades\Log::shouldReceive('warning')
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->with('Failed to fetch fresh version from CDN, using validated cache', Mockery::type('array'));
|
||||
|
||||
@@ -93,16 +98,198 @@ it('uses validated cache when CDN fails and cache is newer', function () {
|
||||
expect($action->latestVersion)->toBe('4.0.10');
|
||||
});
|
||||
|
||||
it('prevents downgrade even with manual update', function () {
|
||||
// Mock Server::find
|
||||
Server::shouldReceive('find')
|
||||
->with(0)
|
||||
->andReturn($this->mockServer);
|
||||
it('passes the saved registry URL to the upgrade script command', function () {
|
||||
Queue::fake();
|
||||
config([
|
||||
'app.env' => 'testing',
|
||||
'constants.coolify.version' => '4.0.9',
|
||||
'constants.coolify.helper_version' => '1.0.14',
|
||||
'constants.coolify.upgrade_script_url' => 'https://cdn.example.com/upgrade.sh',
|
||||
'constants.ssh.mux_enabled' => false,
|
||||
]);
|
||||
|
||||
// Mock instanceSettings
|
||||
$this->app->instance('App\Models\InstanceSettings', function () {
|
||||
return $this->settings;
|
||||
});
|
||||
updateCoolifyTestCreateRootServerAndSettings([
|
||||
'is_auto_update_enabled' => true,
|
||||
'docker_registry_url' => 'ghcr.io',
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.10']],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
(new UpdateCoolify)->handle();
|
||||
|
||||
expect(Activity::query()->latest('id')->first()?->getExtraProperty('command'))->toBe(
|
||||
"curl -fsSL https://cdn.example.com/upgrade.sh -o /data/coolify/source/upgrade.sh\n".
|
||||
"bash /data/coolify/source/upgrade.sh '4.0.10' '1.0.14' 'ghcr.io'"
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to docker io for the upgrade script command when no registry is saved', function () {
|
||||
Queue::fake();
|
||||
config([
|
||||
'app.env' => 'testing',
|
||||
'constants.coolify.version' => '4.0.9',
|
||||
'constants.coolify.helper_version' => '1.0.14',
|
||||
'constants.coolify.registry_url' => 'ghcr.io',
|
||||
'constants.coolify.upgrade_script_url' => 'https://cdn.example.com/upgrade.sh',
|
||||
'constants.ssh.mux_enabled' => false,
|
||||
]);
|
||||
|
||||
updateCoolifyTestCreateRootServerAndSettings([
|
||||
'is_auto_update_enabled' => true,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.10']],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
(new UpdateCoolify)->handle();
|
||||
|
||||
expect(Activity::query()->latest('id')->first()?->getExtraProperty('command'))->toBe(
|
||||
"curl -fsSL https://cdn.example.com/upgrade.sh -o /data/coolify/source/upgrade.sh\n".
|
||||
"bash /data/coolify/source/upgrade.sh '4.0.10' '1.0.14' 'docker.io'"
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults the registry setting to docker io when no registry is saved', function () {
|
||||
config([
|
||||
'app.env' => 'testing',
|
||||
'constants.coolify.registry_url' => 'ghcr.io',
|
||||
'constants.coolify.self_hosted' => true,
|
||||
]);
|
||||
|
||||
updateCoolifyTestCreateRootServerAndSettings();
|
||||
|
||||
$rootTeam = Team::findOrFail(0);
|
||||
$user = User::factory()->create();
|
||||
$rootTeam->members()->attach($user->id, ['role' => 'admin']);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => ['id' => $rootTeam->id]]);
|
||||
|
||||
Livewire::test(Updates::class)
|
||||
->assertSet('docker_registry_url', 'docker.io');
|
||||
});
|
||||
|
||||
it('uses the database registry for helper images when the configured helper image is default', function () {
|
||||
config([
|
||||
'constants.coolify.registry_url' => 'ghcr.io',
|
||||
'constants.coolify.helper_image' => 'ghcr.io/coollabsio/coolify-helper',
|
||||
]);
|
||||
|
||||
updateCoolifyTestCreateRootServerAndSettings([
|
||||
'docker_registry_url' => 'docker.io',
|
||||
]);
|
||||
|
||||
expect(coolifyRegistryUrl())->toBe('docker.io')
|
||||
->and(coolifyHelperImage())->toBe('docker.io/coollabsio/coolify-helper');
|
||||
});
|
||||
|
||||
it('preserves an explicit custom helper image override', function () {
|
||||
config([
|
||||
'constants.coolify.registry_url' => 'docker.io',
|
||||
'constants.coolify.helper_image' => 'registry.example.com/custom/helper',
|
||||
]);
|
||||
|
||||
updateCoolifyTestCreateRootServerAndSettings([
|
||||
'docker_registry_url' => 'ghcr.io',
|
||||
]);
|
||||
|
||||
expect(coolifyHelperImage())->toBe('registry.example.com/custom/helper');
|
||||
});
|
||||
|
||||
it('uses the database registry for sentinel images', function () {
|
||||
$action = file_get_contents(app_path('Actions/Server/StartSentinel.php'));
|
||||
|
||||
expect($action)->toContain("\$image = coolifyRegistryUrl().'/coollabsio/sentinel:'.\$version;");
|
||||
});
|
||||
|
||||
it('rejects invalid registry values and does not sync them', function () {
|
||||
Process::fake();
|
||||
config([
|
||||
'app.env' => 'testing',
|
||||
'constants.coolify.registry_url' => 'docker.io',
|
||||
]);
|
||||
|
||||
updateCoolifyTestCreateRootServerAndSettings([
|
||||
'is_auto_update_enabled' => true,
|
||||
'auto_update_frequency' => '0 0 * * *',
|
||||
'update_check_frequency' => '0 * * * *',
|
||||
'docker_registry_url' => 'docker.io',
|
||||
]);
|
||||
|
||||
$rootTeam = Team::findOrFail(0);
|
||||
$user = User::factory()->create();
|
||||
$rootTeam->members()->attach($user->id, ['role' => 'admin']);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => ['id' => $rootTeam->id]]);
|
||||
|
||||
Livewire::test(Updates::class)
|
||||
->set('docker_registry_url', 'ghcr.io; touch /tmp/pwned')
|
||||
->call('submit')
|
||||
->assertHasErrors(['docker_registry_url' => ['in']]);
|
||||
|
||||
expect(InstanceSettings::findOrFail(0)->docker_registry_url)->toBe('docker.io');
|
||||
Process::assertDidntRun(fn () => true);
|
||||
});
|
||||
|
||||
it('does not save registry changes when syncing the env file fails', function () {
|
||||
config([
|
||||
'app.env' => 'testing',
|
||||
'constants.coolify.registry_url' => 'docker.io',
|
||||
'constants.coolify.self_hosted' => true,
|
||||
]);
|
||||
|
||||
updateCoolifyTestCreateRootServerAndSettings([
|
||||
'is_auto_update_enabled' => true,
|
||||
'auto_update_frequency' => '0 0 * * *',
|
||||
'update_check_frequency' => '0 * * * *',
|
||||
'docker_registry_url' => 'docker.io',
|
||||
]);
|
||||
|
||||
$rootTeam = Team::findOrFail(0);
|
||||
$user = User::factory()->create();
|
||||
$rootTeam->members()->attach($user->id, ['role' => 'admin']);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => ['id' => $rootTeam->id]]);
|
||||
|
||||
$component = new class extends Updates
|
||||
{
|
||||
protected function syncRegistryUrlToEnv(string $registryUrl): void
|
||||
{
|
||||
throw new RuntimeException('sync failed');
|
||||
}
|
||||
};
|
||||
$component->settings = InstanceSettings::findOrFail(0);
|
||||
$component->auto_update_frequency = '0 0 * * *';
|
||||
$component->update_check_frequency = '0 * * * *';
|
||||
$component->is_auto_update_enabled = true;
|
||||
$component->docker_registry_url = 'ghcr.io';
|
||||
|
||||
$component->instantSave();
|
||||
|
||||
expect(InstanceSettings::findOrFail(0)->docker_registry_url)->toBe('docker.io');
|
||||
});
|
||||
|
||||
it('appends registry url to env file when the key is missing', function () {
|
||||
$component = new Updates;
|
||||
$method = new ReflectionMethod(Updates::class, 'registryEnvSyncCommand');
|
||||
|
||||
expect($method->invoke($component, 'ghcr.io'))
|
||||
->toContain("grep -q '^REGISTRY_URL=' /data/coolify/source/.env")
|
||||
->toContain("sed -i 's|^REGISTRY_URL=.*|REGISTRY_URL=ghcr.io|' /data/coolify/source/.env")
|
||||
->toContain("printf '%s\\n' 'REGISTRY_URL=ghcr.io' >> /data/coolify/source/.env");
|
||||
});
|
||||
|
||||
it('prevents downgrade even with manual update', function () {
|
||||
updateCoolifyTestCreateRootServerAndSettings();
|
||||
|
||||
// CDN returns older version
|
||||
Http::fake([
|
||||
@@ -116,7 +303,7 @@ it('prevents downgrade even with manual update', function () {
|
||||
|
||||
$action = new UpdateCoolify;
|
||||
|
||||
\Illuminate\Support\Facades\Log::shouldReceive('error')
|
||||
Log::shouldReceive('error')
|
||||
->once()
|
||||
->with('Downgrade prevented', Mockery::type('array'));
|
||||
|
||||
@@ -124,7 +311,7 @@ it('prevents downgrade even with manual update', function () {
|
||||
try {
|
||||
$action->handle(manual_update: true);
|
||||
expect(false)->toBeTrue('Expected exception was not thrown');
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
expect($e->getMessage())->toContain('Cannot downgrade');
|
||||
expect($e->getMessage())->toContain('4.0.10');
|
||||
expect($e->getMessage())->toContain('4.0.0');
|
||||
|
||||
@@ -44,6 +44,38 @@ it('downloads postgres upgrade script during install and upgrade without auto-ru
|
||||
'nightly upgrade' => 'other/nightly/upgrade.sh',
|
||||
]);
|
||||
|
||||
it('uses the selected registry url when extracting upgrade images', function (string $path) {
|
||||
$script = file_get_contents(getcwd().'/'.$path);
|
||||
|
||||
expect($script)->toContain('IMAGES=$(REGISTRY_URL=${REGISTRY_URL} LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images');
|
||||
})->with([
|
||||
'stable upgrade' => 'scripts/upgrade.sh',
|
||||
'nightly upgrade' => 'other/nightly/upgrade.sh',
|
||||
]);
|
||||
|
||||
it('persists the selected registry url during upgrades', function (string $path) {
|
||||
$script = file_get_contents(getcwd().'/'.$path);
|
||||
|
||||
expect($script)->toContain('set_env_var "REGISTRY_URL" "$REGISTRY_URL"');
|
||||
})->with([
|
||||
'stable upgrade' => 'scripts/upgrade.sh',
|
||||
'nightly upgrade' => 'other/nightly/upgrade.sh',
|
||||
]);
|
||||
|
||||
it('uses the existing env registry url when old callers do not pass a registry argument', function (string $path) {
|
||||
$script = file_get_contents(getcwd().'/'.$path);
|
||||
|
||||
expect($script)
|
||||
->toContain('if [ -n "${3+x}" ]; then')
|
||||
->toContain('REGISTRY_URL="$3"')
|
||||
->toContain('elif [ -f "$ENV_FILE" ] && grep -q "^REGISTRY_URL=" "$ENV_FILE"; then')
|
||||
->toContain("REGISTRY_URL=$(grep \"^REGISTRY_URL=\" \"\$ENV_FILE\" | cut -d '=' -f2- | head -n1)")
|
||||
->toContain('REGISTRY_URL="docker.io"');
|
||||
})->with([
|
||||
'stable upgrade' => 'scripts/upgrade.sh',
|
||||
'nightly upgrade' => 'other/nightly/upgrade.sh',
|
||||
]);
|
||||
|
||||
it('keeps postgres upgrade compose override in future upgrade compose commands', function (string $path) {
|
||||
$script = file_get_contents(getcwd().'/'.$path);
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use App\Rules\ValidS3BucketName;
|
||||
|
||||
function validS3BucketNameRulePasses(string $bucket): bool
|
||||
{
|
||||
$failed = false;
|
||||
|
||||
(new ValidS3BucketName)->validate('bucket', $bucket, function () use (&$failed) {
|
||||
$failed = true;
|
||||
});
|
||||
|
||||
return ! $failed;
|
||||
}
|
||||
|
||||
it('accepts valid s3 bucket names', function (string $bucket) {
|
||||
expect(validS3BucketNameRulePasses($bucket))->toBeTrue("Expected accepted: {$bucket}");
|
||||
})->with([
|
||||
'short' => ['abc'],
|
||||
'simple' => ['coolify-backups'],
|
||||
'dots' => ['coolify.backups'],
|
||||
'digits' => ['backup-123'],
|
||||
'max length' => [str_repeat('a', 63)],
|
||||
]);
|
||||
|
||||
it('rejects invalid s3 bucket names and injection payloads', function (string $bucket) {
|
||||
expect(validS3BucketNameRulePasses($bucket))->toBeFalse("Expected rejected: {$bucket}");
|
||||
})->with([
|
||||
'too short' => ['ab'],
|
||||
'too long' => [str_repeat('a', 64)],
|
||||
'uppercase' => ['CoolifyBackups'],
|
||||
'underscore' => ['coolify_backups'],
|
||||
'leading hyphen' => ['-coolify-backups'],
|
||||
'trailing hyphen' => ['coolify-backups-'],
|
||||
'leading dot' => ['.coolify-backups'],
|
||||
'trailing dot' => ['coolify-backups.'],
|
||||
'consecutive dots' => ['coolify..backups'],
|
||||
'dot hyphen' => ['coolify.-backups'],
|
||||
'hyphen dot' => ['coolify-.backups'],
|
||||
'ipv4 address' => ['192.168.1.1'],
|
||||
'semicolon injection' => ['lab; id; #'],
|
||||
'command substitution' => ['lab$(id)'],
|
||||
'backticks' => ['lab`id`'],
|
||||
'pipe' => ['lab|id'],
|
||||
'ampersand' => ['lab&id'],
|
||||
'space' => ['lab bucket'],
|
||||
'newline' => ["lab\nid"],
|
||||
]);
|
||||
@@ -132,24 +132,31 @@ it('generates nullable dockerNetworkRules when not required', function () {
|
||||
->not->toContain('required');
|
||||
});
|
||||
|
||||
it('accepts Docker-compatible environment variable keys', function (string $key) {
|
||||
it('accepts shell-safe environment variable keys', function (string $key) {
|
||||
expect(ValidationPatterns::isValidEnvironmentVariableKey($key))->toBeTrue();
|
||||
})->with([
|
||||
'letters' => 'APP_ENV',
|
||||
'leading underscore' => '_TOKEN',
|
||||
'railpack control variable' => 'RAILPACK_NODE_VERSION',
|
||||
'digits after first character' => 'NODE_VERSION_20',
|
||||
'starts with digit' => '1BAD',
|
||||
'hyphen' => 'BAD-KEY',
|
||||
'dot' => 'node.name',
|
||||
'lowercase' => 'node_version',
|
||||
'dot notation' => 'node.name',
|
||||
'uppercase dots' => 'XPACK.SECURITY.ENABLED',
|
||||
'semicolon' => 'BAD;KEY',
|
||||
'space' => 'BAD KEY',
|
||||
]);
|
||||
|
||||
it('rejects environment variable keys Docker cannot represent', function (string $key) {
|
||||
it('rejects invalid environment variable keys', function (string $key) {
|
||||
expect(ValidationPatterns::isValidEnvironmentVariableKey($key))->toBeFalse();
|
||||
})->with([
|
||||
'starts with digit' => '1BAD',
|
||||
'hyphen' => 'BAD-KEY',
|
||||
'semicolon' => 'BAD;KEY',
|
||||
'space' => 'BAD KEY',
|
||||
'command substitution' => 'BAD$(id)',
|
||||
'backticks' => 'BAD`id`',
|
||||
'pipe' => 'BAD|id',
|
||||
'ampersand' => 'BAD&id',
|
||||
'newline' => 'BAD
|
||||
KEY',
|
||||
'equals' => 'BAD=KEY',
|
||||
'empty' => '',
|
||||
]);
|
||||
@@ -164,7 +171,7 @@ it('generates environment variable key rules with correct defaults', function ()
|
||||
});
|
||||
|
||||
it('normalizes environment variable keys by trimming surrounding whitespace', function () {
|
||||
expect(ValidationPatterns::normalizeEnvironmentVariableKey(' node.name '))->toBe('node.name');
|
||||
expect(ValidationPatterns::normalizeEnvironmentVariableKey(' APP_ENV '))->toBe('APP_ENV');
|
||||
});
|
||||
|
||||
it('normalizes environment variable keys before model validation', function () {
|
||||
@@ -173,3 +180,14 @@ it('normalizes environment variable keys before model validation', function () {
|
||||
|
||||
expect($environmentVariable->key)->toBe('APP_ENV');
|
||||
});
|
||||
|
||||
it('normalizes application domain scheme and host without lowercasing path query or fragment', function () {
|
||||
$domains = ' HTTPS://EXAMPLE.COM/MixedCase/Path?Token=ABC#Fragment, http://Sub.EXAMPLE.com/Api/V1 ';
|
||||
|
||||
expect(ValidationPatterns::normalizeApplicationDomains($domains))
|
||||
->toBe('https://example.com/MixedCase/Path?Token=ABC#Fragment,http://sub.example.com/Api/V1');
|
||||
});
|
||||
|
||||
it('validates application domains with underscores in the hostname', function () {
|
||||
expect(ValidationPatterns::validateApplicationDomains('https://myapp_service.example.com'))->toBeEmpty();
|
||||
});
|
||||
Reference in new issue
Block a user