TenantAtlas/tests/Pest.php
ahmido 2bf5de4663 085-tenant-operate-hub (#103)
Summary

Consolidates the “Tenant Operate Hub” work (Spec 085) and the follow-up adjustments from the 086 session merge into a single branch ready to merge into dev.
Primary focus: stabilize Ops/Operate Hub UX flows, tighten/align authorization semantics, and make the full Sail test suite green.
Key Changes

Ops UX / Verification
Readonly members can view verification operation runs (reports) while starting verification remains restricted.
Normalized failure reason-code handling and aligned UX expectations with the provider reason-code taxonomy.
Onboarding wizard UX
“Start verification” CTA is hidden while a verification run is active; “Refresh” is shown during in-progress runs.
Treats provider_permission_denied as a blocking reason (while keeping legacy compatibility).
Test + fixture hardening
Standardized use of default provider connection fixtures in tests where sync/restore flows require it.
Fixed multiple Filament URL/tenant-context test cases to avoid 404s and reduce tenancy routing brittleness.
Policy sync / restore safety
Enrollment configuration type collision classification tests now exercise the real sync path (with required provider connection present).
Restore edge-case safety tests updated to reflect current provider-connection requirements.
Testing

vendor/bin/sail artisan test --compact (green)
vendor/bin/sail bin pint --dirty (green)
Notes

Includes merged 086 session work already (no separate PR needed).

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@ebc83aaa-d947-4a08-b88e-bd72ac9645f7.fritz.box>
Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.fritz.box>
Reviewed-on: #103
2026-02-11 13:02:03 +00:00

222 lines
6.3 KiB
PHP

<?php
use App\Models\ProviderConnection;
use App\Models\ProviderCredential;
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 Pest\PendingCalls\TestCall;
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()
{
// ..
}
/**
* Spec test naming helper.
*
* Convention for focused runs:
* - Prefix every Spec 081 test title with "Spec081 ".
* - Keep filenames suffixed with "Spec081Test.php".
* - Command: `vendor/bin/sail artisan test --compact --filter=Spec081`
*/
function spec081(string $description): string
{
$normalized = trim($description);
if ($normalized === '') {
return 'Spec081';
}
return str_starts_with($normalized, 'Spec081 ')
? $normalized
: 'Spec081 '.$normalized;
}
/**
* Convenience wrapper for Spec 081 tests.
*/
function itSpec081(string $description, ?\Closure $closure = null): TestCall
{
$call = it(spec081($description), $closure);
$call->group('spec081');
return $call;
}
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];
}
function ensureDefaultProviderConnection(Tenant $tenant, string $provider = 'microsoft'): ProviderConnection
{
$connection = ProviderConnection::query()
->where('tenant_id', (int) $tenant->getKey())
->where('provider', $provider)
->where('is_default', true)
->orderBy('id')
->first();
if (! $connection instanceof ProviderConnection) {
$connection = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => $provider,
'entra_tenant_id' => (string) ($tenant->tenant_id ?? fake()->uuid()),
'status' => 'connected',
'health_status' => 'ok',
'is_default' => true,
]);
}
$credential = $connection->credential()->first();
if (! $credential instanceof ProviderCredential) {
ProviderCredential::factory()->create([
'provider_connection_id' => (int) $connection->getKey(),
'type' => 'client_secret',
'payload' => [
'client_id' => fake()->uuid(),
'client_secret' => fake()->sha1(),
],
]);
$connection->refresh();
}
return $connection;
}