fix: stabilize tests and spec plan artifacts
This commit is contained in:
parent
629bbef6e9
commit
70fd5d2e68
3
.github/agents/copilot-instructions.md
vendored
3
.github/agents/copilot-instructions.md
vendored
@ -3,6 +3,8 @@ # TenantAtlas Development Guidelines
|
|||||||
Auto-generated from all feature plans. Last updated: 2025-12-22
|
Auto-generated from all feature plans. Last updated: 2025-12-22
|
||||||
|
|
||||||
## Active Technologies
|
## Active Technologies
|
||||||
|
- PHP 8.4.15 + Laravel 12, Filament v4, Livewire v3 (feat/005-bulk-operations)
|
||||||
|
- PostgreSQL (app), SQLite in-memory (tests) (feat/005-bulk-operations)
|
||||||
|
|
||||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||||
|
|
||||||
@ -22,6 +24,7 @@ ## Code Style
|
|||||||
PHP 8.4.15: Follow standard conventions
|
PHP 8.4.15: Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- feat/005-bulk-operations: Added PHP 8.4.15 + Laravel 12, Filament v4, Livewire v3
|
||||||
|
|
||||||
- feat/005-bulk-operations: Added PHP 8.4.15
|
- feat/005-bulk-operations: Added PHP 8.4.15
|
||||||
|
|
||||||
|
|||||||
12
specs/005-bulk-operations/contracts/openapi.yaml
Normal file
12
specs/005-bulk-operations/contracts/openapi.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: TenantPilot - Bulk Operations (Feature 005)
|
||||||
|
version: 0.0.0
|
||||||
|
description: |
|
||||||
|
This feature is implemented via Filament/Livewire actions inside the admin panel.
|
||||||
|
No public, stable HTTP API endpoints are introduced specifically for bulk operations.
|
||||||
|
|
||||||
|
This OpenAPI document is intentionally minimal.
|
||||||
|
servers: []
|
||||||
|
paths: {}
|
||||||
|
components: {}
|
||||||
@ -1,82 +1,38 @@
|
|||||||
# Implementation Plan: Feature 005 - Bulk Operations
|
# Implementation Plan: Feature 005 - Bulk Operations
|
||||||
|
|
||||||
**Branch**: `feat/005-bulk-operations` | **Date**: 2025-12-22 | **Spec**: [spec.md](./spec.md)
|
**Branch**: `feat/005-bulk-operations` | **Date**: 2025-12-25 | **Spec**: [spec.md](./spec.md)
|
||||||
|
**Input**: Feature specification from `/specs/005-bulk-operations/spec.md`
|
||||||
|
|
||||||
|
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Enable efficient bulk operations (delete, export, prune) across TenantPilot's main resources (Policies, Policy Versions, Backup Sets, Restore Runs) with safety gates, progress tracking, and comprehensive audit logging. Technical approach: Filament bulk actions + Laravel Queue jobs with chunked processing + BulkOperationRun tracking model + Livewire polling for progress updates.
|
Add consistent bulk actions (delete/export/restore/prune/sync where applicable) across TenantPilot's primary admin resources (Policies, Policy Versions, Backup Sets, Restore Runs). Bulk operations create a tracking record, enforce permissions, support type-to-confirm for large destructive changes, and run asynchronously via queue for larger selections with progress tracking.
|
||||||
|
|
||||||
## Technical Context
|
## Technical Context
|
||||||
|
|
||||||
|
|
||||||
**Language/Version**: PHP 8.4.15
|
**Language/Version**: PHP 8.4.15
|
||||||
**Framework**: Laravel 12
|
**Primary Dependencies**: Laravel 12, Filament v4, Livewire v3
|
||||||
**Primary Dependencies**:
|
**Storage**: PostgreSQL (app), SQLite in-memory (tests)
|
||||||
- Filament v4 (admin panel + bulk actions)
|
**Testing**: Pest v4 + PHPUnit 12
|
||||||
- Livewire v3 (reactive UI + polling)
|
**Target Platform**: Containerized Linux (Sail/Dokploy)
|
||||||
- Laravel Queue (async job processing)
|
**Project Type**: Web application (Laravel + Filament admin panel)
|
||||||
- PostgreSQL (JSONB for tracking)
|
**Performance Goals**: Handle bulk actions up to hundreds of items with predictable runtime; keep UI responsive via queued processing for larger selections
|
||||||
|
**Constraints**: Tenant isolation; least privilege; safe destructive actions (confirmation + auditability); avoid long locks/timeouts by chunking
|
||||||
**Storage**: PostgreSQL with JSONB fields for:
|
**Scale/Scope**: Admin-focused operations, moderate concurrency, emphasis on correctness/auditability over throughput
|
||||||
- `bulk_operation_runs.item_ids` (array of resource IDs)
|
|
||||||
- `bulk_operation_runs.failures` (per-item error details)
|
|
||||||
- Existing audit logs (metadata column)
|
|
||||||
|
|
||||||
**Testing**: Pest v4 (unit, feature, browser tests)
|
|
||||||
**Target Platform**: Web (Dokploy deployment)
|
|
||||||
**Project Type**: Web application (Filament admin panel)
|
|
||||||
|
|
||||||
**Performance Goals**:
|
|
||||||
- Process 100 items in <2 minutes (queued)
|
|
||||||
- Handle up to 500 items per operation without timeout
|
|
||||||
- Progress notifications update every 5-10 seconds
|
|
||||||
|
|
||||||
**Constraints**:
|
|
||||||
- Queue jobs MUST process in chunks of 10-20 items (memory efficiency)
|
|
||||||
- Progress tracking requires explicit polling (not automatic in Filament)
|
|
||||||
- Type-to-confirm required for ≥20 destructive items
|
|
||||||
- Tenant isolation enforced at job level
|
|
||||||
|
|
||||||
**Scale/Scope**:
|
|
||||||
- 4 primary resources (Policies, PolicyVersions, BackupSets, RestoreRuns)
|
|
||||||
- 8-12 bulk actions (P1/P2 priority)
|
|
||||||
- Estimated 26-34 hours implementation (3 phases for P1/P2)
|
|
||||||
|
|
||||||
## Constitution Check
|
## Constitution Check
|
||||||
|
|
||||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
**Note**: Project constitution is template-only (not populated). Using Laravel/TenantPilot conventions instead.
|
The constitution file at `.specify/memory/constitution.md` is a placeholder template (no concrete principles/gates are defined). For this feature, the effective gates follow repository agent guidelines in `Agents.md`:
|
||||||
|
|
||||||
### Architecture Principles
|
- Spec artifacts exist and are consistent: PASS (`spec.md`, `plan.md`, `tasks.md`, `research.md`, `data-model.md`, `quickstart.md`)
|
||||||
|
- Tests cover changes: PASS (Pest suite; full test run exits 0)
|
||||||
|
- Safe admin operations: PASS (explicit confirmations, type-to-confirm for large destructive ops, audit logging)
|
||||||
|
|
||||||
✅ **Library-First**: N/A (feature extends existing app, no new libraries)
|
Re-check after Phase 1: PASS (no new unknowns introduced).
|
||||||
✅ **Test-First**: TDD enforced - Pest tests required before implementation
|
|
||||||
✅ **Simplicity**: Uses existing patterns (Jobs, Filament bulk actions, Livewire polling)
|
|
||||||
✅ **Sail-First**: Local development uses Laravel Sail (Docker)
|
|
||||||
✅ **Dokploy Deployment**: Production/staging via Dokploy (VPS containers)
|
|
||||||
|
|
||||||
### Laravel Conventions
|
|
||||||
|
|
||||||
✅ **PSR-12**: Code formatting enforced via Laravel Pint
|
|
||||||
✅ **Eloquent-First**: No raw DB queries, use Model::query() patterns
|
|
||||||
✅ **Permission Gates**: Leverage existing RBAC (Feature 001)
|
|
||||||
✅ **Queue Jobs**: Use ShouldQueue interface, chunked processing
|
|
||||||
✅ **Audit Logging**: Extend existing AuditLog model/service
|
|
||||||
|
|
||||||
### Safety Requirements
|
|
||||||
|
|
||||||
✅ **Tenant Isolation**: Job constructor accepts explicit `tenantId`
|
|
||||||
✅ **Audit Trail**: One audit log entry per bulk operation + per-item outcomes
|
|
||||||
✅ **Confirmation**: Type-to-confirm for ≥20 destructive items
|
|
||||||
✅ **Fail-Soft**: Continue processing on individual failures, abort if >50% fail
|
|
||||||
✅ **Immutability**: Policy Versions check eligibility before prune (referenced, current, age)
|
|
||||||
|
|
||||||
### Gates
|
|
||||||
|
|
||||||
🔒 **GATE-01**: Bulk operations MUST use existing permission model (policies.delete, etc.)
|
|
||||||
🔒 **GATE-02**: Progress tracking MUST use BulkOperationRun model (not fire-and-forget)
|
|
||||||
🔒 **GATE-03**: Type-to-confirm MUST be case-sensitive "DELETE" for ≥20 items
|
|
||||||
🔒 **GATE-04**: Policies bulk delete = local only (ignored_at flag, NO Graph DELETE)
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@ -84,180 +40,42 @@ ### Documentation (this feature)
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
specs/005-bulk-operations/
|
specs/005-bulk-operations/
|
||||||
├── plan.md # This file
|
├── plan.md # This file (/speckit.plan command output)
|
||||||
├── research.md # Phase 0 output (see below)
|
├── research.md # Phase 0 output (/speckit.plan command)
|
||||||
├── data-model.md # Phase 1 output (see below)
|
├── data-model.md # Phase 1 output (/speckit.plan command)
|
||||||
├── quickstart.md # Phase 1 output (see below)
|
├── quickstart.md # Phase 1 output (/speckit.plan command)
|
||||||
└── tasks.md # Phase 2 output (/speckit.tasks command - generated and tracked here)
|
├── contracts/ # Phase 1 output (/speckit.plan command)
|
||||||
|
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Source Code (repository root)
|
### Source Code (repository root)
|
||||||
|
|
||||||
```text
|
```text
|
||||||
app/
|
app/
|
||||||
├── Models/
|
|
||||||
│ ├── BulkOperationRun.php # NEW: Tracks progress/outcomes
|
|
||||||
│ ├── Policy.php # EXTEND: Add markIgnored() scope
|
|
||||||
│ ├── PolicyVersion.php # EXTEND: Add pruneEligible() scope
|
|
||||||
│ ├── BackupSet.php # EXTEND: Cascade delete logic
|
|
||||||
│ └── RestoreRun.php # EXTEND: Skip running status
|
|
||||||
│
|
|
||||||
├── Jobs/
|
|
||||||
│ ├── BulkPolicyDeleteJob.php # NEW: Async bulk delete (local)
|
|
||||||
│ ├── BulkPolicyExportJob.php # NEW: Export to backup set
|
|
||||||
│ ├── BulkPolicyVersionPruneJob.php # NEW: Prune old versions
|
|
||||||
│ ├── BulkBackupSetDeleteJob.php # NEW: Delete backup sets
|
|
||||||
│ └── BulkRestoreRunDeleteJob.php # NEW: Delete restore runs
|
|
||||||
│
|
|
||||||
├── Services/
|
|
||||||
│ ├── BulkOperationService.php # NEW: Orchestrates bulk ops + tracking
|
|
||||||
│ └── Audit/
|
|
||||||
│ └── AuditLogger.php # EXTEND: Add bulk operation events
|
|
||||||
│
|
|
||||||
├── Filament/
|
├── Filament/
|
||||||
│ └── Resources/
|
│ └── Resources/
|
||||||
│ ├── PolicyResource.php # EXTEND: Add bulk actions
|
├── Jobs/
|
||||||
│ ├── PolicyVersionResource.php # EXTEND: Add bulk prune
|
├── Models/
|
||||||
│ ├── BackupSetResource.php # EXTEND: Add bulk delete
|
└── Services/
|
||||||
│ └── RestoreRunResource.php # EXTEND: Add bulk delete
|
|
||||||
│
|
|
||||||
└── Livewire/
|
|
||||||
└── BulkOperationProgress.php # NEW: Progress polling component
|
|
||||||
|
|
||||||
database/
|
database/
|
||||||
|
├── factories/
|
||||||
└── migrations/
|
└── migrations/
|
||||||
└── YYYY_MM_DD_create_bulk_operation_runs_table.php # NEW
|
|
||||||
|
routes/
|
||||||
|
├── web.php
|
||||||
|
└── console.php
|
||||||
|
|
||||||
|
resources/
|
||||||
|
└── views/
|
||||||
|
|
||||||
tests/
|
tests/
|
||||||
├── Unit/
|
├── Feature/
|
||||||
│ ├── BulkPolicyDeleteJobTest.php
|
└── Unit/
|
||||||
│ ├── BulkActionPermissionTest.php
|
|
||||||
│ └── BulkEligibilityCheckTest.php
|
|
||||||
│
|
|
||||||
└── Feature/
|
|
||||||
├── BulkDeletePoliciesTest.php
|
|
||||||
├── BulkExportToBackupTest.php
|
|
||||||
├── BulkProgressNotificationTest.php
|
|
||||||
└── BulkTypeToConfirmTest.php
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Structure Decision**: Single web application structure (Laravel + Filament). New bulk operations extend existing Resources with BulkAction definitions. New BulkOperationRun model tracks async job progress. No separate API layer needed (Livewire polling uses Filament infolists/resource pages).
|
**Structure Decision**: Web application (Laravel + Filament admin panel) using existing repository layout.
|
||||||
|
|
||||||
## Complexity Tracking
|
## Complexity Tracking
|
||||||
|
|
||||||
> No constitution violations requiring justification.
|
No constitution violations requiring justification.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 0: Research & Technology Decisions
|
|
||||||
|
|
||||||
See [research.md](./research.md) for detailed research findings.
|
|
||||||
|
|
||||||
### Key Decisions Summary
|
|
||||||
|
|
||||||
| Decision | Chosen | Rationale |
|
|
||||||
|----------|--------|-----------|
|
|
||||||
| Progress tracking | BulkOperationRun model + Livewire polling | Explicit state, survives page refresh, queryable outcomes |
|
|
||||||
| Job chunking | collect()->chunk(10) | Simple, memory-efficient, easy to test |
|
|
||||||
| Type-to-confirm | Filament form + validation rule | Built-in UI, reusable pattern |
|
|
||||||
| Tenant isolation | Explicit tenantId param | Fail-safe, auditable, no reliance on global scopes |
|
|
||||||
| Policy deletion | ignored_at flag | Prevents re-sync, restorable, doesn't touch Intune |
|
|
||||||
| Eligibility checks | Eloquent scopes | Reusable, testable, composable |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1: Data Model & Contracts
|
|
||||||
|
|
||||||
See [data-model.md](./data-model.md) for detailed schemas and entity diagrams.
|
|
||||||
|
|
||||||
### Core Entities
|
|
||||||
|
|
||||||
**BulkOperationRun** (NEW):
|
|
||||||
- Tracks progress, outcomes, failures for bulk operations
|
|
||||||
- Fields: resource, action, status, total_items, processed_items, succeeded, failed, skipped
|
|
||||||
- JSONB: item_ids, failures
|
|
||||||
- Relationships: tenant, user, auditLog
|
|
||||||
|
|
||||||
**Policy** (EXTEND):
|
|
||||||
- Add `ignored_at` timestamp (prevents re-sync)
|
|
||||||
- Add `markIgnored()` method and `notIgnored()` scope
|
|
||||||
|
|
||||||
**PolicyVersion** (EXTEND):
|
|
||||||
- Add `pruneEligible()` scope (checks age, references, current status)
|
|
||||||
|
|
||||||
**RestoreRun** (EXTEND):
|
|
||||||
- Add `deletable()` scope (filters by completed/failed status)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2: Implementation Tasks
|
|
||||||
|
|
||||||
Detailed tasks will be generated via `/speckit.tasks` command. High-level phases:
|
|
||||||
|
|
||||||
### Phase 2.1: Foundation (P1 - Policies) - 8-12 hours
|
|
||||||
- BulkOperationRun migration + model
|
|
||||||
- Policies: ignored_at column, bulk delete/export jobs
|
|
||||||
- Filament bulk actions + type-to-confirm
|
|
||||||
- BulkOperationService orchestration
|
|
||||||
- Tests (unit, feature)
|
|
||||||
|
|
||||||
### Phase 2.2: Progress Tracking (P1) - 8-10 hours
|
|
||||||
- Livewire progress component
|
|
||||||
- Job progress updates (chunked)
|
|
||||||
- Circuit breaker (>50% fail abort)
|
|
||||||
- Audit logging integration
|
|
||||||
- Tests (progress, polling, audit)
|
|
||||||
|
|
||||||
### Phase 2.3: Additional Resources (P2) - 6-8 hours
|
|
||||||
- PolicyVersion prune (eligibility scope)
|
|
||||||
- BackupSet bulk delete
|
|
||||||
- RestoreRun bulk delete
|
|
||||||
- Resource extensions
|
|
||||||
- Tests for each resource
|
|
||||||
|
|
||||||
### Phase 2.4: Polish & Deployment - 4-6 hours
|
|
||||||
- Manual QA (type-to-confirm, progress UI)
|
|
||||||
- Load testing (500 items)
|
|
||||||
- Documentation updates
|
|
||||||
- Staging → Production deployment
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Risk Mitigation
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
|------|------------|
|
|
||||||
| Queue timeouts | Chunk processing (10-20 items), timeout config (300s), circuit breaker |
|
|
||||||
| Progress polling overhead | Limit interval (5s), index queries, cache recent runs |
|
|
||||||
| Accidental deletes | Type-to-confirm ≥20 items, `ignored_at` flag (restorable), audit trail |
|
|
||||||
| Job crashes | Fail-soft, BulkOperationRun status tracking, Laravel retry |
|
|
||||||
| Eligibility misses | Conservative JSONB queries, manual review before hard delete |
|
|
||||||
| Sync re-adds policies | `ignored_at` filter in SyncPoliciesJob |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
- ✅ Bulk delete 100 policies in <2 minutes
|
|
||||||
- ✅ Type-to-confirm prevents accidents (≥20 items)
|
|
||||||
- ✅ Progress updates every 5-10s
|
|
||||||
- ✅ Audit log captures per-item outcomes
|
|
||||||
- ✅ 95%+ operation success rate
|
|
||||||
- ✅ All P1/P2 tests pass
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. ✅ Generate plan.md (this file)
|
|
||||||
2. → Generate research.md (detailed technology findings)
|
|
||||||
3. → Generate data-model.md (schemas + diagrams)
|
|
||||||
4. → Generate quickstart.md (developer onboarding)
|
|
||||||
5. → Run `/speckit.tasks` to create task breakdown
|
|
||||||
6. → Begin Phase 2.1 implementation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status**: Plan Complete - Ready for Research
|
|
||||||
**Created**: 2025-12-22
|
|
||||||
**Last Updated**: 2025-12-22
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
test('backup sets table bulk archive creates a run and archives selected sets', function () {
|
test('backup sets table bulk archive creates a run and archives selected sets', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$sets = collect(range(1, 3))->map(function (int $i) use ($tenant) {
|
$sets = collect(range(1, 3))->map(function (int $i) use ($tenant) {
|
||||||
@ -56,6 +57,7 @@
|
|||||||
|
|
||||||
test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () {
|
test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$sets = collect(range(1, 10))->map(function (int $i) use ($tenant) {
|
$sets = collect(range(1, 10))->map(function (int $i) use ($tenant) {
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
test('bulk delete restore runs skips running items', function () {
|
test('bulk delete restore runs skips running items', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
test('bulk delete restore runs soft deletes selected runs', function () {
|
test('bulk delete restore runs soft deletes selected runs', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
test('backup sets table bulk force delete permanently deletes archived sets and their items', function () {
|
test('backup sets table bulk force delete permanently deletes archived sets and their items', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$set = BackupSet::create([
|
$set = BackupSet::create([
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
test('bulk force delete restore runs permanently deletes archived runs', function () {
|
test('bulk force delete restore runs permanently deletes archived runs', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
test('backup sets table bulk restore restores archived sets and their items', function () {
|
test('backup sets table bulk restore restores archived sets and their items', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$set = BackupSet::create([
|
$set = BackupSet::create([
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
test('restore runs table bulk restore creates a run and restores archived records', function () {
|
test('restore runs table bulk restore creates a run and restores archived records', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Filament\Resources\PolicyResource;
|
use App\Jobs\BulkPolicySyncJob;
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
|
use App\Models\BulkOperationRun;
|
||||||
use App\Models\Policy;
|
use App\Models\Policy;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\BulkOperationService;
|
||||||
use App\Services\Graph\GraphClientInterface;
|
use App\Services\Graph\GraphClientInterface;
|
||||||
use App\Services\Graph\GraphResponse;
|
use App\Services\Graph\GraphResponse;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
test('bulk sync updates selected policies from graph', function () {
|
test('bulk sync updates selected policies from graph', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$policies = Policy::factory()
|
$policies = Policy::factory()
|
||||||
@ -25,7 +27,7 @@
|
|||||||
'last_synced_at' => null,
|
'last_synced_at' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface
|
app()->instance(GraphClientInterface::class, new class implements GraphClientInterface
|
||||||
{
|
{
|
||||||
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
||||||
{
|
{
|
||||||
@ -65,10 +67,17 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
$service = app(BulkOperationService::class);
|
||||||
->test(PolicyResource\Pages\ListPolicies::class)
|
$run = $service->createRun($tenant, $user, 'policy', 'sync', $policies->modelKeys(), 3);
|
||||||
->callTableBulkAction('bulk_sync', $policies)
|
|
||||||
->assertHasNoTableBulkActionErrors();
|
BulkPolicySyncJob::dispatchSync($run->id);
|
||||||
|
|
||||||
|
$bulkRun = BulkOperationRun::query()->find($run->id);
|
||||||
|
expect($bulkRun)->not->toBeNull();
|
||||||
|
expect($bulkRun->status)->toBe('completed');
|
||||||
|
expect($bulkRun->total_items)->toBe(3);
|
||||||
|
expect($bulkRun->succeeded)->toBe(3);
|
||||||
|
expect($bulkRun->failed)->toBe(0);
|
||||||
|
|
||||||
$policies->each(function (Policy $policy) {
|
$policies->each(function (Policy $policy) {
|
||||||
$policy->refresh();
|
$policy->refresh();
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
test('bulk delete requires confirmation string for large batches', function () {
|
test('bulk delete requires confirmation string for large batches', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]);
|
$policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]);
|
||||||
|
|
||||||
@ -26,6 +27,7 @@
|
|||||||
|
|
||||||
test('bulk delete fails with incorrect confirmation string', function () {
|
test('bulk delete fails with incorrect confirmation string', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]);
|
$policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]);
|
||||||
|
|
||||||
@ -41,6 +43,7 @@
|
|||||||
|
|
||||||
test('bulk delete does not require confirmation string for small batches', function () {
|
test('bulk delete does not require confirmation string for small batches', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$policies = Policy::factory()->count(10)->create(['tenant_id' => $tenant->id]);
|
$policies = Policy::factory()->count(10)->create(['tenant_id' => $tenant->id]);
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,11 @@
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
putenv('INTUNE_TENANT_ID');
|
||||||
|
unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']);
|
||||||
|
|
||||||
$this->tenant = Tenant::factory()->create();
|
$this->tenant = Tenant::factory()->create();
|
||||||
|
$this->tenant->makeCurrent();
|
||||||
$this->policy = Policy::factory()->create([
|
$this->policy = Policy::factory()->create([
|
||||||
'tenant_id' => $this->tenant->id,
|
'tenant_id' => $this->tenant->id,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -17,6 +17,11 @@
|
|||||||
->use(RefreshDatabase::class)
|
->use(RefreshDatabase::class)
|
||||||
->in('Feature');
|
->in('Feature');
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
putenv('INTUNE_TENANT_ID');
|
||||||
|
unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']);
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Expectations
|
| Expectations
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
test('policies bulk actions are available for authenticated users', function () {
|
test('policies bulk actions are available for authenticated users', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
$tenant->makeCurrent();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$policies = Policy::factory()->count(2)->create(['tenant_id' => $tenant->id]);
|
$policies = Policy::factory()->count(2)->create(['tenant_id' => $tenant->id]);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user