mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-26 12:28:34 +00:00
Split V5 dashboard behavior into domain controllers and policies, add agent token rotation/revocation, status reconciliation jobs, ingress firewall syncing, and canvas connection APIs. Add migrations for V5 status tracking, server capabilities, resource connection aliases, and revoked agent tokens.
154 lines
5.3 KiB
PHP
154 lines
5.3 KiB
PHP
<?php
|
|
|
|
use App\Http\Middleware\V5\EnsureCurrentTeam;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
|
|
beforeEach(function () {
|
|
Config::set('cache.default', 'array');
|
|
|
|
Schema::dropIfExists('team_user');
|
|
Schema::dropIfExists('teams');
|
|
Schema::dropIfExists('users');
|
|
|
|
Schema::create('users', function ($table) {
|
|
$table->id();
|
|
$table->string('name')->default('Anonymous');
|
|
$table->string('email');
|
|
$table->timestamp('email_verified_at')->nullable();
|
|
$table->string('password')->nullable();
|
|
$table->rememberToken();
|
|
$table->timestamps();
|
|
});
|
|
|
|
Schema::create('teams', function ($table) {
|
|
$table->id();
|
|
$table->string('name');
|
|
$table->string('description')->nullable();
|
|
$table->boolean('personal_team')->default(false);
|
|
$table->boolean('show_boarding')->default(false);
|
|
$table->timestamps();
|
|
});
|
|
|
|
Schema::create('team_user', function ($table) {
|
|
$table->id();
|
|
$table->foreignId('team_id');
|
|
$table->foreignId('user_id');
|
|
$table->string('role')->default('member');
|
|
$table->timestamps();
|
|
|
|
$table->unique(['team_id', 'user_id']);
|
|
});
|
|
});
|
|
|
|
function createEnsureCurrentTeamUser(string $email = 'margaret@example.com'): User
|
|
{
|
|
return User::withoutEvents(fn () => User::query()->create([
|
|
'name' => 'Margaret Hamilton',
|
|
'email' => $email,
|
|
'email_verified_at' => now(),
|
|
'password' => 'password',
|
|
]));
|
|
}
|
|
|
|
function createEnsureCurrentTeamTeam(string $name = 'V5 Tooling Team'): Team
|
|
{
|
|
return Team::withoutEvents(fn () => Team::query()->create([
|
|
'name' => $name,
|
|
'description' => 'The team that ships v5.',
|
|
'personal_team' => false,
|
|
'show_boarding' => false,
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* Runs the middleware against a request the way the v5.authenticated group
|
|
* would: an authenticated user with an active session store.
|
|
*
|
|
* @return array{0: Response, 1: Request}
|
|
*/
|
|
function runEnsureCurrentTeam(?User $user): array
|
|
{
|
|
$request = Request::create('/v5', 'GET');
|
|
$request->setLaravelSession(app('session.store'));
|
|
$request->setUserResolver(fn () => $user);
|
|
|
|
$response = (new EnsureCurrentTeam)->handle($request, fn () => response()->noContent());
|
|
|
|
return [$response, $request];
|
|
}
|
|
|
|
it('aborts 403 when the user belongs to no team', function () {
|
|
$user = createEnsureCurrentTeamUser();
|
|
|
|
expect(fn () => runEnsureCurrentTeam($user))
|
|
->toThrow(fn (HttpException $exception) => expect($exception->getStatusCode())->toBe(403));
|
|
});
|
|
|
|
it('falls back to first team when the session team is no longer a member', function () {
|
|
$user = createEnsureCurrentTeamUser();
|
|
$memberTeam = createEnsureCurrentTeamTeam('Member Team');
|
|
$foreignTeam = createEnsureCurrentTeamTeam('Foreign Team');
|
|
$user->teams()->attach($memberTeam, ['role' => 'owner']);
|
|
|
|
session(['currentTeam' => $foreignTeam]);
|
|
|
|
[, $request] = runEnsureCurrentTeam($user);
|
|
|
|
expect($request->attributes->get('v5.currentTeam')->id)->toBe($memberTeam->id)
|
|
->and(session('currentTeam'))->toBeInstanceOf(Team::class)
|
|
->and(session('currentTeam')->id)->toBe($memberTeam->id)
|
|
->and(session('currentTeam')->name)->toBe('Member Team');
|
|
});
|
|
|
|
it('stores the full team model so v4 session reads keep working', function () {
|
|
$user = createEnsureCurrentTeamUser();
|
|
$team = createEnsureCurrentTeamTeam();
|
|
$user->teams()->attach($team, ['role' => 'owner']);
|
|
|
|
runEnsureCurrentTeam($user);
|
|
|
|
$sessionTeam = session('currentTeam');
|
|
|
|
// v4 reads columns beyond id/name/description/personal_team off the
|
|
// session team (e.g. show_boarding, custom limits), so the stored model
|
|
// must not be a partial select.
|
|
expect($sessionTeam)->toBeInstanceOf(Team::class)
|
|
->and($sessionTeam->getAttributes())->toHaveKeys([
|
|
'id',
|
|
'name',
|
|
'description',
|
|
'personal_team',
|
|
'show_boarding',
|
|
'created_at',
|
|
'updated_at',
|
|
])
|
|
->and($sessionTeam->created_at)->not->toBeNull()
|
|
->and((bool) $sessionTeam->show_boarding)->toBeFalse();
|
|
});
|
|
|
|
it('does not rewrite the session when the team is unchanged', function () {
|
|
$user = createEnsureCurrentTeamUser();
|
|
$team = createEnsureCurrentTeamTeam();
|
|
$user->teams()->attach($team, ['role' => 'owner']);
|
|
|
|
// Prime the session with the same team but a sentinel attribute; if the
|
|
// middleware skips the redundant write, the sentinel survives the request.
|
|
$sessionTeam = Team::query()->findOrFail($team->id);
|
|
$sessionTeam->name = 'Session Sentinel';
|
|
session(['currentTeam' => $sessionTeam]);
|
|
|
|
[, $request] = runEnsureCurrentTeam($user);
|
|
|
|
expect($request->attributes->get('v5.currentTeam'))->toBeInstanceOf(Team::class)
|
|
->and($request->attributes->get('v5.currentTeam')->id)->toBe($team->id)
|
|
->and($request->attributes->get('v5.currentTeam')->name)->toBe('V5 Tooling Team')
|
|
->and(session('currentTeam'))->toBe($sessionTeam)
|
|
->and(session('currentTeam')->name)->toBe('Session Sentinel');
|
|
});
|