docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,252 @@
<?php
declare(strict_types=1);
namespace Tests\Application\Admin\Service;
use App\Application\Admin\Service\AdminLayoutProcessor;
use App\Application\Admin\Service\AdminNavigationService;
use App\Application\Admin\ValueObjects\AdminLayoutData;
use App\Application\Admin\ValueObjects\BreadcrumbCollection;
use App\Application\Admin\ValueObjects\NavigationMenu;
use App\Framework\Http\HttpRequest;
// Test stub for AdminNavigationService since it's final
class TestAdminNavigationService
{
private array $menuData = [];
private array $breadcrumbsData = [];
private bool $shouldFailMenu = false;
private bool $shouldFailBreadcrumbs = false;
public function setMenuData(array $menuData): void
{
$this->menuData = $menuData;
}
public function setBreadcrumbsData(array $breadcrumbsData): void
{
$this->breadcrumbsData = $breadcrumbsData;
}
public function setShouldFailMenu(bool $shouldFail): void
{
$this->shouldFailMenu = $shouldFail;
}
public function setShouldFailBreadcrumbs(bool $shouldFail): void
{
$this->shouldFailBreadcrumbs = $shouldFail;
}
public function getNavigationMenu(): array
{
if ($this->shouldFailMenu) {
throw new \Exception('Navigation service failed');
}
return $this->menuData;
}
public function getBreadcrumbs(string $currentPath): array
{
if ($this->shouldFailBreadcrumbs) {
throw new \Exception('Breadcrumbs service failed');
}
return $this->breadcrumbsData;
}
}
// Test stub for HttpRequest since it's readonly
class TestHttpRequest
{
public string $path = '/admin';
}
describe('AdminLayoutProcessor', function () {
beforeEach(function () {
$this->navigationService = new TestAdminNavigationService();
$this->request = new TestHttpRequest();
// Use reflection to create AdminLayoutProcessor with our test doubles
$reflection = new \ReflectionClass(AdminLayoutProcessor::class);
$constructor = $reflection->getConstructor();
// Create instance using reflection to bypass readonly constraints
$this->processor = $reflection->newInstanceWithoutConstructor();
// Set the private properties
$navProperty = $reflection->getProperty('navigationService');
$navProperty->setAccessible(true);
$navProperty->setValue($this->processor, $this->navigationService);
$requestProperty = $reflection->getProperty('request');
$requestProperty->setAccessible(true);
$requestProperty->setValue($this->processor, $this->request);
});
it('processes admin layout data with navigation and breadcrumbs', function () {
$this->request->path = '/admin/dashboard';
$menuData = [
'System' => [
'icon' => 'server',
'items' => [
'Dashboard' => '/admin',
'Health Check' => '/admin/system/health',
],
],
];
$breadcrumbsData = [
['name' => 'Admin', 'url' => '/admin'],
['name' => 'Dashboard', 'url' => '/admin/dashboard'],
];
$this->navigationService->setMenuData($menuData);
$this->navigationService->setBreadcrumbsData($breadcrumbsData);
$inputData = new AdminLayoutData(
title: 'Test Page',
navigationMenu: new NavigationMenu([]),
breadcrumbs: new BreadcrumbCollection([]),
currentPath: '/admin'
);
$result = $this->processor->processAdminLayout($inputData);
expect($result)->toBeInstanceOf(AdminLayoutData::class);
expect($result->navigationMenu->sections)->toHaveCount(1);
expect($result->navigationMenu->sections[0]->name)->toBe('System');
expect($result->navigationMenu->sections[0]->items)->toHaveCount(2);
expect($result->breadcrumbs->breadcrumbs)->toHaveCount(2);
});
it('handles navigation service failure gracefully', function () {
$this->request->path = '/admin/test';
$this->navigationService
->shouldReceive('getNavigationMenu')
->once()
->andThrow(new \Exception('Navigation service failed'));
$this->navigationService
->shouldReceive('getBreadcrumbs')
->with('/admin/test')
->once()
->andReturn([['name' => 'Admin', 'url' => '/admin']]);
$inputData = new AdminLayoutData(
title: 'Test Page',
navigationMenu: new NavigationMenu([]),
breadcrumbs: new BreadcrumbCollection([]),
currentPath: '/admin'
);
$result = $this->processor->processAdminLayout($inputData);
// Should have fallback menu
expect($result->navigationMenu->sections)->toHaveCount(1);
expect($result->navigationMenu->sections[0]->name)->toBe('System');
expect($result->navigationMenu->sections[0]->items)->toHaveCount(2);
expect($result->navigationMenu->sections[0]->items[0]->name)->toBe('Dashboard');
});
it('handles breadcrumbs service failure gracefully', function () {
$this->request->path = '/admin/test';
$this->navigationService
->shouldReceive('getNavigationMenu')
->once()
->andReturn([]);
$this->navigationService
->shouldReceive('getBreadcrumbs')
->with('/admin/test')
->once()
->andThrow(new \Exception('Breadcrumbs service failed'));
$inputData = new AdminLayoutData(
title: 'Test Page',
navigationMenu: new NavigationMenu([]),
breadcrumbs: new BreadcrumbCollection([]),
currentPath: '/admin'
);
$result = $this->processor->processAdminLayout($inputData);
// Should have fallback breadcrumbs
expect($result->breadcrumbs->breadcrumbs)->toHaveCount(1);
expect($result->breadcrumbs->breadcrumbs[0]->name)->toBe('Admin');
expect($result->breadcrumbs->breadcrumbs[0]->url)->toBe('/admin');
});
it('sets active state for navigation items based on current path', function () {
$this->request->path = '/admin/system/health';
$menuData = [
'System' => [
'items' => [
'Dashboard' => '/admin',
'Health Check' => '/admin/system/health',
],
],
];
$this->navigationService
->shouldReceive('getNavigationMenu')
->once()
->andReturn($menuData);
$this->navigationService
->shouldReceive('getBreadcrumbs')
->once()
->andReturn([]);
$inputData = new AdminLayoutData(
title: 'Health Check',
navigationMenu: new NavigationMenu([]),
breadcrumbs: new BreadcrumbCollection([]),
currentPath: '/admin'
);
$result = $this->processor->processAdminLayout($inputData);
$items = $result->navigationMenu->sections[0]->items;
expect($items[0]->isActive)->toBeFalse(); // Dashboard
expect($items[1]->isActive)->toBeTrue(); // Health Check (current path)
});
it('preserves original layout data properties', function () {
$this->request->path = '/admin';
$this->navigationService
->shouldReceive('getNavigationMenu')
->once()
->andReturn([]);
$this->navigationService
->shouldReceive('getBreadcrumbs')
->once()
->andReturn([]);
$inputData = new AdminLayoutData(
title: 'Original Title',
navigationMenu: new NavigationMenu([]),
breadcrumbs: new BreadcrumbCollection([]),
currentPath: '/admin',
metaDescription: 'Original description',
pageClass: 'original-class'
);
$result = $this->processor->processAdminLayout($inputData);
expect($result->title)->toBe('Original Title');
expect($result->metaDescription)->toBe('Original description');
expect($result->pageClass)->toBe('original-class');
});
});

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Tests\Application\Admin\ValueObjects;
use App\Application\Admin\ValueObjects\AdminLayoutData;
use App\Application\Admin\ValueObjects\Breadcrumb;
use App\Application\Admin\ValueObjects\BreadcrumbCollection;
use App\Application\Admin\ValueObjects\NavigationItem;
use App\Application\Admin\ValueObjects\NavigationMenu;
use App\Application\Admin\ValueObjects\NavigationSection;
describe('AdminLayoutData Value Object', function () {
it('can be created with all required parameters', function () {
$navigationMenu = new NavigationMenu([]);
$breadcrumbs = new BreadcrumbCollection([]);
$layoutData = new AdminLayoutData(
title: 'Test Page',
navigationMenu: $navigationMenu,
breadcrumbs: $breadcrumbs,
currentPath: '/admin/test'
);
expect($layoutData->title)->toBe('Test Page');
expect($layoutData->currentPath)->toBe('/admin/test');
expect($layoutData->navigationMenu)->toBe($navigationMenu);
expect($layoutData->breadcrumbs)->toBe($breadcrumbs);
});
it('can be created from array', function () {
$data = [
'title' => 'Dashboard',
'current_path' => '/admin/dashboard',
'navigation_menu' => [
[
'section' => 'System',
'items' => [
['name' => 'Dashboard', 'url' => '/admin'],
['name' => 'Users', 'url' => '/admin/users'],
],
],
],
'breadcrumbs_data' => [
['name' => 'Admin', 'url' => '/admin'],
['name' => 'Dashboard', 'url' => '/admin/dashboard'],
],
];
$layoutData = AdminLayoutData::fromArray($data);
expect($layoutData->title)->toBe('Dashboard');
expect($layoutData->currentPath)->toBe('/admin/dashboard');
expect($layoutData->navigationMenu->sections)->toHaveCount(1);
expect($layoutData->breadcrumbs->breadcrumbs)->toHaveCount(2);
});
it('converts to array correctly', function () {
$navigationMenu = new NavigationMenu([
new NavigationSection('System', [
new NavigationItem('Dashboard', '/admin'),
]),
]);
$breadcrumbs = new BreadcrumbCollection([
new Breadcrumb('Admin', '/admin'),
]);
$layoutData = new AdminLayoutData(
title: 'Test Page',
navigationMenu: $navigationMenu,
breadcrumbs: $breadcrumbs,
currentPath: '/admin/test'
);
$array = $layoutData->toArray();
expect($array['title'])->toBe('Test Page');
expect($array['page_title'])->toBe('Test Page');
expect($array['current_path'])->toBe('/admin/test');
expect($array['navigation_menu'])->toBeArray();
expect($array['breadcrumbs_data'])->toBeArray();
});
it('supports immutable transformations', function () {
$originalData = new AdminLayoutData(
title: 'Original',
navigationMenu: new NavigationMenu([]),
breadcrumbs: new BreadcrumbCollection([]),
currentPath: '/admin'
);
$newData = $originalData->withTitle('New Title');
expect($originalData->title)->toBe('Original');
expect($newData->title)->toBe('New Title');
expect($newData)->not->toBe($originalData);
});
it('handles optional parameters correctly', function () {
$layoutData = new AdminLayoutData(
title: 'Test',
navigationMenu: new NavigationMenu([]),
breadcrumbs: new BreadcrumbCollection([]),
currentPath: '/admin',
metaDescription: 'Test description',
pageClass: 'admin-page'
);
expect($layoutData->metaDescription)->toBe('Test description');
expect($layoutData->pageClass)->toBe('admin-page');
$array = $layoutData->toArray();
expect($array['meta_description'])->toBe('Test description');
expect($array['page_class'])->toBe('admin-page');
});
});

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Tests\Application\Admin\ValueObjects;
use App\Application\Admin\ValueObjects\NavigationItem;
use App\Application\Admin\ValueObjects\NavigationMenu;
use App\Application\Admin\ValueObjects\NavigationSection;
describe('NavigationMenu Value Object', function () {
it('can be created with sections', function () {
$section = new NavigationSection('System', [
new NavigationItem('Dashboard', '/admin'),
]);
$menu = new NavigationMenu([$section]);
expect($menu->sections)->toHaveCount(1);
expect($menu->sections[0])->toBe($section);
});
it('can be created from array', function () {
$data = [
[
'section' => 'System',
'items' => [
['name' => 'Dashboard', 'url' => '/admin'],
['name' => 'Users', 'url' => '/admin/users'],
],
'icon' => 'server',
],
[
'section' => 'Content',
'items' => [
['name' => 'Pages', 'url' => '/admin/pages'],
],
],
];
$menu = NavigationMenu::fromArray($data);
expect($menu->sections)->toHaveCount(2);
expect($menu->sections[0]->name)->toBe('System');
expect($menu->sections[0]->icon)->toBe('server');
expect($menu->sections[0]->items)->toHaveCount(2);
expect($menu->sections[1]->name)->toBe('Content');
expect($menu->sections[1]->items)->toHaveCount(1);
});
it('converts to array correctly', function () {
$menu = new NavigationMenu([
new NavigationSection('System', [
new NavigationItem('Dashboard', '/admin', 'dashboard-icon'),
], 'server'),
]);
$array = $menu->toArray();
expect($array)->toHaveCount(1);
expect($array[0]['section'])->toBe('System');
expect($array[0]['icon'])->toBe('server');
expect($array[0]['items'])->toHaveCount(1);
expect($array[0]['items'][0]['name'])->toBe('Dashboard');
expect($array[0]['items'][0]['url'])->toBe('/admin');
expect($array[0]['items'][0]['icon'])->toBe('dashboard-icon');
});
it('can add sections immutably', function () {
$originalMenu = new NavigationMenu([]);
$newSection = new NavigationSection('New Section', []);
$newMenu = $originalMenu->addSection($newSection);
expect($originalMenu->sections)->toHaveCount(0);
expect($newMenu->sections)->toHaveCount(1);
expect($newMenu->sections[0])->toBe($newSection);
});
it('can find sections by name', function () {
$systemSection = new NavigationSection('System', []);
$contentSection = new NavigationSection('Content', []);
$menu = new NavigationMenu([$systemSection, $contentSection]);
$found = $menu->findSectionByName('System');
$notFound = $menu->findSectionByName('NonExistent');
expect($found)->toBe($systemSection);
expect($notFound)->toBeNull();
});
it('handles empty menu correctly', function () {
$menu = new NavigationMenu([]);
expect($menu->sections)->toHaveCount(0);
expect($menu->findSectionByName('Any'))->toBeNull();
$array = $menu->toArray();
expect($array)->toHaveCount(0);
});
});

View File

@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
use App\Application\Campaign\ValueObjects\Campaign;
use App\Application\Campaign\ValueObjects\CampaignTrack;
describe('SpotifyCampaignService - Spotify ID Extraction', function () {
it('extracts Spotify ID from URI format (spotify:track:xxx)', function () {
$track = CampaignTrack::fromArray([
'id' => 'track_1',
'position' => 1,
'title' => 'Test Track',
'duration' => 180,
'preview_url' => null,
'spotify_uri' => 'spotify:track:track_id_123',
'isrc' => null,
]);
// Use reflection to test private method
$service = new \ReflectionClass(App\Application\Campaign\Services\SpotifyCampaignService::class);
$method = $service->getMethod('extractSpotifyId');
$method->setAccessible(true);
$extracted = $method->invoke(
$service->newInstanceWithoutConstructor(),
'spotify:track:track_id_123'
);
expect($extracted)->toBe('track_id_123');
});
it('extracts Spotify ID from URL format', function () {
$service = new \ReflectionClass(App\Application\Campaign\Services\SpotifyCampaignService::class);
$method = $service->getMethod('extractSpotifyId');
$method->setAccessible(true);
$extracted = $method->invoke(
$service->newInstanceWithoutConstructor(),
'https://open.spotify.com/track/url_track_id'
);
expect($extracted)->toBe('url_track_id');
});
it('returns ID if already just an ID', function () {
$service = new \ReflectionClass(App\Application\Campaign\Services\SpotifyCampaignService::class);
$method = $service->getMethod('extractSpotifyId');
$method->setAccessible(true);
$extracted = $method->invoke(
$service->newInstanceWithoutConstructor(),
'just_an_id_123'
);
expect($extracted)->toBe('just_an_id_123');
});
it('returns null for null input', function () {
$service = new \ReflectionClass(App\Application\Campaign\Services\SpotifyCampaignService::class);
$method = $service->getMethod('extractSpotifyId');
$method->setAccessible(true);
$extracted = $method->invoke(
$service->newInstanceWithoutConstructor(),
null
);
expect($extracted)->toBeNull();
});
});
describe('Campaign Value Objects', function () {
it('creates campaign with tracks', function () {
$campaign = Campaign::fromArray([
'id' => '123',
'slug' => 'test-album',
'artist_name' => 'Test Artist',
'album_title' => 'Test Album',
'description' => 'Test description',
'artwork_url' => 'https://example.com/art.jpg',
'release_date' => '2024-12-01',
'total_saves' => 100,
'track_count' => 2,
'spotify_enabled' => true,
'apple_music_enabled' => true,
'spotify_uri' => 'spotify:album:abc123',
'apple_music_id' => 'apple123',
'tracks' => [
[
'id' => 'track_1',
'position' => 1,
'title' => 'Track 1',
'duration' => 180,
'preview_url' => null,
'spotify_uri' => 'spotify:track:track1',
'isrc' => 'US1234567890',
],
[
'id' => 'track_2',
'position' => 2,
'title' => 'Track 2',
'duration' => 200,
'preview_url' => null,
'spotify_uri' => 'spotify:track:track2',
'isrc' => 'US0987654321',
]
],
'status' => 'active',
]);
expect($campaign->id)->toBe('123');
expect($campaign->slug)->toBe('test-album');
expect($campaign->spotify_enabled)->toBeTrue();
expect($campaign->tracks)->toHaveCount(2);
expect($campaign->tracks[0]->title)->toBe('Track 1');
});
it('validates campaign status methods', function () {
$activeCampaign = Campaign::fromArray([
'id' => '1',
'slug' => 'active',
'artist_name' => 'Artist',
'album_title' => 'Album',
'description' => null,
'artwork_url' => null,
'release_date' => '2025-12-01',
'total_saves' => 0,
'track_count' => 0,
'spotify_enabled' => false,
'apple_music_enabled' => false,
'spotify_uri' => null,
'apple_music_id' => null,
'status' => 'active',
]);
expect($activeCampaign->isActive())->toBeTrue();
expect($activeCampaign->hasReleased())->toBeFalse();
});
it('creates campaign track value object', function () {
$track = CampaignTrack::fromArray([
'id' => 'track_123',
'position' => 1,
'title' => 'Amazing Song',
'duration' => 240,
'preview_url' => 'https://example.com/preview.mp3',
'spotify_id' => 'abc123',
'apple_music_id' => 'apple456',
]);
expect($track->id)->toBe('track_123');
expect($track->position)->toBe(1);
expect($track->title)->toBe('Amazing Song');
expect($track->duration)->toBe(240);
expect($track->spotify_id)->toBe('abc123');
expect($track->apple_music_id)->toBe('apple456');
});
});

View File

@@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace Tests\Application\Security\Services;
use App\Application\Security\Events\File\SuspiciousFileUploadEvent;
use App\Application\Security\Services\FileUploadSecurityService;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Http\UploadedFile;
use App\Framework\Http\UploadError;
use Mockery;
describe('FileUploadSecurityService', function () {
beforeEach(function () {
$this->eventDispatcher = Mockery::mock(EventDispatcher::class);
$this->service = new FileUploadSecurityService($this->eventDispatcher);
});
afterEach(function () {
Mockery::close();
});
it('validates a safe uploaded file', function () {
$file = UploadedFile::createForTesting(
name: 'test.jpg',
type: 'image/jpeg',
size: 1024 * 1024, // 1MB
tmpName: '/tmp/test_upload',
error: UploadError::OK
);
// Mock the file system
file_put_contents('/tmp/test_upload', 'fake image content');
$result = $this->service->validateUpload($file);
expect($result)->toBeTrue();
unlink('/tmp/test_upload');
});
it('rejects files with upload errors', function () {
$file = UploadedFile::createForTesting(
name: 'test.jpg',
type: 'image/jpeg',
size: 1024,
tmpName: '/tmp/test_upload',
error: UploadError::PARTIAL
);
$this->eventDispatcher
->shouldReceive('dispatch')
->once()
->with(Mockery::type(SuspiciousFileUploadEvent::class));
$result = $this->service->validateUpload($file);
expect($result)->toBeFalse();
});
it('rejects files that are too large', function () {
$file = UploadedFile::createForTesting(
name: 'large.jpg',
type: 'image/jpeg',
size: 20 * 1024 * 1024, // 20MB (exceeds 10MB limit)
tmpName: '/tmp/large_upload',
error: UploadError::OK
);
$this->eventDispatcher
->shouldReceive('dispatch')
->once()
->with(Mockery::type(SuspiciousFileUploadEvent::class));
$result = $this->service->validateUpload($file);
expect($result)->toBeFalse();
});
it('rejects files with dangerous extensions', function () {
$file = UploadedFile::createForTesting(
name: 'malicious.php',
type: 'application/x-php',
size: 1024,
tmpName: '/tmp/malicious_upload',
error: UploadError::OK
);
$this->eventDispatcher
->shouldReceive('dispatch')
->once()
->with(Mockery::type(SuspiciousFileUploadEvent::class));
$result = $this->service->validateUpload($file);
expect($result)->toBeFalse();
});
it('rejects files with forbidden MIME types', function () {
$file = UploadedFile::createForTesting(
name: 'test.txt',
type: 'application/x-executable',
size: 1024,
tmpName: '/tmp/executable_upload',
error: UploadError::OK
);
// Create mock file content
file_put_contents('/tmp/executable_upload', 'fake executable');
$this->eventDispatcher
->shouldReceive('dispatch')
->once()
->with(Mockery::type(SuspiciousFileUploadEvent::class));
$result = $this->service->validateUpload($file);
expect($result)->toBeFalse();
unlink('/tmp/executable_upload');
});
it('rejects files with malware signatures', function () {
$file = UploadedFile::createForTesting(
name: 'suspicious.txt',
type: 'text/plain',
size: 1024,
tmpName: '/tmp/suspicious_upload',
error: UploadError::OK
);
// Create file with malware signature
file_put_contents('/tmp/suspicious_upload', 'normal content <?php eval($_POST["cmd"]); ?>');
$this->eventDispatcher
->shouldReceive('dispatch')
->once()
->with(Mockery::type(SuspiciousFileUploadEvent::class));
$result = $this->service->validateUpload($file);
expect($result)->toBeFalse();
unlink('/tmp/suspicious_upload');
});
it('rejects files with double extensions', function () {
$file = UploadedFile::createForTesting(
name: 'image.jpg.php',
type: 'image/jpeg',
size: 1024,
tmpName: '/tmp/double_ext_upload',
error: UploadError::OK
);
// Create mock file
file_put_contents('/tmp/double_ext_upload', 'fake image content');
$this->eventDispatcher
->shouldReceive('dispatch')
->once()
->with(Mockery::type(SuspiciousFileUploadEvent::class));
$result = $this->service->validateUpload($file);
expect($result)->toBeFalse();
unlink('/tmp/double_ext_upload');
});
it('accepts various safe file types', function () {
$safeFiles = [
['test.jpg', 'image/jpeg'],
['test.png', 'image/png'],
['test.gif', 'image/gif'],
['test.webp', 'image/webp'],
['document.pdf', 'application/pdf'],
['data.csv', 'text/csv'],
];
foreach ($safeFiles as [$filename, $mimeType]) {
$file = UploadedFile::createForTesting(
name: $filename,
type: $mimeType,
size: 1024,
tmpName: '/tmp/safe_upload_' . $filename,
error: UploadError::OK
);
file_put_contents('/tmp/safe_upload_' . $filename, 'safe content');
$result = $this->service->validateUpload($file);
expect($result)->toBeTrue("File $filename should be valid");
unlink('/tmp/safe_upload_' . $filename);
}
});
it('handles multiple malware signatures', function () {
$malwareSignatures = [
'eval(',
'base64_decode(',
'system(',
'exec(',
'shell_exec(',
'<?php',
'<%',
'<script',
'javascript:',
];
foreach ($malwareSignatures as $signature) {
$file = UploadedFile::createForTesting(
name: 'test.txt',
type: 'text/plain',
size: 1024,
tmpName: '/tmp/malware_test',
error: UploadError::OK
);
file_put_contents('/tmp/malware_test', 'Normal content ' . $signature . ' more content');
$this->eventDispatcher
->shouldReceive('dispatch')
->once()
->with(Mockery::type(SuspiciousFileUploadEvent::class));
$result = $this->service->validateUpload($file);
expect($result)->toBeFalse("Signature '$signature' should be detected");
unlink('/tmp/malware_test');
}
});
});