Merge remote-tracking branch 'origin/next' into api-sensitive-data-scrubber
This commit is contained in:
commit
0888198a81
13 changed files with 248 additions and 92 deletions
|
|
@ -153,7 +153,7 @@ public function disable_api(Request $request)
|
|||
return response()->json(['message' => 'API disabled.'], 200);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
#[OA\Post(
|
||||
summary: 'Enable MCP Server',
|
||||
description: 'Enable the MCP server endpoint at /mcp (only with root permissions).',
|
||||
path: '/mcp/enable',
|
||||
|
|
@ -209,7 +209,7 @@ public function enable_mcp(Request $request)
|
|||
return response()->json(['message' => 'MCP server enabled.'], 200);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
#[OA\Post(
|
||||
summary: 'Disable MCP Server',
|
||||
description: 'Disable the MCP server endpoint at /mcp (only with root permissions).',
|
||||
path: '/mcp/disable',
|
||||
|
|
|
|||
|
|
@ -31,10 +31,13 @@ public function handle(Request $request): Response
|
|||
}
|
||||
|
||||
$tagName = $request->get('tag');
|
||||
if ($tagName !== null && (! is_string($tagName) || trim($tagName) === '')) {
|
||||
return Response::error('tag argument must be a non-empty string.');
|
||||
}
|
||||
$args = $this->paginationArgs($request);
|
||||
|
||||
$query = Application::ownedByCurrentTeamAPI($teamId)
|
||||
->when($tagName, function ($query, $tagName) {
|
||||
->when($tagName !== null, function ($query) use ($tagName) {
|
||||
$query->whereHas('tags', fn ($q) => $q->where('name', $tagName));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use App\Mcp\Concerns\BuildsResponse;
|
||||
use App\Mcp\Concerns\ResolvesTeam;
|
||||
use App\Models\Project;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
|
|
@ -32,17 +32,15 @@ public function handle(Request $request): Response
|
|||
|
||||
$args = $this->paginationArgs($request);
|
||||
|
||||
$projects = Project::where('team_id', $teamId)->get();
|
||||
$services = collect();
|
||||
foreach ($projects as $project) {
|
||||
$services = $services->merge($project->services()->get());
|
||||
}
|
||||
$query = Service::whereHas('environment.project', fn ($q) => $q->where('team_id', $teamId));
|
||||
|
||||
$total = $services->count();
|
||||
$total = (clone $query)->count();
|
||||
|
||||
$summaries = $services
|
||||
->sortBy('name')
|
||||
->slice($args['offset'], $args['per_page'])
|
||||
$summaries = $query
|
||||
->orderBy('name')
|
||||
->skip($args['offset'])
|
||||
->take($args['per_page'])
|
||||
->get()
|
||||
->map(fn ($svc) => [
|
||||
'uuid' => $svc->uuid,
|
||||
'name' => $svc->name,
|
||||
|
|
|
|||
|
|
@ -134,8 +134,11 @@ public function databases()
|
|||
$mongodbs = $this->mongodbs;
|
||||
$mysqls = $this->mysqls;
|
||||
$mariadbs = $this->mariadbs;
|
||||
$keydbs = $this->keydbs;
|
||||
$dragonflies = $this->dragonflies;
|
||||
$clickhouses = $this->clickhouses;
|
||||
|
||||
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
|
||||
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
|
||||
}
|
||||
|
||||
public function attachedTo()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,26 @@
|
|||
<?php
|
||||
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
|
||||
const REDACTED = '<REDACTED>';
|
||||
const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb', 'keydb', 'dragonfly', 'clickhouse'];
|
||||
const STANDALONE_DATABASE_MODELS = [
|
||||
'postgresql' => StandalonePostgresql::class,
|
||||
'redis' => StandaloneRedis::class,
|
||||
'mongodb' => StandaloneMongodb::class,
|
||||
'mysql' => StandaloneMysql::class,
|
||||
'mariadb' => StandaloneMariadb::class,
|
||||
'keydb' => StandaloneKeydb::class,
|
||||
'dragonfly' => StandaloneDragonfly::class,
|
||||
'clickhouse' => StandaloneClickhouse::class,
|
||||
];
|
||||
const VALID_CRON_STRINGS = [
|
||||
'every_minute' => '* * * * *',
|
||||
'hourly' => '0 * * * *',
|
||||
|
|
|
|||
|
|
@ -1058,44 +1058,17 @@ function getResourceByUuid(string $uuid, ?int $teamId = null)
|
|||
}
|
||||
function queryDatabaseByUuidWithinTeam(string $uuid, string $teamId)
|
||||
{
|
||||
$postgresql = StandalonePostgresql::whereUuid($uuid)->first();
|
||||
if ($postgresql && $postgresql->team()->id == $teamId) {
|
||||
return $postgresql->unsetRelation('environment');
|
||||
}
|
||||
$redis = StandaloneRedis::whereUuid($uuid)->first();
|
||||
if ($redis && $redis->team()->id == $teamId) {
|
||||
return $redis->unsetRelation('environment');
|
||||
}
|
||||
$mongodb = StandaloneMongodb::whereUuid($uuid)->first();
|
||||
if ($mongodb && $mongodb->team()->id == $teamId) {
|
||||
return $mongodb->unsetRelation('environment');
|
||||
}
|
||||
$mysql = StandaloneMysql::whereUuid($uuid)->first();
|
||||
if ($mysql && $mysql->team()->id == $teamId) {
|
||||
return $mysql->unsetRelation('environment');
|
||||
}
|
||||
$mariadb = StandaloneMariadb::whereUuid($uuid)->first();
|
||||
if ($mariadb && $mariadb->team()->id == $teamId) {
|
||||
return $mariadb->unsetRelation('environment');
|
||||
}
|
||||
$keydb = StandaloneKeydb::whereUuid($uuid)->first();
|
||||
if ($keydb && $keydb->team()->id == $teamId) {
|
||||
return $keydb->unsetRelation('environment');
|
||||
}
|
||||
$dragonfly = StandaloneDragonfly::whereUuid($uuid)->first();
|
||||
if ($dragonfly && $dragonfly->team()->id == $teamId) {
|
||||
return $dragonfly->unsetRelation('environment');
|
||||
}
|
||||
$clickhouse = StandaloneClickhouse::whereUuid($uuid)->first();
|
||||
if ($clickhouse && $clickhouse->team()->id == $teamId) {
|
||||
return $clickhouse->unsetRelation('environment');
|
||||
foreach (STANDALONE_DATABASE_MODELS as $modelClass) {
|
||||
$database = $modelClass::whereUuid($uuid)->first();
|
||||
if ($database && $database->team()->id == $teamId) {
|
||||
return $database->unsetRelation('environment');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
function queryResourcesByUuid(string $uuid)
|
||||
{
|
||||
$resource = null;
|
||||
$application = Application::whereUuid($uuid)->first();
|
||||
if ($application) {
|
||||
return $application;
|
||||
|
|
@ -1104,37 +1077,11 @@ function queryResourcesByUuid(string $uuid)
|
|||
if ($service) {
|
||||
return $service;
|
||||
}
|
||||
$postgresql = StandalonePostgresql::whereUuid($uuid)->first();
|
||||
if ($postgresql) {
|
||||
return $postgresql;
|
||||
}
|
||||
$redis = StandaloneRedis::whereUuid($uuid)->first();
|
||||
if ($redis) {
|
||||
return $redis;
|
||||
}
|
||||
$mongodb = StandaloneMongodb::whereUuid($uuid)->first();
|
||||
if ($mongodb) {
|
||||
return $mongodb;
|
||||
}
|
||||
$mysql = StandaloneMysql::whereUuid($uuid)->first();
|
||||
if ($mysql) {
|
||||
return $mysql;
|
||||
}
|
||||
$mariadb = StandaloneMariadb::whereUuid($uuid)->first();
|
||||
if ($mariadb) {
|
||||
return $mariadb;
|
||||
}
|
||||
$keydb = StandaloneKeydb::whereUuid($uuid)->first();
|
||||
if ($keydb) {
|
||||
return $keydb;
|
||||
}
|
||||
$dragonfly = StandaloneDragonfly::whereUuid($uuid)->first();
|
||||
if ($dragonfly) {
|
||||
return $dragonfly;
|
||||
}
|
||||
$clickhouse = StandaloneClickhouse::whereUuid($uuid)->first();
|
||||
if ($clickhouse) {
|
||||
return $clickhouse;
|
||||
foreach (STANDALONE_DATABASE_MODELS as $modelClass) {
|
||||
$database = $modelClass::whereUuid($uuid)->first();
|
||||
if ($database) {
|
||||
return $database;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for ServiceDatabase by its own UUID
|
||||
|
|
@ -1143,7 +1090,7 @@ function queryResourcesByUuid(string $uuid)
|
|||
return $serviceDatabase;
|
||||
}
|
||||
|
||||
return $resource;
|
||||
return null;
|
||||
}
|
||||
function generateTagDeployWebhook($tag_name)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8651,7 +8651,7 @@
|
|||
}
|
||||
},
|
||||
"\/mcp\/enable": {
|
||||
"get": {
|
||||
"post": {
|
||||
"summary": "Enable MCP Server",
|
||||
"description": "Enable the MCP server endpoint at \/mcp (only with root permissions).",
|
||||
"operationId": "enable-mcp",
|
||||
|
|
@ -8703,7 +8703,7 @@
|
|||
}
|
||||
},
|
||||
"\/mcp\/disable": {
|
||||
"get": {
|
||||
"post": {
|
||||
"summary": "Disable MCP Server",
|
||||
"description": "Disable the MCP server endpoint at \/mcp (only with root permissions).",
|
||||
"operationId": "disable-mcp",
|
||||
|
|
|
|||
|
|
@ -5485,7 +5485,7 @@ paths:
|
|||
-
|
||||
bearerAuth: []
|
||||
/mcp/enable:
|
||||
get:
|
||||
post:
|
||||
summary: 'Enable MCP Server'
|
||||
description: 'Enable the MCP server endpoint at /mcp (only with root permissions).'
|
||||
operationId: enable-mcp
|
||||
|
|
@ -5514,7 +5514,7 @@ paths:
|
|||
-
|
||||
bearerAuth: []
|
||||
/mcp/disable:
|
||||
get:
|
||||
post:
|
||||
summary: 'Disable MCP Server'
|
||||
description: 'Disable the MCP server endpoint at /mcp (only with root permissions).'
|
||||
operationId: disable-mcp
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@
|
|||
], function () {
|
||||
Route::get('/enable', [OtherController::class, 'enable_api']);
|
||||
Route::get('/disable', [OtherController::class, 'disable_api']);
|
||||
Route::get('/mcp/enable', [OtherController::class, 'enable_mcp']);
|
||||
Route::get('/mcp/disable', [OtherController::class, 'disable_mcp']);
|
||||
Route::post('/mcp/enable', [OtherController::class, 'enable_mcp']);
|
||||
Route::post('/mcp/disable', [OtherController::class, 'disable_mcp']);
|
||||
});
|
||||
Route::group([
|
||||
'middleware' => ['auth:sanctum', ApiAllowed::class, 'api.sensitive'],
|
||||
|
|
|
|||
|
|
@ -43,25 +43,25 @@ function makeNonRootMcpToken(User $user, Team $team, array $abilities = ['write'
|
|||
return $token->plainTextToken;
|
||||
}
|
||||
|
||||
test('GET /api/v1/mcp/enable enables MCP server with root token', function () {
|
||||
test('POST /api/v1/mcp/enable enables MCP server with root token', function () {
|
||||
$token = makeRootMcpToken($this->user);
|
||||
|
||||
$response = test()->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/mcp/enable');
|
||||
])->postJson('/api/v1/mcp/enable');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['message' => 'MCP server enabled.']);
|
||||
expect(InstanceSettings::find(0)->is_mcp_server_enabled)->toBeTrue();
|
||||
});
|
||||
|
||||
test('GET /api/v1/mcp/disable disables MCP server with root token', function () {
|
||||
test('POST /api/v1/mcp/disable disables MCP server with root token', function () {
|
||||
InstanceSettings::query()->where('id', 0)->update(['is_mcp_server_enabled' => true]);
|
||||
$token = makeRootMcpToken($this->user);
|
||||
|
||||
$response = test()->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/mcp/disable');
|
||||
])->postJson('/api/v1/mcp/disable');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['message' => 'MCP server disabled.']);
|
||||
|
|
@ -73,7 +73,7 @@ function makeNonRootMcpToken(User $user, Team $team, array $abilities = ['write'
|
|||
|
||||
$response = test()->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/mcp/enable');
|
||||
])->postJson('/api/v1/mcp/enable');
|
||||
|
||||
$response->assertStatus(403);
|
||||
expect(InstanceSettings::find(0)->is_mcp_server_enabled)->toBeFalse();
|
||||
|
|
@ -85,14 +85,14 @@ function makeNonRootMcpToken(User $user, Team $team, array $abilities = ['write'
|
|||
|
||||
$response = test()->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/mcp/disable');
|
||||
])->postJson('/api/v1/mcp/disable');
|
||||
|
||||
$response->assertStatus(403);
|
||||
expect(InstanceSettings::find(0)->is_mcp_server_enabled)->toBeTrue();
|
||||
});
|
||||
|
||||
test('unauthenticated request to /api/v1/mcp/enable returns 401', function () {
|
||||
$response = test()->getJson('/api/v1/mcp/enable');
|
||||
$response = test()->postJson('/api/v1/mcp/enable');
|
||||
$response->assertStatus(401);
|
||||
});
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ function makeNonRootMcpToken(User $user, Team $team, array $abilities = ['write'
|
|||
|
||||
$response = test()->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->getJson('/api/v1/mcp/enable');
|
||||
])->postJson('/api/v1/mcp/enable');
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
|
|
|||
70
tests/Feature/QueryDatabaseByUuidWithinTeamTest.php
Normal file
70
tests/Feature/QueryDatabaseByUuidWithinTeamTest.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->teamA = Team::factory()->create();
|
||||
$this->teamB = Team::factory()->create();
|
||||
|
||||
$this->serverA = Server::factory()->create(['team_id' => $this->teamA->id]);
|
||||
$this->destinationA = StandaloneDocker::where('server_id', $this->serverA->id)->first();
|
||||
$this->projectA = Project::factory()->create(['team_id' => $this->teamA->id]);
|
||||
$this->envA = Environment::factory()->create(['project_id' => $this->projectA->id]);
|
||||
});
|
||||
|
||||
test('queryDatabaseByUuidWithinTeam returns database when team owns it', function () {
|
||||
$database = StandalonePostgresql::create([
|
||||
'name' => 'pg-team-a',
|
||||
'image' => 'postgres:15-alpine',
|
||||
'postgres_user' => 'postgres',
|
||||
'postgres_password' => 'password',
|
||||
'postgres_db' => 'postgres',
|
||||
'environment_id' => $this->envA->id,
|
||||
'destination_id' => $this->destinationA->id,
|
||||
'destination_type' => $this->destinationA->getMorphClass(),
|
||||
]);
|
||||
|
||||
$found = queryDatabaseByUuidWithinTeam($database->uuid, (string) $this->teamA->id);
|
||||
|
||||
expect($found)->not->toBeNull();
|
||||
expect($found->uuid)->toBe($database->uuid);
|
||||
expect($found)->toBeInstanceOf(StandalonePostgresql::class);
|
||||
});
|
||||
|
||||
test('queryDatabaseByUuidWithinTeam returns null when team does not own the database', function () {
|
||||
$database = StandalonePostgresql::create([
|
||||
'name' => 'pg-team-a',
|
||||
'image' => 'postgres:15-alpine',
|
||||
'postgres_user' => 'postgres',
|
||||
'postgres_password' => 'password',
|
||||
'postgres_db' => 'postgres',
|
||||
'environment_id' => $this->envA->id,
|
||||
'destination_id' => $this->destinationA->id,
|
||||
'destination_type' => $this->destinationA->getMorphClass(),
|
||||
]);
|
||||
|
||||
$found = queryDatabaseByUuidWithinTeam($database->uuid, (string) $this->teamB->id);
|
||||
|
||||
expect($found)->toBeNull();
|
||||
});
|
||||
|
||||
test('queryDatabaseByUuidWithinTeam returns null for unknown uuid', function () {
|
||||
$found = queryDatabaseByUuidWithinTeam('does-not-exist', (string) $this->teamA->id);
|
||||
|
||||
expect($found)->toBeNull();
|
||||
});
|
||||
|
||||
test('queryDatabaseByUuidWithinTeam can query every registered standalone database type without error', function () {
|
||||
foreach (STANDALONE_DATABASE_MODELS as $slug => $modelClass) {
|
||||
$count = $modelClass::query()->whereUuid('non-existent-uuid')->count();
|
||||
expect($count)->toBe(0, "{$modelClass} ({$slug}) failed whereUuid() smoke query");
|
||||
}
|
||||
});
|
||||
71
tests/Feature/StandaloneDockerDatabasesTest.php
Normal file
71
tests/Feature/StandaloneDockerDatabasesTest.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->team = Team::factory()->create();
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
});
|
||||
|
||||
function attachDb(string $modelClass, array $extra, $destination, $environment)
|
||||
{
|
||||
return $modelClass::create(array_merge([
|
||||
'name' => 'test-'.strtolower(class_basename($modelClass)),
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
], $extra));
|
||||
}
|
||||
|
||||
test('StandaloneDocker::databases() includes attached keydb', function () {
|
||||
attachDb(StandaloneKeydb::class, ['keydb_password' => 'pw'], $this->destination, $this->environment);
|
||||
|
||||
expect($this->destination->databases()->count())->toBe(1);
|
||||
expect($this->destination->attachedTo())->toBeTrue();
|
||||
});
|
||||
|
||||
test('StandaloneDocker::databases() includes attached dragonfly', function () {
|
||||
attachDb(StandaloneDragonfly::class, ['dragonfly_password' => 'pw'], $this->destination, $this->environment);
|
||||
|
||||
expect($this->destination->databases()->count())->toBe(1);
|
||||
expect($this->destination->attachedTo())->toBeTrue();
|
||||
});
|
||||
|
||||
test('StandaloneDocker::databases() includes attached clickhouse', function () {
|
||||
attachDb(StandaloneClickhouse::class, ['clickhouse_admin_password' => 'pw'], $this->destination, $this->environment);
|
||||
|
||||
expect($this->destination->databases()->count())->toBe(1);
|
||||
expect($this->destination->attachedTo())->toBeTrue();
|
||||
});
|
||||
|
||||
test('StandaloneDocker::databases() includes all 8 standalone database types', function () {
|
||||
attachDb(StandalonePostgresql::class, ['postgres_password' => 'pw'], $this->destination, $this->environment);
|
||||
attachDb(StandaloneRedis::class, ['redis_password' => 'pw'], $this->destination, $this->environment);
|
||||
attachDb(StandaloneMongodb::class, ['mongo_initdb_root_password' => 'pw'], $this->destination, $this->environment);
|
||||
attachDb(StandaloneMysql::class, ['mysql_root_password' => 'pw', 'mysql_password' => 'pw'], $this->destination, $this->environment);
|
||||
attachDb(StandaloneMariadb::class, ['mariadb_root_password' => 'pw', 'mariadb_password' => 'pw'], $this->destination, $this->environment);
|
||||
attachDb(StandaloneKeydb::class, ['keydb_password' => 'pw'], $this->destination, $this->environment);
|
||||
attachDb(StandaloneDragonfly::class, ['dragonfly_password' => 'pw'], $this->destination, $this->environment);
|
||||
attachDb(StandaloneClickhouse::class, ['clickhouse_admin_password' => 'pw'], $this->destination, $this->environment);
|
||||
|
||||
expect($this->destination->databases()->count())->toBe(8);
|
||||
expect($this->destination->attachedTo())->toBeTrue();
|
||||
});
|
||||
45
tests/Unit/StandaloneDatabaseRegistryTest.php
Normal file
45
tests/Unit/StandaloneDatabaseRegistryTest.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
use App\Models\StandaloneDocker;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Guards STANDALONE_DATABASE_MODELS against drift.
|
||||
*
|
||||
* MCP and API endpoints rely on this registry for team-scoped UUID lookups.
|
||||
* If a new App\Models\Standalone* model lands without a registry entry, the
|
||||
* helpers in bootstrap/helpers/shared.php silently fail to resolve it.
|
||||
*/
|
||||
test('STANDALONE_DATABASE_MODELS contains every Standalone* model on disk', function () {
|
||||
$files = glob(dirname(__DIR__, 2).'/app/Models/Standalone*.php');
|
||||
expect($files)->not->toBeEmpty();
|
||||
|
||||
$onDisk = collect($files)
|
||||
->map(fn (string $path) => 'App\\Models\\'.basename($path, '.php'))
|
||||
->reject(fn (string $class) => $class === StandaloneDocker::class)
|
||||
->sort()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$registered = collect(STANDALONE_DATABASE_MODELS)->values()->sort()->values()->all();
|
||||
|
||||
expect($registered)->toBe(
|
||||
$onDisk,
|
||||
'STANDALONE_DATABASE_MODELS in bootstrap/helpers/constants.php is out of sync with the App\\Models\\Standalone* classes on disk. '
|
||||
.'Add the missing model(s) to the registry (and to DATABASE_TYPES) so MCP/API helpers can resolve them.'
|
||||
);
|
||||
});
|
||||
|
||||
test('STANDALONE_DATABASE_MODELS keys mirror DATABASE_TYPES', function () {
|
||||
expect(array_keys(STANDALONE_DATABASE_MODELS))->toEqualCanonicalizing(DATABASE_TYPES);
|
||||
});
|
||||
|
||||
test('every STANDALONE_DATABASE_MODELS entry is an Eloquent model with whereUuid scope', function () {
|
||||
foreach (STANDALONE_DATABASE_MODELS as $slug => $modelClass) {
|
||||
expect(class_exists($modelClass))->toBeTrue("{$slug} maps to non-existent class {$modelClass}");
|
||||
expect(is_subclass_of($modelClass, Model::class))
|
||||
->toBeTrue("{$modelClass} is not an Eloquent model");
|
||||
expect(method_exists($modelClass, 'team'))
|
||||
->toBeTrue("{$modelClass} is missing team() accessor required by queryDatabaseByUuidWithinTeam()");
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue