Compare commits

..

No commits in common. "5e70cdaad4fa20d2dac6a7a36f2994e3d22bfcb5" and "91fe8d29ff9d390a7ff8339ce8096474524ba3d2" have entirely different histories.

58 changed files with 172 additions and 46 deletions

View File

@ -32,6 +32,7 @@ ## Tests
- [x] T021 Security: tenant isolation (cannot see other tenant edges)
## Finalization
- [x] T022 Run full test suite (`php artisan test`)
- [ ] T022 Run full test suite (`php artisan test`)
Note: Attempted; blocked by unrelated legacy test configuration error.
- [x] T023 Run Pint (`vendor/bin/pint`)
- [x] T024 Update checklist items in `checklists/pr-gate.md`

View File

@ -4,7 +4,6 @@
use App\Models\BackupSchedule;
use App\Models\BackupScheduleRun;
use App\Models\BackupSet;
use App\Services\BulkOperationService;
use App\Services\Intune\BackupService;
use App\Services\Intune\PolicySyncService;
use Carbon\CarbonImmutable;
@ -72,7 +71,6 @@ public function createBackupSet($tenant, $policyIds, ?string $actorEmail = null,
app(\App\Services\BackupScheduling\ScheduleTimeService::class),
app(\App\Services\Intune\AuditLogger::class),
app(\App\Services\BackupScheduling\RunErrorMapper::class),
app(BulkOperationService::class),
);
$run->refresh();
@ -116,7 +114,6 @@ public function createBackupSet($tenant, $policyIds, ?string $actorEmail = null,
app(\App\Services\BackupScheduling\ScheduleTimeService::class),
app(\App\Services\Intune\AuditLogger::class),
app(\App\Services\BackupScheduling\RunErrorMapper::class),
app(BulkOperationService::class),
);
$run->refresh();

View File

@ -1,6 +1,9 @@
<?php
use App\Services\Intune\PolicyNormalizer;
use Tests\TestCase;
uses(TestCase::class);
test('app protection normalizer formats blocked/required booleans and durations', function () {
$normalizer = app(PolicyNormalizer::class);

View File

@ -9,8 +9,10 @@
use App\Services\Graph\ScopeTagResolver;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery\MockInterface;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
it('enriches assignment filter names when filter data is stored at root', function () {
$tenant = Tenant::factory()->create([
'tenant_id' => 'tenant-123',

View File

@ -6,8 +6,10 @@
use App\Services\Graph\GraphResponse;
use App\Services\Graph\MicrosoftGraphClient;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
$this->graphClient = Mockery::mock(MicrosoftGraphClient::class);
$this->fetcher = new AssignmentFetcher($this->graphClient, app(GraphContractRegistry::class));

View File

@ -5,8 +5,10 @@
use App\Services\Graph\MicrosoftGraphClient;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Cache::flush();
$this->graphClient = Mockery::mock(MicrosoftGraphClient::class);

View File

@ -9,6 +9,9 @@
use App\Services\Graph\GraphLogger;
use App\Services\Graph\GraphResponse;
use App\Services\Intune\AuditLogger;
use Tests\TestCase;
uses(TestCase::class);
beforeEach(function () {
config()->set('graph_contracts.types.deviceManagementScript', [

View File

@ -2,8 +2,10 @@
use App\Models\BackupItem;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('assignments cast works', function () {
$backupItem = BackupItem::factory()->create([
'assignments' => [

View File

@ -3,6 +3,9 @@
use App\Models\BackupSchedule;
use App\Services\BackupScheduling\ScheduleTimeService;
use Carbon\CarbonImmutable;
use Tests\TestCase;
uses(TestCase::class);
it('skips nonexistent DST local time slots for daily schedules', function () {
$schedule = new BackupSchedule;

View File

@ -7,8 +7,10 @@
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('policies bulk actions are available for authenticated users', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();

View File

@ -8,8 +8,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('bulk backup set delete job archives sets and cascades to backup items', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();

View File

@ -9,8 +9,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('bulk backup set force delete job permanently deletes archived sets and their items', function () {
$tenant = Tenant::factory()->create(['is_current' => true]);
$user = User::factory()->create();

View File

@ -8,8 +8,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('bulk backup set restore job restores archived sets and their items', function () {
$tenant = Tenant::factory()->create(['is_current' => true]);
$user = User::factory()->create();

View File

@ -6,7 +6,8 @@
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
uses(Tests\TestCase::class, RefreshDatabase::class);
it('can abort a bulk operation run', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();

View File

@ -4,8 +4,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('bulk operation service updates progress counters', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();

View File

@ -6,8 +6,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('job processes bulk delete successfully', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();

View File

@ -9,8 +9,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('job processes bulk export successfully', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();

View File

@ -7,8 +7,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('job force deletes archived versions and skips active versions', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();

View File

@ -7,8 +7,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('job prunes eligible versions and skips ineligible with reasons', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();

View File

@ -8,8 +8,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('bulk policy version restore restores archived versions', function () {
$tenant = Tenant::factory()->create(['is_current' => true]);
$user = User::factory()->create();

View File

@ -7,8 +7,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('job soft deletes deletable restore runs', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();

View File

@ -8,8 +8,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('bulk restore run restore restores archived runs', function () {
$tenant = Tenant::factory()->create(['is_current' => true]);
$user = User::factory()->create();

View File

@ -6,8 +6,10 @@
use App\Models\User;
use App\Services\BulkOperationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('bulk delete aborts when more than half of items fail', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create();

View File

@ -2,6 +2,8 @@
use App\Services\Intune\CompliancePolicyNormalizer;
uses(Tests\TestCase::class);
it('groups compliance policy fields into structured blocks', function () {
$normalizer = app(CompliancePolicyNormalizer::class);

View File

@ -2,6 +2,8 @@
use App\Services\Intune\DefaultPolicyNormalizer;
uses(Tests\TestCase::class);
it('flattens normalized settings with section prefixes for diffs', function () {
$normalizer = app(DefaultPolicyNormalizer::class);

View File

@ -2,6 +2,8 @@
use App\Services\Intune\DeviceConfigurationPolicyNormalizer;
uses(Tests\TestCase::class);
it('builds a configuration block for device configuration policies', function () {
$normalizer = app(DeviceConfigurationPolicyNormalizer::class);

View File

@ -9,8 +9,10 @@
use App\Services\Intune\FoundationSnapshotService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery\MockInterface;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
class FoundationMappingGraphClient implements GraphClientInterface
{
public array $requests = [];

View File

@ -5,7 +5,9 @@
use App\Services\Graph\GraphResponse;
use App\Services\Intune\FoundationSnapshotService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class);
uses(RefreshDatabase::class);
class FoundationSnapshotGraphClient implements GraphClientInterface

View File

@ -5,6 +5,9 @@
use App\Services\Graph\MicrosoftGraphClient;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
uses(TestCase::class);
beforeEach(function () {
config()->set('graph.base_url', 'https://graph.microsoft.com');

View File

@ -7,6 +7,9 @@
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
uses(TestCase::class);
it('falls back to default graph scope when config is empty', function () {
Cache::flush();

View File

@ -5,6 +5,9 @@
use App\Services\Graph\MicrosoftGraphClient;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
uses(TestCase::class);
beforeEach(function () {
config()->set('graph_contracts.types.deviceConfiguration', [

View File

@ -1,6 +1,9 @@
<?php
use App\Services\Graph\GraphContractRegistry;
use Tests\TestCase;
uses(TestCase::class);
beforeEach(function () {
$this->registry = app(GraphContractRegistry::class);

View File

@ -1,6 +1,9 @@
<?php
use App\Services\Graph\GraphContractRegistry;
use Tests\TestCase;
uses(TestCase::class);
beforeEach(function () {
config()->set('graph_contracts.types.settingsCatalogPolicy', [

View File

@ -1,6 +1,9 @@
<?php
use App\Services\Graph\GraphContractRegistry;
use Tests\TestCase;
uses(TestCase::class);
it('builds settings write method and path from the contract', function () {
config()->set('graph_contracts.types.settingsCatalogPolicy', [

View File

@ -2,6 +2,9 @@
use App\Services\Graph\GraphContractRegistry;
use App\Services\Graph\GraphResponse;
use Tests\TestCase;
uses(TestCase::class);
beforeEach(function () {
config()->set('graph_contracts.types.deviceConfiguration', [

View File

@ -6,8 +6,10 @@
use App\Services\Graph\MicrosoftGraphClient;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Cache::flush();
$this->graphClient = Mockery::mock(MicrosoftGraphClient::class);

View File

@ -1,6 +1,9 @@
<?php
use App\Services\Intune\PolicyNormalizer;
use Tests\TestCase;
uses(TestCase::class);
test('managed device app configuration normalizer shows app config keys and values', function () {
$normalizer = app(PolicyNormalizer::class);

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
uses(Tests\TestCase::class);
use App\Services\Graph\GraphContractRegistry;
use App\Services\Graph\GraphLogger;
use App\Services\Graph\MicrosoftGraphClient;

View File

@ -6,6 +6,9 @@
use Illuminate\Http\Client\RequestException as HttpRequestException;
use Illuminate\Http\Client\Response as HttpClientResponse;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
uses(TestCase::class);
it('returns a graph response when the HTTP client throws for a 400 response', function () {
config()->set('graph.client_id', 'client-id');

View File

@ -10,8 +10,10 @@
use App\Services\Intune\PolicySnapshotService;
use App\Services\Intune\VersionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('capture returns failure when snapshot fetch fails (no exception)', function () {
$tenant = Tenant::factory()->create([
'tenant_id' => 'tenant-1',

View File

@ -4,6 +4,8 @@
use App\Services\Intune\PolicyNormalizer;
use App\Services\Intune\PolicyTypeNormalizer;
uses(Tests\TestCase::class);
it('routes to the first matching policy type normalizer', function () {
$defaultNormalizer = app(DefaultPolicyNormalizer::class);

View File

@ -4,7 +4,9 @@
use App\Models\SettingsCatalogDefinition;
use App\Services\Intune\PolicyNormalizer;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class);
uses(RefreshDatabase::class);
beforeEach(function () {

View File

@ -2,7 +2,9 @@
use App\Services\Intune\PolicyNormalizer;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class);
uses(RefreshDatabase::class);
beforeEach(function () {

View File

@ -1,6 +1,9 @@
<?php
use App\Services\Intune\PolicyNormalizer;
use Tests\TestCase;
uses(TestCase::class);
beforeEach(function () {
$this->normalizer = app(PolicyNormalizer::class);

View File

@ -1,5 +1,9 @@
<?php
use Tests\TestCase;
uses(TestCase::class);
it('shortens external ids for picker display', function () {
expect(\App\Livewire\BackupSetPolicyPickerTable::externalIdShort('00000000-0000-0000-0000-1234abcd'))
->toBe('1234abcd');

View File

@ -6,7 +6,9 @@
use App\Services\Graph\GraphResponse;
use App\Services\Intune\PolicySnapshotService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class);
uses(RefreshDatabase::class);
class PolicySnapshotGraphClient implements GraphClientInterface

View File

@ -4,8 +4,10 @@
use App\Models\PolicyVersion;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('pruneEligible returns only old non-current versions', function () {
$tenant = Tenant::factory()->create();
$policy = Policy::factory()->create(['tenant_id' => $tenant->id]);

View File

@ -5,8 +5,10 @@
use App\Services\Graph\GraphResponse;
use App\Services\Intune\RbacOnboardingService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
config()->set('tenantpilot.features.conditional_access', false);
});

View File

@ -4,8 +4,10 @@
use App\Models\RestoreRun;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('deletable scope includes only finished statuses', function () {
$tenant = Tenant::factory()->create();

View File

@ -2,8 +2,10 @@
use App\Models\RestoreRun;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
test('group_mapping cast works', function () {
$restoreRun = RestoreRun::factory()->create([
'group_mapping' => [

View File

@ -7,15 +7,17 @@
use App\Services\Graph\ScopeTagResolver;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Cache::flush();
});
test('resolves scope tag IDs to objects with id and displayName', function () {
$tenant = Tenant::factory()->create();
$mockGraphClient = Mockery::mock(MicrosoftGraphClient::class);
$mockGraphClient->shouldReceive('request')
->with('GET', '/deviceManagement/roleScopeTags', Mockery::on(function ($options) use ($tenant) {
@ -37,7 +39,7 @@
));
$mockLogger = Mockery::mock(GraphLogger::class);
$resolver = new ScopeTagResolver($mockGraphClient, $mockLogger);
$result = $resolver->resolve(['0', '1', '2'], $tenant);
@ -50,7 +52,7 @@
test('caches scope tag objects for 1 hour', function () {
$tenant = Tenant::factory()->create();
$mockGraphClient = Mockery::mock(MicrosoftGraphClient::class);
$mockGraphClient->shouldReceive('request')
->once() // Only called once due to caching
@ -64,12 +66,12 @@
));
$mockLogger = Mockery::mock(GraphLogger::class);
$resolver = new ScopeTagResolver($mockGraphClient, $mockLogger);
// First call - fetches from API
$result1 = $resolver->resolve(['0'], $tenant);
// Second call - should use cache
$result2 = $resolver->resolve(['0'], $tenant);
@ -81,7 +83,7 @@
$tenant = Tenant::factory()->create();
$mockGraphClient = Mockery::mock(MicrosoftGraphClient::class);
$mockLogger = Mockery::mock(GraphLogger::class);
$resolver = new ScopeTagResolver($mockGraphClient, $mockLogger);
$result = $resolver->resolve([], $tenant);
@ -90,7 +92,7 @@
test('handles 403 forbidden gracefully', function () {
$tenant = Tenant::factory()->create();
$mockGraphClient = Mockery::mock(MicrosoftGraphClient::class);
$mockGraphClient->shouldReceive('request')
->once()
@ -101,7 +103,7 @@
));
$mockLogger = Mockery::mock(GraphLogger::class);
$resolver = new ScopeTagResolver($mockGraphClient, $mockLogger);
$result = $resolver->resolve(['0', '1'], $tenant);
@ -111,7 +113,7 @@
test('filters returned scope tags to requested IDs', function () {
$tenant = Tenant::factory()->create();
$mockGraphClient = Mockery::mock(MicrosoftGraphClient::class);
$mockGraphClient->shouldReceive('request')
->once()
@ -127,7 +129,7 @@
));
$mockLogger = Mockery::mock(GraphLogger::class);
$resolver = new ScopeTagResolver($mockGraphClient, $mockLogger);
// Request only IDs 0 and 2
$result = $resolver->resolve(['0', '2'], $tenant);
@ -137,3 +139,4 @@
expect($result[0])->toBe(['id' => '0', 'displayName' => 'Default']);
expect($result[2])->toBe(['id' => '2', 'displayName' => 'Verbund-2']);
});

View File

@ -1,6 +1,9 @@
<?php
use App\Services\Intune\PolicyNormalizer;
use Tests\TestCase;
uses(TestCase::class);
it('normalizes deviceManagementScript into readable settings', function () {
$normalizer = app(PolicyNormalizer::class);

View File

@ -3,7 +3,8 @@
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
uses(Tests\TestCase::class, RefreshDatabase::class);
it('builds a settings table for settings catalog policies', function () {
$normalizer = app(SettingsCatalogPolicyNormalizer::class);

View File

@ -2,8 +2,10 @@
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
function restoreIntuneTenantId(string|false $original): void
{
$original !== false

View File

@ -6,8 +6,10 @@
use App\Services\Graph\GraphResponse;
use App\Services\Intune\TenantPermissionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
function requiredPermissions(): array
{
$service = app(TenantPermissionService::class);

View File

@ -3,8 +3,10 @@
use App\Filament\Resources\TenantResource;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
it('includes scope parameter in admin consent url', function () {
// The adminConsentUrl builds scopes from intune_permissions config, not graph.scope
$tenant = Tenant::create([

View File

@ -2,8 +2,10 @@
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
uses(RefreshDatabase::class);
it('finds tenant by guid without casting to bigint', function () {
$tenant = Tenant::create([
'tenant_id' => 'b0091e5d-944f-4a34-bcd9-12cbfb7b75cf',

View File

@ -1,6 +1,9 @@
<?php
use App\Services\Intune\PolicyNormalizer;
use Tests\TestCase;
uses(TestCase::class);
it('normalizes windows driver update profiles into readable settings', function () {
$normalizer = app(PolicyNormalizer::class);