Implements workspace-scoped managed tenant onboarding wizard (Filament v5 / Livewire v4) with strict RBAC (404/403 semantics), resumable sessions, provider connection selection/creation, verification OperationRun, and optional bootstrap. Removes legacy onboarding entrypoints and adds Pest coverage + spec artifacts (073).
149 lines
4.2 KiB
PHP
149 lines
4.2 KiB
PHP
<?php
|
|
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Services\Graph\GraphClientInterface;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\Support\AssertsNoOutboundHttp;
|
|
use Tests\Support\FailHardGraphClient;
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Test Case
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
|
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
|
| need to change it using the "pest()" function to bind a different classes or traits.
|
|
|
|
|
*/
|
|
|
|
pest()->extend(Tests\TestCase::class)
|
|
->use(RefreshDatabase::class)
|
|
->in('Feature');
|
|
|
|
pest()->extend(Tests\TestCase::class)
|
|
->in('Unit');
|
|
|
|
pest()->extend(Tests\TestCase::class)
|
|
->in('Deprecation');
|
|
|
|
beforeEach(function () {
|
|
putenv('INTUNE_TENANT_ID');
|
|
unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']);
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Expectations
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| When you're writing tests, you often need to check that values meet certain conditions. The
|
|
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
|
| to assert different things. Of course, you may extend the Expectation API at any time.
|
|
|
|
|
*/
|
|
|
|
expect()->extend('toBeOne', function () {
|
|
return $this->toBe(1);
|
|
});
|
|
|
|
function fakeIdToken(string $tenantId): string
|
|
{
|
|
$header = rtrim(strtr(base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])), '+/', '-_'), '=');
|
|
$payload = rtrim(strtr(base64_encode(json_encode(['tid' => $tenantId])), '+/', '-_'), '=');
|
|
|
|
return $header.'.'.$payload.'.signature';
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Functions
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
|
|
| project that you don't want to repeat in every file. Here you can also expose helpers as
|
|
| global functions to help you to reduce the number of lines of code in your test files.
|
|
|
|
|
*/
|
|
|
|
function something()
|
|
{
|
|
// ..
|
|
}
|
|
|
|
function bindFailHardGraphClient(): void
|
|
{
|
|
app()->instance(GraphClientInterface::class, new FailHardGraphClient);
|
|
}
|
|
|
|
function assertNoOutboundHttp(Closure $callback): mixed
|
|
{
|
|
return AssertsNoOutboundHttp::run($callback);
|
|
}
|
|
|
|
/**
|
|
* @return array{0: User, 1: Tenant}
|
|
*/
|
|
function createUserWithTenant(
|
|
?Tenant $tenant = null,
|
|
?User $user = null,
|
|
string $role = 'owner',
|
|
?string $workspaceRole = null,
|
|
): array {
|
|
$user ??= User::factory()->create();
|
|
$tenant ??= Tenant::factory()->create();
|
|
|
|
$workspaceRole ??= $role;
|
|
|
|
$validWorkspaceRoles = array_map(
|
|
static fn (\App\Support\Auth\WorkspaceRole $role): string => $role->value,
|
|
\App\Support\Auth\WorkspaceRole::cases(),
|
|
);
|
|
|
|
if (! in_array($workspaceRole, $validWorkspaceRoles, true)) {
|
|
$workspaceRole = \App\Support\Auth\WorkspaceRole::Owner->value;
|
|
}
|
|
|
|
$workspace = null;
|
|
|
|
if ($tenant->workspace_id !== null) {
|
|
$workspace = Workspace::query()->whereKey($tenant->workspace_id)->first();
|
|
}
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
$tenant->forceFill([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
])->save();
|
|
}
|
|
|
|
WorkspaceMembership::query()->updateOrCreate([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
], [
|
|
'role' => $workspaceRole,
|
|
]);
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
$user->forceFill(['last_workspace_id' => (int) $workspace->getKey()])->save();
|
|
|
|
$user->tenants()->syncWithoutDetaching([
|
|
$tenant->getKey() => ['role' => $role],
|
|
]);
|
|
|
|
return [$user, $tenant];
|
|
}
|
|
|
|
/**
|
|
* @return array{tenant: string}
|
|
*/
|
|
function filamentTenantRouteParams(Tenant $tenant): array
|
|
{
|
|
return ['tenant' => (string) $tenant->external_id];
|
|
}
|