Officially support PostgreSQL database (#1066)

* Just skip this table because it no longer exists

* Add postgresql

* This no longer needs to be there

* These are the same output in mysql, but different in postgresql

* Fix these migrations for postgresql

* This table no longer exists

* This is expected to be a json column for json operations, required for postgresql

* Shoot for the stars

* Fix pint

* Why was this missing

* Updates

* Restore this

* This needs to be explicit

* Don’t like strings

* Fix these classes

* Use different method to compare dates

* Apparently postgresql doesn’t like case insensitivity

* Postgresql orders it backwards

* Ordered different by postgresql

* Unnecessary and breaking

* Make sure the order is correct for postresql

* Fix this with the order too

* Remove this

* Force email to be lowercased

* Update app/Models/User.php
This commit is contained in:
Lance Pioch 2025-03-30 14:44:03 -04:00 committed by GitHub
parent bca02ced86
commit 8261184b57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 270 additions and 384 deletions

View File

@ -3,5 +3,4 @@ APP_DEBUG=false
APP_KEY= APP_KEY=
APP_URL=http://panel.test APP_URL=http://panel.test
APP_INSTALLED=false APP_INSTALLED=false
APP_TIMEZONE=UTC
APP_LOCALE=en APP_LOCALE=en

View File

@ -213,3 +213,79 @@ jobs:
- name: Integration tests - name: Integration tests
run: vendor/bin/pest tests/Integration run: vendor/bin/pest tests/Integration
postgresql:
name: PostgreSQL
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
database: ["postgres:14"]
services:
database:
image: ${{ matrix.database }}
env:
POSTGRES_DB: testing
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: postgres
DB_PASSWORD: postgres
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Unit tests
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration

View File

@ -33,6 +33,13 @@ class CreateUser extends CreateRecord
return []; return [];
} }
protected function prepareForValidation($attributes): array
{
$attributes['data']['email'] = mb_strtolower($attributes['data']['email']);
return $attributes;
}
protected function handleRecordCreation(array $data): Model protected function handleRecordCreation(array $data): Model
{ {
$data['root_admin'] = false; $data['root_admin'] = false;

View File

@ -19,6 +19,7 @@ class DatabaseStep
'sqlite' => 'SQLite', 'sqlite' => 'SQLite',
'mariadb' => 'MariaDB', 'mariadb' => 'MariaDB',
'mysql' => 'MySQL', 'mysql' => 'MySQL',
'pgsql' => 'PostgreSQL',
]; ];
public static function make(PanelInstaller $installer): Step public static function make(PanelInstaller $installer): Step
@ -39,15 +40,24 @@ class DatabaseStep
->afterStateUpdated(function ($state, Set $set, Get $get) { ->afterStateUpdated(function ($state, Set $set, Get $get) {
$set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel'); $set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel');
if ($state === 'sqlite') { switch ($state) {
case 'sqlite':
$set('env_database.DB_HOST', null); $set('env_database.DB_HOST', null);
$set('env_database.DB_PORT', null); $set('env_database.DB_PORT', null);
$set('env_database.DB_USERNAME', null); $set('env_database.DB_USERNAME', null);
$set('env_database.DB_PASSWORD', null); $set('env_database.DB_PASSWORD', null);
} else { break;
case 'mariadb':
case 'mysql':
$set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1'); $set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
$set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306');
$set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican'); $set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
$set('env_database.DB_PORT', '3306');
break;
case 'pgsql':
$set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
$set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
$set('env_database.DB_PORT', '5432');
break;
} }
}), }),
TextInput::make('env_database.DB_DATABASE') TextInput::make('env_database.DB_DATABASE')
@ -114,7 +124,6 @@ class DatabaseStep
'database' => $database, 'database' => $database,
'username' => $username, 'username' => $username,
'password' => $password, 'password' => $password,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci', 'collation' => 'utf8mb4_unicode_ci',
'strict' => true, 'strict' => true,
]); ]);

View File

@ -178,6 +178,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return true; return true;
}); });
static::saving(function (self $user) {
$user->email = mb_strtolower($user->email);
});
static::deleting(function (self $user) { static::deleting(function (self $user) {
throw_if($user->servers()->count() > 0, new DisplayException(trans('exceptions.users.has_servers'))); throw_if($user->servers()->count() > 0, new DisplayException(trans('exceptions.users.has_servers')));

View File

@ -155,8 +155,8 @@ class AllocationSelectionService
$query->whereIn('node_id', $nodes); $query->whereIn('node_id', $nodes);
} }
return $query->groupBy('ip') return $query
->get() ->groupBy('ip')
->pluck('ip') ->pluck('ip')
->toArray(); ->toArray();
} }

View File

@ -89,6 +89,21 @@ return [
]) : [], ]) : [],
], ],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'panel'),
'username' => env('DB_USERNAME', 'pelican'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
], ],
/* /*

View File

@ -12,7 +12,7 @@ return new class extends Migration
{ {
Schema::create('api_keys', function (Blueprint $table) { Schema::create('api_keys', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->char('public', 16); $table->string('public', 16)->nullable();
$table->text('secret'); $table->text('secret');
$table->text('allowed_ips')->nullable(); $table->text('allowed_ips')->nullable();
$table->timestamps(); $table->timestamps();

View File

@ -12,8 +12,8 @@ return new class extends Migration
{ {
Schema::create('downloads', function (Blueprint $table) { Schema::create('downloads', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->char('token', 36)->unique(); $table->string('token', 36)->unique();
$table->char('server', 36); $table->string('server', 36);
$table->text('path'); $table->text('path');
$table->timestamps(); $table->timestamps();
}); });

View File

@ -21,7 +21,7 @@ return new class extends Migration
$table->mediumInteger('memory_overallocate')->unsigned()->nullable(); $table->mediumInteger('memory_overallocate')->unsigned()->nullable();
$table->integer('disk')->unsigned(); $table->integer('disk')->unsigned();
$table->mediumInteger('disk_overallocate')->unsigned()->nullable(); $table->mediumInteger('disk_overallocate')->unsigned()->nullable();
$table->char('daemonSecret', 36)->unique(); $table->string('daemonSecret', 36)->unique();
$table->smallInteger('daemonListen')->unsigned()->default(8080); $table->smallInteger('daemonListen')->unsigned()->default(8080);
$table->smallInteger('daemonSFTP')->unsgined()->default(2022); $table->smallInteger('daemonSFTP')->unsgined()->default(2022);
$table->string('daemonBase')->default('/home/daemon-files'); $table->string('daemonBase')->default('/home/daemon-files');

View File

@ -12,8 +12,8 @@ return new class extends Migration
{ {
Schema::create('servers', function (Blueprint $table) { Schema::create('servers', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->char('uuid', 36)->unique(); $table->string('uuid', 36)->unique();
$table->char('uuidShort', 8)->unique(); $table->string('uuidShort', 8)->unique();
$table->mediumInteger('node')->unsigned(); $table->mediumInteger('node')->unsigned();
$table->string('name'); $table->string('name');
$table->tinyInteger('active')->unsigned(); $table->tinyInteger('active')->unsigned();
@ -29,7 +29,7 @@ return new class extends Migration
$table->mediumInteger('service')->unsigned(); $table->mediumInteger('service')->unsigned();
$table->mediumInteger('option')->unsigned(); $table->mediumInteger('option')->unsigned();
$table->text('startup'); $table->text('startup');
$table->char('daemonSecret', 36)->unique(); $table->string('daemonSecret', 36)->unique();
$table->string('username')->unique(); $table->string('username')->unique();
$table->tinyInteger('installed')->unsigned()->default(0); $table->tinyInteger('installed')->unsigned()->default(0);
$table->timestamps(); $table->timestamps();

View File

@ -14,7 +14,7 @@ return new class extends Migration
$table->increments('id'); $table->increments('id');
$table->integer('user_id')->unsigned(); $table->integer('user_id')->unsigned();
$table->integer('server_id')->unsigned(); $table->integer('server_id')->unsigned();
$table->char('daemonSecret', 36)->unique(); $table->string('daemonSecret', 36)->unique();
$table->timestamps(); $table->timestamps();
}); });
} }

View File

@ -12,14 +12,14 @@ return new class extends Migration
{ {
Schema::create('users', function (Blueprint $table) { Schema::create('users', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->char('uuid', 36)->unique(); $table->string('uuid', 36)->unique();
$table->string('email')->unique(); $table->string('email')->unique();
$table->text('password'); $table->text('password');
$table->string('remember_token')->nullable(); $table->string('remember_token')->nullable();
$table->char('language', 5)->default('en'); $table->string('language', 5)->default('en');
$table->tinyInteger('root_admin')->unsigned()->default(0); $table->tinyInteger('root_admin')->unsigned()->default(0);
$table->tinyInteger('use_totp')->unsigned(); $table->tinyInteger('use_totp')->unsigned();
$table->char('totp_secret', 16)->nullable(); $table->string('totp_secret', 16)->nullable();
$table->timestamps(); $table->timestamps();
}); });
} }

View File

@ -1,25 +0,0 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('permissions', function (Blueprint $table) {
$table->renameColumn('permissions', 'permission');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('permissions', function (Blueprint $table) {});
}
};

View File

@ -14,7 +14,7 @@ return new class extends Migration
$table->string('id')->primary(); $table->string('id')->primary();
$table->string('type'); $table->string('type');
$table->morphs('notifiable'); $table->morphs('notifiable');
$table->text('data'); $table->json('data');
$table->timestamp('read_at')->nullable(); $table->timestamp('read_at')->nullable();
$table->timestamps(); $table->timestamps();
}); });

View File

@ -12,7 +12,7 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::table('services', function (Blueprint $table) { Schema::table('services', function (Blueprint $table) {
$table->char('author', 36)->after('id'); $table->string('author', 36)->after('id');
}); });
} }

View File

@ -15,8 +15,8 @@ return new class extends Migration
$table->increments('id'); $table->increments('id');
$table->boolean('authorized'); $table->boolean('authorized');
$table->text('error')->nullable(); $table->text('error')->nullable();
$table->char('key', 16)->nullable(); $table->string('key', 16)->nullable();
$table->char('method', 6); $table->string('method', 6);
$table->text('route'); $table->text('route');
$table->text('content')->nullable(); $table->text('content')->nullable();
$table->text('user_agent'); $table->text('user_agent');

View File

@ -1,33 +0,0 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('permissions', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users');
$table->foreign('server_id')->references('id')->on('servers');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('permissions', function (Blueprint $table) {
$table->dropForeign('permissions_user_id_foreign');
$table->dropForeign('permissions_server_id_foreign');
$table->dropIndex('permissions_user_id_foreign');
$table->dropIndex('permissions_server_id_foreign');
});
}
};

View File

@ -14,7 +14,7 @@ return new class extends Migration
Schema::create('service_packs', function (Blueprint $table) { Schema::create('service_packs', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->unsignedInteger('option'); $table->unsignedInteger('option');
$table->char('uuid', 36)->unique(); $table->string('uuid', 36)->unique();
$table->string('name'); $table->string('name');
$table->string('version'); $table->string('version');
$table->text('description')->nullable(); $table->text('description')->nullable();

View File

@ -13,7 +13,7 @@ return new class extends Migration
{ {
Schema::create('node_configuration_tokens', function (Blueprint $table) { Schema::create('node_configuration_tokens', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->char('token', 32); $table->string('token', 32);
$table->timestamp('expires_at'); $table->timestamp('expires_at');
$table->integer('node')->unsigned(); $table->integer('node')->unsigned();
$table->foreign('node')->references('id')->on('nodes'); $table->foreign('node')->references('id')->on('nodes');

View File

@ -19,7 +19,7 @@ return new class extends Migration
$table->dropForeign(['option']); $table->dropForeign(['option']);
$table->dropForeign(['pack']); $table->dropForeign(['pack']);
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropIndex('servers_node_foreign'); $table->dropIndex('servers_node_foreign');
$table->dropIndex('servers_owner_foreign'); $table->dropIndex('servers_owner_foreign');
$table->dropIndex('servers_allocation_foreign'); $table->dropIndex('servers_allocation_foreign');

View File

@ -14,7 +14,7 @@ return new class extends Migration
Schema::table('nodes', function (Blueprint $table) { Schema::table('nodes', function (Blueprint $table) {
$table->dropForeign(['location']); $table->dropForeign(['location']);
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropIndex('nodes_location_foreign'); $table->dropIndex('nodes_location_foreign');
} }
@ -31,7 +31,7 @@ return new class extends Migration
Schema::table('nodes', function (Blueprint $table) { Schema::table('nodes', function (Blueprint $table) {
$table->dropForeign(['location_id']); $table->dropForeign(['location_id']);
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropIndex('nodes_location_id_foreign'); $table->dropIndex('nodes_location_id_foreign');
} }

View File

@ -15,7 +15,7 @@ return new class extends Migration
$table->dropForeign(['node']); $table->dropForeign(['node']);
$table->dropForeign(['assigned_to']); $table->dropForeign(['assigned_to']);
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropIndex('allocations_node_foreign'); $table->dropIndex('allocations_node_foreign');
$table->dropIndex('allocations_assigned_to_foreign'); $table->dropIndex('allocations_assigned_to_foreign');
} }
@ -36,7 +36,7 @@ return new class extends Migration
$table->dropForeign(['node_id']); $table->dropForeign(['node_id']);
$table->dropForeign(['server_id']); $table->dropForeign(['server_id']);
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropIndex('allocations_node_id_foreign'); $table->dropIndex('allocations_node_id_foreign');
$table->dropIndex('allocations_server_id_foreign'); $table->dropIndex('allocations_server_id_foreign');
} }

View File

@ -14,7 +14,7 @@ return new class extends Migration
Schema::table('service_options', function (Blueprint $table) { Schema::table('service_options', function (Blueprint $table) {
$table->dropForeign(['parent_service']); $table->dropForeign(['parent_service']);
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropIndex('service_options_parent_service_foreign'); $table->dropIndex('service_options_parent_service_foreign');
} }
@ -31,7 +31,7 @@ return new class extends Migration
Schema::table('service_options', function (Blueprint $table) { Schema::table('service_options', function (Blueprint $table) {
$table->dropForeign(['service_id']); $table->dropForeign(['service_id']);
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropIndex('service_options_service_id_foreign'); $table->dropIndex('service_options_service_id_foreign');
} }

View File

@ -14,7 +14,7 @@ return new class extends Migration
Schema::table('service_packs', function (Blueprint $table) { Schema::table('service_packs', function (Blueprint $table) {
$table->dropForeign(['option']); $table->dropForeign(['option']);
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropIndex('service_packs_option_foreign'); $table->dropIndex('service_packs_option_foreign');
} }
@ -31,7 +31,7 @@ return new class extends Migration
Schema::table('service_packs', function (Blueprint $table) { Schema::table('service_packs', function (Blueprint $table) {
$table->dropForeign(['option_id']); $table->dropForeign(['option_id']);
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropIndex('service_packs_option_id_foreign'); $table->dropIndex('service_packs_option_id_foreign');
} }

View File

@ -1,81 +0,0 @@
<?php
use App\Models\Subuser;
use App\Models\Permission;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('permissions', function (Blueprint $table) {
$table->unsignedInteger('subuser_id')->after('id');
});
DB::transaction(function () {
foreach (Subuser::all() as &$subuser) {
Permission::where('user_id', $subuser->user_id)->where('server_id', $subuser->server_id)->update([
'subuser_id' => $subuser->id,
]);
}
});
Schema::table('permissions', function (Blueprint $table) {
$table->dropForeign(['server_id']);
$table->dropForeign(['user_id']);
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
$table->dropIndex('permissions_server_id_foreign');
$table->dropIndex('permissions_user_id_foreign');
} else {
$table->dropForeign(['server_id']);
$table->dropForeign(['user_id']);
}
$table->dropColumn('server_id');
$table->dropColumn('user_id');
$table->dropColumn('created_at');
$table->dropColumn('updated_at');
$table->foreign('subuser_id')->references('id')->on('subusers');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('permissions', function (Blueprint $table) {
$table->unsignedInteger('server_id')->after('subuser_id');
$table->unsignedInteger('user_id')->after('server_id');
$table->timestamps();
});
DB::transaction(function () {
foreach (Subuser::all() as &$subuser) {
Permission::where('subuser_id', $subuser->id)->update([
'user_id' => $subuser->user_id,
'server_id' => $subuser->server_id,
]);
}
});
Schema::table('permissions', function (Blueprint $table) {
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
$table->dropForeign('permissions_subuser_id_foreign');
$table->dropIndex('permissions_subuser_id_foreign');
}
$table->dropColumn('subuser_id');
$table->foreign('server_id')->references('id')->on('servers');
$table->foreign('user_id')->references('id')->on('users');
});
}
};

View File

@ -12,7 +12,7 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::table('api_keys', function (Blueprint $table) { Schema::table('api_keys', function (Blueprint $table) {
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropForeign('api_keys_user_foreign'); $table->dropForeign('api_keys_user_foreign');
$table->dropIndex('api_keys_user_foreign'); $table->dropIndex('api_keys_user_foreign');
} }
@ -28,7 +28,7 @@ return new class extends Migration
public function down(): void public function down(): void
{ {
Schema::table('api_keys', function (Blueprint $table) { Schema::table('api_keys', function (Blueprint $table) {
if (Schema::getConnection()->getDriverName() !== 'sqlite') { if (!in_array(Schema::getConnection()->getDriverName(), ['sqlite', 'pgsql'])) {
$table->dropForeign('api_keys_user_id_foreign'); $table->dropForeign('api_keys_user_id_foreign');
$table->dropIndex('api_keys_user_id_foreign'); $table->dropIndex('api_keys_user_id_foreign');
} }

View File

@ -21,8 +21,8 @@ return new class extends Migration
{ {
Schema::create('downloads', function (Blueprint $table) { Schema::create('downloads', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->char('token', 36)->unique(); $table->string('token', 36)->unique();
$table->char('server', 36); $table->string('server', 36);
$table->text('path'); $table->text('path');
$table->timestamps(); $table->timestamps();
}); });

View File

@ -21,7 +21,7 @@ return new class extends Migration
{ {
Schema::create('node_configuration_tokens', function (Blueprint $table) { Schema::create('node_configuration_tokens', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->char('token', 32); $table->string('token', 32);
$table->unsignedInteger('node_id'); $table->unsignedInteger('node_id');
$table->timestamps(); $table->timestamps();
}); });

View File

@ -11,12 +11,6 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::table('permissions', function (Blueprint $table) {
$table->dropForeign(['subuser_id']);
$table->foreign('subuser_id')->references('id')->on('subusers')->onDelete('cascade');
});
Schema::table('subusers', function (Blueprint $table) { Schema::table('subusers', function (Blueprint $table) {
$table->dropForeign(['user_id']); $table->dropForeign(['user_id']);
$table->dropForeign(['server_id']); $table->dropForeign(['server_id']);
@ -38,11 +32,5 @@ return new class extends Migration
$table->foreign('user_id')->references('id')->on('users'); $table->foreign('user_id')->references('id')->on('users');
$table->foreign('server_id')->references('id')->on('servers'); $table->foreign('server_id')->references('id')->on('servers');
}); });
Schema::table('permissions', function (Blueprint $table) {
$table->dropForeign(['subuser_id']);
$table->foreign('subuser_id')->references('id')->on('subusers');
});
} }
}; };

View File

@ -1,43 +0,0 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$permissions = DB::table('permissions')->where('permission', 'like', '%-task%')->get();
foreach ($permissions as $record) {
$parts = explode('-', $record->permission);
if (!in_array(array_get($parts, 1), ['tasks', 'task']) || count($parts) !== 2) {
continue;
}
$newPermission = $parts[0] . '-' . str_replace('task', 'schedule', $parts[1]);
DB::table('permissions')->where('id', '=', $record->id)->update(['permission' => $newPermission]);
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$permissions = DB::table('permissions')->where('permission', 'like', '%-schedule%')->get();
foreach ($permissions as $record) {
$parts = explode('-', $record->permission);
if (!in_array(array_get($parts, 1), ['schedules', 'schedule']) || count($parts) !== 2) {
continue;
}
$newPermission = $parts[0] . '-' . str_replace('schedule', 'task', $parts[1]);
DB::table('permissions')->where('id', '=', $record->id)->update(['permission' => $newPermission]);
}
}
};

View File

@ -44,7 +44,7 @@ return new class extends Migration
public function down(): void public function down(): void
{ {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->char('daemonSecret', 36)->after('startup')->unique(); $table->string('daemonSecret', 36)->after('startup')->unique();
}); });
DB::table('daemon_keys')->truncate(); DB::table('daemon_keys')->truncate();

View File

@ -42,7 +42,7 @@ return new class extends Migration
public function down(): void public function down(): void
{ {
Schema::table('subusers', function (Blueprint $table) { Schema::table('subusers', function (Blueprint $table) {
$table->char('daemonSecret', 36)->after('server_id'); $table->string('daemonSecret', 36)->after('server_id');
}); });
$subusers = DB::table('subusers')->get(); $subusers = DB::table('subusers')->get();

View File

@ -18,7 +18,7 @@ return new class extends Migration
$table->dropUnique(['file']); $table->dropUnique(['file']);
$table->string('author')->change(); $table->string('author')->change();
$table->char('uuid', 36)->after('id'); $table->string('uuid', 36)->after('id');
$table->dropColumn('folder'); $table->dropColumn('folder');
$table->dropColumn('startup'); $table->dropColumn('startup');
$table->dropColumn('index_file'); $table->dropColumn('index_file');

View File

@ -14,7 +14,7 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::table('service_options', function (Blueprint $table) { Schema::table('service_options', function (Blueprint $table) {
$table->char('uuid', 36)->after('id'); $table->string('uuid', 36)->after('id');
$table->string('author')->after('service_id'); $table->string('author')->after('service_id');
$table->dropColumn('tag'); $table->dropColumn('tag');
}); });

View File

@ -31,10 +31,21 @@ return new class extends Migration
if (Schema::getConnection()->getDriverName() === 'sqlite') { if (Schema::getConnection()->getDriverName() === 'sqlite') {
Schema::table('api_keys', function (Blueprint $table) { Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn('public'); $table->dropColumn('public');
$table->char('secret', 32)->change(); $table->string('secret', 32)->change();
$table->renameColumn('secret', 'token'); $table->renameColumn('secret', 'token');
$table->string('token', 32)->unique()->change(); $table->string('token', 32)->unique()->change();
}); });
} elseif (Schema::getConnection()->getDriverName() === 'pgsql') {
// Rename column 'secret' to 'token'
DB::statement('ALTER TABLE api_keys RENAME COLUMN secret TO token');
// Change data type of 'token' to CHAR(32) and set NOT NULL constraint
DB::statement('ALTER TABLE api_keys ALTER COLUMN token TYPE CHAR(32)');
DB::statement('ALTER TABLE api_keys ALTER COLUMN token SET NOT NULL');
// Add unique constraint on 'token'
DB::statement('ALTER TABLE api_keys ADD CONSTRAINT api_keys_token_unique UNIQUE (token)');
} else { } else {
DB::statement('ALTER TABLE `api_keys` CHANGE `secret` `token` CHAR(32) NOT NULL, ADD UNIQUE INDEX `api_keys_token_unique` (`token`(32))'); DB::statement('ALTER TABLE `api_keys` CHANGE `secret` `token` CHAR(32) NOT NULL, ADD UNIQUE INDEX `api_keys_token_unique` (`token`(32))');
} }
@ -49,7 +60,7 @@ return new class extends Migration
DB::statement('ALTER TABLE `api_keys` CHANGE `token` `secret` TEXT, DROP INDEX `api_keys_token_unique`'); DB::statement('ALTER TABLE `api_keys` CHANGE `token` `secret` TEXT, DROP INDEX `api_keys_token_unique`');
Schema::table('api_keys', function (Blueprint $table) { Schema::table('api_keys', function (Blueprint $table) {
$table->char('public', 16)->after('user_id'); $table->string('public', 16)->after('user_id');
}); });
DB::transaction(function () { DB::transaction(function () {

View File

@ -16,7 +16,7 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::table('api_keys', function (Blueprint $table) { Schema::table('api_keys', function (Blueprint $table) {
$table->char('identifier', 16)->nullable()->unique()->after('user_id'); $table->string('identifier', 16)->nullable()->unique()->after('user_id');
$table->dropUnique(['token']); $table->dropUnique(['token']);
}); });

View File

@ -1,64 +1,11 @@
<?php <?php
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use App\Models\Permission;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use App\Models\Permission as P;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
return new class extends Migration return new class extends Migration
{ {
/**
* A list of all pre-1.0 permissions available to a user and their associated
* casting for the new permissions system.
*
* @var array
*/
protected static $permissionsMap = [
'power-start' => P::ACTION_CONTROL_START,
'power-stop' => P::ACTION_CONTROL_STOP,
'power-restart' => P::ACTION_CONTROL_RESTART,
'power-kill' => P::ACTION_CONTROL_STOP,
'send-command' => P::ACTION_CONTROL_CONSOLE,
'list-subusers' => P::ACTION_USER_READ,
'view-subuser' => P::ACTION_USER_READ,
'edit-subuser' => P::ACTION_USER_UPDATE,
'create-subuser' => P::ACTION_USER_CREATE,
'delete-subuser' => P::ACTION_USER_DELETE,
'view-allocations' => P::ACTION_ALLOCATION_READ,
'edit-allocation' => P::ACTION_ALLOCATION_UPDATE,
'view-startup' => P::ACTION_STARTUP_READ,
'edit-startup' => P::ACTION_STARTUP_UPDATE,
'view-databases' => P::ACTION_DATABASE_READ,
// Better to just break this flow a bit than accidentally grant a dangerous permission.
'reset-db-password' => P::ACTION_DATABASE_UPDATE,
'delete-database' => P::ACTION_DATABASE_DELETE,
'create-database' => P::ACTION_DATABASE_CREATE,
'access-sftp' => P::ACTION_FILE_SFTP,
'list-files' => P::ACTION_FILE_READ,
'edit-files' => P::ACTION_FILE_READ_CONTENT,
'save-files' => P::ACTION_FILE_UPDATE,
'create-files' => P::ACTION_FILE_CREATE,
'delete-files' => P::ACTION_FILE_DELETE,
'compress-files' => P::ACTION_FILE_ARCHIVE,
'list-schedules' => P::ACTION_SCHEDULE_READ,
'view-schedule' => P::ACTION_SCHEDULE_READ,
'edit-schedule' => P::ACTION_SCHEDULE_UPDATE,
'create-schedule' => P::ACTION_SCHEDULE_CREATE,
'delete-schedule' => P::ACTION_SCHEDULE_DELETE,
// Skipping these permissions as they are granted if you have more specific read/write permissions.
'move-files' => null,
'copy-files' => null,
'decompress-files' => null,
'upload-files' => null,
'download-files' => null,
// These permissions do not exist in 1.0
'toggle-schedule' => null,
'queue-schedule' => null,
];
/** /**
* Run the migrations. * Run the migrations.
*/ */
@ -67,31 +14,6 @@ return new class extends Migration
Schema::table('subusers', function (Blueprint $table) { Schema::table('subusers', function (Blueprint $table) {
$table->json('permissions')->nullable()->after('server_id'); $table->json('permissions')->nullable()->after('server_id');
}); });
$cursor = DB::table('permissions')
->select(['subuser_id'])
->selectRaw('GROUP_CONCAT(permission) as permissions')
->from('permissions')
->groupBy(['subuser_id'])
->cursor();
DB::transaction(function () use (&$cursor) {
$cursor->each(function ($datum) {
$updated = Collection::make(explode(',', $datum->permissions))
->map(function ($value) {
return self::$permissionsMap[$value] ?? null;
})->filter(function ($value) {
return !is_null($value) && $value !== Permission::ACTION_WEBSOCKET_CONNECT;
})
// All subusers get this permission, so make sure it gets pushed into the array.
->merge([Permission::ACTION_WEBSOCKET_CONNECT])
->unique()
->values()
->toJson();
DB::update('UPDATE subusers SET permissions = ? WHERE id = ?', [$updated, $datum->subuser_id]);
});
});
} }
/** /**
@ -99,25 +21,6 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
$flipped = array_flip(array_filter(self::$permissionsMap));
foreach (DB::select('SELECT id, permissions FROM subusers') as $datum) {
$values = [];
foreach (json_decode($datum->permissions, true) as $permission) {
$v = $flipped[$permission] ?? null;
if (!empty($v)) {
$values[] = $datum->id;
$values[] = $v;
}
}
if (!empty($values)) {
$string = 'VALUES ' . implode(', ', array_fill(0, count($values) / 2, '(?, ?)'));
DB::insert('INSERT INTO permissions(`subuser_id`, `permission`) ' . $string, $values);
}
}
Schema::table('subusers', function (Blueprint $table) { Schema::table('subusers', function (Blueprint $table) {
$table->dropColumn('permissions'); $table->dropColumn('permissions');
}); });

View File

@ -14,7 +14,7 @@ return new class extends Migration
Schema::create('backups', function (Blueprint $table) { Schema::create('backups', function (Blueprint $table) {
$table->bigIncrements('id'); $table->bigIncrements('id');
$table->unsignedInteger('server_id'); $table->unsignedInteger('server_id');
$table->char('uuid', 36); $table->string('uuid', 36);
$table->string('name'); $table->string('name');
$table->text('ignored_files'); $table->text('ignored_files');
$table->string('disk'); $table->string('disk');

View File

@ -21,8 +21,8 @@ return new class extends Migration
}); });
Schema::table('nodes', function (Blueprint $table) { Schema::table('nodes', function (Blueprint $table) {
$table->char('uuid', 36)->after('id'); $table->string('uuid', 36)->after('id');
$table->char('daemon_token_id', 16)->after('upload_size'); $table->string('daemon_token_id', 16)->after('upload_size');
$table->renameColumn('daemonSecret', 'daemon_token'); $table->renameColumn('daemonSecret', 'daemon_token');
}); });

View File

@ -13,7 +13,7 @@ return new class extends Migration
{ {
Schema::create('mounts', function (Blueprint $table) { Schema::create('mounts', function (Blueprint $table) {
$table->increments('id')->unique(); $table->increments('id')->unique();
$table->char('uuid', 36)->unique(); $table->string('uuid', 36)->unique();
$table->string('name')->unique(); $table->string('name')->unique();
$table->text('description')->nullable(); $table->text('description')->nullable();
$table->string('source'); $table->string('source');

View File

@ -22,7 +22,7 @@ return new class extends Migration
Schema::create('packs', function (Blueprint $table) { Schema::create('packs', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->unsignedInteger('egg_id'); $table->unsignedInteger('egg_id');
$table->char('uuid', 36)->unique(); $table->string('uuid', 36)->unique();
$table->string('name'); $table->string('name');
$table->string('version'); $table->string('version');
$table->text('description')->nullable(); $table->text('description')->nullable();

View File

@ -18,7 +18,11 @@ return new class extends Migration
}); });
Schema::table('eggs', function (Blueprint $table) { Schema::table('eggs', function (Blueprint $table) {
DB::statement('UPDATE `eggs` SET `docker_images` = JSON_ARRAY(docker_image)'); if (Schema::getConnection()->getDriverName() === 'pgsql') {
DB::statement('UPDATE eggs SET docker_images = json_build_array(docker_image)');
} else {
DB::statement('UPDATE eggs SET docker_images = JSON_ARRAY(docker_image)');
}
}); });
Schema::table('eggs', function (Blueprint $table) { Schema::table('eggs', function (Blueprint $table) {
@ -36,7 +40,7 @@ return new class extends Migration
}); });
Schema::table('eggs', function (Blueprint $table) { Schema::table('eggs', function (Blueprint $table) {
DB::statement('UPDATE `eggs` SET `docker_image` = JSON_UNQUOTE(JSON_EXTRACT(docker_images, "$[0]"))'); DB::statement('UPDATE eggs SET docker_image = JSON_UNQUOTE(JSON_EXTRACT(docker_images, "$[0]"))');
}); });
Schema::table('eggs', function (Blueprint $table) { Schema::table('eggs', function (Blueprint $table) {

View File

@ -1,5 +1,6 @@
<?php <?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
@ -11,6 +12,19 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
if (Schema::getConnection()->getDriverName() === 'pgsql') {
// Drop existing default first
DB::statement('ALTER TABLE server_transfers ALTER COLUMN successful DROP DEFAULT');
// Change type to boolean explicitly casting smallint values
DB::statement('ALTER TABLE server_transfers ALTER COLUMN successful TYPE BOOLEAN USING (successful <> 0)');
// Set column nullable if desired
DB::statement('ALTER TABLE server_transfers ALTER COLUMN successful DROP NOT NULL');
return;
}
Schema::table('server_transfers', function (Blueprint $table) { Schema::table('server_transfers', function (Blueprint $table) {
$table->boolean('successful')->nullable()->default(null)->change(); $table->boolean('successful')->nullable()->default(null)->change();
}); });
@ -21,6 +35,17 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
if (Schema::getConnection()->getDriverName() === 'pgsql') {
// Convert boolean back to smallint
DB::statement('ALTER TABLE server_transfers ALTER COLUMN successful TYPE SMALLINT USING (CASE WHEN successful THEN 1 ELSE 0 END)');
// Restore previous defaults and constraints
DB::statement('ALTER TABLE server_transfers ALTER COLUMN successful SET DEFAULT 0');
DB::statement('ALTER TABLE server_transfers ALTER COLUMN successful SET NOT NULL');
return;
}
Schema::table('server_transfers', function (Blueprint $table) { Schema::table('server_transfers', function (Blueprint $table) {
$table->boolean('successful')->default(0)->change(); $table->boolean('successful')->default(0)->change();
}); });

View File

@ -18,7 +18,7 @@ return new class extends Migration
// Update archived to all be true on existing transfers. // Update archived to all be true on existing transfers.
Schema::table('server_transfers', function (Blueprint $table) { Schema::table('server_transfers', function (Blueprint $table) {
DB::statement('UPDATE `server_transfers` SET `archived` = 1 WHERE `successful` = 1'); DB::table('server_transfers')->where('successful', 1)->update(['archived' => 1]);
}); });
} }

View File

@ -11,6 +11,15 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
if (Schema::getConnection()->getDriverName() === 'pgsql') {
Schema::table('server_transfers', function (Blueprint $table) {
DB::statement('ALTER TABLE server_transfers ALTER COLUMN old_additional_allocations TYPE JSON USING old_additional_allocations::json');
DB::statement('ALTER TABLE server_transfers ALTER COLUMN new_additional_allocations TYPE JSON USING new_additional_allocations::json');
});
return;
}
Schema::table('server_transfers', function (Blueprint $table) { Schema::table('server_transfers', function (Blueprint $table) {
$table->json('old_additional_allocations')->nullable()->change(); $table->json('old_additional_allocations')->nullable()->change();
$table->json('new_additional_allocations')->nullable()->change(); $table->json('new_additional_allocations')->nullable()->change();

View File

@ -13,7 +13,7 @@ return new class extends Migration
{ {
Schema::create('audit_logs', function (Blueprint $table) { Schema::create('audit_logs', function (Blueprint $table) {
$table->id(); $table->id();
$table->char('uuid', 36); $table->string('uuid', 36);
$table->boolean('is_system')->default(false); $table->boolean('is_system')->default(false);
$table->unsignedInteger('user_id')->nullable(); $table->unsignedInteger('user_id')->nullable();
$table->unsignedInteger('server_id')->nullable(); $table->unsignedInteger('server_id')->nullable();

View File

@ -17,9 +17,9 @@ return new class extends Migration
}); });
DB::transaction(function () { DB::transaction(function () {
DB::update('UPDATE servers SET `status` = \'suspended\' WHERE `suspended` = 1'); DB::table('servers')->where('suspended', 1)->update(['status' => 'suspended']);
DB::update('UPDATE servers SET `status` = \'installing\' WHERE `installed` = 0'); DB::table('servers')->where('suspended', 0)->update(['status' => 'installing']);
DB::update('UPDATE servers SET `status` = \'install_failed\' WHERE `installed` = 2'); DB::table('servers')->where('suspended', 2)->update(['status' => 'install_failed']);
}); });
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {

View File

@ -13,7 +13,9 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::table('schedules', function (Blueprint $table) { Schema::table('schedules', function (Blueprint $table) {
DB::update("UPDATE `schedules` SET `cron_month` = '*' WHERE `cron_month` = ''"); DB::table('schedules')
->where('cron_month', '')
->update(['cron_month' => '*']);
}); });
} }

View File

@ -2,6 +2,7 @@
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
return new class extends Migration return new class extends Migration
{ {
@ -10,6 +11,14 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
if (Schema::getConnection()->getDriverName() === 'pgsql') {
DB::table('eggs')->update([
'config_startup' => DB::raw("config_startup::jsonb - 'userInteraction'"),
]);
return;
}
// Remove User Interaction from startup config // Remove User Interaction from startup config
DB::table('eggs')->update([ DB::table('eggs')->update([
'config_startup' => DB::raw('JSON_REMOVE(config_startup, \'$.userInteraction\')'), 'config_startup' => DB::raw('JSON_REMOVE(config_startup, \'$.userInteraction\')'),

View File

@ -13,7 +13,7 @@ return new class extends Migration
{ {
Schema::create('activity_logs', function (Blueprint $table) { Schema::create('activity_logs', function (Blueprint $table) {
$table->id(); $table->id();
$table->char('batch', 36)->nullable(); $table->string('batch', 36)->nullable();
$table->string('event')->index(); $table->string('event')->index();
$table->string('ip'); $table->string('ip');
$table->text('description')->nullable(); $table->text('description')->nullable();

View File

@ -58,7 +58,7 @@ return new class extends Migration
Schema::create('nests', function (Blueprint $table) { Schema::create('nests', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->char('uuid', 36)->unique(); $table->string('uuid', 36)->unique();
$table->string('author'); $table->string('author');
$table->string('name'); $table->string('name');
$table->text('description')->nullable(); $table->text('description')->nullable();

View File

@ -16,6 +16,12 @@ return new class extends Migration
DB::table('egg_variables')->where('id', $eggVariable->id)->update(['rules' => explode('|', $eggVariable->rules)]); DB::table('egg_variables')->where('id', $eggVariable->id)->update(['rules' => explode('|', $eggVariable->rules)]);
}); });
if (Schema::getConnection()->getDriverName() === 'pgsql') {
DB::statement('ALTER TABLE egg_variables ALTER COLUMN rules TYPE JSON USING rules::json');
return;
}
Schema::table('egg_variables', function (Blueprint $table) { Schema::table('egg_variables', function (Blueprint $table) {
$table->json('rules')->change(); $table->json('rules')->change();
}); });

View File

@ -101,7 +101,7 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase
*/ */
public function test_get_missing_egg(): void public function test_get_missing_egg(): void
{ {
$response = $this->getJson('/api/application/eggs/nil'); $response = $this->getJson('/api/application/eggs/12345');
$this->assertNotFoundJson($response); $this->assertNotFoundJson($response);
} }

View File

@ -50,7 +50,7 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase
*/ */
public function test_get_missing_user(): void public function test_get_missing_user(): void
{ {
$response = $this->getJson('/api/application/users/external/nil'); $response = $this->getJson('/api/application/users/external/12345');
$this->assertNotFoundJson($response); $this->assertNotFoundJson($response);
} }

View File

@ -182,7 +182,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
*/ */
public function test_get_missing_user(): void public function test_get_missing_user(): void
{ {
$response = $this->getJson('/api/application/users/nil'); $response = $this->getJson('/api/application/users/12345');
$this->assertNotFoundJson($response); $this->assertNotFoundJson($response);
} }

View File

@ -3,15 +3,13 @@
namespace App\Tests\Integration\Api\Client; namespace App\Tests\Integration\Api\Client;
use App\Models\Task; use App\Models\Task;
use App\Models\Model;
use App\Models\Backup; use App\Models\Backup;
use App\Models\Server; use App\Models\Server;
use App\Models\Schedule; use App\Models\Schedule;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Models\Allocation; use App\Models\Allocation;
use App\Tests\Integration\TestResponse;
use App\Tests\Integration\IntegrationTestCase; use App\Tests\Integration\IntegrationTestCase;
use Illuminate\Database\Eloquent\Model as EloquentModel; use Illuminate\Database\Eloquent\Model;
use App\Transformers\Api\Client\BaseClientTransformer; use App\Transformers\Api\Client\BaseClientTransformer;
abstract class ClientApiIntegrationTestCase extends IntegrationTestCase abstract class ClientApiIntegrationTestCase extends IntegrationTestCase
@ -57,7 +55,7 @@ abstract class ClientApiIntegrationTestCase extends IntegrationTestCase
* Asserts that the data passed through matches the output of the data from the transformer. This * Asserts that the data passed through matches the output of the data from the transformer. This
* will remove the "relationships" key when performing the comparison. * will remove the "relationships" key when performing the comparison.
*/ */
protected function assertJsonTransformedWith(array $data, Model|EloquentModel $model): void protected function assertJsonTransformedWith(array $data, Model $model): void
{ {
$reflect = new \ReflectionClass($model); $reflect = new \ReflectionClass($model);
$transformer = sprintf('\\App\\Transformers\\Api\\Client\\%sTransformer', $reflect->getShortName()); $transformer = sprintf('\\App\\Transformers\\Api\\Client\\%sTransformer', $reflect->getShortName());

View File

@ -53,13 +53,13 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
/** @var \App\Models\Server[] $servers */ /** @var \App\Models\Server[] $servers */
$servers = [ $servers = [
$this->createServerModel(['user_id' => $users[0]->id, 'name' => 'Julia']), $this->createServerModel(['user_id' => $users[0]->id, 'name' => 'julia']),
$this->createServerModel(['user_id' => $users[1]->id, 'uuid_short' => '12121212', 'name' => 'Janice']), $this->createServerModel(['user_id' => $users[1]->id, 'uuid_short' => '12121212', 'name' => 'janice']),
$this->createServerModel(['user_id' => $users[1]->id, 'uuid' => '88788878-12356789', 'external_id' => 'ext123', 'name' => 'Julia']), $this->createServerModel(['user_id' => $users[1]->id, 'uuid' => '88788878-12356789', 'external_id' => 'ext123', 'name' => 'julia']),
$this->createServerModel(['user_id' => $users[1]->id, 'uuid' => '88788878-abcdefgh', 'name' => 'Jennifer']), $this->createServerModel(['user_id' => $users[1]->id, 'uuid' => '88788878-abcdefgh', 'name' => 'jennifer']),
]; ];
$this->actingAs($users[1])->getJson('/api/client?filter[*]=Julia') $this->actingAs($users[1])->getJson('/api/client?filter[*]=julia')
->assertOk() ->assertOk()
->assertJsonCount(1, 'data') ->assertJsonCount(1, 'data')
->assertJsonPath('data.0.attributes.identifier', $servers[2]->uuid_short); ->assertJsonPath('data.0.attributes.identifier', $servers[2]->uuid_short);
@ -90,7 +90,7 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
->assertJsonCount(1, 'data') ->assertJsonCount(1, 'data')
->assertJsonPath('data.0.attributes.identifier', $servers[3]->uuid_short); ->assertJsonPath('data.0.attributes.identifier', $servers[3]->uuid_short);
$this->actingAs($users[0])->getJson('/api/client?filter[*]=Julia&type=admin-all') $this->actingAs($users[0])->getJson('/api/client?filter[*]=julia&type=admin-all')
->assertOk() ->assertOk()
->assertJsonCount(2, 'data') ->assertJsonCount(2, 'data')
->assertJsonPath('data.0.attributes.identifier', $servers[0]->uuid_short) ->assertJsonPath('data.0.attributes.identifier', $servers[0]->uuid_short)

View File

@ -42,7 +42,7 @@ class GetStartupAndVariablesTest extends ClientApiIntegrationTestCase
$response->assertJsonPath('object', 'list'); $response->assertJsonPath('object', 'list');
$response->assertJsonCount(1, 'data'); $response->assertJsonCount(1, 'data');
$response->assertJsonPath('data.0.object', EggVariable::RESOURCE_NAME); $response->assertJsonPath('data.0.object', EggVariable::RESOURCE_NAME);
$this->assertJsonTransformedWith($response->json('data.0.attributes'), $egg->variables[1]); $this->assertJsonTransformedWith($response->json('data.0.attributes'), $egg->variables()->where('user_viewable', true)->first());
} }
/** /**

View File

@ -142,7 +142,7 @@ class TwoFactorControllerTest extends ClientApiIntegrationTestCase
$user = $user->refresh(); $user = $user->refresh();
$this->assertFalse($user->use_totp); $this->assertFalse($user->use_totp);
$this->assertNotNull($user->totp_authenticated_at); $this->assertNotNull($user->totp_authenticated_at);
$this->assertSame(Carbon::now()->toAtomString(), $user->totp_authenticated_at->toAtomString()); $this->assertTrue(now()->isSameAs('Y-m-d H:i:s', $user->totp_authenticated_at));
} }
/** /**

View File

@ -118,8 +118,8 @@ class ServerCreationServiceTest extends IntegrationTestCase
$this->assertSame($response->uuid_short, substr($response->uuid, 0, 8)); $this->assertSame($response->uuid_short, substr($response->uuid, 0, 8));
$this->assertSame($egg->id, $response->egg_id); $this->assertSame($egg->id, $response->egg_id);
$this->assertCount(2, $response->variables); $this->assertCount(2, $response->variables);
$this->assertSame('123', $response->variables[0]->server_value); $this->assertSame('123', $response->variables()->firstWhere(['env_variable' => 'BUNGEE_VERSION'])->server_value);
$this->assertSame('server2.jar', $response->variables[1]->server_value); $this->assertSame('server2.jar', $response->variables()->firstWhere(['env_variable' => 'SERVER_JARFILE'])->server_value);
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
if (in_array($key, ['allocation_additional', 'environment', 'start_on_completion'])) { if (in_array($key, ['allocation_additional', 'environment', 'start_on_completion'])) {

View File

@ -106,7 +106,7 @@ class StartupModificationServiceTest extends IntegrationTestCase
$clone = $this->cloneEggAndVariables($server->egg); $clone = $this->cloneEggAndVariables($server->egg);
// This makes the BUNGEE_VERSION variable not user editable. // This makes the BUNGEE_VERSION variable not user editable.
$clone->variables()->first()->update([ $clone->variables()->firstWhere(['env_variable' => 'BUNGEE_VERSION'])->update([
'user_editable' => false, 'user_editable' => false,
]); ]);
@ -115,7 +115,7 @@ class StartupModificationServiceTest extends IntegrationTestCase
ServerVariable::query()->updateOrCreate([ ServerVariable::query()->updateOrCreate([
'server_id' => $server->id, 'server_id' => $server->id,
'variable_id' => $server->variables[0]->id, 'variable_id' => $server->variables()->firstWhere(['env_variable' => 'BUNGEE_VERSION'])->id,
], ['variable_value' => 'EXIST']); ], ['variable_value' => 'EXIST']);
$response = $this->getService()->handle($server, [ $response = $this->getService()->handle($server, [
@ -126,8 +126,8 @@ class StartupModificationServiceTest extends IntegrationTestCase
]); ]);
$this->assertCount(2, $response->variables); $this->assertCount(2, $response->variables);
$this->assertSame('EXIST', $response->variables[0]->server_value); $this->assertSame('EXIST', $response->variables()->firstWhere(['env_variable' => 'BUNGEE_VERSION'])->server_value);
$this->assertSame('test.jar', $response->variables[1]->server_value); $this->assertSame('test.jar', $response->variables()->firstWhere(['env_variable' => 'SERVER_JARFILE'])->server_value);
$response = $this->getService() $response = $this->getService()
->setUserLevel(User::USER_LEVEL_ADMIN) ->setUserLevel(User::USER_LEVEL_ADMIN)
@ -139,8 +139,8 @@ class StartupModificationServiceTest extends IntegrationTestCase
]); ]);
$this->assertCount(2, $response->variables); $this->assertCount(2, $response->variables);
$this->assertSame('1234', $response->variables[0]->server_value); $this->assertSame('1234', $response->variables()->firstWhere(['env_variable' => 'BUNGEE_VERSION'])->server_value);
$this->assertSame('test.jar', $response->variables[1]->server_value); $this->assertSame('test.jar', $response->variables()->firstWhere(['env_variable' => 'SERVER_JARFILE'])->server_value);
} }
/** /**

View File

@ -107,12 +107,15 @@ class VariableValidatorServiceTest extends IntegrationTestCase
'SERVER_JARFILE' => 'server.jar', 'SERVER_JARFILE' => 'server.jar',
]); ]);
$bungeeVersion = $response->firstWhere('key', 'BUNGEE_VERSION');
$serverJarfile = $response->firstWhere('key', 'SERVER_JARFILE');
$this->assertInstanceOf(Collection::class, $response); $this->assertInstanceOf(Collection::class, $response);
$this->assertCount(2, $response); $this->assertCount(2, $response);
$this->assertSame('BUNGEE_VERSION', $response->get(0)->key); $this->assertSame('BUNGEE_VERSION', $bungeeVersion->key);
$this->assertSame('123', $response->get(0)->value); $this->assertSame('123', $bungeeVersion->value);
$this->assertSame('SERVER_JARFILE', $response->get(1)->key); $this->assertSame('SERVER_JARFILE', $serverJarfile->key);
$this->assertSame('server.jar', $response->get(1)->value); $this->assertSame('server.jar', $serverJarfile->value);
} }
public function test_nullable_environment_variables_can_be_used_correctly(): void public function test_nullable_environment_variables_can_be_used_correctly(): void