create(); expect($report)->toBeInstanceOf(StoredReport::class) ->and($report->report_type)->toBe(StoredReport::REPORT_TYPE_PERMISSION_POSTURE) ->and($report->payload)->toBeArray() ->and($report->payload)->toHaveKeys(['posture_score', 'required_count', 'granted_count', 'checked_at', 'permissions']); }); it('casts payload to array', function (): void { $report = StoredReport::factory()->create(); $fresh = StoredReport::query()->find($report->getKey()); expect($fresh->payload)->toBeArray() ->and($fresh->payload['posture_score'])->toBe(86); }); it('belongs to a tenant', function (): void { $report = StoredReport::factory()->create(); expect($report->tenant)->toBeInstanceOf(Tenant::class) ->and($report->tenant->getKey())->toBe($report->tenant_id); }); it('belongs to a workspace via DerivesWorkspaceIdFromTenant', function (): void { [$user, $tenant] = createUserWithTenant(); $report = StoredReport::factory()->create([ 'tenant_id' => $tenant->getKey(), ]); expect($report->workspace_id)->toBe($tenant->workspace_id); }); it('has the correct REPORT_TYPE_PERMISSION_POSTURE constant', function (): void { expect(StoredReport::REPORT_TYPE_PERMISSION_POSTURE)->toBe('permission_posture'); }); // --- T024: Generator integration scenarios --- it('generator creates report with correct report_type and payload schema', function (): void { [$user, $tenant] = createUserWithTenant(); $generator = app(FindingGeneratorContract::class); $comparison = [ 'overall_status' => 'missing', 'permissions' => [ ['key' => 'Perm.A', 'type' => 'application', 'status' => 'granted', 'features' => ['backup']], ['key' => 'Perm.B', 'type' => 'application', 'status' => 'missing', 'features' => ['restore']], ['key' => 'Perm.C', 'type' => 'delegated', 'status' => 'granted', 'features' => ['sync']], ], 'last_refreshed_at' => now()->toIso8601String(), ]; $generator->generate($tenant, $comparison); $report = StoredReport::query() ->where('tenant_id', $tenant->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE) ->first(); expect($report)->not->toBeNull() ->and($report->report_type)->toBe(StoredReport::REPORT_TYPE_PERMISSION_POSTURE) ->and($report->payload)->toHaveKeys(['posture_score', 'required_count', 'granted_count', 'checked_at', 'permissions']) ->and($report->payload['required_count'])->toBe(3) ->and($report->payload['granted_count'])->toBe(2) ->and($report->payload['permissions'])->toHaveCount(3); }); it('generator report posture_score matches PostureScoreCalculator output', function (): void { [$user, $tenant] = createUserWithTenant(); $comparison = [ 'overall_status' => 'missing', 'permissions' => [ ['key' => 'Perm.A', 'type' => 'application', 'status' => 'granted', 'features' => []], ['key' => 'Perm.B', 'type' => 'application', 'status' => 'missing', 'features' => []], ['key' => 'Perm.C', 'type' => 'application', 'status' => 'missing', 'features' => []], ], 'last_refreshed_at' => now()->toIso8601String(), ]; $expectedScore = (new PostureScoreCalculator)->calculate($comparison); $generator = app(FindingGeneratorContract::class); $result = $generator->generate($tenant, $comparison); $report = StoredReport::query() ->where('tenant_id', $tenant->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE) ->first(); expect($report->payload['posture_score'])->toBe($expectedScore) ->and($result->postureScore)->toBe($expectedScore) ->and($expectedScore)->toBe(33); }); // --- T025: Temporal ordering --- it('multiple posture reports for same tenant ordered by created_at descending', function (): void { [$user, $tenant] = createUserWithTenant(); // Create reports at different timestamps $first = StoredReport::factory()->create([ 'tenant_id' => $tenant->getKey(), 'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE, 'created_at' => now()->subHours(3), ]); $second = StoredReport::factory()->create([ 'tenant_id' => $tenant->getKey(), 'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE, 'created_at' => now()->subHours(1), ]); $third = StoredReport::factory()->create([ 'tenant_id' => $tenant->getKey(), 'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE, 'created_at' => now(), ]); $results = StoredReport::query() ->where('tenant_id', $tenant->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE) ->orderByDesc('created_at') ->pluck('id'); expect($results->count())->toBe(3) ->and($results[0])->toBe($third->getKey()) ->and($results[1])->toBe($second->getKey()) ->and($results[2])->toBe($first->getKey()); }); it('reports queryable by tenant_id and report_type', function (): void { [$user, $tenantA] = createUserWithTenant(); [$user2, $tenantB] = createUserWithTenant(); StoredReport::factory()->count(2)->create([ 'tenant_id' => $tenantA->getKey(), 'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE, ]); StoredReport::factory()->create([ 'tenant_id' => $tenantB->getKey(), 'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE, ]); $tenantAReports = StoredReport::query() ->where('tenant_id', $tenantA->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE) ->count(); expect($tenantAReports)->toBe(2); }); // --- T026: Polymorphic reusability --- it('different report_type coexists with permission_posture reports', function (): void { [$user, $tenant] = createUserWithTenant(); StoredReport::factory()->create([ 'tenant_id' => $tenant->getKey(), 'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE, ]); StoredReport::create([ 'tenant_id' => (int) $tenant->getKey(), 'report_type' => 'compliance_summary', 'payload' => ['compliant' => 5, 'noncompliant' => 2], ]); $postureReports = StoredReport::query() ->where('tenant_id', $tenant->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE) ->count(); $complianceReports = StoredReport::query() ->where('tenant_id', $tenant->getKey()) ->where('report_type', 'compliance_summary') ->count(); $allReports = StoredReport::query() ->where('tenant_id', $tenant->getKey()) ->count(); expect($postureReports)->toBe(1) ->and($complianceReports)->toBe(1) ->and($allReports)->toBe(2); });