Merge branch 'next' into rename-github-app
This commit is contained in:
		@@ -168,18 +168,42 @@ class ExecuteContainerCommand extends Component
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            // Validate container name format
 | 
			
		||||
            if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $this->selected_container)) {
 | 
			
		||||
                throw new \InvalidArgumentException('Invalid container name format');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Verify container exists in our allowed list
 | 
			
		||||
            $container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
 | 
			
		||||
            if (is_null($container)) {
 | 
			
		||||
                throw new \RuntimeException('Container not found.');
 | 
			
		||||
            }
 | 
			
		||||
            $server = data_get($this->container, 'server');
 | 
			
		||||
 | 
			
		||||
            // Verify server ownership and status
 | 
			
		||||
            $server = data_get($container, 'server');
 | 
			
		||||
            if (! $server || ! $server instanceof Server) {
 | 
			
		||||
                throw new \RuntimeException('Invalid server configuration.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ($server->isForceDisabled()) {
 | 
			
		||||
                throw new \RuntimeException('Server is disabled.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Additional ownership verification based on resource type
 | 
			
		||||
            $resourceServer = match ($this->type) {
 | 
			
		||||
                'application' => $this->resource->destination->server,
 | 
			
		||||
                'database' => $this->resource->destination->server,
 | 
			
		||||
                'service' => $this->resource->server,
 | 
			
		||||
                default => throw new \RuntimeException('Invalid resource type.')
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if ($server->id !== $resourceServer->id && ! $this->resource->additional_servers->contains('id', $server->id)) {
 | 
			
		||||
                throw new \RuntimeException('Server ownership verification failed.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->dispatch(
 | 
			
		||||
                'send-terminal-command',
 | 
			
		||||
                isset($container),
 | 
			
		||||
                true,
 | 
			
		||||
                data_get($container, 'container.Names'),
 | 
			
		||||
                data_get($container, 'server.uuid')
 | 
			
		||||
            );
 | 
			
		||||
 
 | 
			
		||||
@@ -29,11 +29,20 @@ class Terminal extends Component
 | 
			
		||||
        $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
 | 
			
		||||
 | 
			
		||||
        if ($isContainer) {
 | 
			
		||||
            // Validate container identifier format (alphanumeric, dashes, and underscores only)
 | 
			
		||||
            if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $identifier)) {
 | 
			
		||||
                throw new \InvalidArgumentException('Invalid container identifier format');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Verify container exists and belongs to the user's team
 | 
			
		||||
            $status = getContainerStatus($server, $identifier);
 | 
			
		||||
            if ($status !== 'running') {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
 | 
			
		||||
 | 
			
		||||
            // Escape the identifier for shell usage
 | 
			
		||||
            $escapedIdentifier = escapeshellarg($identifier);
 | 
			
		||||
            $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
 | 
			
		||||
        } else {
 | 
			
		||||
            $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi');
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ namespace App\Livewire\Server;
 | 
			
		||||
use App\Actions\Server\StartSentinel;
 | 
			
		||||
use App\Actions\Server\StopSentinel;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use Livewire\Attributes\Locked;
 | 
			
		||||
use Livewire\Attributes\Computed;
 | 
			
		||||
use Livewire\Attributes\Validate;
 | 
			
		||||
use Livewire\Component;
 | 
			
		||||
 | 
			
		||||
@@ -79,9 +79,6 @@ class Show extends Component
 | 
			
		||||
    #[Validate(['required'])]
 | 
			
		||||
    public string $serverTimezone;
 | 
			
		||||
 | 
			
		||||
    #[Locked]
 | 
			
		||||
    public array $timezones;
 | 
			
		||||
 | 
			
		||||
    public function getListeners()
 | 
			
		||||
    {
 | 
			
		||||
        $teamId = auth()->user()->currentTeam()->id;
 | 
			
		||||
@@ -96,13 +93,21 @@ class Show extends Component
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
 | 
			
		||||
            $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
 | 
			
		||||
            $this->syncData();
 | 
			
		||||
        } catch (\Throwable $e) {
 | 
			
		||||
            return handleError($e, $this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Computed]
 | 
			
		||||
    public function timezones(): array
 | 
			
		||||
    {
 | 
			
		||||
        return collect(timezone_identifiers_list())
 | 
			
		||||
            ->sort()
 | 
			
		||||
            ->values()
 | 
			
		||||
            ->toArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function syncData(bool $toModel = false)
 | 
			
		||||
    {
 | 
			
		||||
        if ($toModel) {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ use App\Models\InstanceSettings;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use Illuminate\Support\Facades\Auth;
 | 
			
		||||
use Illuminate\Support\Facades\Hash;
 | 
			
		||||
use Livewire\Attributes\Locked;
 | 
			
		||||
use Livewire\Attributes\Computed;
 | 
			
		||||
use Livewire\Attributes\Validate;
 | 
			
		||||
use Livewire\Component;
 | 
			
		||||
 | 
			
		||||
@@ -17,9 +17,6 @@ class Index extends Component
 | 
			
		||||
 | 
			
		||||
    protected Server $server;
 | 
			
		||||
 | 
			
		||||
    #[Locked]
 | 
			
		||||
    public $timezones;
 | 
			
		||||
 | 
			
		||||
    #[Validate('boolean')]
 | 
			
		||||
    public bool $is_auto_update_enabled;
 | 
			
		||||
 | 
			
		||||
@@ -101,12 +98,20 @@ class Index extends Component
 | 
			
		||||
            $this->is_api_enabled = $this->settings->is_api_enabled;
 | 
			
		||||
            $this->auto_update_frequency = $this->settings->auto_update_frequency;
 | 
			
		||||
            $this->update_check_frequency = $this->settings->update_check_frequency;
 | 
			
		||||
            $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
 | 
			
		||||
            $this->instance_timezone = $this->settings->instance_timezone;
 | 
			
		||||
            $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Computed]
 | 
			
		||||
    public function timezones(): array
 | 
			
		||||
    {
 | 
			
		||||
        return collect(timezone_identifiers_list())
 | 
			
		||||
            ->sort()
 | 
			
		||||
            ->values()
 | 
			
		||||
            ->toArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function instantSave($isSave = true)
 | 
			
		||||
    {
 | 
			
		||||
        $this->validate();
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ namespace App\Models;
 | 
			
		||||
 | 
			
		||||
use App\Enums\ApplicationDeploymentStatus;
 | 
			
		||||
use Illuminate\Database\Eloquent\Casts\Attribute;
 | 
			
		||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
			
		||||
use Illuminate\Database\Eloquent\SoftDeletes;
 | 
			
		||||
use Illuminate\Process\InvokedProcess;
 | 
			
		||||
@@ -104,7 +105,7 @@ use Visus\Cuid2\Cuid2;
 | 
			
		||||
 | 
			
		||||
class Application extends BaseModel
 | 
			
		||||
{
 | 
			
		||||
    use SoftDeletes;
 | 
			
		||||
    use HasFactory, SoftDeletes;
 | 
			
		||||
 | 
			
		||||
    private static $parserVersion = '4';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ use App\Notifications\Server\Reachable;
 | 
			
		||||
use App\Notifications\Server\Unreachable;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Database\Eloquent\Casts\Attribute;
 | 
			
		||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
 | 
			
		||||
use Illuminate\Database\Eloquent\SoftDeletes;
 | 
			
		||||
use Illuminate\Support\Carbon;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
@@ -48,7 +49,7 @@ use Symfony\Component\Yaml\Yaml;
 | 
			
		||||
 | 
			
		||||
class Server extends BaseModel
 | 
			
		||||
{
 | 
			
		||||
    use SchemalessAttributesTrait, SoftDeletes;
 | 
			
		||||
    use HasFactory, SchemalessAttributesTrait, SoftDeletes;
 | 
			
		||||
 | 
			
		||||
    public static $batch_counter = 0;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -90,8 +90,11 @@ function metrics_dir(): string
 | 
			
		||||
    return base_configuration_dir().'/metrics';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sanitize_string(string $input): string
 | 
			
		||||
function sanitize_string(?string $input = null): ?string
 | 
			
		||||
{
 | 
			
		||||
    if (is_null($input)) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
    // Remove any HTML/PHP tags
 | 
			
		||||
    $sanitized = strip_tags($input);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,22 @@ return [
 | 
			
		||||
            'search_path' => 'public',
 | 
			
		||||
            'sslmode' => 'prefer',
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
        'testing' => [
 | 
			
		||||
            'driver' => 'pgsql',
 | 
			
		||||
            'url' => env('DATABASE_TEST_URL'),
 | 
			
		||||
            'host' => env('DB_TEST_HOST', 'postgres'),
 | 
			
		||||
            'port' => env('DB_TEST_PORT', '5432'),
 | 
			
		||||
            'database' => env('DB_TEST_DATABASE', 'coolify_test'),
 | 
			
		||||
            'username' => env('DB_TEST_USERNAME', 'coolify'),
 | 
			
		||||
            'password' => env('DB_TEST_PASSWORD', 'password'),
 | 
			
		||||
            'charset' => 'utf8',
 | 
			
		||||
            'prefix' => '',
 | 
			
		||||
            'prefix_indexes' => true,
 | 
			
		||||
            'search_path' => 'public',
 | 
			
		||||
            'sslmode' => 'prefer',
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								database/factories/ApplicationFactory.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								database/factories/ApplicationFactory.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Database\Factories;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Eloquent\Factories\Factory;
 | 
			
		||||
 | 
			
		||||
class ApplicationFactory extends Factory
 | 
			
		||||
{
 | 
			
		||||
    public function definition(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'name' => fake()->unique()->name(),
 | 
			
		||||
            'destination_id' => 1,
 | 
			
		||||
            'git_repository' => fake()->url(),
 | 
			
		||||
            'git_branch' => fake()->word(),
 | 
			
		||||
            'build_pack' => 'nixpacks',
 | 
			
		||||
            'ports_exposes' => '3000',
 | 
			
		||||
            'environment_id' => 1,
 | 
			
		||||
            'destination_id' => 1,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								database/factories/ServerFactory.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								database/factories/ServerFactory.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Database\Factories;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Eloquent\Factories\Factory;
 | 
			
		||||
 | 
			
		||||
class ServerFactory extends Factory
 | 
			
		||||
{
 | 
			
		||||
    public function definition(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'name' => fake()->unique()->name(),
 | 
			
		||||
            'ip' => fake()->unique()->ipv4(),
 | 
			
		||||
            'private_key_id' => 1,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,8 +13,8 @@
 | 
			
		||||
    <env name="APP_ENV" value="testing"/>
 | 
			
		||||
    <env name="BCRYPT_ROUNDS" value="4"/>
 | 
			
		||||
    <env name="CACHE_DRIVER" value="array"/>
 | 
			
		||||
    <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
 | 
			
		||||
    <!-- <env name="DB_DATABASE" value=":memory:"/> -->
 | 
			
		||||
    <env name="DB_CONNECTION" value="testing"/>
 | 
			
		||||
    <env name="DB_TEST_DATABASE" value="coolify_test"/>
 | 
			
		||||
    <env name="MAIL_MAILER" value="array"/>
 | 
			
		||||
    <env name="QUEUE_CONNECTION" value="sync"/>
 | 
			
		||||
    <env name="SESSION_DRIVER" value="array"/>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								public/svgs/plex.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/svgs/plex.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="15%" fill="#282a2d"/><path d="M256 70H148l108 186-108 186h108l108-186z" fill="#e5a00d"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 191 B  | 
@@ -88,7 +88,7 @@
 | 
			
		||||
                    <div class="w-full" x-data="{
 | 
			
		||||
                        open: false,
 | 
			
		||||
                        search: '{{ $serverTimezone ?: '' }}',
 | 
			
		||||
                        timezones: @js($timezones),
 | 
			
		||||
                        timezones: @js($this->timezones),
 | 
			
		||||
                        placeholder: '{{ $serverTimezone ? 'Search timezone...' : 'Select Server Timezone' }}',
 | 
			
		||||
                        init() {
 | 
			
		||||
                            this.$watch('search', value => {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
                    <div class="w-full" x-data="{
 | 
			
		||||
                        open: false,
 | 
			
		||||
                        search: '{{ $settings->instance_timezone ?: '' }}',
 | 
			
		||||
                        timezones: @js($timezones),
 | 
			
		||||
                        timezones: @js($this->timezones),
 | 
			
		||||
                        placeholder: '{{ $settings->instance_timezone ? 'Search timezone...' : 'Select Server Timezone' }}',
 | 
			
		||||
                        init() {
 | 
			
		||||
                            this.$watch('search', value => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								templates/compose/plex.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								templates/compose/plex.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
# documentation: https://docs.linuxserver.io/images/docker-plex/
 | 
			
		||||
# slogan: Plex organizes video, music and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices.
 | 
			
		||||
# tags: media, server, movies, tv, music
 | 
			
		||||
# logo: svgs/plex.svg
 | 
			
		||||
# port: 32400
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  plex:
 | 
			
		||||
    image: lscr.io/linuxserver/plex:latest
 | 
			
		||||
    environment:
 | 
			
		||||
      - SERVICE_FQDN_PLEX_32400
 | 
			
		||||
      - _APP_URL=$SERVICE_FQDN_PLEX
 | 
			
		||||
      - PUID=1000
 | 
			
		||||
      - PGID=1000
 | 
			
		||||
      - TZ=${TZ:-America/Toronto}
 | 
			
		||||
      - PLEX_CLAIM=${PLEX_CLAIM}
 | 
			
		||||
    #devices:
 | 
			
		||||
    #  - "/dev/dri:/dev/dri"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - plex-config:/config
 | 
			
		||||
      - plex-tv:/tv
 | 
			
		||||
      - plex-movies:/movies
 | 
			
		||||
    healthcheck:
 | 
			
		||||
      test: ["CMD", "curl", "-f", "http://localhost:32400/identity"]
 | 
			
		||||
      interval: 2s
 | 
			
		||||
      timeout: 10s
 | 
			
		||||
      retries: 15
 | 
			
		||||
@@ -2170,6 +2170,21 @@
 | 
			
		||||
        "logo": "svgs/plane.svg",
 | 
			
		||||
        "minversion": "0.0.0"
 | 
			
		||||
    },
 | 
			
		||||
    "plex": {
 | 
			
		||||
        "documentation": "https://docs.linuxserver.io/images/docker-plex/?utm_source=coolify.io",
 | 
			
		||||
        "slogan": "Plex organizes video, music and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices.",
 | 
			
		||||
        "compose": "c2VydmljZXM6CiAgcGxleDoKICAgIGltYWdlOiAnbHNjci5pby9saW51eHNlcnZlci9wbGV4OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QTEVYXzMyNDAwCiAgICAgIC0gX0FQUF9VUkw9JFNFUlZJQ0VfRlFETl9QTEVYCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gJ1RaPSR7VFo6LUFtZXJpY2EvVG9yb250b30nCiAgICAgIC0gJ1BMRVhfQ0xBSU09JHtQTEVYX0NMQUlNfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BsZXgtY29uZmlnOi9jb25maWcnCiAgICAgIC0gJ3BsZXgtdHY6L3R2JwogICAgICAtICdwbGV4LW1vdmllczovbW92aWVzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjMyNDAwL2lkZW50aXR5JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==",
 | 
			
		||||
        "tags": [
 | 
			
		||||
            "media",
 | 
			
		||||
            "server",
 | 
			
		||||
            "movies",
 | 
			
		||||
            "tv",
 | 
			
		||||
            "music"
 | 
			
		||||
        ],
 | 
			
		||||
        "logo": "svgs/plex.svg",
 | 
			
		||||
        "minversion": "0.0.0",
 | 
			
		||||
        "port": "32400"
 | 
			
		||||
    },
 | 
			
		||||
    "plunk": {
 | 
			
		||||
        "documentation": "https://docs.useplunk.com/getting-started/introduction?utm_source=coolify.io",
 | 
			
		||||
        "slogan": "Plunk, The Open-Source Email Platform for AWS",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										57
									
								
								tests/Feature/ExecuteContainerCommandTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								tests/Feature/ExecuteContainerCommandTest.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Tests\Feature;
 | 
			
		||||
 | 
			
		||||
use App\Models\Application;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use PHPUnit\Framework\Attributes\Test;
 | 
			
		||||
use Tests\TestCase;
 | 
			
		||||
use Tests\Traits\HandlesTestDatabase;
 | 
			
		||||
 | 
			
		||||
class ExecuteContainerCommandTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use HandlesTestDatabase;
 | 
			
		||||
 | 
			
		||||
    private $user;
 | 
			
		||||
 | 
			
		||||
    private $team;
 | 
			
		||||
 | 
			
		||||
    private $server;
 | 
			
		||||
 | 
			
		||||
    private $application;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::setUp();
 | 
			
		||||
 | 
			
		||||
        // Only set up database for tests that need it
 | 
			
		||||
        if ($this->shouldSetUpDatabase()) {
 | 
			
		||||
            $this->setUpTestDatabase();
 | 
			
		||||
        }
 | 
			
		||||
        // Create test data
 | 
			
		||||
        $this->user = User::factory()->create();
 | 
			
		||||
        $this->team = $this->user->teams()->first();
 | 
			
		||||
        $this->server = Server::factory()->create(['team_id' => $this->team->id]);
 | 
			
		||||
        $this->application = Application::factory()->create();
 | 
			
		||||
 | 
			
		||||
        // Login the user
 | 
			
		||||
        $this->actingAs($this->user);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function tearDown(): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->shouldSetUpDatabase()) {
 | 
			
		||||
            $this->tearDownTestDatabase();
 | 
			
		||||
        }
 | 
			
		||||
        parent::tearDown();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function shouldSetUpDatabase(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($this->name(), [
 | 
			
		||||
            'it_allows_valid_container_access',
 | 
			
		||||
            'it_prevents_cross_server_container_access',
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										78
									
								
								tests/Traits/HandlesTestDatabase.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								tests/Traits/HandlesTestDatabase.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Tests\Traits;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Support\Facades\Artisan;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
 | 
			
		||||
trait HandlesTestDatabase
 | 
			
		||||
{
 | 
			
		||||
    protected function setUpTestDatabase(): void
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            // Create test database if it doesn't exist
 | 
			
		||||
            $database = config('database.connections.testing.database');
 | 
			
		||||
            $this->createTestDatabase($database);
 | 
			
		||||
 | 
			
		||||
            // Run migrations
 | 
			
		||||
            Artisan::call('migrate:fresh', [
 | 
			
		||||
                '--database' => 'testing',
 | 
			
		||||
                '--seed' => false,
 | 
			
		||||
            ]);
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            $this->tearDownTestDatabase();
 | 
			
		||||
            throw $e;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function tearDownTestDatabase(): void
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            // Drop test database
 | 
			
		||||
            $database = config('database.connections.testing.database');
 | 
			
		||||
            $this->dropTestDatabase($database);
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            // Log error but don't throw
 | 
			
		||||
            error_log('Failed to tear down test database: '.$e->getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function createTestDatabase($database)
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            // Connect to postgres database to create/drop test database
 | 
			
		||||
            config(['database.connections.pgsql.database' => 'postgres']);
 | 
			
		||||
            DB::purge('pgsql');
 | 
			
		||||
            DB::reconnect('pgsql');
 | 
			
		||||
 | 
			
		||||
            // Drop if exists and create new database
 | 
			
		||||
            DB::connection('pgsql')->statement("DROP DATABASE IF EXISTS $database WITH (FORCE);");
 | 
			
		||||
            DB::connection('pgsql')->statement("CREATE DATABASE $database;");
 | 
			
		||||
 | 
			
		||||
            // Switch back to testing connection
 | 
			
		||||
            DB::disconnect('pgsql');
 | 
			
		||||
            DB::reconnect('testing');
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            $this->tearDownTestDatabase();
 | 
			
		||||
            throw new \Exception('Could not create test database: '.$e->getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function dropTestDatabase($database)
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            // Connect to postgres database to drop test database
 | 
			
		||||
            config(['database.connections.pgsql.database' => 'postgres']);
 | 
			
		||||
            DB::purge('pgsql');
 | 
			
		||||
            DB::reconnect('pgsql');
 | 
			
		||||
 | 
			
		||||
            // Drop the test database
 | 
			
		||||
            DB::connection('pgsql')->statement("DROP DATABASE IF EXISTS $database WITH (FORCE);");
 | 
			
		||||
 | 
			
		||||
            DB::disconnect('pgsql');
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            // Log error but don't throw
 | 
			
		||||
            error_log('Failed to drop test database: '.$e->getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user