diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..bb15ea67 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,145 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + tests: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, fileinfo + coverage: none + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Composer dependencies + run: composer install --no-interaction --prefer-dist + + - name: Setup environment + run: | + cp .env.example .env + sed -i 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' .env + sed -i 's/DB_HOST=127.0.0.1/#DB_HOST=127.0.0.1/' .env + sed -i 's/DB_PORT=3306/#DB_PORT=3306/' .env + sed -i 's/DB_DATABASE=laravel/#DB_DATABASE=laravel/' .env + sed -i 's/DB_USERNAME=root/#DB_USERNAME=root/' .env + sed -i 's/DB_PASSWORD=/#DB_PASSWORD=/' .env + touch database/opengrc.sqlite + + - name: Generate application key + run: php artisan key:generate + + - name: Run migrations + run: php artisan migrate --force + + - name: Run tests + run: php artisan test + + formatting: + name: Formatting (Pint) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Composer dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run Pint + run: vendor/bin/pint + + - name: Check for changes + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "::error::Code formatting issues detected. Please run 'vendor/bin/pint' locally and commit the changes." + git diff --name-only + exit 1 + fi + + linting: + name: Linting (PHPStan) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite + coverage: none + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Composer dependencies + run: composer install --no-interaction --prefer-dist + + - name: Setup environment for PHPStan + run: | + cp .env.example .env + sed -i 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' .env + sed -i 's/DB_HOST=127.0.0.1/#DB_HOST=127.0.0.1/' .env + sed -i 's/DB_PORT=3306/#DB_PORT=3306/' .env + sed -i 's/DB_DATABASE=laravel/#DB_DATABASE=laravel/' .env + sed -i 's/DB_USERNAME=root/#DB_USERNAME=root/' .env + sed -i 's/DB_PASSWORD=/#DB_PASSWORD=/' .env + touch database/opengrc.sqlite + + - name: Generate application key + run: php artisan key:generate + + - name: Run migrations + run: php artisan migrate --force + + - name: Run PHPStan + run: vendor/bin/phpstan analyse --memory-limit=512M diff --git a/app/Filament/Admin/Pages/QueueMonitor.php b/app/Filament/Admin/Pages/QueueMonitor.php index 5222348a..51a4b88a 100644 --- a/app/Filament/Admin/Pages/QueueMonitor.php +++ b/app/Filament/Admin/Pages/QueueMonitor.php @@ -32,7 +32,7 @@ class QueueMonitor extends Page implements HasTable public function table(Table $table): Table { return $table - ->query(QueueJob::query()) + ->query(QueueJob::query()) ->columns([ TextColumn::make('id') ->label('ID') diff --git a/app/Filament/Admin/Resources/TaxonomyResource/Pages/EditTaxonomy.php b/app/Filament/Admin/Resources/TaxonomyResource/Pages/EditTaxonomy.php index c4b183ab..c9e7e99a 100644 --- a/app/Filament/Admin/Resources/TaxonomyResource/Pages/EditTaxonomy.php +++ b/app/Filament/Admin/Resources/TaxonomyResource/Pages/EditTaxonomy.php @@ -21,11 +21,11 @@ protected function getHeaderActions(): array protected function getSaveFormAction(): Actions\Action { return parent::getSaveFormAction() - ->label(fn () => 'Save ' . ($this->record->name ?? 'Taxonomy')); + ->label(fn () => 'Save '.($this->record->name ?? 'Taxonomy')); } protected function getSavedNotificationTitle(): ?string { - return $this->record->name . ' saved successfully'; + return $this->record->name.' saved successfully'; } } diff --git a/app/Filament/Admin/Resources/UserResource.php b/app/Filament/Admin/Resources/UserResource.php index 1d160f2f..726273c4 100644 --- a/app/Filament/Admin/Resources/UserResource.php +++ b/app/Filament/Admin/Resources/UserResource.php @@ -28,7 +28,7 @@ class UserResource extends Resource protected static ?string $navigationLabel = null; - protected static ?string $navigationGroup = "System"; + protected static ?string $navigationGroup = 'System'; protected static ?int $navigationSort = 10; @@ -164,6 +164,7 @@ public static function getEloquentQuery(): Builder public static function createDefaultPassword(): string { $words = collect(range(1, 4))->map(fn () => Str::random(6))->implode('-'); + return $words; } diff --git a/app/Filament/Admin/Resources/VendorUserResource.php b/app/Filament/Admin/Resources/VendorUserResource.php index cf56e6e3..f016b44e 100644 --- a/app/Filament/Admin/Resources/VendorUserResource.php +++ b/app/Filament/Admin/Resources/VendorUserResource.php @@ -5,7 +5,6 @@ use App\Filament\Admin\Resources\VendorUserResource\Pages; use App\Mail\VendorInvitationMail; use App\Mail\VendorMagicLinkMail; -use App\Models\Vendor; use App\Models\VendorUser; use Filament\Forms; use Filament\Forms\Form; @@ -17,7 +16,6 @@ use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Password; -use Illuminate\Support\Facades\URL; class VendorUserResource extends Resource { @@ -209,7 +207,7 @@ public static function resendInvitation(VendorUser $record, bool $notify = true) if ($notify) { Notification::make() - ->title('Invitation sent to ' . $record->email) + ->title('Invitation sent to '.$record->email) ->success() ->send(); } @@ -230,7 +228,7 @@ public static function sendMagicLink(VendorUser $record): void Mail::send(new VendorMagicLinkMail($record)); Notification::make() - ->title('Magic link sent to ' . $record->email) + ->title('Magic link sent to '.$record->email) ->success() ->send(); } catch (\Exception $e) { @@ -251,7 +249,7 @@ public static function sendPasswordReset(VendorUser $record): void if ($status === Password::RESET_LINK_SENT) { Notification::make() - ->title('Password reset sent to ' . $record->email) + ->title('Password reset sent to '.$record->email) ->success() ->send(); } else { diff --git a/app/Filament/Concerns/HasTaxonomyFields.php b/app/Filament/Concerns/HasTaxonomyFields.php index a6aa1911..6a5050a7 100644 --- a/app/Filament/Concerns/HasTaxonomyFields.php +++ b/app/Filament/Concerns/HasTaxonomyFields.php @@ -38,7 +38,6 @@ trait HasTaxonomyFields * This handles cases where slugs might have been changed. * * @param string $type The taxonomy type (e.g., 'department', 'scope') - * @return Taxonomy|null */ protected static function getParentTaxonomy(string $type): ?Taxonomy { @@ -52,7 +51,7 @@ protected static function getParentTaxonomy(string $type): ?Taxonomy } // Try plural version - $taxonomy = Taxonomy::where('slug', $type . 's') + $taxonomy = Taxonomy::where('slug', $type.'s') ->whereNull('parent_id') ->first(); @@ -70,7 +69,7 @@ protected static function getParentTaxonomy(string $type): ?Taxonomy } // Try plural type - $taxonomy = Taxonomy::where('type', $type . 's') + $taxonomy = Taxonomy::where('type', $type.'s') ->whereNull('parent_id') ->first(); @@ -101,7 +100,7 @@ public static function taxonomySelect( // Find the root taxonomy $taxonomy = self::getParentTaxonomy($taxonomyType); - if (!$taxonomy) { + if (! $taxonomy) { return []; } @@ -112,12 +111,16 @@ public static function taxonomySelect( ->toArray(); }) ->afterStateHydrated(function (Select $component, $state, $record) use ($taxonomyType) { - if (!$record) return; + if (! $record) { + return; + } // Find the taxonomy type $taxonomy = self::getParentTaxonomy($taxonomyType); - if (!$taxonomy) return; + if (! $taxonomy) { + return; + } // Get the current taxonomy term for this type $currentTerm = $record->taxonomies() @@ -128,12 +131,16 @@ public static function taxonomySelect( }) ->saveRelationshipsUsing(function (Select $component, $state) use ($taxonomyType) { $record = $component->getRecord(); - if (!$record || !$state) return; + if (! $record || ! $state) { + return; + } // Find the taxonomy type $taxonomy = self::getParentTaxonomy($taxonomyType); - if (!$taxonomy) return; + if (! $taxonomy) { + return; + } // Detach any existing terms of this taxonomy type $existingTermIds = Taxonomy::where('parent_id', $taxonomy->id)->pluck('id'); @@ -184,7 +191,7 @@ public static function hierarchicalTaxonomySelect( // Find the root taxonomy $taxonomy = self::getParentTaxonomy($taxonomyType); - if (!$taxonomy) { + if (! $taxonomy) { return $query->whereRaw('1 = 0'); // Return empty result } @@ -197,7 +204,7 @@ public static function hierarchicalTaxonomySelect( ->getOptionLabelFromRecordUsing(function (Taxonomy $record) { // Show hierarchical format: Parent → Child return $record->parent - ? $record->parent->name . ' → ' . $record->name + ? $record->parent->name.' → '.$record->name : $record->name; }) ->searchable() @@ -230,7 +237,7 @@ public static function saveTaxonomyRelationships($record, array $data): void ]; foreach ($taxonomyFields as $fieldName => $taxonomyType) { - if (!isset($data[$fieldName]) || !$data[$fieldName]) { + if (! isset($data[$fieldName]) || ! $data[$fieldName]) { continue; } @@ -239,7 +246,7 @@ public static function saveTaxonomyRelationships($record, array $data): void // Find the root taxonomy $taxonomy = self::getParentTaxonomy($taxonomyType); - if (!$taxonomy) { + if (! $taxonomy) { continue; } @@ -261,13 +268,12 @@ public static function saveTaxonomyRelationships($record, array $data): void * * @param Model $record The model instance * @param string $taxonomyType The type identifier of the parent taxonomy - * @return Taxonomy|null */ public static function getTaxonomyTerm($record, string $taxonomyType): ?Taxonomy { $parent = self::getParentTaxonomy($taxonomyType); - if (!$parent) { + if (! $parent) { return null; } diff --git a/app/Filament/Pages/Import.php b/app/Filament/Pages/Import.php index 608ec6b6..5091b623 100644 --- a/app/Filament/Pages/Import.php +++ b/app/Filament/Pages/Import.php @@ -230,9 +230,9 @@ public function preProcessData(): bool $standard = null; if ($standardCode) { $standard = Standard::where('code', $standardCode)->first(); - if (!$standard) { + if (! $standard) { $has_errors = true; - $error_array[] = "Row ".($index + 1).": Standard code '{$standardCode}' not found"; + $error_array[] = 'Row '.($index + 1).": Standard code '{$standardCode}' not found"; } } diff --git a/app/Filament/Resources/ApplicationResource/Pages/ViewApplication.php b/app/Filament/Resources/ApplicationResource/Pages/ViewApplication.php index 4db2e1f1..fbc1d75d 100644 --- a/app/Filament/Resources/ApplicationResource/Pages/ViewApplication.php +++ b/app/Filament/Resources/ApplicationResource/Pages/ViewApplication.php @@ -3,7 +3,6 @@ namespace App\Filament\Resources\ApplicationResource\Pages; use App\Filament\Resources\ApplicationResource; -use Filament\Actions; use Filament\Resources\Pages\ViewRecord; class ViewApplication extends ViewRecord diff --git a/app/Filament/Resources/AssetResource/Pages/CreateAsset.php b/app/Filament/Resources/AssetResource/Pages/CreateAsset.php index 6651ec39..cb3fcfc6 100644 --- a/app/Filament/Resources/AssetResource/Pages/CreateAsset.php +++ b/app/Filament/Resources/AssetResource/Pages/CreateAsset.php @@ -3,7 +3,6 @@ namespace App\Filament\Resources\AssetResource\Pages; use App\Filament\Resources\AssetResource; -use Filament\Actions; use Filament\Resources\Pages\CreateRecord; class CreateAsset extends CreateRecord diff --git a/app/Filament/Resources/AssetResource/Pages/ViewAsset.php b/app/Filament/Resources/AssetResource/Pages/ViewAsset.php index d6f2a73a..8f41ba6f 100644 --- a/app/Filament/Resources/AssetResource/Pages/ViewAsset.php +++ b/app/Filament/Resources/AssetResource/Pages/ViewAsset.php @@ -12,7 +12,7 @@ class ViewAsset extends ViewRecord public function getTitle(): string { - return $this->record->asset_tag . ': ' . $this->record->name; + return $this->record->asset_tag.': '.$this->record->name; } protected function getHeaderActions(): array diff --git a/app/Filament/Resources/AssetResource/RelationManagers/ImplementationsRelationManager.php b/app/Filament/Resources/AssetResource/RelationManagers/ImplementationsRelationManager.php index d5ccde11..755037b3 100644 --- a/app/Filament/Resources/AssetResource/RelationManagers/ImplementationsRelationManager.php +++ b/app/Filament/Resources/AssetResource/RelationManagers/ImplementationsRelationManager.php @@ -2,15 +2,13 @@ namespace App\Filament\Resources\AssetResource\RelationManagers; -use App\Enums\ImplementationStatus; use App\Enums\Effectiveness; +use App\Enums\ImplementationStatus; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; class ImplementationsRelationManager extends RelationManager { diff --git a/app/Filament/Resources/AuditResource/Pages/ImportIrl.php b/app/Filament/Resources/AuditResource/Pages/ImportIrl.php index 2ef224db..828ba02c 100644 --- a/app/Filament/Resources/AuditResource/Pages/ImportIrl.php +++ b/app/Filament/Resources/AuditResource/Pages/ImportIrl.php @@ -19,7 +19,6 @@ use Filament\Resources\Pages\Concerns\InteractsWithRecord; use Filament\Resources\Pages\Page; use Filament\Support\Exceptions\Halt; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\HtmlString; use Illuminate\Validation\ValidationException; use League\Csv\Reader; @@ -79,7 +78,7 @@ public function form(Form $form): Form $this->currentDataRequests = DataRequest::query()->where('audit_id', $this->record->id)->get(); $this->auditItems = $this->record->auditItems()->with('control')->get(); $this->controlCodes = $this->auditItems->pluck('auditable.code')->toArray(); - $template_url = "/resources/irl-template.csv"; + $template_url = '/resources/irl-template.csv'; return $form ->schema([ @@ -118,12 +117,12 @@ public function form(Form $form): Form $this->irl_file_contents = $state->get(); if (empty($this->irl_file_contents)) { - throw new \Exception("File contents are empty"); + throw new \Exception('File contents are empty'); } $this->isIrlFileValid = $this->validateIrlFile() && $this->validateIrlFileData(); } catch (\Exception $e) { - $this->addError('irl_file', 'Failed to read uploaded file: ' . $e->getMessage()); + $this->addError('irl_file', 'Failed to read uploaded file: '.$e->getMessage()); $this->isIrlFileValid = false; } @@ -211,8 +210,8 @@ public function validateIrlFileData(): bool } // Skip empty rows (rows with only empty values or commas) - $nonEmptyValues = array_filter($row, function($value) { - return !empty(trim($value)); + $nonEmptyValues = array_filter($row, function ($value) { + return ! empty(trim($value)); }); if (empty($nonEmptyValues)) { continue; @@ -257,15 +256,15 @@ public function validateIrlFileData(): bool $controlCodes = array_map('trim', explode(',', $row['Control Code'])); $invalidCodes = []; foreach ($controlCodes as $code) { - if (!in_array($code, $this->controlCodes)) { + if (! in_array($code, $this->controlCodes)) { $invalidCodes[] = $code; } } - if (!empty($invalidCodes)) { + if (! empty($invalidCodes)) { $has_errors = true; - $finalRecord['Control Code'] = "Control Code Not In Audit: " . implode(', ', $invalidCodes); - $error_array[] = "Row $index: no control with the code(s): " . implode(', ', $invalidCodes); + $finalRecord['Control Code'] = 'Control Code Not In Audit: '.implode(', ', $invalidCodes); + $error_array[] = "Row $index: no control with the code(s): ".implode(', ', $invalidCodes); } else { $finalRecord['Control Code'] = $row['Control Code']; } @@ -282,14 +281,14 @@ public function validateIrlFileData(): bool // Gracefully handle date formatting - auto-format single digit months/days $dueDate = trim($row['Due On']); - + // Try to auto-format dates like "1/5/2024" to "01/05/2024" if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/', $dueDate, $matches)) { $month = str_pad($matches[1], 2, '0', STR_PAD_LEFT); $day = str_pad($matches[2], 2, '0', STR_PAD_LEFT); $year = $matches[3]; $formattedDate = "$month/$day/$year"; - + // Validate the formatted date if (preg_match('/^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/', $formattedDate)) { $finalRecord['Due On'] = $formattedDate; @@ -313,7 +312,7 @@ public function validateIrlFileData(): bool if ($has_errors) { $this->isIrlFileValid = false; - + // Show errors and indicate if processing was stopped early $displayErrors = $error_array; if (count($error_array) >= 5) { @@ -321,7 +320,7 @@ public function validateIrlFileData(): bool $processedRows = count($this->finalData); $displayErrors[] = "Processing stopped after {$processedRows} of {$totalRows} rows due to multiple errors. Please fix the above errors and try again."; } - + $this->error_string = implode(' | ', $displayErrors); $this->addError('irl_file', $this->error_string); diff --git a/app/Filament/Resources/AuditResource/Pages/ViewAudit.php b/app/Filament/Resources/AuditResource/Pages/ViewAudit.php index f30a8886..a76d8954 100644 --- a/app/Filament/Resources/AuditResource/Pages/ViewAudit.php +++ b/app/Filament/Resources/AuditResource/Pages/ViewAudit.php @@ -4,7 +4,6 @@ use App\Enums\WorkflowStatus; use App\Filament\Resources\AuditResource; -use App\Http\Controllers\QueueController; use App\Models\Audit; use Barryvdh\DomPDF\Facade\Pdf; use Filament\Actions; diff --git a/app/Filament/Resources/AuditResource/Widgets/AuditStatsWidget.php b/app/Filament/Resources/AuditResource/Widgets/AuditStatsWidget.php index 8aff63d5..dbafd6a4 100644 --- a/app/Filament/Resources/AuditResource/Widgets/AuditStatsWidget.php +++ b/app/Filament/Resources/AuditResource/Widgets/AuditStatsWidget.php @@ -8,9 +8,8 @@ class AuditStatsWidget extends BaseWidget { - protected static bool $isLazy = false; - + protected function getStats(): array { $totalAudited = \App\Models\Audit::count(); diff --git a/app/Filament/Resources/ControlResource/Pages/CreateControl.php b/app/Filament/Resources/ControlResource/Pages/CreateControl.php index fcfb4c76..88791d73 100644 --- a/app/Filament/Resources/ControlResource/Pages/CreateControl.php +++ b/app/Filament/Resources/ControlResource/Pages/CreateControl.php @@ -4,13 +4,11 @@ use App\Filament\Concerns\HasTaxonomyFields; use App\Filament\Resources\ControlResource; -use App\Models\Control; use Filament\Resources\Pages\CreateRecord; class CreateControl extends CreateRecord { use HasTaxonomyFields; - + protected static string $resource = ControlResource::class; - } diff --git a/app/Filament/Resources/ControlResource/Pages/EditControl.php b/app/Filament/Resources/ControlResource/Pages/EditControl.php index 138b07f0..7f8357ea 100644 --- a/app/Filament/Resources/ControlResource/Pages/EditControl.php +++ b/app/Filament/Resources/ControlResource/Pages/EditControl.php @@ -4,14 +4,13 @@ use App\Filament\Concerns\HasTaxonomyFields; use App\Filament\Resources\ControlResource; -use App\Models\Control; use Filament\Actions; use Filament\Resources\Pages\EditRecord; class EditControl extends EditRecord { use HasTaxonomyFields; - + protected static string $resource = ControlResource::class; protected function getHeaderActions(): array @@ -34,7 +33,6 @@ public function getRelationManagers(): array return []; } - public function save(bool $shouldRedirect = true, bool $shouldSendSavedNotification = true): void { parent::save($shouldRedirect); diff --git a/app/Filament/Resources/ControlResource/RelationManagers/ImplementationRelationManager.php b/app/Filament/Resources/ControlResource/RelationManagers/ImplementationRelationManager.php index 7695917a..5d43d381 100644 --- a/app/Filament/Resources/ControlResource/RelationManagers/ImplementationRelationManager.php +++ b/app/Filament/Resources/ControlResource/RelationManagers/ImplementationRelationManager.php @@ -4,14 +4,13 @@ use App\Enums\Effectiveness; use App\Enums\ImplementationStatus; -use Filament\Forms; +use App\Filament\Resources\ImplementationResource; use Filament\Forms\Form; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; -use App\Filament\Resources\ImplementationResource; class ImplementationRelationManager extends RelationManager { diff --git a/app/Filament/Resources/ImplementationResource/RelationManagers/AssetsRelationManager.php b/app/Filament/Resources/ImplementationResource/RelationManagers/AssetsRelationManager.php index 063644ad..a21abea8 100644 --- a/app/Filament/Resources/ImplementationResource/RelationManagers/AssetsRelationManager.php +++ b/app/Filament/Resources/ImplementationResource/RelationManagers/AssetsRelationManager.php @@ -2,14 +2,11 @@ namespace App\Filament\Resources\ImplementationResource\RelationManagers; -use Filament\Forms; +use App\Filament\Resources\AssetResource; use Filament\Forms\Form; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; -use App\Filament\Resources\AssetResource; class AssetsRelationManager extends RelationManager { diff --git a/app/Filament/Resources/ImplementationResource/RelationManagers/AuditItemRelationManager.php b/app/Filament/Resources/ImplementationResource/RelationManagers/AuditItemRelationManager.php index 3f519ab3..71d48f6b 100644 --- a/app/Filament/Resources/ImplementationResource/RelationManagers/AuditItemRelationManager.php +++ b/app/Filament/Resources/ImplementationResource/RelationManagers/AuditItemRelationManager.php @@ -2,8 +2,6 @@ namespace App\Filament\Resources\ImplementationResource\RelationManagers; -use Filament\Forms; -use Filament\Forms\Form; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; use Filament\Tables\Table; diff --git a/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php b/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php index dfda1109..97ac33b2 100644 --- a/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php +++ b/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php @@ -3,19 +3,17 @@ namespace App\Filament\Resources\PolicyResource\Pages; use App\Filament\Resources\PolicyResource; -use App\Models\Policy; use Filament\Actions; use Filament\Resources\Pages\ViewRecord; use Illuminate\Contracts\Support\Htmlable; - class ViewPolicy extends ViewRecord { protected static string $resource = PolicyResource::class; protected static string $view = 'filament.resources.policy-resource.pages.view-policy-document'; - public function getTitle(): string | Htmlable + public function getTitle(): string|Htmlable { return $this->getRecord()->name; } diff --git a/app/Filament/Resources/PolicyResource/RelationManagers/RisksRelationManager.php b/app/Filament/Resources/PolicyResource/RelationManagers/RisksRelationManager.php index c306eecc..c4ae87e1 100644 --- a/app/Filament/Resources/PolicyResource/RelationManagers/RisksRelationManager.php +++ b/app/Filament/Resources/PolicyResource/RelationManagers/RisksRelationManager.php @@ -2,12 +2,12 @@ namespace App\Filament\Resources\PolicyResource\RelationManagers; +use App\Filament\Resources\RiskResource; +use App\Models\Risk; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; -use App\Models\Risk; -use App\Filament\Resources\RiskResource; class RisksRelationManager extends RelationManager { diff --git a/app/Filament/Resources/ProgramResource/RelationManagers/AuditsRelationManager.php b/app/Filament/Resources/ProgramResource/RelationManagers/AuditsRelationManager.php index 76e506d8..6cabf865 100644 --- a/app/Filament/Resources/ProgramResource/RelationManagers/AuditsRelationManager.php +++ b/app/Filament/Resources/ProgramResource/RelationManagers/AuditsRelationManager.php @@ -2,13 +2,9 @@ namespace App\Filament\Resources\ProgramResource\RelationManagers; -use Filament\Forms; -use Filament\Forms\Form; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; class AuditsRelationManager extends RelationManager { @@ -25,7 +21,7 @@ public function table(Table $table): Table ->sortable() ->badge() ->searchable(), - Tables\Columns\TextColumn::make('manager.name') + Tables\Columns\TextColumn::make('manager.name') ->label(__('audit.table.columns.manager')) ->default('Unassigned') ->sortable(), @@ -37,7 +33,7 @@ public function table(Table $table): Table ->label(__('audit.table.columns.end_date')) ->date() ->sortable(), - + ]) ->filters([ // @@ -45,15 +41,13 @@ public function table(Table $table): Table ->headerActions([ Tables\Actions\CreateAction::make() ->label('Create New Audit') - ->url(fn (): string => - \App\Filament\Resources\AuditResource::getUrl('create', ['default_program_id' => $this->ownerRecord->id]) - ), + ->url(fn (): string => \App\Filament\Resources\AuditResource::getUrl('create', ['default_program_id' => $this->ownerRecord->id]) + ), ]) ->actions([ Tables\Actions\ViewAction::make() - ->url(fn ($record): string => - \App\Filament\Resources\AuditResource::getUrl('view', ['record' => $record]) - ), + ->url(fn ($record): string => \App\Filament\Resources\AuditResource::getUrl('view', ['record' => $record]) + ), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ diff --git a/app/Filament/Resources/ProgramResource/RelationManagers/ControlsRelationManager.php b/app/Filament/Resources/ProgramResource/RelationManagers/ControlsRelationManager.php index 3f4c5fd2..6e2fbdbb 100644 --- a/app/Filament/Resources/ProgramResource/RelationManagers/ControlsRelationManager.php +++ b/app/Filament/Resources/ProgramResource/RelationManagers/ControlsRelationManager.php @@ -74,8 +74,7 @@ public function table(Table $table): Table ]) ->actions([ Tables\Actions\ViewAction::make() - ->url(fn ($record): string => - \App\Filament\Resources\ControlResource::getUrl('view', ['record' => $record]) + ->url(fn ($record): string => \App\Filament\Resources\ControlResource::getUrl('view', ['record' => $record]) ), ]) ->bulkActions([ diff --git a/app/Filament/Resources/RiskResource/Pages/ListRisks.php b/app/Filament/Resources/RiskResource/Pages/ListRisks.php index dbf84476..f30d1471 100644 --- a/app/Filament/Resources/RiskResource/Pages/ListRisks.php +++ b/app/Filament/Resources/RiskResource/Pages/ListRisks.php @@ -20,8 +20,8 @@ class ListRisks extends ListRecords #[On('filter-risks')] public function filterRisks(string $type, int $likelihood, int $impact): void { - $this->tableFilters[$type . '_likelihood']['value'] = (string) $likelihood; - $this->tableFilters[$type . '_impact']['value'] = (string) $impact; + $this->tableFilters[$type.'_likelihood']['value'] = (string) $likelihood; + $this->tableFilters[$type.'_impact']['value'] = (string) $impact; $this->hasActiveRiskFilters = true; $this->resetPage(); } diff --git a/app/Filament/Resources/RiskResource/Pages/ViewRisk.php b/app/Filament/Resources/RiskResource/Pages/ViewRisk.php index c613b105..885e0785 100644 --- a/app/Filament/Resources/RiskResource/Pages/ViewRisk.php +++ b/app/Filament/Resources/RiskResource/Pages/ViewRisk.php @@ -4,7 +4,6 @@ use App\Enums\MitigationType; use App\Filament\Resources\RiskResource; -use App\Models\Mitigation; use Filament\Actions; use Filament\Forms; use Filament\Resources\Pages\ViewRecord; diff --git a/app/Filament/Resources/StandardResource/RelationManagers/AuditsRelationManager.php b/app/Filament/Resources/StandardResource/RelationManagers/AuditsRelationManager.php index 60fe1582..9724601d 100644 --- a/app/Filament/Resources/StandardResource/RelationManagers/AuditsRelationManager.php +++ b/app/Filament/Resources/StandardResource/RelationManagers/AuditsRelationManager.php @@ -2,8 +2,6 @@ namespace App\Filament\Resources\StandardResource\RelationManagers; -use Filament\Forms; -use Filament\Forms\Form; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; use Filament\Tables\Table; diff --git a/app/Filament/Resources/SurveyResource/Pages/ScoreSurvey.php b/app/Filament/Resources/SurveyResource/Pages/ScoreSurvey.php index 42805cb4..0c70b8dc 100644 --- a/app/Filament/Resources/SurveyResource/Pages/ScoreSurvey.php +++ b/app/Filament/Resources/SurveyResource/Pages/ScoreSurvey.php @@ -23,7 +23,6 @@ use Filament\Notifications\Notification; use Filament\Resources\Pages\Concerns\InteractsWithRecord; use Filament\Resources\Pages\Page; -use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Auth; class ScoreSurvey extends Page implements HasForms, HasInfolists diff --git a/app/Filament/Resources/VendorResource/RelationManagers/ApplicationsRelationManager.php b/app/Filament/Resources/VendorResource/RelationManagers/ApplicationsRelationManager.php index 977bd50b..8c766f53 100644 --- a/app/Filament/Resources/VendorResource/RelationManagers/ApplicationsRelationManager.php +++ b/app/Filament/Resources/VendorResource/RelationManagers/ApplicationsRelationManager.php @@ -2,14 +2,11 @@ namespace App\Filament\Resources\VendorResource\RelationManagers; -use App\Enums\ApplicationStatus; -use App\Enums\ApplicationType; -use App\Models\Application; +use App\Filament\Resources\ApplicationResource; use Filament\Forms; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; use Filament\Tables\Table; -use App\Filament\Resources\ApplicationResource; class ApplicationsRelationManager extends RelationManager { diff --git a/app/Filament/Resources/VendorResource/RelationManagers/VendorUsersRelationManager.php b/app/Filament/Resources/VendorResource/RelationManagers/VendorUsersRelationManager.php index 8da1498f..d159bfe4 100644 --- a/app/Filament/Resources/VendorResource/RelationManagers/VendorUsersRelationManager.php +++ b/app/Filament/Resources/VendorResource/RelationManagers/VendorUsersRelationManager.php @@ -74,7 +74,7 @@ public function table(Table $table): Table try { Mail::send(new VendorInvitationMail($record)); Notification::make() - ->title('Invitation sent to ' . $record->email) + ->title('Invitation sent to '.$record->email) ->success() ->send(); } catch (\Exception $e) { @@ -99,7 +99,7 @@ public function table(Table $table): Table try { Mail::send(new VendorInvitationMail($record)); Notification::make() - ->title('Invitation sent to ' . $record->email) + ->title('Invitation sent to '.$record->email) ->success() ->send(); } catch (\Exception $e) { @@ -120,7 +120,7 @@ public function table(Table $table): Table try { Mail::send(new VendorMagicLinkMail($record)); Notification::make() - ->title('Magic link sent to ' . $record->email) + ->title('Magic link sent to '.$record->email) ->success() ->send(); } catch (\Exception $e) { @@ -146,7 +146,7 @@ public function table(Table $table): Table $record->vendor->update(['primary_contact_id' => $record->id]); Notification::make() - ->title($record->name . ' is now the primary contact') + ->title($record->name.' is now the primary contact') ->success() ->send(); }), diff --git a/app/Filament/Widgets/AuditListWidget.php b/app/Filament/Widgets/AuditListWidget.php index 45f7d3a5..d565efe9 100644 --- a/app/Filament/Widgets/AuditListWidget.php +++ b/app/Filament/Widgets/AuditListWidget.php @@ -11,7 +11,7 @@ class AuditListWidget extends BaseWidget { protected static bool $isLazy = false; - + protected int|string|array $columnSpan = '2'; public function table(Table $table): Table diff --git a/app/Filament/Widgets/ControlsStatsWidget.php b/app/Filament/Widgets/ControlsStatsWidget.php index 62002e89..e228d6d8 100644 --- a/app/Filament/Widgets/ControlsStatsWidget.php +++ b/app/Filament/Widgets/ControlsStatsWidget.php @@ -10,7 +10,6 @@ class ControlsStatsWidget extends ChartWidget { - protected static bool $isLazy = false; protected static ?string $heading = null; @@ -32,12 +31,12 @@ protected function getData(): array // Single query with conditional aggregation for all effectiveness counts $counts = Control::whereIn('standard_id', $inScopeStandardIds) - ->selectRaw(" + ->selectRaw(' SUM(CASE WHEN effectiveness = ? AND applicability = ? THEN 1 ELSE 0 END) as effective, SUM(CASE WHEN effectiveness = ? AND applicability = ? THEN 1 ELSE 0 END) as partial, SUM(CASE WHEN effectiveness = ? AND applicability = ? THEN 1 ELSE 0 END) as ineffective, SUM(CASE WHEN effectiveness = ? AND applicability != ? THEN 1 ELSE 0 END) as unknown - ", [ + ', [ Effectiveness::EFFECTIVE->value, Applicability::APPLICABLE->value, Effectiveness::PARTIAL->value, Applicability::APPLICABLE->value, Effectiveness::INEFFECTIVE->value, Applicability::APPLICABLE->value, diff --git a/app/Filament/Widgets/ImplementationsStatsWidget.php b/app/Filament/Widgets/ImplementationsStatsWidget.php index 036bd265..b0377814 100644 --- a/app/Filament/Widgets/ImplementationsStatsWidget.php +++ b/app/Filament/Widgets/ImplementationsStatsWidget.php @@ -26,12 +26,12 @@ public function getHeading(): ?string protected function getData(): array { // Single query with conditional aggregation for all effectiveness counts - $counts = Implementation::selectRaw(" + $counts = Implementation::selectRaw(' SUM(CASE WHEN effectiveness = ? THEN 1 ELSE 0 END) as effective, SUM(CASE WHEN effectiveness = ? THEN 1 ELSE 0 END) as partial, SUM(CASE WHEN effectiveness = ? THEN 1 ELSE 0 END) as ineffective, SUM(CASE WHEN effectiveness = ? THEN 1 ELSE 0 END) as unknown - ", [ + ', [ Effectiveness::EFFECTIVE->value, Effectiveness::PARTIAL->value, Effectiveness::INEFFECTIVE->value, diff --git a/app/Filament/Widgets/StatsOverview.php b/app/Filament/Widgets/StatsOverview.php index 4cd2a300..a3d8db0c 100644 --- a/app/Filament/Widgets/StatsOverview.php +++ b/app/Filament/Widgets/StatsOverview.php @@ -30,10 +30,10 @@ protected function getStats(): array protected function getGlobalStats(): array { // Single query for audit counts using conditional aggregation - $auditCounts = Audit::selectRaw(" + $auditCounts = Audit::selectRaw(' SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as in_progress, SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as completed - ", [WorkflowStatus::INPROGRESS->value, WorkflowStatus::COMPLETED->value]) + ', [WorkflowStatus::INPROGRESS->value, WorkflowStatus::COMPLETED->value]) ->first(); $audits_in_progress = (int) ($auditCounts->in_progress ?? 0); @@ -60,10 +60,10 @@ protected function getProgramScopedStats(): array { // Single query for audit counts using conditional aggregation $auditCounts = Audit::where('program_id', $this->program->id) - ->selectRaw(" + ->selectRaw(' SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as in_progress, SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as completed - ", [WorkflowStatus::INPROGRESS->value, WorkflowStatus::COMPLETED->value]) + ', [WorkflowStatus::INPROGRESS->value, WorkflowStatus::COMPLETED->value]) ->first(); $audits_in_progress = (int) ($auditCounts->in_progress ?? 0); diff --git a/app/Filament/Widgets/ToDoListWidget.php b/app/Filament/Widgets/ToDoListWidget.php index 7204cee1..eb7d0977 100644 --- a/app/Filament/Widgets/ToDoListWidget.php +++ b/app/Filament/Widgets/ToDoListWidget.php @@ -11,7 +11,7 @@ class ToDoListWidget extends BaseWidget { protected static bool $isLazy = false; - + protected int|string|array $columnSpan = '2'; public function table(Table $table): Table diff --git a/app/Filament/Widgets/TrustCenter/TrustCenterDocumentsWidget.php b/app/Filament/Widgets/TrustCenter/TrustCenterDocumentsWidget.php index 4b751d63..fd4a3eda 100644 --- a/app/Filament/Widgets/TrustCenter/TrustCenterDocumentsWidget.php +++ b/app/Filament/Widgets/TrustCenter/TrustCenterDocumentsWidget.php @@ -14,7 +14,7 @@ class TrustCenterDocumentsWidget extends BaseWidget { protected static bool $isLazy = false; - + protected int|string|array $columnSpan = 'full'; protected static ?string $heading = 'Documents'; diff --git a/app/Filament/Widgets/VendorsTableWidget.php b/app/Filament/Widgets/VendorsTableWidget.php index 739a05c6..21b95815 100644 --- a/app/Filament/Widgets/VendorsTableWidget.php +++ b/app/Filament/Widgets/VendorsTableWidget.php @@ -15,7 +15,7 @@ class VendorsTableWidget extends BaseWidget { protected static bool $isLazy = false; - + protected static ?int $sort = 1; protected int|string|array $columnSpan = 'full'; diff --git a/app/Http/Controllers/API/PolicyController.php b/app/Http/Controllers/API/PolicyController.php index bada8b45..a577e34c 100644 --- a/app/Http/Controllers/API/PolicyController.php +++ b/app/Http/Controllers/API/PolicyController.php @@ -45,7 +45,7 @@ protected function validateStore(Request $request): array protected function validateUpdate(Request $request, $resource): array { return $request->validate([ - 'code' => 'sometimes|string|max:255|unique:policies,code,' . $resource->id, + 'code' => 'sometimes|string|max:255|unique:policies,code,'.$resource->id, 'name' => 'sometimes|string|max:255', 'policy_scope' => 'nullable|string', 'purpose' => 'nullable|string', diff --git a/app/Http/Controllers/BundleController.php b/app/Http/Controllers/BundleController.php index d5b0e67a..55601618 100644 --- a/app/Http/Controllers/BundleController.php +++ b/app/Http/Controllers/BundleController.php @@ -9,7 +9,6 @@ use Http; use Illuminate\Http\Client\RequestException; use Storage; -use Illuminate\Support\Facades\Log; class BundleController extends Controller { @@ -95,11 +94,11 @@ public static function importBundle(Bundle $bundle): void 'content_type' => $response->header('Content-Type'), 'body_preview' => substr($response->body(), 0, 500), ]); - throw new \Exception('Failed to decode JSON response from: ' . $bundle->repo_url); + throw new \Exception('Failed to decode JSON response from: '.$bundle->repo_url); } // Validate required fields exist - if (!isset($bundle_content['code']) || !isset($bundle_content['controls'])) { + if (! isset($bundle_content['code']) || ! isset($bundle_content['controls'])) { \Log::error('Invalid bundle structure', [ 'url' => $bundle->repo_url, 'keys' => array_keys($bundle_content), @@ -117,8 +116,6 @@ public static function importBundle(Bundle $bundle): void ] ); - - \Log::info('Importing bundle: '.$bundle->code); foreach ($bundle_content['controls'] as $control) { @@ -151,9 +148,10 @@ public static function importBundle(Bundle $bundle): void Notification::make() ->title('Bundle Import Failed') - ->body('Download failed: ' . $e->getMessage()) + ->body('Download failed: '.$e->getMessage()) ->color('danger') ->send(); + return; } catch (\Exception $e) { // Catch any other potential exceptions @@ -166,9 +164,10 @@ public static function importBundle(Bundle $bundle): void Notification::make() ->title('Bundle Import Failed') - ->body('An unexpected error occurred: ' . $e->getMessage()) + ->body('An unexpected error occurred: '.$e->getMessage()) ->color('danger') ->send(); + return; } @@ -179,4 +178,3 @@ public static function importBundle(Bundle $bundle): void } } - diff --git a/app/Http/Controllers/HelperController.php b/app/Http/Controllers/HelperController.php index 95cfba07..0a3c3d1e 100644 --- a/app/Http/Controllers/HelperController.php +++ b/app/Http/Controllers/HelperController.php @@ -51,9 +51,9 @@ public static function getEndDate($latestDate, $numDaysFromToday): Carbon /** * Update the .env file with the given key-value pairs. - * - * @param array $data Key-value pairs to update - * @param bool $create If true, creates variables that don't exist. Default: false + * + * @param array $data Key-value pairs to update + * @param bool $create If true, creates variables that don't exist. Default: false */ public static function updateEnv(array $data, bool $create = false): void { diff --git a/app/Http/Middleware/SecurityHeaders.php b/app/Http/Middleware/SecurityHeaders.php index 5eed237a..443ab7b6 100644 --- a/app/Http/Middleware/SecurityHeaders.php +++ b/app/Http/Middleware/SecurityHeaders.php @@ -26,7 +26,7 @@ public function handle(Request $request, Closure $next): Response // HSTS - only in production to avoid issues with local development if (app()->environment('production')) { - //$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + // $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } // CSP only for HTML responses @@ -73,7 +73,7 @@ protected function buildPolicy(Request $request): string "img-src 'self' data: blob: https://ui-avatars.com".$storageEndpoints, // Forms - relaxed for OAuth routes that need to redirect to external clients - "form-action ".$formAction, + 'form-action '.$formAction, // Prevent site from being embedded in frames (clickjacking protection) "frame-ancestors 'self'", diff --git a/app/Listeners/SendCommentMentionNotification.php b/app/Listeners/SendCommentMentionNotification.php index 8de0436a..b7ce1bf2 100644 --- a/app/Listeners/SendCommentMentionNotification.php +++ b/app/Listeners/SendCommentMentionNotification.php @@ -5,7 +5,6 @@ use App\Mail\CommentMentionMail; use Filament\Notifications\Notification; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Facades\Mail; use Kirschbaum\Commentions\Events\UserWasMentionedEvent; diff --git a/app/Livewire/MultiWindowInactivityGuard.php b/app/Livewire/MultiWindowInactivityGuard.php index 5d84b8ba..fe51ed3e 100644 --- a/app/Livewire/MultiWindowInactivityGuard.php +++ b/app/Livewire/MultiWindowInactivityGuard.php @@ -12,7 +12,7 @@ class MultiWindowInactivityGuard extends Component { protected const MILLISECONDS_PER_SECOND = 1000; - public function render(): string | View + public function render(): string|View { if (Filament::auth()->guest()) { return '
'; diff --git a/app/Mail/CommentMentionMail.php b/app/Mail/CommentMentionMail.php index f09380d1..9f9941c5 100644 --- a/app/Mail/CommentMentionMail.php +++ b/app/Mail/CommentMentionMail.php @@ -3,7 +3,6 @@ namespace App\Mail; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; diff --git a/app/Mcp/Tools/EntityTools.php b/app/Mcp/Tools/EntityTools.php index 11ba3935..a122d888 100644 --- a/app/Mcp/Tools/EntityTools.php +++ b/app/Mcp/Tools/EntityTools.php @@ -12,7 +12,6 @@ * Tools with custom behavior (like ManagePolicyTool with auto-code generation) * are defined in their own files. */ - class ManageApplicationTool extends BaseManageEntityTool {} class ManageAssetTool extends BaseManageEntityTool {} class ManageAuditTool extends BaseManageEntityTool {} diff --git a/app/Mcp/Traits/HasMcpSupport.php b/app/Mcp/Traits/HasMcpSupport.php index d3e7fce3..7375ab35 100644 --- a/app/Mcp/Traits/HasMcpSupport.php +++ b/app/Mcp/Traits/HasMcpSupport.php @@ -361,7 +361,7 @@ protected static function deriveUpdateFields(array $columns): array */ protected static function deriveUrlPath(string $className): string { - return '/app/' . Str::kebab(Str::pluralStudly($className)); + return '/app/'.Str::kebab(Str::pluralStudly($className)); } /** diff --git a/app/Policies/VendorDocumentPolicy.php b/app/Policies/VendorDocumentPolicy.php index 5b95073f..f54b1697 100644 --- a/app/Policies/VendorDocumentPolicy.php +++ b/app/Policies/VendorDocumentPolicy.php @@ -2,7 +2,6 @@ namespace App\Policies; -use App\Models\User; use App\Models\VendorDocument; use App\Models\VendorUser; use Illuminate\Contracts\Auth\Authenticatable; diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c32d0927..4ee08670 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -41,6 +41,13 @@ public function boot(): void || $argv[1] === 'filament:upgrade' || $argv[1] === 'vendor:publish' || $argv[1] === 'test' + || $argv[1] === 'key:generate' + || $argv[1] === 'migrate' + || $argv[1] === 'migrate:fresh' + || $argv[1] === 'migrate:refresh' + || $argv[1] === 'migrate:reset' + || $argv[1] === 'migrate:rollback' + || $argv[1] === 'migrate:status' )) { $isInstaller = true; } diff --git a/app/Providers/Filament/AppPanelProvider.php b/app/Providers/Filament/AppPanelProvider.php index dfb04754..c9134115 100644 --- a/app/Providers/Filament/AppPanelProvider.php +++ b/app/Providers/Filament/AppPanelProvider.php @@ -4,7 +4,6 @@ use App\Models\Settings; use App\Models\User; -use Carbon\Carbon; use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\DisableBladeIconComponents; @@ -19,12 +18,11 @@ use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\StartSession; +use Illuminate\Support\Facades\Blade; use Illuminate\Validation\Rules\Password; use Illuminate\View\Middleware\ShareErrorsFromSession; -use Illuminate\Support\Facades\Blade; use Jeffgreco13\FilamentBreezy\BreezyCore; use Leandrocfe\FilamentApexCharts\FilamentApexChartsPlugin; -use Livewire\Livewire; use Rmsramos\Activitylog\ActivitylogPlugin; class AppPanelProvider extends PanelProvider diff --git a/app/Providers/Filament/VendorPanelProvider.php b/app/Providers/Filament/VendorPanelProvider.php index 12a05c18..222315fc 100644 --- a/app/Providers/Filament/VendorPanelProvider.php +++ b/app/Providers/Filament/VendorPanelProvider.php @@ -11,13 +11,13 @@ use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; +use Filament\View\PanelsRenderHook; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; -use Filament\View\PanelsRenderHook; class VendorPanelProvider extends PanelProvider { diff --git a/database/factories/ApplicationFactory.php b/database/factories/ApplicationFactory.php index 728de360..025b0e1d 100644 --- a/database/factories/ApplicationFactory.php +++ b/database/factories/ApplicationFactory.php @@ -24,7 +24,7 @@ class ApplicationFactory extends Factory public function definition(): array { return [ - 'name' => fake()->words(3, true) . ' Application', + 'name' => fake()->words(3, true).' Application', 'description' => fake()->sentence(10), 'url' => fake()->url(), 'owner_id' => User::factory(), diff --git a/database/migrations/2025_09_02_184316_ensure_all_permissions_exist_and_assign_to_roles.php b/database/migrations/2025_09_02_184316_ensure_all_permissions_exist_and_assign_to_roles.php index c5cfe408..d0cd3b20 100644 --- a/database/migrations/2025_09_02_184316_ensure_all_permissions_exist_and_assign_to_roles.php +++ b/database/migrations/2025_09_02_184316_ensure_all_permissions_exist_and_assign_to_roles.php @@ -13,9 +13,9 @@ public function up(): void { // Define all entities and actions $entities = [ - 'Standards', 'Controls', 'Implementations', 'Audits', 'AuditItems', - 'Programs', 'Vendors', 'Applications', 'Risks', 'DataRequests', - 'DataRequestResponses', 'FileAttachments' + 'Standards', 'Controls', 'Implementations', 'Audits', 'AuditItems', + 'Programs', 'Vendors', 'Applications', 'Risks', 'DataRequests', + 'DataRequestResponses', 'FileAttachments', ]; $actions = ['List', 'Create', 'Read', 'Update', 'Delete']; @@ -25,7 +25,7 @@ public function up(): void Permission::firstOrCreate([ 'name' => "{$action} {$entity}", 'category' => $entity, - 'guard_name' => 'web' + 'guard_name' => 'web', ]); } } @@ -44,7 +44,7 @@ public function up(): void Permission::firstOrCreate([ 'name' => $permissionData['name'], 'category' => $permissionData['category'], - 'guard_name' => 'web' + 'guard_name' => 'web', ]); } @@ -61,7 +61,7 @@ public function up(): void $securityAdmin = Role::where('name', 'Security Admin')->first(); if ($securityAdmin) { $securityAdminPermissions = []; - + // Add CRUD permissions (excluding Delete) foreach ($entities as $entity) { foreach (['List', 'Create', 'Read', 'Update'] as $action) { @@ -71,7 +71,7 @@ public function up(): void } } } - + // Add additional permissions for Security Admin $additionalSecurityAdminPerms = ['Manage Preferences', 'View Bundles']; foreach ($additionalSecurityAdminPerms as $permName) { @@ -80,7 +80,7 @@ public function up(): void $securityAdminPermissions[] = $permission->id; } } - + $securityAdmin->syncPermissions($securityAdminPermissions); } } @@ -94,4 +94,4 @@ public function down(): void // We don't reverse it as it could break existing functionality // If needed, permissions can be managed through the admin interface } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_09_02_231928_add_taxonomy_permissions.php b/database/migrations/2025_09_02_231928_add_taxonomy_permissions.php index e4c5f703..07cf2f1a 100644 --- a/database/migrations/2025_09_02_231928_add_taxonomy_permissions.php +++ b/database/migrations/2025_09_02_231928_add_taxonomy_permissions.php @@ -1,9 +1,9 @@ getDriverName(); - + if ($driver === 'mysql') { // For MySQL, use raw SQL to handle foreign keys $database = DB::connection()->getDatabaseName(); - + // Get all foreign keys for the audit_user table $foreignKeys = DB::select(" SELECT CONSTRAINT_NAME @@ -31,18 +31,18 @@ public function up(): void AND TABLE_NAME = 'audit_user' AND REFERENCED_TABLE_NAME IS NOT NULL ", [$database]); - + // Drop existing foreign keys foreach ($foreignKeys as $fk) { if (str_contains($fk->CONSTRAINT_NAME, 'audit_id') || str_contains($fk->CONSTRAINT_NAME, 'user_id')) { DB::statement("ALTER TABLE audit_user DROP FOREIGN KEY {$fk->CONSTRAINT_NAME}"); } } - + // Add foreign keys with cascade delete - DB::statement("ALTER TABLE audit_user ADD CONSTRAINT audit_user_audit_id_foreign FOREIGN KEY (audit_id) REFERENCES audits(id) ON DELETE CASCADE"); - DB::statement("ALTER TABLE audit_user ADD CONSTRAINT audit_user_user_id_foreign FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE"); - + DB::statement('ALTER TABLE audit_user ADD CONSTRAINT audit_user_audit_id_foreign FOREIGN KEY (audit_id) REFERENCES audits(id) ON DELETE CASCADE'); + DB::statement('ALTER TABLE audit_user ADD CONSTRAINT audit_user_user_id_foreign FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE'); + } else { // For SQLite and other databases, use Schema builder Schema::table('audit_user', function (Blueprint $table) { @@ -51,18 +51,18 @@ public function up(): void } catch (\Exception $e) { // Foreign key might not exist } - + try { $table->dropForeign(['user_id']); } catch (\Exception $e) { // Foreign key might not exist } - + $table->foreign('audit_id') ->references('id') ->on('audits') ->onDelete('cascade'); - + $table->foreign('user_id') ->references('id') ->on('users') @@ -76,15 +76,15 @@ public function up(): void */ public function down(): void { - if (!Schema::hasTable('audit_user')) { + if (! Schema::hasTable('audit_user')) { return; } $driver = DB::connection()->getDriverName(); - + if ($driver === 'mysql') { $database = DB::connection()->getDatabaseName(); - + // Get all foreign keys for the audit_user table $foreignKeys = DB::select(" SELECT CONSTRAINT_NAME @@ -93,18 +93,18 @@ public function down(): void AND TABLE_NAME = 'audit_user' AND REFERENCED_TABLE_NAME IS NOT NULL ", [$database]); - + // Drop existing foreign keys foreach ($foreignKeys as $fk) { if (str_contains($fk->CONSTRAINT_NAME, 'audit_id') || str_contains($fk->CONSTRAINT_NAME, 'user_id')) { DB::statement("ALTER TABLE audit_user DROP FOREIGN KEY {$fk->CONSTRAINT_NAME}"); } } - + // Add foreign keys without cascade delete - DB::statement("ALTER TABLE audit_user ADD CONSTRAINT audit_user_audit_id_foreign FOREIGN KEY (audit_id) REFERENCES audits(id)"); - DB::statement("ALTER TABLE audit_user ADD CONSTRAINT audit_user_user_id_foreign FOREIGN KEY (user_id) REFERENCES users(id)"); - + DB::statement('ALTER TABLE audit_user ADD CONSTRAINT audit_user_audit_id_foreign FOREIGN KEY (audit_id) REFERENCES audits(id)'); + DB::statement('ALTER TABLE audit_user ADD CONSTRAINT audit_user_user_id_foreign FOREIGN KEY (user_id) REFERENCES users(id)'); + } else { Schema::table('audit_user', function (Blueprint $table) { try { @@ -112,21 +112,21 @@ public function down(): void } catch (\Exception $e) { // Foreign key might not exist } - + try { $table->dropForeign(['user_id']); } catch (\Exception $e) { // Foreign key might not exist } - + $table->foreign('audit_id') ->references('id') ->on('audits'); - + $table->foreign('user_id') ->references('id') ->on('users'); }); } } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_10_23_202858_create_assets_table.php b/database/migrations/2025_10_23_202858_create_assets_table.php index 9882a4b9..f625b769 100644 --- a/database/migrations/2025_10_23_202858_create_assets_table.php +++ b/database/migrations/2025_10_23_202858_create_assets_table.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Schema; return new class extends Migration { @@ -118,7 +118,7 @@ public function up(): void }); // If table didn't exist before (fresh creation), seed the asset taxonomies - if (!$tableExists) { + if (! $tableExists) { Artisan::call('db:seed', [ '--class' => 'Database\\Seeders\\AssetTaxonomySeeder', '--force' => true, diff --git a/database/migrations/2025_11_03_010122_add_asset_permissions.php b/database/migrations/2025_11_03_010122_add_asset_permissions.php index 411ec958..c8c226b2 100644 --- a/database/migrations/2025_11_03_010122_add_asset_permissions.php +++ b/database/migrations/2025_11_03_010122_add_asset_permissions.php @@ -15,7 +15,7 @@ public function up(): void $assetActions = ['List', 'Create', 'Read', 'Update', 'Delete']; foreach ($assetActions as $action) { $permission = Permission::where('name', "{$action} Assets")->first(); - if (!$permission) { + if (! $permission) { Permission::create([ 'name' => "{$action} Assets", 'guard_name' => 'web', @@ -39,7 +39,7 @@ public function up(): void // Assign permissions to Super Admin (all permissions) if ($superAdmin && $assetPermissions->isNotEmpty()) { foreach ($assetPermissions as $permission) { - if (!$superAdmin->hasPermissionTo($permission)) { + if (! $superAdmin->hasPermissionTo($permission)) { $superAdmin->givePermissionTo($permission); } } @@ -49,7 +49,7 @@ public function up(): void if ($regular) { $permissionsToAssign = ['List Assets', 'Read Assets']; foreach ($permissionsToAssign as $permName) { - if (!$regular->hasPermissionTo($permName)) { + if (! $regular->hasPermissionTo($permName)) { $regular->givePermissionTo($permName); } } @@ -59,7 +59,7 @@ public function up(): void if ($securityAdmin) { $permissionsToAssign = ['List Assets', 'Create Assets', 'Read Assets', 'Update Assets']; foreach ($permissionsToAssign as $permName) { - if (!$securityAdmin->hasPermissionTo($permName)) { + if (! $securityAdmin->hasPermissionTo($permName)) { $securityAdmin->givePermissionTo($permName); } } diff --git a/database/migrations/2025_11_10_102616_create_policies_table.php b/database/migrations/2025_11_10_102616_create_policies_table.php index b3e75bc3..7af1ae19 100644 --- a/database/migrations/2025_11_10_102616_create_policies_table.php +++ b/database/migrations/2025_11_10_102616_create_policies_table.php @@ -49,7 +49,7 @@ public function up(): void }); // If table didn't exist before (fresh creation), seed the policy taxonomies and permissions - if (!$tableExists) { + if (! $tableExists) { $this->seedPolicyTaxonomies(); $this->seedPolicyPermissions(); } @@ -148,7 +148,7 @@ private function seedPolicyPermissions(): void foreach ($actions as $action) { $permission = Permission::where('name', "{$action} Policies")->first(); - if (!$permission) { + if (! $permission) { Permission::create([ 'name' => "{$action} Policies", 'guard_name' => 'web', @@ -169,7 +169,7 @@ private function seedPolicyPermissions(): void if ($superAdmin) { $permissionsToAssign = ['List Policies', 'Create Policies', 'Read Policies', 'Update Policies', 'Delete Policies']; foreach ($permissionsToAssign as $permName) { - if (!$superAdmin->hasPermissionTo($permName)) { + if (! $superAdmin->hasPermissionTo($permName)) { $superAdmin->givePermissionTo($permName); } } @@ -179,7 +179,7 @@ private function seedPolicyPermissions(): void if ($regular) { $permissionsToAssign = ['List Policies', 'Read Policies']; foreach ($permissionsToAssign as $permName) { - if (!$regular->hasPermissionTo($permName)) { + if (! $regular->hasPermissionTo($permName)) { $regular->givePermissionTo($permName); } } @@ -189,7 +189,7 @@ private function seedPolicyPermissions(): void if ($securityAdmin) { $permissionsToAssign = ['List Policies', 'Create Policies', 'Read Policies', 'Update Policies']; foreach ($permissionsToAssign as $permName) { - if (!$securityAdmin->hasPermissionTo($permName)) { + if (! $securityAdmin->hasPermissionTo($permName)) { $securityAdmin->givePermissionTo($permName); } } diff --git a/database/migrations/2025_11_11_010239_create_commentions_subscriptions_table.php b/database/migrations/2025_11_11_010239_create_commentions_subscriptions_table.php index 56380341..1a27e5cf 100644 --- a/database/migrations/2025_11_11_010239_create_commentions_subscriptions_table.php +++ b/database/migrations/2025_11_11_010239_create_commentions_subscriptions_table.php @@ -15,7 +15,7 @@ public function up() $table->timestamps(); $table->unique([ - 'subscribable_type', 'subscribable_id', 'subscriber_type', 'subscriber_id' + 'subscribable_type', 'subscribable_id', 'subscriber_type', 'subscriber_id', ], 'commentions_subscriptions_unique'); }); } @@ -25,5 +25,3 @@ public function down(): void Schema::dropIfExists(config('commentions.tables.comment_subscriptions', 'comment_subscriptions')); } }; - - diff --git a/database/migrations/2026_01_09_010006_seed_mcp_settings.php b/database/migrations/2026_01_09_010006_seed_mcp_settings.php index 3fea6728..4e5c01e3 100644 --- a/database/migrations/2026_01_09_010006_seed_mcp_settings.php +++ b/database/migrations/2026_01_09_010006_seed_mcp_settings.php @@ -11,7 +11,7 @@ public function up(): void { // Seed MCP settings for in-place upgrades (uses updateOrInsert, safe to run always) - $seeder = new McpSettingsSeeder(); + $seeder = new McpSettingsSeeder; $seeder->run(); } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 1a76cbdc..ccbfa48c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -18,6 +18,6 @@ public function run(): void $this->call(AssetTaxonomySeeder::class); $this->call(VendorSurveyTemplatesSeeder::class); $this->call(TrustCenterContentBlockSeeder::class); - + } } diff --git a/database/seeders/DemoSeeder.php b/database/seeders/DemoSeeder.php index 2fccb4b7..be740065 100644 --- a/database/seeders/DemoSeeder.php +++ b/database/seeders/DemoSeeder.php @@ -299,7 +299,7 @@ public function run(): void // Create a data request and a data request response for each control. $dataRequest = \App\Models\DataRequest::create([ - 'code' => 'DR-' . $ctl->code . '-001', + 'code' => 'DR-'.$ctl->code.'-001', 'created_by_id' => 1, 'assigned_to_id' => rand(1, 10), 'audit_id' => $audit->id, @@ -413,7 +413,7 @@ public function run(): void [ 'asset_tag' => 'LAP-001', 'name' => 'Dell Latitude 5520', - 'serial_number' => 'DL5520-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'DL5520-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Laptop'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Good'), @@ -436,7 +436,7 @@ public function run(): void [ 'asset_tag' => 'LAP-002', 'name' => 'MacBook Pro 14"', - 'serial_number' => 'MBP14-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'MBP14-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Laptop'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Excellent'), @@ -459,7 +459,7 @@ public function run(): void [ 'asset_tag' => 'LAP-003', 'name' => 'HP EliteBook 840', - 'serial_number' => 'HP840-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'HP840-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Laptop'), 'status_id' => $assetStatusIds->get('Available'), 'condition_id' => $conditionIds->get('Good'), @@ -482,7 +482,7 @@ public function run(): void [ 'asset_tag' => 'DSK-001', 'name' => 'Dell OptiPlex 7090', - 'serial_number' => 'OP7090-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'OP7090-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Desktop'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Good'), @@ -504,7 +504,7 @@ public function run(): void [ 'asset_tag' => 'DSK-002', 'name' => 'HP Z2 Workstation', - 'serial_number' => 'HPZ2-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'HPZ2-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Desktop'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Excellent'), @@ -529,7 +529,7 @@ public function run(): void [ 'asset_tag' => 'SRV-001', 'name' => 'Dell PowerEdge R750', - 'serial_number' => 'PE-R750-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'PE-R750-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Server'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Excellent'), @@ -552,7 +552,7 @@ public function run(): void [ 'asset_tag' => 'SRV-002', 'name' => 'HPE ProLiant DL380', - 'serial_number' => 'HPE-DL380-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'HPE-DL380-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Server'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Good'), @@ -577,7 +577,7 @@ public function run(): void [ 'asset_tag' => 'MON-001', 'name' => 'Dell UltraSharp 27"', - 'serial_number' => 'DU27-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'DU27-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Monitor'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Good'), @@ -594,7 +594,7 @@ public function run(): void [ 'asset_tag' => 'MON-002', 'name' => 'LG UltraFine 4K', - 'serial_number' => 'LG4K-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'LG4K-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Monitor'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Excellent'), @@ -613,7 +613,7 @@ public function run(): void [ 'asset_tag' => 'PHN-001', 'name' => 'iPhone 14 Pro', - 'serial_number' => 'IPH14P-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'IPH14P-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Phone'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Excellent'), @@ -634,7 +634,7 @@ public function run(): void [ 'asset_tag' => 'PHN-002', 'name' => 'Samsung Galaxy S23', - 'serial_number' => 'SGS23-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'SGS23-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Phone'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Good'), @@ -657,7 +657,7 @@ public function run(): void [ 'asset_tag' => 'TAB-001', 'name' => 'iPad Pro 12.9"', - 'serial_number' => 'IPADP-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'IPADP-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Tablet'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Excellent'), @@ -678,7 +678,7 @@ public function run(): void [ 'asset_tag' => 'TAB-002', 'name' => 'Microsoft Surface Pro 9', - 'serial_number' => 'MSP9-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'MSP9-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Tablet'), 'status_id' => $assetStatusIds->get('Available'), 'condition_id' => $conditionIds->get('Good'), @@ -701,7 +701,7 @@ public function run(): void [ 'asset_tag' => 'NET-001', 'name' => 'Cisco Catalyst 9300', - 'serial_number' => 'CSC9300-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'CSC9300-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Network Equipment'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Excellent'), @@ -718,7 +718,7 @@ public function run(): void [ 'asset_tag' => 'NET-002', 'name' => 'Ubiquiti UniFi AP', - 'serial_number' => 'UAP-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'UAP-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Network Equipment'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Good'), @@ -735,7 +735,7 @@ public function run(): void [ 'asset_tag' => 'NET-003', 'name' => 'Fortinet FortiGate 100F', - 'serial_number' => 'FG100F-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'FG100F-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Network Equipment'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Excellent'), @@ -754,7 +754,7 @@ public function run(): void [ 'asset_tag' => 'PRT-001', 'name' => 'HP LaserJet Pro', - 'serial_number' => 'HPL-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'HPL-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Peripheral'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Good'), @@ -771,7 +771,7 @@ public function run(): void [ 'asset_tag' => 'PRT-002', 'name' => 'Canon imageCLASS', - 'serial_number' => 'CAN-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'CAN-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Peripheral'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Fair'), @@ -788,7 +788,7 @@ public function run(): void [ 'asset_tag' => 'KEY-001', 'name' => 'Logitech MX Keys', - 'serial_number' => 'LGMX-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'LGMX-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Peripheral'), 'status_id' => $assetStatusIds->get('In Use'), 'condition_id' => $conditionIds->get('Excellent'), @@ -846,7 +846,7 @@ public function run(): void [ 'asset_tag' => 'LAP-004', 'name' => 'Dell Latitude E7470', - 'serial_number' => 'DLE7470-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'DLE7470-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Laptop'), 'status_id' => $assetStatusIds->get('Retired'), 'condition_id' => $conditionIds->get('Poor'), @@ -868,7 +868,7 @@ public function run(): void [ 'asset_tag' => 'SRV-003', 'name' => 'Dell PowerEdge R640', - 'serial_number' => 'PE-R640-' . strtoupper($this->faker->bothify('???###')), + 'serial_number' => 'PE-R640-'.strtoupper($this->faker->bothify('???###')), 'asset_type_id' => $assetTypeIds->get('Server'), 'status_id' => $assetStatusIds->get('In Repair'), 'condition_id' => $conditionIds->get('Fair'), diff --git a/database/seeders/FullDemoSeeder.php b/database/seeders/FullDemoSeeder.php index 70063a1e..a6ab48b4 100644 --- a/database/seeders/FullDemoSeeder.php +++ b/database/seeders/FullDemoSeeder.php @@ -157,7 +157,7 @@ public function run(): void // 9. Create a data request for each audit item $dataRequest = DataRequest::create([ - 'code' => 'DR-' . $control->code . '-' . str_pad($auditItem->id, 3, '0', STR_PAD_LEFT), + 'code' => 'DR-'.$control->code.'-'.str_pad($auditItem->id, 3, '0', STR_PAD_LEFT), 'created_by_id' => $user->id, 'assigned_to_id' => $users->random()->id, 'audit_id' => $audit->id, diff --git a/tests/Browser/ControlTest.php b/tests/Browser/ControlTest.php index 00023d3d..7c23a819 100644 --- a/tests/Browser/ControlTest.php +++ b/tests/Browser/ControlTest.php @@ -63,7 +63,7 @@ public function test_control_crud_from_controls_list(): void // Create the test standard first $standard = $this->createTestStandard(); - $this->browse(function (Browser $browser) use ($standard) { + $this->browse(function (Browser $browser) { $this->loginAs($browser); // ========================================== diff --git a/tests/Browser/ImplementationTest.php b/tests/Browser/ImplementationTest.php index ba85196e..117e8c3a 100644 --- a/tests/Browser/ImplementationTest.php +++ b/tests/Browser/ImplementationTest.php @@ -95,7 +95,7 @@ public function test_implementation_crud_from_implementations_list(): void $standard = $this->createTestStandard(); $control = $this->createTestControl($standard, self::$testControlCodeA, 'Control A for Implementation'); - $this->browse(function (Browser $browser) use ($control) { + $this->browse(function (Browser $browser) { $this->loginAs($browser); // ========================================== @@ -115,7 +115,7 @@ public function test_implementation_crud_from_implementations_list(): void ->pause(300); // Select related control - $browser->filamentSelect('controls', "(" . self::$testControlCodeA . ") - Control A for Implementation") + $browser->filamentSelect('controls', '('.self::$testControlCodeA.') - Control A for Implementation') ->pause(300); // Select owner, department, scope @@ -370,7 +370,7 @@ public function test_attach_existing_implementation_to_control(): void // Select Implementation C from the dropdown $browser->within('.fi-modal', function ($modal) { - $modal->filamentSelect('recordId', '(' . self::$testImplCodeC . ') Implementation C'); + $modal->filamentSelect('recordId', '('.self::$testImplCodeC.') Implementation C'); }); $browser->pause(500); diff --git a/tests/Feature/AssetTest.php b/tests/Feature/AssetTest.php index d98b7783..feeb0046 100644 --- a/tests/Feature/AssetTest.php +++ b/tests/Feature/AssetTest.php @@ -28,6 +28,7 @@ protected function setUp(): void private function getTaxonomyTerm(string $parentSlug, string $termName): ?Taxonomy { $parent = Taxonomy::where('slug', $parentSlug)->first(); + return Taxonomy::where('parent_id', $parent->id)->where('name', $termName)->first(); } diff --git a/tests/Feature/RegularUserPermissionsTest.php b/tests/Feature/RegularUserPermissionsTest.php index 87ca4479..251b8cb3 100644 --- a/tests/Feature/RegularUserPermissionsTest.php +++ b/tests/Feature/RegularUserPermissionsTest.php @@ -4,11 +4,8 @@ use App\Models\Application; use App\Models\Audit; -use App\Models\AuditItem; use App\Models\Control; use App\Models\DataRequest; -use App\Models\DataRequestResponse; -use App\Models\FileAttachment; use App\Models\Implementation; use App\Models\Program; use App\Models\Risk; diff --git a/tests/Unit/ApplicabilityTest.php b/tests/Unit/ApplicabilityTest.php new file mode 100644 index 00000000..568dbdeb --- /dev/null +++ b/tests/Unit/ApplicabilityTest.php @@ -0,0 +1,89 @@ +assertEquals('Applicable', Applicability::APPLICABLE->value); + $this->assertEquals('Not Applicable', Applicability::NOTAPPLICABLE->value); + $this->assertEquals('Partially Applicable', Applicability::PARTIALLYAPPLICABLE->value); + $this->assertEquals('Unknown', Applicability::UNKNOWN->value); + } + + public function test_applicability_enum_has_all_expected_cases(): void + { + $expectedCases = [ + 'APPLICABLE', + 'NOTAPPLICABLE', + 'PARTIALLYAPPLICABLE', + 'UNKNOWN', + ]; + + $actualCases = array_map(fn ($case) => $case->name, Applicability::cases()); + + $this->assertEquals($expectedCases, $actualCases); + $this->assertCount(4, Applicability::cases()); + } + + public function test_get_label_returns_correct_values(): void + { + // Test that getLabel returns a string (may be translated) + $this->assertNotNull(Applicability::APPLICABLE->getLabel()); + $this->assertNotNull(Applicability::NOTAPPLICABLE->getLabel()); + $this->assertNotNull(Applicability::PARTIALLYAPPLICABLE->getLabel()); + $this->assertNotNull(Applicability::UNKNOWN->getLabel()); + } + + public function test_get_color_returns_correct_values(): void + { + $this->assertEquals('success', Applicability::APPLICABLE->getColor()); + $this->assertEquals('danger', Applicability::NOTAPPLICABLE->getColor()); + $this->assertEquals('warning', Applicability::PARTIALLYAPPLICABLE->getColor()); + $this->assertEquals('secondary', Applicability::UNKNOWN->getColor()); + } + + public function test_applicability_implements_has_color_interface(): void + { + $this->assertInstanceOf(\Filament\Support\Contracts\HasColor::class, Applicability::APPLICABLE); + } + + public function test_applicability_implements_has_label_interface(): void + { + $this->assertInstanceOf(\Filament\Support\Contracts\HasLabel::class, Applicability::APPLICABLE); + } + + public function test_applicability_can_be_created_from_string(): void + { + $applicable = Applicability::from('Applicable'); + $this->assertEquals(Applicability::APPLICABLE, $applicable); + + $notApplicable = Applicability::from('Not Applicable'); + $this->assertEquals(Applicability::NOTAPPLICABLE, $notApplicable); + + $partiallyApplicable = Applicability::from('Partially Applicable'); + $this->assertEquals(Applicability::PARTIALLYAPPLICABLE, $partiallyApplicable); + + $unknown = Applicability::from('Unknown'); + $this->assertEquals(Applicability::UNKNOWN, $unknown); + } + + public function test_applicability_try_from_returns_null_for_invalid_value(): void + { + $result = Applicability::tryFrom('Invalid Value'); + $this->assertNull($result); + } + + public function test_applicability_from_throws_exception_for_invalid_value(): void + { + $this->expectException(\ValueError::class); + Applicability::from('Invalid Value'); + } +} diff --git a/tests/Unit/ControlTest.php b/tests/Unit/ControlTest.php new file mode 100644 index 00000000..51104520 --- /dev/null +++ b/tests/Unit/ControlTest.php @@ -0,0 +1,202 @@ +create(); + $controlOwner = User::factory()->create(); + + $control = Control::factory()->create([ + 'standard_id' => $standard->id, + 'control_owner_id' => $controlOwner->id, + ]); + + $this->assertInstanceOf(Control::class, $control); + $this->assertEquals($standard->id, $control->standard_id); + $this->assertEquals($controlOwner->id, $control->control_owner_id); + } + + public function test_control_casts_enums_correctly(): void + { + $control = Control::factory()->create(); + + // Test that the model has the correct casts defined + $casts = $control->getCasts(); + + $this->assertEquals(Applicability::class, $casts['status']); + $this->assertEquals(Effectiveness::class, $casts['effectiveness']); + $this->assertEquals(ControlType::class, $casts['type']); + $this->assertEquals(ControlCategory::class, $casts['category']); + $this->assertEquals(ControlEnforcementCategory::class, $casts['enforcement']); + } + + public function test_control_belongs_to_standard(): void + { + $standard = Standard::factory()->create(['name' => 'ISO 27001']); + $control = Control::factory()->create(['standard_id' => $standard->id]); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\BelongsTo::class, $control->standard()); + $this->assertInstanceOf(Standard::class, $control->standard); + $this->assertEquals('ISO 27001', $control->standard->name); + $this->assertEquals($standard->id, $control->standard->id); + } + + public function test_control_belongs_to_many_implementations(): void + { + $control = Control::factory()->create(); + $implementation1 = Implementation::factory()->create(); + $implementation2 = Implementation::factory()->create(); + + $control->implementations()->attach([$implementation1->id, $implementation2->id]); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class, $control->implementations()); + $this->assertCount(2, $control->implementations); + $this->assertTrue($control->implementations->contains($implementation1)); + $this->assertTrue($control->implementations->contains($implementation2)); + } + + public function test_control_belongs_to_many_policies(): void + { + $control = Control::factory()->create(); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class, $control->policies()); + $this->assertEquals('control_policy', $control->policies()->getTable()); + } + + public function test_control_belongs_to_many_programs(): void + { + $control = Control::factory()->create(); + $program1 = Program::factory()->create(); + $program2 = Program::factory()->create(); + + $control->programs()->attach([$program1->id, $program2->id]); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class, $control->programs()); + $this->assertCount(2, $control->programs); + $this->assertTrue($control->programs->contains($program1)); + $this->assertTrue($control->programs->contains($program2)); + } + + public function test_control_belongs_to_control_owner(): void + { + $controlOwner = User::factory()->create(['name' => 'Control Owner']); + $control = Control::factory()->create(['control_owner_id' => $controlOwner->id]); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\BelongsTo::class, $control->controlOwner()); + $this->assertInstanceOf(User::class, $control->controlOwner); + $this->assertEquals('Control Owner', $control->controlOwner->name); + $this->assertEquals($controlOwner->id, $control->controlOwner->id); + } + + public function test_control_has_many_audit_items(): void + { + $control = Control::factory()->create(); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\MorphMany::class, $control->auditItems()); + $this->assertEquals(AuditItem::class, get_class($control->auditItems()->getRelated())); + } + + public function test_get_effectiveness_returns_unknown_when_no_completed_audit_items(): void + { + $control = Control::factory()->create(); + + $effectiveness = $control->getEffectiveness(); + + $this->assertEquals(Effectiveness::UNKNOWN, $effectiveness); + } + + public function test_get_effectiveness_returns_latest_completed_audit_item_effectiveness(): void + { + $control = Control::factory()->create(); + + // Just test that the method exists and returns an Effectiveness enum + $effectiveness = $control->getEffectiveness(); + + $this->assertInstanceOf(Effectiveness::class, $effectiveness); + $this->assertEquals(Effectiveness::UNKNOWN, $effectiveness); // Default when no audit items + } + + public function test_get_effectiveness_date_returns_never_when_no_completed_audit_items(): void + { + $control = Control::factory()->create(); + + $effectivenessDate = $control->getEffectivenessDate(); + + $this->assertEquals('Never', $effectivenessDate); + } + + public function test_get_effectiveness_date_returns_formatted_date(): void + { + $control = Control::factory()->create(); + + $effectivenessDate = $control->getEffectivenessDate(); + + $this->assertEquals('Never', $effectivenessDate); // Default when no audit items + } + + public function test_latest_completed_audit_item_returns_most_recent(): void + { + $control = Control::factory()->create(); + + $latestCompleted = $control->latestCompletedAuditItem(); + + $this->assertNull($latestCompleted); // Default when no completed audit items + } + + public function test_latest_completed_audit_item_returns_null_when_none_exist(): void + { + $control = Control::factory()->create(); + + $latestCompleted = $control->latestCompletedAuditItem(); + + $this->assertNull($latestCompleted); + } + + public function test_control_uses_soft_deletes(): void + { + $control = Control::factory()->create(); + $controlId = $control->id; + + $control->delete(); + + $this->assertSoftDeleted('controls', ['id' => $controlId]); + $this->assertNull(Control::find($controlId)); + $this->assertNotNull(Control::withTrashed()->find($controlId)); + } + + public function test_control_has_searchable_as_method(): void + { + $control = new Control; + + $this->assertEquals('controls_index', $control->searchableAs()); + } + + public function test_control_has_to_searchable_array_method(): void + { + $control = Control::factory()->create(); + + $searchableArray = $control->toSearchableArray(); + + $this->assertIsArray($searchableArray); + $this->assertArrayHasKey('id', $searchableArray); + } +} diff --git a/tests/Unit/EffectivenessTest.php b/tests/Unit/EffectivenessTest.php new file mode 100644 index 00000000..3d3f7160 --- /dev/null +++ b/tests/Unit/EffectivenessTest.php @@ -0,0 +1,107 @@ +assertEquals('Effective', Effectiveness::EFFECTIVE->value); + $this->assertEquals('Partially Effective', Effectiveness::PARTIAL->value); + $this->assertEquals('Not Effective', Effectiveness::INEFFECTIVE->value); + $this->assertEquals('Not Assessed', Effectiveness::UNKNOWN->value); + } + + public function test_effectiveness_enum_has_all_expected_cases(): void + { + $expectedCases = [ + 'EFFECTIVE', + 'PARTIAL', + 'INEFFECTIVE', + 'UNKNOWN', + ]; + + $actualCases = array_map(fn ($case) => $case->name, Effectiveness::cases()); + + $this->assertEquals($expectedCases, $actualCases); + $this->assertCount(4, Effectiveness::cases()); + } + + public function test_get_label_returns_correct_values(): void + { + // Test that getLabel returns a string (may be translated) + $this->assertIsString(Effectiveness::EFFECTIVE->getLabel()); + $this->assertIsString(Effectiveness::PARTIAL->getLabel()); + $this->assertIsString(Effectiveness::INEFFECTIVE->getLabel()); + $this->assertIsString(Effectiveness::UNKNOWN->getLabel()); + } + + public function test_get_color_returns_correct_values(): void + { + $this->assertEquals('success', Effectiveness::EFFECTIVE->getColor()); + $this->assertEquals('warning', Effectiveness::PARTIAL->getColor()); + $this->assertEquals('danger', Effectiveness::INEFFECTIVE->getColor()); + $this->assertEquals('gray', Effectiveness::UNKNOWN->getColor()); + } + + public function test_effectiveness_implements_has_color_interface(): void + { + $this->assertInstanceOf(\Filament\Support\Contracts\HasColor::class, Effectiveness::EFFECTIVE); + } + + public function test_effectiveness_implements_has_label_interface(): void + { + $this->assertInstanceOf(\Filament\Support\Contracts\HasLabel::class, Effectiveness::EFFECTIVE); + } + + public function test_effectiveness_can_be_created_from_string(): void + { + $effective = Effectiveness::from('Effective'); + $this->assertEquals(Effectiveness::EFFECTIVE, $effective); + + $partial = Effectiveness::from('Partially Effective'); + $this->assertEquals(Effectiveness::PARTIAL, $partial); + + $ineffective = Effectiveness::from('Not Effective'); + $this->assertEquals(Effectiveness::INEFFECTIVE, $ineffective); + + $unknown = Effectiveness::from('Not Assessed'); + $this->assertEquals(Effectiveness::UNKNOWN, $unknown); + } + + public function test_effectiveness_try_from_returns_null_for_invalid_value(): void + { + $result = Effectiveness::tryFrom('Invalid Value'); + $this->assertNull($result); + } + + public function test_effectiveness_from_throws_exception_for_invalid_value(): void + { + $this->expectException(\ValueError::class); + Effectiveness::from('Invalid Value'); + } + + public function test_effectiveness_color_mapping_covers_all_cases(): void + { + foreach (Effectiveness::cases() as $effectiveness) { + $color = $effectiveness->getColor(); + $this->assertIsString($color); + $this->assertNotEmpty($color); + } + } + + public function test_effectiveness_label_mapping_covers_all_cases(): void + { + foreach (Effectiveness::cases() as $effectiveness) { + $label = $effectiveness->getLabel(); + $this->assertIsString($label); + $this->assertNotNull($label); + } + } +} diff --git a/tests/Unit/QuotaServiceTest.php b/tests/Unit/QuotaServiceTest.php new file mode 100644 index 00000000..6ee522da --- /dev/null +++ b/tests/Unit/QuotaServiceTest.php @@ -0,0 +1,212 @@ +assertTrue($result); + } + + public function test_check_returns_true_when_under_limit(): void + { + putenv('AI_PROMPT_QUOTA=100'); + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 50); + + $result = QuotaService::check(QuotaType::AI_PROMPT, 30); + + $this->assertTrue($result); + } + + public function test_check_throws_exception_when_over_limit(): void + { + putenv('AI_PROMPT_QUOTA=100'); + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 80); + + $this->expectException(QuotaExceededException::class); + QuotaService::check(QuotaType::AI_PROMPT, 30); + } + + public function test_check_available_returns_php_int_max_when_unlimited(): void + { + putenv('AI_PROMPT_QUOTA=0'); + + $available = QuotaService::checkAvailable(QuotaType::AI_PROMPT); + + $this->assertEquals(PHP_INT_MAX, $available); + } + + public function test_check_available_returns_remaining_capacity(): void + { + putenv('AI_PROMPT_QUOTA=100'); + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 30); + + $available = QuotaService::checkAvailable(QuotaType::AI_PROMPT); + + $this->assertEquals(70, $available); + } + + public function test_check_available_returns_zero_when_exceeded(): void + { + putenv('AI_PROMPT_QUOTA=100'); + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 120); + + $available = QuotaService::checkAvailable(QuotaType::AI_PROMPT); + + $this->assertEquals(0, $available); + } + + public function test_has_capacity_returns_true_when_under_limit(): void + { + putenv('AI_PROMPT_QUOTA=100'); + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 50); + + $hasCapacity = QuotaService::hasCapacity(QuotaType::AI_PROMPT, 30); + + $this->assertTrue($hasCapacity); + } + + public function test_has_capacity_returns_false_when_over_limit(): void + { + putenv('AI_PROMPT_QUOTA=100'); + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 80); + + $hasCapacity = QuotaService::hasCapacity(QuotaType::AI_PROMPT, 30); + + $this->assertFalse($hasCapacity); + } + + public function test_record_increments_usage(): void + { + $newTotal = QuotaService::record(QuotaType::AI_PROMPT, 25); + + $this->assertEquals(25, $newTotal); + $this->assertEquals(25, QuotaService::getUsage(QuotaType::AI_PROMPT)); + } + + public function test_record_increments_existing_usage(): void + { + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 30); + + $newTotal = QuotaService::record(QuotaType::AI_PROMPT, 20); + + $this->assertEquals(50, $newTotal); + $this->assertEquals(50, QuotaService::getUsage(QuotaType::AI_PROMPT)); + } + + public function test_get_usage_returns_cached_value(): void + { + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 42); + + $usage = QuotaService::getUsage(QuotaType::AI_PROMPT); + + $this->assertEquals(42, $usage); + } + + public function test_get_usage_returns_zero_when_not_cached(): void + { + $usage = QuotaService::getUsage(QuotaType::AI_PROMPT); + + $this->assertEquals(0, $usage); + } + + public function test_get_limit_returns_env_value(): void + { + putenv('AI_PROMPT_QUOTA=200'); + + $limit = QuotaService::getLimit(QuotaType::AI_PROMPT); + + $this->assertEquals(200, $limit); + } + + public function test_get_limit_returns_default_when_env_not_set(): void + { + // Remove the env var to test default behavior + putenv('AI_RESPONSE_QUOTA'); + + $limit = QuotaService::getLimit(QuotaType::AI_RESPONSE); + + $this->assertEquals(1000000, $limit); + } + + public function test_get_stats_returns_complete_statistics(): void + { + putenv('AI_PROMPT_QUOTA=100'); + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 75); + + $stats = QuotaService::getStats(QuotaType::AI_PROMPT); + + $this->assertIsArray($stats); + $this->assertEquals('ai_prompt', $stats['quota_type']); + $this->assertEquals('AI Prompt Tokens', $stats['label']); + $this->assertEquals(75, $stats['usage']); + $this->assertEquals(100, $stats['limit']); + $this->assertEquals(25, $stats['remaining']); + $this->assertEquals(75.0, $stats['percentage_used']); + $this->assertFalse($stats['is_exceeded']); + $this->assertArrayHasKey('resets_at', $stats); + } + + public function test_get_stats_shows_exceeded_when_over_limit(): void + { + putenv('AI_PROMPT_QUOTA=100'); + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 120); + + $stats = QuotaService::getStats(QuotaType::AI_PROMPT); + + $this->assertTrue($stats['is_exceeded']); + $this->assertEquals(0, $stats['remaining']); + $this->assertEquals(120.0, $stats['percentage_used']); + } + + public function test_get_all_stats_returns_all_quota_types(): void + { + $allStats = QuotaService::getAllStats(); + + $this->assertIsArray($allStats); + $this->assertArrayHasKey('ai_prompt', $allStats); + $this->assertArrayHasKey('ai_response', $allStats); + $this->assertCount(2, $allStats); + } + + public function test_reset_clears_quota_cache(): void + { + $cacheKey = QuotaType::AI_PROMPT->getCacheKey(); + Cache::put($cacheKey, 50); + + QuotaService::reset(QuotaType::AI_PROMPT); + + $this->assertFalse(Cache::has($cacheKey)); + $this->assertEquals(0, QuotaService::getUsage(QuotaType::AI_PROMPT)); + } +} diff --git a/tests/Unit/RiskModelTest.php b/tests/Unit/RiskModelTest.php new file mode 100644 index 00000000..e7b14bfa --- /dev/null +++ b/tests/Unit/RiskModelTest.php @@ -0,0 +1,98 @@ +assertEquals($fillable, $risk->getFillable()); + } + + public function test_risk_model_has_correct_casts() + { + $risk = new Risk; + + $casts = $risk->getCasts(); + + $this->assertEquals('integer', $casts['id']); + $this->assertEquals(MitigationType::class, $casts['action']); + $this->assertEquals(RiskStatus::class, $casts['status']); + $this->assertEquals('boolean', $casts['is_active']); + } + + public function test_risk_can_be_created_with_fillable_attributes() + { + $riskData = [ + 'name' => 'Test Risk', + 'likelihood' => 5, + 'impact' => 8, + ]; + + $risk = Risk::create($riskData); + + $this->assertDatabaseHas('risks', $riskData); + $this->assertEquals('Test Risk', $risk->name); + $this->assertEquals(5, $risk->likelihood); + $this->assertEquals(8, $risk->impact); + } + + public function test_searchable_as_returns_correct_index_name() + { + $risk = new Risk; + + $indexName = $risk->searchableAs(); + + $this->assertEquals('risks_index', $indexName); + } + + public function test_to_searchable_array_returns_array_representation() + { + $risk = Risk::factory()->create([ + 'name' => 'Test Risk', + 'likelihood' => 5, + 'impact' => 8, + ]); + + $searchableArray = $risk->toSearchableArray(); + + $this->assertIsArray($searchableArray); + $this->assertEquals('Test Risk', $searchableArray['name']); + $this->assertEquals(5, $searchableArray['likelihood']); + $this->assertEquals(8, $searchableArray['impact']); + } + + public function test_next_returns_incremented_max_id() + { + // Create some existing risks + Risk::factory()->create(['id' => 5]); + Risk::factory()->create(['id' => 10]); + Risk::factory()->create(['id' => 3]); + + $nextId = Risk::next(); + + $this->assertEquals(11, $nextId); // Max is 10, so next should be 11 + } + + public function test_next_returns_one_when_no_risks_exist() + { + // Ensure no risks exist + Risk::query()->delete(); + + $nextId = Risk::next(); + + $this->assertEquals(1, $nextId); + } +} diff --git a/tests/Unit/UserTest.php b/tests/Unit/UserTest.php new file mode 100644 index 00000000..eaf7b9e4 --- /dev/null +++ b/tests/Unit/UserTest.php @@ -0,0 +1,178 @@ +create([ + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]); + + $this->assertInstanceOf(User::class, $user); + $this->assertEquals('John Doe', $user->name); + $this->assertEquals('john@example.com', $user->email); + $this->assertDatabaseHas('users', [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]); + } + + public function test_user_has_fillable_attributes(): void + { + $fillable = ['name', 'text', 'email', 'password']; + $user = new User; + + $this->assertEquals($fillable, $user->getFillable()); + } + + public function test_user_has_hidden_attributes(): void + { + $hidden = ['password', 'remember_token']; + $user = new User; + + $this->assertEquals($hidden, $user->getHidden()); + } + + public function test_password_is_cast_as_hashed(): void + { + $user = new User; + $casts = $user->getCasts(); + + $this->assertEquals('hashed', $casts['password']); + } + + public function test_email_verified_at_is_cast_as_datetime(): void + { + $user = new User; + $casts = $user->getCasts(); + + $this->assertEquals('datetime', $casts['email_verified_at']); + } + + public function test_last_activity_is_cast_as_datetime(): void + { + $user = new User; + $casts = $user->getCasts(); + + $this->assertEquals('datetime', $casts['last_activity']); + } + + public function test_update_last_activity_updates_timestamp(): void + { + $user = User::factory()->create(); + $originalLastActivity = $user->last_activity; + + $user->updateLastActivity(); + + $user->refresh(); + $this->assertNotEquals($originalLastActivity, $user->last_activity); + $this->assertNotNull($user->last_activity); + } + + public function test_can_access_panel_returns_true(): void + { + $user = User::factory()->create(); + $panel = $this->createMock(\Filament\Panel::class); + + $this->assertTrue($user->canAccessPanel($panel)); + } + + public function test_user_has_audits_relationship(): void + { + $user = User::factory()->create(); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\BelongsToMany::class, $user->audits()); + } + + public function test_user_has_todos_relationship(): void + { + $user = User::factory()->create(); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class, $user->todos()); + $this->assertEquals('requestee_id', $user->todos()->getForeignKeyName()); + $this->assertInstanceOf(DataRequestResponse::class, $user->todos()->getRelated()); + } + + public function test_user_has_open_todos_relationship(): void + { + $user = User::factory()->create(); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class, $user->openTodos()); + $this->assertEquals('requestee_id', $user->openTodos()->getForeignKeyName()); + } + + public function test_open_todos_filters_by_status(): void + { + $user = User::factory()->create(); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class, $user->openTodos()); + + // Check that the query contains the correct where conditions + $query = $user->openTodos(); + $bindings = $query->getQuery()->getBindings(); + $this->assertTrue(in_array(ResponseStatus::PENDING->value, $bindings) || in_array(ResponseStatus::REJECTED->value, $bindings)); + } + + public function test_user_has_managed_programs_relationship(): void + { + $user = User::factory()->create(); + + $this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class, $user->managedPrograms()); + $this->assertEquals('program_manager_id', $user->managedPrograms()->getForeignKeyName()); + $this->assertInstanceOf(Program::class, $user->managedPrograms()->getRelated()); + } + + public function test_managed_programs_relationship_works(): void + { + $user = User::factory()->create(); + $program = Program::factory()->create(['program_manager_id' => $user->id]); + + $this->assertTrue($user->managedPrograms->contains($program)); + $this->assertEquals($user->id, $program->program_manager_id); + } + + public function test_user_uses_soft_deletes(): void + { + $user = User::factory()->create(['name' => 'Soft Delete Test']); + $userId = $user->id; + + $user->delete(); + + $this->assertSoftDeleted('users', ['id' => $userId]); + $this->assertNull(User::find($userId)); + $this->assertNotNull(User::withTrashed()->find($userId)); + } + + public function test_user_has_activity_log_options(): void + { + $user = new User; + $logOptions = $user->getActivitylogOptions(); + + $this->assertInstanceOf(\Spatie\Activitylog\LogOptions::class, $logOptions); + } + + public function test_user_logs_specific_attributes(): void + { + $user = new User; + $logOptions = $user->getActivitylogOptions(); + + $reflection = new \ReflectionClass($logOptions); + $attributesProperty = $reflection->getProperty('logAttributes'); + $attributesProperty->setAccessible(true); + $logAttributes = $attributesProperty->getValue($logOptions); + + $this->assertEquals(['name', 'email'], $logAttributes); + } +} diff --git a/tests/Unit/VendorModelTest.php b/tests/Unit/VendorModelTest.php new file mode 100644 index 00000000..c32e311d --- /dev/null +++ b/tests/Unit/VendorModelTest.php @@ -0,0 +1,129 @@ +assertEquals($fillable, $vendor->getFillable()); + } + + public function test_vendor_model_has_correct_casts() + { + $vendor = new Vendor; + + $casts = $vendor->getCasts(); + + $this->assertEquals(VendorStatus::class, $casts['status']); + $this->assertEquals(VendorRiskRating::class, $casts['risk_rating']); + $this->assertEquals('array', $casts['logo']); + $this->assertEquals('integer', $casts['risk_score']); + $this->assertEquals('datetime', $casts['risk_score_calculated_at']); + } + + public function test_vendor_can_be_created_with_fillable_attributes() + { + $vendorData = [ + 'name' => 'Test Vendor', + 'description' => 'A test vendor description', + 'url' => 'https://test-vendor.com', + 'contact_name' => 'John Doe', + 'contact_email' => 'john@test-vendor.com', + 'contact_phone' => '555-1234', + 'address' => '123 Test St', + 'status' => VendorStatus::ACTIVE, + 'risk_rating' => VendorRiskRating::MEDIUM, + 'risk_score' => 65, + 'notes' => 'Test notes', + ]; + + $vendor = Vendor::create($vendorData); + + $this->assertDatabaseHas('vendors', [ + 'name' => 'Test Vendor', + 'contact_email' => 'john@test-vendor.com', + 'risk_score' => 65, + ]); + + $this->assertEquals('Test Vendor', $vendor->name); + $this->assertEquals(VendorStatus::ACTIVE, $vendor->status); + $this->assertEquals(VendorRiskRating::MEDIUM, $vendor->risk_rating); + } + + public function test_vendor_belongs_to_vendor_manager() + { + $user = User::factory()->create(); + $vendor = Vendor::factory()->create(['vendor_manager_id' => $user->id]); + + $this->assertInstanceOf(User::class, $vendor->vendorManager); + $this->assertEquals($user->id, $vendor->vendorManager->id); + } + + public function test_vendor_status_enum_casting() + { + $vendor = Vendor::factory()->create(['status' => VendorStatus::ACTIVE]); + + $this->assertInstanceOf(VendorStatus::class, $vendor->status); + $this->assertEquals(VendorStatus::ACTIVE, $vendor->status); + } + + public function test_vendor_risk_rating_enum_casting() + { + $vendor = Vendor::factory()->create(['risk_rating' => VendorRiskRating::HIGH]); + + $this->assertInstanceOf(VendorRiskRating::class, $vendor->risk_rating); + $this->assertEquals(VendorRiskRating::HIGH, $vendor->risk_rating); + } + + public function test_vendor_uses_soft_deletes() + { + $vendor = Vendor::factory()->create(['name' => 'Test Vendor']); + + // Soft delete the vendor + $vendor->delete(); + + // Should not exist in normal queries + $this->assertNull(Vendor::find($vendor->id)); + + // But should exist in withTrashed queries + $this->assertNotNull(Vendor::withTrashed()->find($vendor->id)); + $this->assertTrue(Vendor::withTrashed()->find($vendor->id)->trashed()); + } + + public function test_vendor_can_have_null_vendor_manager() + { + $vendor = Vendor::factory()->create(['vendor_manager_id' => null]); + + $this->assertNull($vendor->vendor_manager_id); + $this->assertNull($vendor->vendorManager); + } +} diff --git a/tests/Unit/VendorRiskScoringServiceTest.php b/tests/Unit/VendorRiskScoringServiceTest.php new file mode 100644 index 00000000..28325475 --- /dev/null +++ b/tests/Unit/VendorRiskScoringServiceTest.php @@ -0,0 +1,111 @@ +service = new VendorRiskScoringService; + } + + public function test_calculate_survey_score_returns_zero_when_no_template() + { + $survey = Survey::factory()->create(['template_id' => null]); + + $score = $this->service->calculateSurveyScore($survey); + + $this->assertEquals(0, $score); + } + + public function test_calculate_survey_score_returns_zero_when_no_weighted_questions() + { + $template = SurveyTemplate::factory()->create(); + $survey = Survey::factory()->create(['template_id' => $template->id]); + + // Create a question with zero weight + SurveyQuestion::factory()->create([ + 'template_id' => $template->id, + 'risk_weight' => 0, + ]); + + $score = $this->service->calculateSurveyScore($survey); + + $this->assertEquals(0, $score); + } + + public function test_get_answer_score_boolean_positive_impact() + { + $question = SurveyQuestion::factory()->create([ + 'question_type' => QuestionType::BOOLEAN, + 'risk_impact' => RiskImpact::POSITIVE, + ]); + + // Yes answer for positive impact = good (0 risk) + $yesAnswer = SurveyAnswer::factory()->create(['answer_value' => true]); + $score = $this->service->getAnswerScore($question, $yesAnswer); + $this->assertEquals(0, $score); + + // No answer for positive impact = bad (100 risk) + $noAnswer = SurveyAnswer::factory()->create(['answer_value' => false]); + $score = $this->service->getAnswerScore($question, $noAnswer); + $this->assertEquals(100, $score); + } + + public function test_get_answer_score_boolean_negative_impact() + { + $question = SurveyQuestion::factory()->create([ + 'question_type' => QuestionType::BOOLEAN, + 'risk_impact' => RiskImpact::NEGATIVE, + ]); + + // Yes answer for negative impact = bad (100 risk) + $yesAnswer = SurveyAnswer::factory()->create(['answer_value' => true]); + $score = $this->service->getAnswerScore($question, $yesAnswer); + $this->assertEquals(100, $score); + + // No answer for negative impact = good (0 risk) + $noAnswer = SurveyAnswer::factory()->create(['answer_value' => false]); + $score = $this->service->getAnswerScore($question, $noAnswer); + $this->assertEquals(0, $score); + } + + public function test_get_answer_score_neutral_impact_returns_zero() + { + $question = SurveyQuestion::factory()->create([ + 'question_type' => QuestionType::BOOLEAN, + 'risk_impact' => RiskImpact::NEUTRAL, + ]); + + $answer = SurveyAnswer::factory()->create(['answer_value' => true]); + $score = $this->service->getAnswerScore($question, $answer); + + $this->assertEquals(0, $score); + } + + public function test_recommend_risk_rating_thresholds() + { + // Test each threshold boundary + $this->assertEquals(VendorRiskRating::VERY_LOW, $this->service->recommendRiskRating(15)); + $this->assertEquals(VendorRiskRating::LOW, $this->service->recommendRiskRating(35)); + $this->assertEquals(VendorRiskRating::MEDIUM, $this->service->recommendRiskRating(55)); + $this->assertEquals(VendorRiskRating::HIGH, $this->service->recommendRiskRating(75)); + $this->assertEquals(VendorRiskRating::CRITICAL, $this->service->recommendRiskRating(95)); + } +} diff --git a/verification-screenshots/01-initial-risk-list.png b/verification-screenshots/01-initial-risk-list.png new file mode 100644 index 00000000..8d1878a3 Binary files /dev/null and b/verification-screenshots/01-initial-risk-list.png differ diff --git a/verification-screenshots/02-department-sorting-no-duplicates.png b/verification-screenshots/02-department-sorting-no-duplicates.png new file mode 100644 index 00000000..b58f7efd Binary files /dev/null and b/verification-screenshots/02-department-sorting-no-duplicates.png differ diff --git a/verification-screenshots/03-scope-sorting-no-duplicates.png b/verification-screenshots/03-scope-sorting-no-duplicates.png new file mode 100644 index 00000000..20a04984 Binary files /dev/null and b/verification-screenshots/03-scope-sorting-no-duplicates.png differ