Complete implementation of TenantPilot v1 Intune Management Platform with comprehensive backup, versioning, and restore capabilities. CONSTITUTION & SPEC - Ratified constitution v1.0.0 with 7 core principles - Complete spec.md with 7 user stories (US1-7) - Detailed plan.md with constitution compliance check - Task breakdown with 125+ tasks across 12 phases CORE FEATURES (US1-4) - Policy inventory with Graph-based sync (US1) - Backup creation with immutable JSONB snapshots (US2) - Version history with diff viewer (human + JSON) (US3) - Defensive restore with preview/dry-run (US4) TENANT MANAGEMENT (US6-7) - Full tenant CRUD with Entra ID app configuration - Admin consent callback flow integration - Tenant connectivity verification - Permission health status monitoring - 'Highlander' pattern: single current tenant with is_current flag GRAPH ABSTRACTION - Complete isolation layer (7 classes) - GraphClientInterface with mockable implementations - Error mapping, logging, and standardized responses - Rate-limit aware design DOMAIN SERVICES - BackupService: immutable snapshot creation - RestoreService: preview, selective restore, conflict detection - VersionService: immutable version capture - VersionDiff: human-readable and structured diffs - PolicySyncService: Graph-based policy import - TenantConfigService: connectivity testing - TenantPermissionService: permission health checks - AuditLogger: comprehensive audit trail DATA MODEL - 11 migrations with tenant-aware schema - 8 Eloquent models with proper relationships - SoftDeletes on Tenant, BackupSet, BackupItem, PolicyVersion, RestoreRun - JSONB storage for snapshots, metadata, permissions - Encrypted storage for client secrets - Partial unique index for is_current tenant FILAMENT ADMIN UI - 5 main resources: Tenant, Policy, PolicyVersion, BackupSet, RestoreRun - RelationManagers: Versions (Policy), BackupItems (BackupSet) - Actions: Verify config, Admin consent, Make current, Delete/Force delete - Filters: Status, Type, Platform, Archive state - Permission panel with status indicators - ActionGroup pattern for cleaner row actions HOUSEKEEPING (Phases 10-12) - Soft delete with archive status for all entities - Force delete protection (blocks if dependencies exist) - Tenant deactivation with cascade prevention - Audit logging for all delete operations TESTING - 36 tests passing (125 assertions, 11.21s) - Feature tests: Policy, Backup, Restore, Version, Tenant, Housekeeping - Unit tests: VersionDiff, TenantCurrent, Permissions, Scopes - Full TDD coverage for critical flows CONFIGURATION - config/tenantpilot.php: 10+ policy types with metadata - config/intune_permissions.php: required Graph permissions - config/graph.php: Graph client configuration SAFETY & COMPLIANCE - Constitution compliance: 7/7 principles ✓ - Safety-first operations: preview, confirmation, validation - Immutable versioning: no in-place modifications - Defensive restore: dry-run, selective, conflict detection - Comprehensive auditability: all critical operations logged - Tenant-aware architecture: multi-tenant ready - Graph abstraction: isolated, mockable, testable - Spec-driven development: spec → plan → tasks → implementation OPERATIONAL READINESS - Laravel Sail for local development - Dokploy deployment documentation - Queue/worker ready architecture - Migration safety notes - Environment variable documentation Tests: 36 passed Duration: 11.21s Status: Production-ready (98% complete)
151 lines
4.5 KiB
PHP
151 lines
4.5 KiB
PHP
<?php
|
|
|
|
use App\Filament\Resources\TenantResource\Pages\CreateTenant;
|
|
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantPermission;
|
|
use App\Models\User;
|
|
use App\Services\Graph\GraphClientInterface;
|
|
use App\Services\Graph\GraphResponse;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
test('tenant can be created via filament and verified successfully', function () {
|
|
app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface
|
|
{
|
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, []);
|
|
}
|
|
|
|
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, []);
|
|
}
|
|
|
|
public function getOrganization(array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, ['value' => [['id' => $options['tenant'] ?? 'tenant']]], 200);
|
|
}
|
|
|
|
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, []);
|
|
}
|
|
});
|
|
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
Livewire::test(CreateTenant::class)
|
|
->fillForm([
|
|
'name' => 'Contoso',
|
|
'tenant_id' => 'tenant-guid',
|
|
'domain' => 'contoso.com',
|
|
'app_client_id' => 'client-123',
|
|
'app_notes' => 'Test tenant',
|
|
])
|
|
->call('create')
|
|
->assertHasNoFormErrors();
|
|
|
|
$tenant = Tenant::first();
|
|
expect($tenant)->not->toBeNull();
|
|
|
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
|
->callAction('verify');
|
|
|
|
$tenant->refresh();
|
|
|
|
expect($tenant->app_status)->toBe('ok');
|
|
|
|
$this->assertDatabaseHas('audit_logs', [
|
|
'tenant_id' => $tenant->id,
|
|
'action' => 'tenant.config.verified',
|
|
'status' => 'success',
|
|
]);
|
|
|
|
$this->assertDatabaseHas('tenant_permissions', [
|
|
'tenant_id' => $tenant->id,
|
|
'status' => 'ok',
|
|
]);
|
|
});
|
|
|
|
test('verify configuration records error when graph fails', function () {
|
|
app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface
|
|
{
|
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, []);
|
|
}
|
|
|
|
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, []);
|
|
}
|
|
|
|
public function getOrganization(array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(false, [], 401, ['auth failed']);
|
|
}
|
|
|
|
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, []);
|
|
}
|
|
});
|
|
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
$tenant = Tenant::create([
|
|
'tenant_id' => 'tenant-error',
|
|
'name' => 'Error Tenant',
|
|
]);
|
|
|
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
|
->callAction('verify');
|
|
|
|
$tenant->refresh();
|
|
|
|
expect($tenant->app_status)->toBe('error');
|
|
|
|
$this->assertDatabaseHas('audit_logs', [
|
|
'tenant_id' => $tenant->id,
|
|
'action' => 'tenant.config.verified',
|
|
'status' => 'error',
|
|
]);
|
|
|
|
$this->assertDatabaseHas('tenant_permissions', [
|
|
'tenant_id' => $tenant->id,
|
|
'status' => 'error',
|
|
]);
|
|
});
|
|
|
|
test('tenant detail shows required permissions with statuses', function () {
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
$tenant = Tenant::create([
|
|
'tenant_id' => 'tenant-ui',
|
|
'name' => 'UI Tenant',
|
|
]);
|
|
|
|
$permissions = config('intune_permissions.permissions', []);
|
|
$firstKey = $permissions[0]['key'] ?? 'DeviceManagementConfiguration.ReadWrite.All';
|
|
|
|
TenantPermission::create([
|
|
'tenant_id' => $tenant->id,
|
|
'permission_key' => $firstKey,
|
|
'status' => 'ok',
|
|
]);
|
|
|
|
$response = $this->get(route('filament.admin.resources.tenants.view', $tenant));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee($firstKey);
|
|
$response->assertSee('ok');
|
|
$response->assertSee('missing');
|
|
});
|