mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 03:01:34 +00:00
Refactor additional postprocessing
This commit is contained in:
@@ -3,15 +3,23 @@
|
||||
namespace Tests\Unit\AdditionalProcessing;
|
||||
|
||||
use App\Models\Release;
|
||||
use App\Services\AdditionalProcessing\AdditionalWorkPlanner;
|
||||
use App\Services\AdditionalProcessing\ArchiveExtractionService;
|
||||
use App\Services\AdditionalProcessing\ConsoleOutputService;
|
||||
use App\Services\AdditionalProcessing\DTO\DownloadMetrics;
|
||||
use App\Services\AdditionalProcessing\Enums\DownloadKind;
|
||||
use App\Services\AdditionalProcessing\Enums\ProcessingOutcome;
|
||||
use App\Services\AdditionalProcessing\Enums\ProcessingStage;
|
||||
use App\Services\AdditionalProcessing\MediaExtractionService;
|
||||
use App\Services\AdditionalProcessing\NzbContentParser;
|
||||
use App\Services\AdditionalProcessing\ReleaseFileManager;
|
||||
use App\Services\AdditionalProcessing\ReleaseFilesArchiveFallback;
|
||||
use App\Services\AdditionalProcessing\ReleaseProcessor;
|
||||
use App\Services\AdditionalProcessing\ReleaseSearchSyncCoordinator;
|
||||
use App\Services\AdditionalProcessing\State\PersistenceMetricsCollector;
|
||||
use App\Services\AdditionalProcessing\State\ReleaseProcessingContext;
|
||||
use App\Services\AdditionalProcessing\UsenetDownloadService;
|
||||
use App\Services\Releases\ReleaseBrowseService;
|
||||
use App\Services\TempWorkspaceService;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
@@ -44,9 +52,16 @@ class ReleaseProcessorTest extends TestCase
|
||||
->shouldReceive('warning')->once()->with('broken nzb')->andReturnNull()->getMock()
|
||||
);
|
||||
|
||||
$processor->process($this->makeContext(), '/tmp/main/');
|
||||
$result = $processor->process($this->makeContext(), '/tmp/main/');
|
||||
|
||||
$this->assertTrue(true);
|
||||
$this->assertSame(ProcessingOutcome::DeletedBrokenNzb, $result->outcome);
|
||||
$this->assertTrue($result->outcome->isDeleted());
|
||||
$this->assertFalse($result->isSuccessful());
|
||||
$this->assertSame('broken nzb', $result->reason);
|
||||
$this->assertGreaterThan(0.0, $result->elapsedSeconds);
|
||||
$this->assertArrayHasKey(ProcessingStage::WorkspacePreparation->value, $result->stageDurations);
|
||||
$this->assertArrayHasKey(ProcessingStage::NzbParsing->value, $result->stageDurations);
|
||||
$this->assertArrayHasKey(ProcessingStage::WorkspaceCleanup->value, $result->stageDurations);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -58,19 +73,22 @@ class ReleaseProcessorTest extends TestCase
|
||||
'error' => null,
|
||||
'contents' => [['title' => 'file.nzb', 'segments' => []]],
|
||||
]);
|
||||
$nzbParser->shouldReceive('extractMessageIDs')->once()->andReturn([
|
||||
'hasCompressedFile' => false,
|
||||
'sampleMessageIDs' => [],
|
||||
'jpgMessageIDs' => [],
|
||||
'mediaInfoMessageID' => '',
|
||||
'audioInfoMessageID' => '',
|
||||
'audioInfoExtension' => '',
|
||||
'bookFileCount' => 0,
|
||||
]);
|
||||
|
||||
$releaseManager = Mockery::mock(ReleaseFileManager::class);
|
||||
$releaseManager->shouldReceive('processReleaseNameFromNzbContents')->once()->andReturnFalse();
|
||||
$releaseManager->shouldReceive('finalizeRelease')->once()->andReturnNull();
|
||||
$persistenceMetrics = new PersistenceMetricsCollector;
|
||||
$synchronizedReleaseIds = [];
|
||||
$searchSyncCoordinator = new ReleaseSearchSyncCoordinator(
|
||||
$persistenceMetrics,
|
||||
static function (int $releaseId) use (&$synchronizedReleaseIds): void {
|
||||
$synchronizedReleaseIds[] = $releaseId;
|
||||
},
|
||||
);
|
||||
$releaseManager->shouldReceive('finalizeRelease')
|
||||
->once()
|
||||
->andReturnUsing(static function () use ($searchSyncCoordinator): void {
|
||||
$searchSyncCoordinator->request(1);
|
||||
$searchSyncCoordinator->request(1);
|
||||
});
|
||||
|
||||
$tempWorkspace = Mockery::mock(TempWorkspaceService::class);
|
||||
$tempWorkspace->shouldReceive('createReleaseTempFolder')->once()->andReturn('/tmp/ap-release/');
|
||||
@@ -83,18 +101,80 @@ class ReleaseProcessorTest extends TestCase
|
||||
$processor = new ReleaseProcessor(
|
||||
$config,
|
||||
$nzbParser,
|
||||
new AdditionalWorkPlanner($config),
|
||||
Mockery::mock(ArchiveExtractionService::class),
|
||||
Mockery::mock(MediaExtractionService::class),
|
||||
Mockery::mock(UsenetDownloadService::class),
|
||||
$this->scopedDownloadService(),
|
||||
$releaseManager,
|
||||
Mockery::mock(ReleaseFilesArchiveFallback::class),
|
||||
$tempWorkspace,
|
||||
$output
|
||||
$output,
|
||||
$searchSyncCoordinator,
|
||||
$persistenceMetrics,
|
||||
);
|
||||
|
||||
$processor->process($this->makeContext(), '/tmp/main/');
|
||||
$context = $this->makeContext();
|
||||
$context->release->nfostatus = 1;
|
||||
$result = $processor->process($context, '/tmp/main/');
|
||||
|
||||
$this->assertTrue(true);
|
||||
$this->assertSame(ProcessingOutcome::NoUsefulArtifacts, $result->outcome);
|
||||
$this->assertTrue($result->isSuccessful());
|
||||
$this->assertFalse($result->artifactsCreated);
|
||||
$this->assertSame('Processing completed without creating useful artifacts.', $result->reason);
|
||||
$this->assertSame(0, $result->releaseFilesAdded);
|
||||
$this->assertSame([1], $synchronizedReleaseIds);
|
||||
$this->assertSame(2, $result->persistenceMetrics?->searchSyncRequests);
|
||||
$this->assertSame(1, $result->persistenceMetrics?->searchSyncExecutions);
|
||||
$this->assertGreaterThan(0.0, $result->elapsedSeconds);
|
||||
$this->assertSame([
|
||||
ProcessingStage::WorkspacePreparation->value,
|
||||
ProcessingStage::NzbParsing->value,
|
||||
ProcessingStage::ReleaseInitialization->value,
|
||||
ProcessingStage::MessageIdSelection->value,
|
||||
ProcessingStage::Finalization->value,
|
||||
ProcessingStage::WorkspaceCleanup->value,
|
||||
], array_keys($result->stageDurations));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_always_uses_the_v2_work_plan_for_message_id_selection(): void
|
||||
{
|
||||
$config = $this->makeConfig();
|
||||
$nzbContents = [['title' => 'file.nzb', 'segments' => ['<message>']]];
|
||||
$nzbParser = Mockery::mock(NzbContentParser::class);
|
||||
$nzbParser->shouldReceive('parseNzb')->once()->with('guid-1')->andReturn([
|
||||
'error' => null,
|
||||
'contents' => $nzbContents,
|
||||
]);
|
||||
$nzbParser->shouldNotReceive('extractMessageIDs');
|
||||
|
||||
$releaseManager = Mockery::mock(ReleaseFileManager::class);
|
||||
$releaseManager->shouldReceive('processReleaseNameFromNzbContents')->once()->andReturnFalse();
|
||||
$releaseManager->shouldReceive('finalizeRelease')->once()->andReturnNull();
|
||||
|
||||
$processor = new ReleaseProcessor(
|
||||
$config,
|
||||
$nzbParser,
|
||||
new AdditionalWorkPlanner($config),
|
||||
Mockery::mock(ArchiveExtractionService::class),
|
||||
Mockery::mock(MediaExtractionService::class),
|
||||
$this->scopedDownloadService(),
|
||||
$releaseManager,
|
||||
Mockery::mock(ReleaseFilesArchiveFallback::class),
|
||||
$this->successfulTempWorkspace(),
|
||||
Mockery::mock(ConsoleOutputService::class)
|
||||
->shouldReceive('echoReleaseStart')->once()->andReturnNull()
|
||||
->shouldReceive('setProcessTitle')->once()->andReturnNull()
|
||||
->getMock(),
|
||||
);
|
||||
|
||||
$context = $this->makeContext();
|
||||
$context->release->nfostatus = 1;
|
||||
$result = $processor->process($context, '/tmp/main/');
|
||||
|
||||
$this->assertSame(ProcessingOutcome::NoUsefulArtifacts, $result->outcome);
|
||||
$this->assertNotNull($context->workPlan);
|
||||
$this->assertSame(0, $result->duplicateMessageIdCount);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -124,9 +204,10 @@ class ReleaseProcessorTest extends TestCase
|
||||
$processor = new ReleaseProcessor(
|
||||
$config,
|
||||
$nzbParser,
|
||||
new AdditionalWorkPlanner($config),
|
||||
Mockery::mock(ArchiveExtractionService::class),
|
||||
Mockery::mock(MediaExtractionService::class),
|
||||
Mockery::mock(UsenetDownloadService::class),
|
||||
$this->scopedDownloadService(),
|
||||
$releaseManager,
|
||||
Mockery::mock(ReleaseFilesArchiveFallback::class),
|
||||
$tempWorkspace,
|
||||
@@ -136,9 +217,172 @@ class ReleaseProcessorTest extends TestCase
|
||||
$context = $this->makeContext();
|
||||
$context->startTime = hrtime(true) - 2_000_000_000;
|
||||
|
||||
$processor->process($context, '/tmp/main/');
|
||||
$result = $processor->process($context, '/tmp/main/');
|
||||
|
||||
$this->assertTrue(true);
|
||||
$this->assertSame(ProcessingOutcome::TimedOut, $result->outcome);
|
||||
$this->assertFalse($result->isSuccessful());
|
||||
$this->assertStringContainsString('exceeded', $result->reason);
|
||||
$this->assertArrayHasKey(ProcessingStage::TimeoutHandling->value, $result->stageDurations);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_reports_a_deleted_timeout_separately(): void
|
||||
{
|
||||
$config = $this->makeConfig(['releaseProcessingTimeout' => 1, 'maxPpTimeoutCount' => 1]);
|
||||
$nzbParser = Mockery::mock(NzbContentParser::class);
|
||||
$nzbParser->shouldReceive('parseNzb')->once()->andReturn(['error' => null, 'contents' => []]);
|
||||
|
||||
$releaseManager = Mockery::mock(ReleaseFileManager::class);
|
||||
$releaseManager->shouldReceive('handleReleaseTimeout')->once()->andReturn(true);
|
||||
$releaseManager->shouldNotReceive('finalizeRelease');
|
||||
|
||||
$tempWorkspace = Mockery::mock(TempWorkspaceService::class);
|
||||
$tempWorkspace->shouldReceive('createReleaseTempFolder')->once()->andReturn('/tmp/ap-release/');
|
||||
$tempWorkspace->shouldReceive('clearDirectory')->twice()->with('/tmp/ap-release/', false)->andReturnNull();
|
||||
|
||||
$output = Mockery::mock(ConsoleOutputService::class);
|
||||
$output->shouldReceive('echoReleaseStart')->once()->andReturnNull();
|
||||
$output->shouldReceive('setProcessTitle')->once()->andReturnNull();
|
||||
$output->shouldReceive('echoReleaseTimeoutDeleted')->once()->andReturnNull();
|
||||
|
||||
$processor = new ReleaseProcessor(
|
||||
$config,
|
||||
$nzbParser,
|
||||
new AdditionalWorkPlanner($config),
|
||||
Mockery::mock(ArchiveExtractionService::class),
|
||||
Mockery::mock(MediaExtractionService::class),
|
||||
$this->scopedDownloadService(),
|
||||
$releaseManager,
|
||||
Mockery::mock(ReleaseFilesArchiveFallback::class),
|
||||
$tempWorkspace,
|
||||
$output
|
||||
);
|
||||
|
||||
$context = $this->makeContext();
|
||||
$context->startTime = hrtime(true) - 2_000_000_000;
|
||||
|
||||
$result = $processor->process($context, '/tmp/main/');
|
||||
|
||||
$this->assertSame(ProcessingOutcome::DeletedAfterTimeout, $result->outcome);
|
||||
$this->assertTrue($result->outcome->isDeleted());
|
||||
$this->assertFalse($result->isSuccessful());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_reports_a_temporary_workspace_failure(): void
|
||||
{
|
||||
$tempWorkspace = Mockery::mock(TempWorkspaceService::class);
|
||||
$tempWorkspace->shouldReceive('createReleaseTempFolder')
|
||||
->once()
|
||||
->andThrow(new \RuntimeException('workspace is not writable'));
|
||||
|
||||
$output = Mockery::mock(ConsoleOutputService::class);
|
||||
$output->shouldReceive('echoReleaseStart')->once()->andReturnNull();
|
||||
$output->shouldReceive('setProcessTitle')->once()->andReturnNull();
|
||||
$output->shouldReceive('warning')->once()->with('Unable to prepare release temp directory: workspace is not writable')->andReturnNull();
|
||||
|
||||
$processor = $this->makeProcessor(tempWorkspace: $tempWorkspace, output: $output);
|
||||
|
||||
$context = $this->makeContext();
|
||||
$context->release->nfostatus = 1;
|
||||
$result = $processor->process($context, '/tmp/main/');
|
||||
|
||||
$this->assertSame(ProcessingOutcome::TemporaryWorkspaceUnavailable, $result->outcome);
|
||||
$this->assertFalse($result->isSuccessful());
|
||||
$this->assertSame('workspace is not writable', $result->reason);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_reports_password_detection_as_a_successful_outcome(): void
|
||||
{
|
||||
$config = $this->makeConfig(['processPasswords' => true]);
|
||||
$nzbParser = $this->compressedNzbParser();
|
||||
|
||||
$archiveService = Mockery::mock(ArchiveExtractionService::class);
|
||||
$archiveService->shouldReceive('processCompressedData')->once()->andReturn([
|
||||
'success' => false,
|
||||
'files' => [],
|
||||
'hasPassword' => true,
|
||||
'passwordStatus' => ReleaseBrowseService::PASSWD_RAR,
|
||||
]);
|
||||
|
||||
$downloadService = Mockery::mock(UsenetDownloadService::class);
|
||||
$this->expectDownloadScope($downloadService);
|
||||
$downloadService->shouldReceive('download')
|
||||
->once()
|
||||
->with(DownloadKind::Compressed, ['<archive>'], '', 1, 'archive.part01.rar')
|
||||
->andReturn(['success' => true, 'data' => 'ARCHIVE', 'groupUnavailable' => false, 'error' => null]);
|
||||
|
||||
$releaseManager = Mockery::mock(ReleaseFileManager::class);
|
||||
$releaseManager->shouldReceive('processReleaseNameFromNzbContents')->once()->andReturnFalse();
|
||||
$releaseManager->shouldReceive('finalizeRelease')->once()->andReturnNull();
|
||||
|
||||
$processor = new ReleaseProcessor(
|
||||
$config,
|
||||
$nzbParser,
|
||||
new AdditionalWorkPlanner($config),
|
||||
$archiveService,
|
||||
Mockery::mock(MediaExtractionService::class),
|
||||
$downloadService,
|
||||
$releaseManager,
|
||||
Mockery::mock(ReleaseFilesArchiveFallback::class),
|
||||
$this->successfulTempWorkspace(),
|
||||
$this->passwordOutput()
|
||||
);
|
||||
|
||||
$context = $this->makeContext();
|
||||
$context->release->nfostatus = 1;
|
||||
$result = $processor->process($context, '/tmp/main/');
|
||||
|
||||
$this->assertSame(ProcessingOutcome::Passworded, $result->outcome);
|
||||
$this->assertTrue($result->isSuccessful());
|
||||
$this->assertFalse($result->artifactsCreated);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_reports_group_unavailability_as_an_unsuccessful_outcome(): void
|
||||
{
|
||||
$config = $this->makeConfig(['processPasswords' => true]);
|
||||
$nzbParser = $this->compressedNzbParser();
|
||||
|
||||
$downloadService = Mockery::mock(UsenetDownloadService::class);
|
||||
$this->expectDownloadScope($downloadService);
|
||||
$downloadService->shouldReceive('download')->once()->andReturn([
|
||||
'success' => false,
|
||||
'data' => null,
|
||||
'groupUnavailable' => true,
|
||||
'error' => 'Group unavailable',
|
||||
]);
|
||||
|
||||
$releaseManager = Mockery::mock(ReleaseFileManager::class);
|
||||
$releaseManager->shouldReceive('processReleaseNameFromNzbContents')->once()->andReturnFalse();
|
||||
$releaseManager->shouldReceive('finalizeRelease')->once()->andReturnNull();
|
||||
|
||||
$output = Mockery::mock(ConsoleOutputService::class);
|
||||
$output->shouldReceive('echoReleaseStart')->once()->andReturnNull();
|
||||
$output->shouldReceive('setProcessTitle')->once()->andReturnNull();
|
||||
$output->shouldReceive('echoGroupUnavailable')->once()->andReturnNull();
|
||||
|
||||
$processor = new ReleaseProcessor(
|
||||
$config,
|
||||
$nzbParser,
|
||||
new AdditionalWorkPlanner($config),
|
||||
Mockery::mock(ArchiveExtractionService::class),
|
||||
Mockery::mock(MediaExtractionService::class),
|
||||
$downloadService,
|
||||
$releaseManager,
|
||||
Mockery::mock(ReleaseFilesArchiveFallback::class),
|
||||
$this->successfulTempWorkspace(),
|
||||
$output
|
||||
);
|
||||
|
||||
$context = $this->makeContext();
|
||||
$context->release->nfostatus = 1;
|
||||
$result = $processor->process($context, '/tmp/main/');
|
||||
|
||||
$this->assertSame(ProcessingOutcome::GroupUnavailable, $result->outcome);
|
||||
$this->assertFalse($result->isSuccessful());
|
||||
$this->assertStringContainsString('unavailable', $result->reason);
|
||||
}
|
||||
|
||||
private function makeProcessor(
|
||||
@@ -147,12 +391,15 @@ class ReleaseProcessorTest extends TestCase
|
||||
?TempWorkspaceService $tempWorkspace = null,
|
||||
?ConsoleOutputService $output = null
|
||||
): ReleaseProcessor {
|
||||
$config = $this->makeConfig();
|
||||
|
||||
return new ReleaseProcessor(
|
||||
$this->makeConfig(),
|
||||
$config,
|
||||
$nzbParser ?? Mockery::mock(NzbContentParser::class),
|
||||
new AdditionalWorkPlanner($config),
|
||||
Mockery::mock(ArchiveExtractionService::class),
|
||||
Mockery::mock(MediaExtractionService::class),
|
||||
Mockery::mock(UsenetDownloadService::class),
|
||||
$this->scopedDownloadService(),
|
||||
$releaseManager ?? Mockery::mock(ReleaseFileManager::class),
|
||||
Mockery::mock(ReleaseFilesArchiveFallback::class),
|
||||
$tempWorkspace ?? Mockery::mock(TempWorkspaceService::class),
|
||||
@@ -171,4 +418,46 @@ class ReleaseProcessorTest extends TestCase
|
||||
'pp_timeout_count' => 0,
|
||||
]));
|
||||
}
|
||||
|
||||
private function compressedNzbParser(): NzbContentParser
|
||||
{
|
||||
$parser = Mockery::mock(NzbContentParser::class);
|
||||
$parser->shouldReceive('parseNzb')->once()->andReturn([
|
||||
'error' => null,
|
||||
'contents' => [['title' => 'archive.part01.rar', 'segments' => ['<archive>']]],
|
||||
]);
|
||||
|
||||
return $parser;
|
||||
}
|
||||
|
||||
private function scopedDownloadService(): UsenetDownloadService
|
||||
{
|
||||
$downloadService = Mockery::mock(UsenetDownloadService::class);
|
||||
$this->expectDownloadScope($downloadService);
|
||||
|
||||
return $downloadService;
|
||||
}
|
||||
|
||||
private function expectDownloadScope(UsenetDownloadService $downloadService): void
|
||||
{
|
||||
$downloadService->shouldReceive('beginReleaseScope')->once()->andReturnNull();
|
||||
$downloadService->shouldReceive('finishReleaseScope')->once()->andReturn(new DownloadMetrics);
|
||||
}
|
||||
|
||||
private function successfulTempWorkspace(): TempWorkspaceService
|
||||
{
|
||||
return Mockery::mock(TempWorkspaceService::class)
|
||||
->shouldReceive('createReleaseTempFolder')->once()->andReturn('/tmp/ap-release/')
|
||||
->shouldReceive('clearDirectory')->once()->with('/tmp/ap-release/', false)->andReturnNull()
|
||||
->getMock();
|
||||
}
|
||||
|
||||
private function passwordOutput(): ConsoleOutputService
|
||||
{
|
||||
return Mockery::mock(ConsoleOutputService::class)
|
||||
->shouldReceive('echoReleaseStart')->once()->andReturnNull()
|
||||
->shouldReceive('setProcessTitle')->once()->andReturnNull()
|
||||
->shouldReceive('echoCompressedDownload')->once()->andReturnNull()
|
||||
->getMock();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user