From 0b5cdfdf87037f0c1af777a5385fc9a761619469 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 14:28:47 +0200 Subject: [PATCH 1/9] added some more unittests --- ci/phpunit/TestBase.php | 53 ++- .../inc/utils/FileDownloadUtilsTest.php | 80 ++++ ci/phpunit/inc/utils/FileUtilsTest.php | 146 +++++++ ci/phpunit/inc/utils/HashtypeUtilsTest.php | 74 ++++ ci/phpunit/inc/utils/HealthUtilsTest.php | 197 ++++++++++ ci/phpunit/inc/utils/JwtTokenUtilsTest.php | 60 +++ ci/phpunit/inc/utils/LockUtilsTest.php | 104 +++++ .../inc/utils/PreprocessorUtilsTest.php | 358 ++++++++++++++++++ 8 files changed, 1068 insertions(+), 4 deletions(-) create mode 100644 ci/phpunit/inc/utils/FileDownloadUtilsTest.php create mode 100644 ci/phpunit/inc/utils/FileUtilsTest.php create mode 100644 ci/phpunit/inc/utils/HashtypeUtilsTest.php create mode 100644 ci/phpunit/inc/utils/HealthUtilsTest.php create mode 100644 ci/phpunit/inc/utils/JwtTokenUtilsTest.php create mode 100644 ci/phpunit/inc/utils/LockUtilsTest.php create mode 100644 ci/phpunit/inc/utils/PreprocessorUtilsTest.php diff --git a/ci/phpunit/TestBase.php b/ci/phpunit/TestBase.php index ec24393ad..0baf738aa 100644 --- a/ci/phpunit/TestBase.php +++ b/ci/phpunit/TestBase.php @@ -13,14 +13,23 @@ use Hashtopolis\dba\models\CrackerBinary; use Hashtopolis\dba\models\CrackerBinaryType; use Hashtopolis\dba\models\File; +use Hashtopolis\dba\models\FileDownload; use Hashtopolis\dba\models\FileTask; use Hashtopolis\dba\models\Hashlist; use Hashtopolis\dba\models\HashType; +use Hashtopolis\dba\models\HealthCheck; +use Hashtopolis\dba\models\HealthCheckAgent; +use Hashtopolis\dba\models\JwtApiKey; use Hashtopolis\dba\models\RightGroup; use Hashtopolis\dba\models\Task; use Hashtopolis\dba\models\TaskWrapper; use Hashtopolis\dba\models\User; use Hashtopolis\dba\models\UserFactory; +use Hashtopolis\inc\defines\DHealthCheckAgentStatus; +use Hashtopolis\inc\defines\DHealthCheckMode; +use Hashtopolis\inc\defines\DHealthCheckStatus; +use Hashtopolis\inc\defines\DHealthCheckType; +use Hashtopolis\inc\defines\DFileDownloadStatus; use Hashtopolis\inc\defines\DHashlistFormat; use Hashtopolis\inc\defines\DTaskTypes; use Hashtopolis\inc\utils\UserUtils; @@ -157,19 +166,46 @@ protected function createCrackerBinary(CrackerBinaryType $crackerBinaryType): Cr return $crackerBinary; } - protected function createTask(TaskWrapper $taskWrapper, CrackerBinary $crackerBinary, CrackerBinaryType $crackerBinaryType): Task { + protected function createTask(TaskWrapper $taskWrapper, CrackerBinary $crackerBinary, CrackerBinaryType $crackerBinaryType, ?int $usePreprocessor = null, string $preprocessorCommand = ''): Task { $task = $this->createDatabaseObject( Factory::getTaskFactory(), - new Task(null, 'task_' . uniqid(), '--attack-mode 0', 60, 30, 0, 0, 1, 1, '#ffffff', 0, 0, 0, 0, $crackerBinary->getId(), $crackerBinaryType->getId(), $taskWrapper->getId(), 0, '', 0, 0, 0, 0, '') + new Task(null, 'task_' . uniqid(), '--attack-mode 0', 60, 30, 0, 0, 1, 1, '#ffffff', 0, 0, 0, 0, $crackerBinary->getId(), $crackerBinaryType->getId(), $taskWrapper->getId(), 0, '', 0, 0, 0, $usePreprocessor ?? 0, $preprocessorCommand) ); $this->assertTrue($task instanceof Task); return $task; } - protected function createFile(AccessGroup $group, int $isSecret = 0): File { + protected function createJwtApiKey(User $user, ?int $startValid = null, ?int $endValid = null, int $isRevoked = 0): JwtApiKey { + $key = $this->createDatabaseObject( + Factory::getJwtApiKeyFactory(), + new JwtApiKey(null, $startValid ?? time(), $endValid ?? time() + 3600, $user->getId(), $isRevoked) + ); + $this->assertTrue($key instanceof JwtApiKey); + return $key; + } + + protected function createHealthCheck(CrackerBinary $crackerBinary, int $status = DHealthCheckStatus::PENDING, int $checkType = DHealthCheckType::BRUTE_FORCE, int $hashtypeId = DHealthCheckMode::MD5, int $expectedCracks = 0, string $attackCmd = ''): HealthCheck { + $check = $this->createDatabaseObject( + Factory::getHealthCheckFactory(), + new HealthCheck(null, time(), $status, $checkType, $hashtypeId, $crackerBinary->getId(), $expectedCracks, $attackCmd) + ); + $this->assertTrue($check instanceof HealthCheck); + return $check; + } + + protected function createHealthCheckAgent(HealthCheck $healthCheck, Agent $agent, int $status = DHealthCheckAgentStatus::PENDING, int $cracked = 0, int $numGpus = 0, int $start = 0, int $end = 0, string $errors = ''): HealthCheckAgent { + $agentCheck = $this->createDatabaseObject( + Factory::getHealthCheckAgentFactory(), + new HealthCheckAgent(null, $healthCheck->getId(), $agent->getId(), $status, $cracked, $numGpus, $start, $end, $errors) + ); + $this->assertTrue($agentCheck instanceof HealthCheckAgent); + return $agentCheck; + } + + protected function createFile(AccessGroup $group, int $isSecret = 0, ?string $filename = null, int $size = 0, int $fileType = 0, int $lineCount = 0): File { $file = $this->createDatabaseObject( Factory::getFileFactory(), - new File(null, 'file_' . uniqid(), 0, $isSecret, 0, $group->getId(), 0) + new File(null, $filename ?? 'file_' . uniqid(), $size, $isSecret, $fileType, $group->getId(), $lineCount) ); $this->assertTrue($file instanceof File); return $file; @@ -183,6 +219,15 @@ protected function createFileTask(File $file, Task $task): FileTask { $this->assertTrue($fileTask instanceof FileTask); return $fileTask; } + + protected function createFileDownload(int $fileId, int $status = DFileDownloadStatus::PENDING): FileDownload { + $fileDownload = $this->createDatabaseObject( + Factory::getFileDownloadFactory(), + new FileDownload(null, time(), $fileId, $status) + ); + $this->assertTrue($fileDownload instanceof FileDownload); + return $fileDownload; + } protected function createAgent(string $prefix, int $isTrusted = 1): Agent { $suffix = uniqid(); diff --git a/ci/phpunit/inc/utils/FileDownloadUtilsTest.php b/ci/phpunit/inc/utils/FileDownloadUtilsTest.php new file mode 100644 index 000000000..f3f19ffe0 --- /dev/null +++ b/ci/phpunit/inc/utils/FileDownloadUtilsTest.php @@ -0,0 +1,80 @@ +createAccessGroup('fdl_group'); + $this->file = $this->createFile($group); + $this->fileDownload = $this->createFileDownload($this->file->getId(), DFileDownloadStatus::DONE); + } + + public function testAddDownloadCreatesPendingDownload(): void { + $group = $this->createAccessGroup('fdl_new'); + $newFile = $this->createFile($group); + + FileDownloadUtils::addDownload($newFile->getId()); + + $qF1 = new QueryFilter(FileDownload::FILE_ID, $newFile->getId(), '='); + $qF2 = new QueryFilter(FileDownload::STATUS, DFileDownloadStatus::PENDING, '='); + $result = Factory::getFileDownloadFactory()->filter([Factory::FILTER => [$qF1, $qF2]], true); + + $this->assertInstanceOf(FileDownload::class, $result); + $this->assertSame($newFile->getId(), $result->getFileId()); + $this->assertSame(DFileDownloadStatus::PENDING, $result->getStatus()); + $this->registerDatabaseObject(Factory::getFileDownloadFactory(), $result); + } + + public function testAddDownloadSkipsExistingPending(): void { + $this->createFileDownload($this->file->getId()); + + FileDownloadUtils::addDownload($this->file->getId()); + + $qF1 = new QueryFilter(FileDownload::FILE_ID, $this->file->getId(), '='); + $qF2 = new QueryFilter(FileDownload::STATUS, DFileDownloadStatus::PENDING, '='); + $pending = Factory::getFileDownloadFactory()->filter([Factory::FILTER => [$qF1, $qF2]]); + $this->assertCount(1, $pending); + } + + public function testAddDownloadCreatesNewForCompletedFile(): void { + FileDownloadUtils::addDownload($this->file->getId()); + + $qF1 = new QueryFilter(FileDownload::FILE_ID, $this->file->getId(), '='); + $qF2 = new QueryFilter(FileDownload::STATUS, DFileDownloadStatus::PENDING, '='); + $pending = Factory::getFileDownloadFactory()->filter([Factory::FILTER => [$qF1, $qF2]], true); + + $this->assertInstanceOf(FileDownload::class, $pending); + $this->assertSame($this->file->getId(), $pending->getFileId()); + $this->assertSame(DFileDownloadStatus::PENDING, $pending->getStatus()); + $this->registerDatabaseObject(Factory::getFileDownloadFactory(), $pending); + } + + public function testRemoveFileDeletesDownloads(): void { + FileDownloadUtils::removeFile($this->fileDownload->getFileId()); + + $qF = new QueryFilter(FileDownload::FILE_ID, $this->fileDownload->getFileId(), '='); + $remaining = Factory::getFileDownloadFactory()->filter([Factory::FILTER => $qF]); + $this->assertSame([], $remaining); + } + + public function testRemoveFileIsNoopForNonExistent(): void { + FileDownloadUtils::removeFile(-1); + $this->assertTrue(true); + } +} diff --git a/ci/phpunit/inc/utils/FileUtilsTest.php b/ci/phpunit/inc/utils/FileUtilsTest.php new file mode 100644 index 000000000..1febcbb5c --- /dev/null +++ b/ci/phpunit/inc/utils/FileUtilsTest.php @@ -0,0 +1,146 @@ +user = $this->createUser('fu_user'); + $this->group = $this->createAccessGroup('fu_group'); + $this->createAccessGroupUser($this->user, $this->group); + + $this->file = $this->createFile($this->group); + $this->ruleFile = $this->createFile($this->group, 0, 'test_rule_' . uniqid() . '.rule', 512, DFileType::RULE); + $this->wordlistFile = $this->createFile($this->group); + $this->otherFile = $this->createFile($this->group, 0, 'test_other_' . uniqid() . '.bin', 256, DFileType::OTHER); + } + + public function testGetFileReturnsFileForAuthorizedUser(): void { + $result = FileUtils::getFile($this->file->getId(), $this->user); + $this->assertInstanceOf(File::class, $result); + $this->assertSame($this->file->getId(), $result->getId()); + } + + public function testGetFileThrowsForInvalidId(): void { + $this->expectException(HTException::class); + FileUtils::getFile(-1, $this->user); + } + + public function testGetFileThrowsForUnauthorizedUser(): void { + $otherGroup = $this->createAccessGroup('fu_other'); + $otherFile = $this->createFile($otherGroup); + + $this->expectException(HTException::class); + FileUtils::getFile($otherFile->getId(), $this->user); + } + + public function testSetFileTypeUpdatesType(): void { + FileUtils::setFileType($this->file->getId(), DFileType::RULE, $this->user); + + $updated = Factory::getFileFactory()->get($this->file->getId()); + $this->assertSame(DFileType::RULE, $updated->getFileType()); + } + + public function testSetFileTypeThrowsForInvalidType(): void { + $this->expectException(HTException::class); + FileUtils::setFileType($this->file->getId(), 999, $this->user); + } + + public function testSwitchSecretTogglesSecret(): void { + FileUtils::switchSecret($this->file->getId(), 1, $this->user); + + $updated = Factory::getFileFactory()->get($this->file->getId()); + $this->assertSame(1, $updated->getIsSecret()); + + FileUtils::switchSecret($this->file->getId(), 0, $this->user); + + $updated = Factory::getFileFactory()->get($this->file->getId()); + $this->assertSame(0, $updated->getIsSecret()); + } + + public function testGetFilesReturnsFilesInUserAccessGroups(): void { + $files = FileUtils::getFiles($this->user); + $fileIds = array_map(fn(File $f) => $f->getId(), $files); + + $this->assertContains($this->file->getId(), $fileIds); + $this->assertContains($this->ruleFile->getId(), $fileIds); + $this->assertContains($this->wordlistFile->getId(), $fileIds); + $this->assertContains($this->otherFile->getId(), $fileIds); + } + + public function testGetFilesExcludesTemporaryFiles(): void { + $tempFile = $this->createFile($this->group, 0, 'temp_' . uniqid() . '.tmp', 0, DFileType::TEMPORARY); + + $files = FileUtils::getFiles($this->user); + $fileIds = array_map(fn(File $f) => $f->getId(), $files); + + $this->assertNotContains($tempFile->getId(), $fileIds); + } + + public function testLoadFilesByCategoryCategorizesFiles(): void { + [$rules, $wordlists, $other] = FileUtils::loadFilesByCategory($this->user, []); + + $ruleIds = array_map(fn($set) => $set->getAllValues()['file']->getId(), $rules); + $wlIds = array_map(fn($set) => $set->getAllValues()['file']->getId(), $wordlists); + $otherIds = array_map(fn($set) => $set->getAllValues()['file']->getId(), $other); + + $this->assertContains($this->ruleFile->getId(), $ruleIds); + $this->assertContains($this->file->getId(), $wlIds); + $this->assertContains($this->wordlistFile->getId(), $wlIds); + $this->assertContains($this->otherFile->getId(), $otherIds); + } + + public function testLoadFilesByCategoryMarksCheckedFiles(): void { + [$rules, $wordlists, $other] = FileUtils::loadFilesByCategory($this->user, [$this->file->getId()]); + + $checkedIds = []; + foreach (array_merge($rules, $wordlists, $other) as $set) { + $data = $set->getAllValues(); + if ($data['checked'] === '1') { + $checkedIds[] = $data['file']->getId(); + } + } + + $this->assertContains($this->file->getId(), $checkedIds); + } + + public function testDeleteThrowsForInvalidId(): void { + $this->expectException(HTException::class); + FileUtils::delete(-1, $this->user); + } + + public function testDeleteThrowsWhenFileInUseByTask(): void { + $this->expectException(HTException::class); + + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($this->group, $hashType); + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $taskWrapper = $this->createTaskWrapper($this->group, $hashlist); + $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); + $this->createFileTask($this->file, $task); + + FileUtils::delete($this->file->getId(), $this->user); + } +} diff --git a/ci/phpunit/inc/utils/HashtypeUtilsTest.php b/ci/phpunit/inc/utils/HashtypeUtilsTest.php new file mode 100644 index 000000000..30ee5d4f2 --- /dev/null +++ b/ci/phpunit/inc/utils/HashtypeUtilsTest.php @@ -0,0 +1,74 @@ +user = $this->createUser('ht_user'); + } + + public function testAddHashtypeCreatesNewHashtype(): void { + $hashtypeId = 999001; + $description = 'test_hashtype_' . uniqid(); + + $hashtype = HashtypeUtils::addHashtype($hashtypeId, $description, 0, false, $this->user); + + $this->assertSame($hashtypeId, $hashtype->getId()); + $this->assertStringContainsString($description, $hashtype->getDescription()); + + Factory::getHashTypeFactory()->delete($hashtype); + } + + public function testAddHashtypeThrowsForDuplicateId(): void { + $existing = $this->createHashType(); + + $this->expectException(HttpError::class); + HashtypeUtils::addHashtype($existing->getId(), 'new_desc', 0, false, $this->user); + } + + public function testAddHashtypeThrowsForEmptyDescription(): void { + $this->expectException(HttpError::class); + HashtypeUtils::addHashtype(999003, '', 0, false, $this->user); + } + + public function testAddHashtypeThrowsForNegativeId(): void { + $this->expectException(HttpError::class); + HashtypeUtils::addHashtype(-1, 'desc', 0, false, $this->user); + } + + public function testDeleteHashtypeRemovesHashtype(): void { + $hashtype = $this->createHashType(); + + HashtypeUtils::deleteHashtype($hashtype->getId()); + + $this->assertNull(Factory::getHashTypeFactory()->get($hashtype->getId())); + } + + public function testDeleteHashtypeThrowsForInvalidId(): void { + $this->expectException(HTException::class); + HashtypeUtils::deleteHashtype(-1); + } + + public function testDeleteHashtypeThrowsWhenHashlistsExist(): void { + $hashtype = $this->createHashType(); + $accessGroup = $this->createAccessGroup('ht_del'); + $this->createHashlist($accessGroup, $hashtype); + + $this->expectException(HTException::class); + HashtypeUtils::deleteHashtype($hashtype->getId()); + } +} diff --git a/ci/phpunit/inc/utils/HealthUtilsTest.php b/ci/phpunit/inc/utils/HealthUtilsTest.php new file mode 100644 index 000000000..0301dc6d2 --- /dev/null +++ b/ci/phpunit/inc/utils/HealthUtilsTest.php @@ -0,0 +1,197 @@ +createCrackerBinaryType(); + $this->crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $this->agent = $this->createAgent('hc_agent'); + $this->otherAgent = $this->createAgent('hc_other'); + + $this->healthCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::PENDING, DHealthCheckType::BRUTE_FORCE, DHealthCheckMode::MD5, 50, '-a 3 -1 ?l?u?d ?1?1?1?1?1'); + + $this->healthCheckAgent = $this->createHealthCheckAgent($this->healthCheck, $this->agent); + + $this->completedAgent = $this->createHealthCheckAgent($this->healthCheck, $this->otherAgent, DHealthCheckAgentStatus::COMPLETED, 10, 2, 100, 200); + } + + #[Override] + protected function tearDown(): void { + $tmpFile = '/tmp/health-check-' . ($this->healthCheck->getId() ?? 0) . '.txt'; + if (file_exists($tmpFile)) { + unlink($tmpFile); + } + parent::tearDown(); + } + + public function testGenerateHashMd5(): void { + $plain = 'testplain'; + $hash = HealthUtils::generateHash(DHealthCheckMode::MD5, $plain); + $this->assertSame(md5($plain), $hash); + } + + public function testGenerateHashBcrypt(): void { + $plain = 'abc'; + $hash = HealthUtils::generateHash(DHealthCheckMode::BCRYPT, $plain); + $this->assertNotFalse(password_verify($plain, $hash)); + } + + public function testGenerateHashThrowsForUnknownHashtype(): void { + $this->expectException(HTException::class); + HealthUtils::generateHash(999999, 'plain'); + } + + public function testGetAttackModeBruteForce(): void { + $mode = $this->callPrivateMethod('getAttackMode', DHealthCheckType::BRUTE_FORCE); + $this->assertSame(' -a 3', $mode); + } + + public function testGetAttackInputMd5BruteForce(): void { + $input = $this->callPrivateMethod('getAttackInput', DHealthCheckMode::MD5, DHealthCheckType::BRUTE_FORCE); + $this->assertSame(' -1 ?l?u?d ?1?1?1?1?1', $input); + } + + public function testGetAttackInputBcryptBruteForce(): void { + $input = $this->callPrivateMethod('getAttackInput', DHealthCheckMode::BCRYPT, DHealthCheckType::BRUTE_FORCE); + $this->assertSame(' ?l?l?l', $input); + } + + public function testGetAttackNumHashesMd5(): void { + $num = $this->callPrivateMethod('getAttackNumHashes', DHealthCheckMode::MD5); + $this->assertSame(100, $num); + } + + public function testGetAttackNumHashesBcrypt(): void { + $num = $this->callPrivateMethod('getAttackNumHashes', DHealthCheckMode::BCRYPT); + $this->assertSame(10, $num); + } + + public function testGetAttackNumHashesUnknown(): void { + $num = $this->callPrivateMethod('getAttackNumHashes', 999); + $this->assertSame(100, $num); + } + + public function testCheckNeededReturnsPendingAgentCheck(): void { + $result = HealthUtils::checkNeeded($this->agent); + $this->assertInstanceOf(HealthCheckAgent::class, $result); + $this->assertSame($this->healthCheckAgent->getId(), $result->getId()); + } + + public function testCheckNeededReturnsFalseWhenAgentHasNoPending(): void { + $freshAgent = $this->createAgent('hc_fresh'); + $result = HealthUtils::checkNeeded($freshAgent); + $this->assertFalse($result); + } + + public function testCheckNeededReturnsFalseWhenHealthCheckIsAborted(): void { + $abortedCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::ABORTED); + $isolatedAgent = $this->createAgent('hc_isolated'); + $pendingAgent = $this->createHealthCheckAgent($abortedCheck, $isolatedAgent); + + $result = HealthUtils::checkNeeded($isolatedAgent); + $this->assertFalse($result); + } + + public function testCheckCompletionMarksCompleteWhenAllAgentsDone(): void { + $allDoneCheck = $this->createHealthCheck($this->crackerBinary); + $this->createHealthCheckAgent($allDoneCheck, $this->agent, DHealthCheckAgentStatus::COMPLETED, 5, 1, 0, 10); + $this->createHealthCheckAgent($allDoneCheck, $this->otherAgent, DHealthCheckAgentStatus::FAILED, 0, 0, 0, 0, 'error'); + + HealthUtils::checkCompletion($allDoneCheck); + + $updated = Factory::getHealthCheckFactory()->get($allDoneCheck->getId()); + $this->assertSame(DHealthCheckStatus::COMPLETED, $updated->getStatus()); + } + + public function testCheckCompletionDoesNotCompleteWhenAgentPending(): void { + HealthUtils::checkCompletion($this->healthCheck); + + $updated = Factory::getHealthCheckFactory()->get($this->healthCheck->getId()); + $this->assertSame(DHealthCheckStatus::PENDING, $updated->getStatus()); + } + + public function testResetAgentCheckResetsPendingAgent(): void { + HealthUtils::resetAgentCheck($this->healthCheckAgent->getId()); + + $updated = Factory::getHealthCheckAgentFactory()->get($this->healthCheckAgent->getId()); + $this->assertSame(DHealthCheckAgentStatus::PENDING, $updated->getStatus()); + $this->assertSame(0, $updated->getStart()); + $this->assertSame(0, $updated->getEnd()); + $this->assertSame('', $updated->getErrors()); + $this->assertSame(0, $updated->getCracked()); + $this->assertSame(0, $updated->getNumGpus()); + } + + public function testResetAgentCheckReopensCompletedHealthCheck(): void { + $completedCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::COMPLETED); + $agentCheck = $this->createHealthCheckAgent($completedCheck, $this->agent, DHealthCheckAgentStatus::COMPLETED, 5, 1, 0, 10); + + HealthUtils::resetAgentCheck($agentCheck->getId()); + + $updatedCheck = Factory::getHealthCheckFactory()->get($completedCheck->getId()); + $this->assertSame(DHealthCheckStatus::PENDING, $updatedCheck->getStatus()); + } + + public function testResetAgentCheckThrowsForAbortedHealthCheck(): void { + $abortedCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::ABORTED); + $agentCheck = $this->createHealthCheckAgent($abortedCheck, $this->agent, DHealthCheckAgentStatus::FAILED, 5, 1, 0, 10); + + $this->expectException(HTException::class); + HealthUtils::resetAgentCheck($agentCheck->getId()); + } + + public function testResetAgentCheckThrowsForInvalidId(): void { + $this->expectException(HTException::class); + HealthUtils::resetAgentCheck(-1); + } + + public function testDeleteHealthCheckRemovesCheckAndAgents(): void { + HealthUtils::deleteHealthCheck($this->healthCheck->getId()); + + $this->assertNull(Factory::getHealthCheckFactory()->get($this->healthCheck->getId())); + + $qF = new QueryFilter(HealthCheckAgent::HEALTH_CHECK_ID, $this->healthCheck->getId(), '='); + $remaining = Factory::getHealthCheckAgentFactory()->filter([Factory::FILTER => $qF]); + $this->assertSame([], $remaining); + } + + public function testDeleteHealthCheckThrowsForInvalidId(): void { + $this->expectException(HTException::class); + HealthUtils::deleteHealthCheck(-1); + } + + private function callPrivateMethod(string $name, ...$args): mixed { + $ref = new ReflectionClass(HealthUtils::class); + $method = $ref->getMethod($name); + return $method->invoke(null, ...$args); + } +} diff --git a/ci/phpunit/inc/utils/JwtTokenUtilsTest.php b/ci/phpunit/inc/utils/JwtTokenUtilsTest.php new file mode 100644 index 000000000..ed8015531 --- /dev/null +++ b/ci/phpunit/inc/utils/JwtTokenUtilsTest.php @@ -0,0 +1,60 @@ +user = $this->createUser('jwt_user'); + } + + public function testCreateKeyCreatesValidKey(): void { + $start = time(); + $end = $start + 3600; + + $key = JwtTokenUtils::createKey($this->user->getId(), $start, $end); + + $this->assertInstanceOf(JwtApiKey::class, $key); + $this->assertSame($start, $key->getStartValid()); + $this->assertSame($end, $key->getEndValid()); + $this->assertSame($this->user->getId(), $key->getUserId()); + $this->assertNotNull($key->getId()); + $this->registerDatabaseObject(Factory::getJwtApiKeyFactory(), $key); + } + + public function testCreateKeyThrowsForInvalidUser(): void { + $this->expectException(HttpError::class); + JwtTokenUtils::createKey(-1, time(), time() + 3600); + } + + public function testDeleteKeyDeletesExpiredKey(): void { + $start = time() - 7200; + $end = time() - 3600; + $key = $this->createJwtApiKey($this->user, $start, $end); + + JwtTokenUtils::deleteKey($key); + + $this->assertNull(Factory::getJwtApiKeyFactory()->get($key->getId())); + } + + public function testDeleteKeyThrowsForUnexpiredKey(): void { + $key = $this->createJwtApiKey($this->user); + + $this->expectException(HttpForbidden::class); + JwtTokenUtils::deleteKey($key); + } +} diff --git a/ci/phpunit/inc/utils/LockUtilsTest.php b/ci/phpunit/inc/utils/LockUtilsTest.php new file mode 100644 index 000000000..f4fd36de8 --- /dev/null +++ b/ci/phpunit/inc/utils/LockUtilsTest.php @@ -0,0 +1,104 @@ +releaseTestLock(); + $this->cleanupLockFiles(); + } + + #[Override] + protected function tearDown(): void { + $this->releaseTestLock(); + $this->cleanupLockFiles(); + parent::tearDown(); + } + + private function releaseTestLock(): void { + LockUtils::release(self::TEST_LOCK); + } + + private function cleanupLockFiles(): void { + $prefixes = [Lock::CHUNKING, self::TEST_LOCK]; + foreach ($prefixes as $prefix) { + $path = self::LOCK_DIR . '/' . $prefix; + if (is_file($path)) { + unlink($path); + } + } + } + + public function testGetCreatesAndAcquiresLock(): void { + LockUtils::get(self::TEST_LOCK); + $lockFile = self::LOCK_DIR . '/' . self::TEST_LOCK; + $this->assertFileExists($lockFile); + LockUtils::release(self::TEST_LOCK); + } + + public function testGetReturnsCachedInstance(): void { + LockUtils::get(self::TEST_LOCK); + LockUtils::get(self::TEST_LOCK); + LockUtils::release(self::TEST_LOCK); + $this->assertTrue(true); + } + + public function testReleaseReleasesLockForReacquisition(): void { + LockUtils::get(self::TEST_LOCK); + LockUtils::release(self::TEST_LOCK); + + LockUtils::get(self::TEST_LOCK); + LockUtils::release(self::TEST_LOCK); + $this->assertTrue(true); + } + + public function testReleaseIsNoopForUnknownLock(): void { + LockUtils::release('nonexistent.lock'); + $this->assertTrue(true); + } + + public function testDeleteLockFileRemovesExistingLockFile(): void { + $taskId = 999001; + $lockFilePath = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskId; + + touch($lockFilePath); + $this->assertFileExists($lockFilePath); + + LockUtils::deleteLockFile($taskId); + + $this->assertFileDoesNotExist($lockFilePath); + } + + public function testDeleteLockFileDoesNotThrowForMissingFile(): void { + LockUtils::deleteLockFile(999002); + $this->assertTrue(true); + } + + public function testDeleteLockFileCleansUpOnlySpecifiedTask(): void { + $taskIdA = 999003; + $taskIdB = 999004; + $pathA = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskIdA; + $pathB = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskIdB; + + touch($pathA); + touch($pathB); + + LockUtils::deleteLockFile($taskIdA); + + $this->assertFileDoesNotExist($pathA); + $this->assertFileExists($pathB); + + unlink($pathB); + } +} diff --git a/ci/phpunit/inc/utils/PreprocessorUtilsTest.php b/ci/phpunit/inc/utils/PreprocessorUtilsTest.php new file mode 100644 index 000000000..8b125d219 --- /dev/null +++ b/ci/phpunit/inc/utils/PreprocessorUtilsTest.php @@ -0,0 +1,358 @@ +preprocessor = PreprocessorUtils::addPreprocessor( + 'test_pp_' . uniqid(), + 'test_binary_' . uniqid(), + 'https://example.com/test.zip', + '--keyspace', + '--skip', + '--limit' + ); + } + + #[Override] + protected function tearDown(): void { + try { + PreprocessorUtils::delete($this->preprocessor->getId()); + } + catch (Exception) { + } + parent::tearDown(); + } + + private function createPreprocessor(string $suffix = ''): Preprocessor { + $suffix = $suffix ?: uniqid(); + $pp = PreprocessorUtils::addPreprocessor( + 'tmp_pp_' . $suffix, + 'tmp_binary_' . $suffix, + 'https://example.com/' . $suffix . '.zip', + '--ks', + '--sk', + '--lm' + ); + $this->registerDatabaseObject(Factory::getPreprocessorFactory(), $pp); + return $pp; + } + + public function testAddPreprocessorCreatesWithValidData(): void { + $name = 'add_create_' . uniqid(); + $binaryName = 'add_binary_' . uniqid(); + $url = 'https://example.com/add_create.zip'; + + $pp = PreprocessorUtils::addPreprocessor($name, $binaryName, $url, '--keyspace', '--skip', '--limit'); + $this->registerDatabaseObject(Factory::getPreprocessorFactory(), $pp); + + $this->assertInstanceOf(Preprocessor::class, $pp); + $this->assertSame($name, $pp->getName()); + $this->assertSame($binaryName, $pp->getBinaryName()); + $this->assertSame($url, $pp->getUrl()); + $this->assertSame('--keyspace', $pp->getKeyspaceCommand()); + $this->assertSame('--skip', $pp->getSkipCommand()); + $this->assertSame('--limit', $pp->getLimitCommand()); + $this->assertNotNull($pp->getId()); + } + + public function testAddPreprocessorConvertsEmptyCommandsToNull(): void { + $pp = PreprocessorUtils::addPreprocessor( + 'add_null_cmds_' . uniqid(), + 'binary_null', + 'https://example.com/null.zip', + '', '', '' + ); + $this->registerDatabaseObject(Factory::getPreprocessorFactory(), $pp); + + $this->assertNull($pp->getKeyspaceCommand()); + $this->assertNull($pp->getSkipCommand()); + $this->assertNull($pp->getLimitCommand()); + } + + public function testAddPreprocessorThrowsForDuplicateName(): void { + $this->expectException(HttpConflict::class); + PreprocessorUtils::addPreprocessor( + $this->preprocessor->getName(), + 'binary_dup', + 'https://example.com/dup.zip', + '', '', '' + ); + } + + public function testAddPreprocessorThrowsForEmptyName(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor('', 'binary', 'https://example.com/e.zip', '', '', ''); + } + + public function testAddPreprocessorThrowsForEmptyBinaryName(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor('name', '', 'https://example.com/e.zip', '', '', ''); + } + + public function testAddPreprocessorThrowsForEmptyUrl(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor('name', 'binary', '', '', '', ''); + } + + public function testAddPreprocessorThrowsForBlacklistedBinaryName(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor('name', 'bad|binary', 'https://example.com/b.zip', '', '', ''); + } + + public function testAddPreprocessorThrowsForBlacklistedKeyspace(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor( + 'name', 'binary', 'https://example.com/b.zip', + '--keyspace;rm', '--skip', '--limit' + ); + } + + public function testAddPreprocessorThrowsForBlacklistedSkip(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor( + 'name', 'binary', 'https://example.com/b.zip', + '--keyspace', '--skip$test', '--limit' + ); + } + + public function testAddPreprocessorThrowsForBlacklistedLimit(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor( + 'name', 'binary', 'https://example.com/b.zip', + '--keyspace', '--skip', '--limit`test`' + ); + } + + public function testGetPreprocessorReturnsPreprocessor(): void { + $retrieved = PreprocessorUtils::getPreprocessor($this->preprocessor->getId()); + $this->assertInstanceOf(Preprocessor::class, $retrieved); + $this->assertSame($this->preprocessor->getId(), $retrieved->getId()); + } + + public function testGetPreprocessorThrowsForInvalidId(): void { + $this->expectException(HTException::class); + PreprocessorUtils::getPreprocessor(-1); + } + + public function testDeleteRemovesPreprocessor(): void { + $pp = $this->createPreprocessor('del_test'); + $ppId = $pp->getId(); + + PreprocessorUtils::delete($ppId); + + $this->assertNull(Factory::getPreprocessorFactory()->get($ppId)); + } + + public function testDeleteThrowsForNonExistentPreprocessor(): void { + $this->expectException(HTException::class); + PreprocessorUtils::delete(-1); + } + + public function testDeleteThrowsWhenTaskUsesPreprocessor(): void { + $pp = $this->createPreprocessor('del_task'); + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($this->createAccessGroup('del_pp'), $hashType); + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $taskWrapper = $this->createTaskWrapper($this->createAccessGroup('del_pp'), $hashlist); + $this->createDatabaseObject( + Factory::getTaskFactory(), + new Task( + null, 'task_with_pp_' . uniqid(), '--attack-mode 0', 60, 30, 0, 0, 1, 1, + '#ffffff', 0, 0, 0, 0, $crackerBinary->getId(), $crackerBinaryType->getId(), + $taskWrapper->getId(), 0, '', 0, 0, 0, $pp->getId(), '' + ) + ); + + $this->expectException(HttpError::class); + PreprocessorUtils::delete($pp->getId()); + } + + public function testEditNameUpdatesName(): void { + $newName = 'rename_pp_' . uniqid(); + PreprocessorUtils::editName($this->preprocessor->getId(), $newName); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newName, $updated->getName()); + } + + public function testEditNameThrowsForDuplicateName(): void { + $other = $this->createPreprocessor('rename_dup'); + + $this->expectException(HTException::class); + PreprocessorUtils::editName($this->preprocessor->getId(), $other->getName()); + } + + public function testEditBinaryNameUpdates(): void { + $newBinary = 'new_binary_' . uniqid(); + PreprocessorUtils::editBinaryName($this->preprocessor->getId(), $newBinary); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newBinary, $updated->getBinaryName()); + } + + public function testEditBinaryNameThrowsForEmpty(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editBinaryName($this->preprocessor->getId(), ''); + } + + public function testEditBinaryNameThrowsForBlacklistedChars(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editBinaryName($this->preprocessor->getId(), 'bad|binary'); + } + + public function testEditKeyspaceCommandUpdates(): void { + $newCmd = '--new-keyspace'; + PreprocessorUtils::editKeyspaceCommand($this->preprocessor->getId(), $newCmd); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newCmd, $updated->getKeyspaceCommand()); + } + + public function testEditKeyspaceCommandThrowsForBlacklistedChars(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editKeyspaceCommand($this->preprocessor->getId(), 'keyspace;rm'); + } + + public function testEditSkipCommandUpdates(): void { + $newCmd = '--new-skip'; + PreprocessorUtils::editSkipCommand($this->preprocessor->getId(), $newCmd); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newCmd, $updated->getSkipCommand()); + } + + public function testEditSkipCommandThrowsForBlacklistedChars(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editSkipCommand($this->preprocessor->getId(), 'skip$test'); + } + + public function testEditLimitCommandUpdates(): void { + $newCmd = '--new-limit'; + PreprocessorUtils::editLimitCommand($this->preprocessor->getId(), $newCmd); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newCmd, $updated->getLimitCommand()); + } + + public function testEditLimitCommandThrowsForBlacklistedChars(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editLimitCommand($this->preprocessor->getId(), 'limit`test`'); + } + + public function testEditPreprocessorUpdatesAllFields(): void { + $newName = 'full_edit_' . uniqid(); + $newBinary = 'full_bin_' . uniqid(); + $newUrl = 'https://example.com/full.zip'; + + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), $newName, $newBinary, $newUrl, + '--ks', '--sk', '--lm' + ); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newName, $updated->getName()); + $this->assertSame($newBinary, $updated->getBinaryName()); + $this->assertSame($newUrl, $updated->getUrl()); + $this->assertSame('--ks', $updated->getKeyspaceCommand()); + $this->assertSame('--sk', $updated->getSkipCommand()); + $this->assertSame('--lm', $updated->getLimitCommand()); + } + + public function testEditPreprocessorThrowsForDuplicateName(): void { + $other = $this->createPreprocessor('full_dup'); + + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), $other->getName(), + 'binary', 'https://example.com/f.zip', + '', '', '' + ); + } + + public function testEditPreprocessorThrowsForEmptyName(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), '', 'binary', 'https://example.com/f.zip', + '', '', '' + ); + } + + public function testEditPreprocessorThrowsForEmptyBinaryName(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name', '', 'https://example.com/f.zip', + '', '', '' + ); + } + + public function testEditPreprocessorThrowsForEmptyUrl(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name', 'binary', '', + '', '', '' + ); + } + + public function testEditPreprocessorThrowsForBlacklistedBinaryName(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name', 'bad|binary', 'https://example.com/f.zip', + '', '', '' + ); + } + + public function testEditPreprocessorThrowsForBlacklistedKeyspace(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name', 'binary', 'https://example.com/f.zip', + 'keyspace;rm', '', '' + ); + } + + public function testEditPreprocessorThrowsForBlacklistedSkip(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name', 'binary', 'https://example.com/f.zip', + '', 'skip$test', '' + ); + } + + public function testEditPreprocessorThrowsForBlacklistedLimit(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name', 'binary', 'https://example.com/f.zip', + '', '', 'limit`test`' + ); + } + + public function testEditPreprocessorConvertsEmptyCommandsToNull(): void { + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name', 'binary', 'https://example.com/f.zip', + '', '', '' + ); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertNull($updated->getKeyspaceCommand()); + $this->assertNull($updated->getSkipCommand()); + $this->assertNull($updated->getLimitCommand()); + } +} From 6bc2cb140125858ca0d65e7bde3dfb6e2844d7d9 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 14:33:02 +0200 Subject: [PATCH 2/9] adjusted styling and added additional test --- .../inc/utils/FileDownloadUtilsTest.php | 30 +++--- ci/phpunit/inc/utils/FileUtilsTest.php | 68 ++++++------- ci/phpunit/inc/utils/HashtypeUtilsTest.php | 30 +++--- ci/phpunit/inc/utils/HealthUtilsTest.php | 97 +++++++++++-------- ci/phpunit/inc/utils/JwtTokenUtilsTest.php | 20 ++-- ci/phpunit/inc/utils/LockUtilsTest.php | 40 ++++---- 6 files changed, 150 insertions(+), 135 deletions(-) diff --git a/ci/phpunit/inc/utils/FileDownloadUtilsTest.php b/ci/phpunit/inc/utils/FileDownloadUtilsTest.php index f3f19ffe0..c06ba1763 100644 --- a/ci/phpunit/inc/utils/FileDownloadUtilsTest.php +++ b/ci/phpunit/inc/utils/FileDownloadUtilsTest.php @@ -15,8 +15,8 @@ final class FileDownloadUtilsTest extends TestBase { private FileDownload $fileDownload; - private File $file; - + private File $file; + #[Override] protected function setUp(): void { parent::setUp(); @@ -24,55 +24,55 @@ protected function setUp(): void { $this->file = $this->createFile($group); $this->fileDownload = $this->createFileDownload($this->file->getId(), DFileDownloadStatus::DONE); } - + public function testAddDownloadCreatesPendingDownload(): void { $group = $this->createAccessGroup('fdl_new'); $newFile = $this->createFile($group); - + FileDownloadUtils::addDownload($newFile->getId()); - + $qF1 = new QueryFilter(FileDownload::FILE_ID, $newFile->getId(), '='); $qF2 = new QueryFilter(FileDownload::STATUS, DFileDownloadStatus::PENDING, '='); $result = Factory::getFileDownloadFactory()->filter([Factory::FILTER => [$qF1, $qF2]], true); - + $this->assertInstanceOf(FileDownload::class, $result); $this->assertSame($newFile->getId(), $result->getFileId()); $this->assertSame(DFileDownloadStatus::PENDING, $result->getStatus()); $this->registerDatabaseObject(Factory::getFileDownloadFactory(), $result); } - + public function testAddDownloadSkipsExistingPending(): void { $this->createFileDownload($this->file->getId()); - + FileDownloadUtils::addDownload($this->file->getId()); - + $qF1 = new QueryFilter(FileDownload::FILE_ID, $this->file->getId(), '='); $qF2 = new QueryFilter(FileDownload::STATUS, DFileDownloadStatus::PENDING, '='); $pending = Factory::getFileDownloadFactory()->filter([Factory::FILTER => [$qF1, $qF2]]); $this->assertCount(1, $pending); } - + public function testAddDownloadCreatesNewForCompletedFile(): void { FileDownloadUtils::addDownload($this->file->getId()); - + $qF1 = new QueryFilter(FileDownload::FILE_ID, $this->file->getId(), '='); $qF2 = new QueryFilter(FileDownload::STATUS, DFileDownloadStatus::PENDING, '='); $pending = Factory::getFileDownloadFactory()->filter([Factory::FILTER => [$qF1, $qF2]], true); - + $this->assertInstanceOf(FileDownload::class, $pending); $this->assertSame($this->file->getId(), $pending->getFileId()); $this->assertSame(DFileDownloadStatus::PENDING, $pending->getStatus()); $this->registerDatabaseObject(Factory::getFileDownloadFactory(), $pending); } - + public function testRemoveFileDeletesDownloads(): void { FileDownloadUtils::removeFile($this->fileDownload->getFileId()); - + $qF = new QueryFilter(FileDownload::FILE_ID, $this->fileDownload->getFileId(), '='); $remaining = Factory::getFileDownloadFactory()->filter([Factory::FILTER => $qF]); $this->assertSame([], $remaining); } - + public function testRemoveFileIsNoopForNonExistent(): void { FileDownloadUtils::removeFile(-1); $this->assertTrue(true); diff --git a/ci/phpunit/inc/utils/FileUtilsTest.php b/ci/phpunit/inc/utils/FileUtilsTest.php index 1febcbb5c..2c480090d 100644 --- a/ci/phpunit/inc/utils/FileUtilsTest.php +++ b/ci/phpunit/inc/utils/FileUtilsTest.php @@ -15,105 +15,105 @@ require_once(dirname(__FILE__) . '/../../../../src/inc/startup/include.php'); final class FileUtilsTest extends TestBase { - private User $user; + private User $user; private AccessGroup $group; - private File $file; - private File $ruleFile; - private File $wordlistFile; - private File $otherFile; - + private File $file; + private File $ruleFile; + private File $wordlistFile; + private File $otherFile; + #[Override] protected function setUp(): void { parent::setUp(); - + $this->user = $this->createUser('fu_user'); $this->group = $this->createAccessGroup('fu_group'); $this->createAccessGroupUser($this->user, $this->group); - + $this->file = $this->createFile($this->group); $this->ruleFile = $this->createFile($this->group, 0, 'test_rule_' . uniqid() . '.rule', 512, DFileType::RULE); $this->wordlistFile = $this->createFile($this->group); $this->otherFile = $this->createFile($this->group, 0, 'test_other_' . uniqid() . '.bin', 256, DFileType::OTHER); } - + public function testGetFileReturnsFileForAuthorizedUser(): void { $result = FileUtils::getFile($this->file->getId(), $this->user); $this->assertInstanceOf(File::class, $result); $this->assertSame($this->file->getId(), $result->getId()); } - + public function testGetFileThrowsForInvalidId(): void { $this->expectException(HTException::class); FileUtils::getFile(-1, $this->user); } - + public function testGetFileThrowsForUnauthorizedUser(): void { $otherGroup = $this->createAccessGroup('fu_other'); $otherFile = $this->createFile($otherGroup); - + $this->expectException(HTException::class); FileUtils::getFile($otherFile->getId(), $this->user); } - + public function testSetFileTypeUpdatesType(): void { FileUtils::setFileType($this->file->getId(), DFileType::RULE, $this->user); - + $updated = Factory::getFileFactory()->get($this->file->getId()); $this->assertSame(DFileType::RULE, $updated->getFileType()); } - + public function testSetFileTypeThrowsForInvalidType(): void { $this->expectException(HTException::class); FileUtils::setFileType($this->file->getId(), 999, $this->user); } - + public function testSwitchSecretTogglesSecret(): void { FileUtils::switchSecret($this->file->getId(), 1, $this->user); - + $updated = Factory::getFileFactory()->get($this->file->getId()); $this->assertSame(1, $updated->getIsSecret()); - + FileUtils::switchSecret($this->file->getId(), 0, $this->user); - + $updated = Factory::getFileFactory()->get($this->file->getId()); $this->assertSame(0, $updated->getIsSecret()); } - + public function testGetFilesReturnsFilesInUserAccessGroups(): void { $files = FileUtils::getFiles($this->user); $fileIds = array_map(fn(File $f) => $f->getId(), $files); - + $this->assertContains($this->file->getId(), $fileIds); $this->assertContains($this->ruleFile->getId(), $fileIds); $this->assertContains($this->wordlistFile->getId(), $fileIds); $this->assertContains($this->otherFile->getId(), $fileIds); } - + public function testGetFilesExcludesTemporaryFiles(): void { $tempFile = $this->createFile($this->group, 0, 'temp_' . uniqid() . '.tmp', 0, DFileType::TEMPORARY); - + $files = FileUtils::getFiles($this->user); $fileIds = array_map(fn(File $f) => $f->getId(), $files); - + $this->assertNotContains($tempFile->getId(), $fileIds); } - + public function testLoadFilesByCategoryCategorizesFiles(): void { [$rules, $wordlists, $other] = FileUtils::loadFilesByCategory($this->user, []); - + $ruleIds = array_map(fn($set) => $set->getAllValues()['file']->getId(), $rules); $wlIds = array_map(fn($set) => $set->getAllValues()['file']->getId(), $wordlists); $otherIds = array_map(fn($set) => $set->getAllValues()['file']->getId(), $other); - + $this->assertContains($this->ruleFile->getId(), $ruleIds); $this->assertContains($this->file->getId(), $wlIds); $this->assertContains($this->wordlistFile->getId(), $wlIds); $this->assertContains($this->otherFile->getId(), $otherIds); } - + public function testLoadFilesByCategoryMarksCheckedFiles(): void { [$rules, $wordlists, $other] = FileUtils::loadFilesByCategory($this->user, [$this->file->getId()]); - + $checkedIds = []; foreach (array_merge($rules, $wordlists, $other) as $set) { $data = $set->getAllValues(); @@ -121,18 +121,18 @@ public function testLoadFilesByCategoryMarksCheckedFiles(): void { $checkedIds[] = $data['file']->getId(); } } - + $this->assertContains($this->file->getId(), $checkedIds); } - + public function testDeleteThrowsForInvalidId(): void { $this->expectException(HTException::class); FileUtils::delete(-1, $this->user); } - + public function testDeleteThrowsWhenFileInUseByTask(): void { $this->expectException(HTException::class); - + $hashType = $this->createHashType(); $hashlist = $this->createHashlist($this->group, $hashType); $crackerBinaryType = $this->createCrackerBinaryType(); @@ -140,7 +140,7 @@ public function testDeleteThrowsWhenFileInUseByTask(): void { $taskWrapper = $this->createTaskWrapper($this->group, $hashlist); $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); $this->createFileTask($this->file, $task); - + FileUtils::delete($this->file->getId(), $this->user); } } diff --git a/ci/phpunit/inc/utils/HashtypeUtilsTest.php b/ci/phpunit/inc/utils/HashtypeUtilsTest.php index 30ee5d4f2..6f07e6c24 100644 --- a/ci/phpunit/inc/utils/HashtypeUtilsTest.php +++ b/ci/phpunit/inc/utils/HashtypeUtilsTest.php @@ -14,60 +14,60 @@ final class HashtypeUtilsTest extends TestBase { private User $user; - + #[Override] protected function setUp(): void { parent::setUp(); $this->user = $this->createUser('ht_user'); } - + public function testAddHashtypeCreatesNewHashtype(): void { $hashtypeId = 999001; $description = 'test_hashtype_' . uniqid(); - + $hashtype = HashtypeUtils::addHashtype($hashtypeId, $description, 0, false, $this->user); - + $this->assertSame($hashtypeId, $hashtype->getId()); $this->assertStringContainsString($description, $hashtype->getDescription()); - + Factory::getHashTypeFactory()->delete($hashtype); } - + public function testAddHashtypeThrowsForDuplicateId(): void { $existing = $this->createHashType(); - + $this->expectException(HttpError::class); HashtypeUtils::addHashtype($existing->getId(), 'new_desc', 0, false, $this->user); } - + public function testAddHashtypeThrowsForEmptyDescription(): void { $this->expectException(HttpError::class); HashtypeUtils::addHashtype(999003, '', 0, false, $this->user); } - + public function testAddHashtypeThrowsForNegativeId(): void { $this->expectException(HttpError::class); HashtypeUtils::addHashtype(-1, 'desc', 0, false, $this->user); } - + public function testDeleteHashtypeRemovesHashtype(): void { $hashtype = $this->createHashType(); - + HashtypeUtils::deleteHashtype($hashtype->getId()); - + $this->assertNull(Factory::getHashTypeFactory()->get($hashtype->getId())); } - + public function testDeleteHashtypeThrowsForInvalidId(): void { $this->expectException(HTException::class); HashtypeUtils::deleteHashtype(-1); } - + public function testDeleteHashtypeThrowsWhenHashlistsExist(): void { $hashtype = $this->createHashType(); $accessGroup = $this->createAccessGroup('ht_del'); $this->createHashlist($accessGroup, $hashtype); - + $this->expectException(HTException::class); HashtypeUtils::deleteHashtype($hashtype->getId()); } diff --git a/ci/phpunit/inc/utils/HealthUtilsTest.php b/ci/phpunit/inc/utils/HealthUtilsTest.php index 0301dc6d2..589d93cda 100644 --- a/ci/phpunit/inc/utils/HealthUtilsTest.php +++ b/ci/phpunit/inc/utils/HealthUtilsTest.php @@ -21,29 +21,29 @@ require_once(dirname(__FILE__) . '/../../../../src/inc/startup/include.php'); final class HealthUtilsTest extends TestBase { - private HealthCheck $healthCheck; + private HealthCheck $healthCheck; private HealthCheckAgent $healthCheckAgent; private HealthCheckAgent $completedAgent; - private Agent $agent; - private Agent $otherAgent; - private CrackerBinary $crackerBinary; - + private Agent $agent; + private Agent $otherAgent; + private CrackerBinary $crackerBinary; + #[Override] protected function setUp(): void { parent::setUp(); - + $crackerBinaryType = $this->createCrackerBinaryType(); $this->crackerBinary = $this->createCrackerBinary($crackerBinaryType); $this->agent = $this->createAgent('hc_agent'); $this->otherAgent = $this->createAgent('hc_other'); - + $this->healthCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::PENDING, DHealthCheckType::BRUTE_FORCE, DHealthCheckMode::MD5, 50, '-a 3 -1 ?l?u?d ?1?1?1?1?1'); - + $this->healthCheckAgent = $this->createHealthCheckAgent($this->healthCheck, $this->agent); - + $this->completedAgent = $this->createHealthCheckAgent($this->healthCheck, $this->otherAgent, DHealthCheckAgentStatus::COMPLETED, 10, 2, 100, 200); } - + #[Override] protected function tearDown(): void { $tmpFile = '/tmp/health-check-' . ($this->healthCheck->getId() ?? 0) . '.txt'; @@ -52,96 +52,96 @@ protected function tearDown(): void { } parent::tearDown(); } - + public function testGenerateHashMd5(): void { $plain = 'testplain'; $hash = HealthUtils::generateHash(DHealthCheckMode::MD5, $plain); $this->assertSame(md5($plain), $hash); } - + public function testGenerateHashBcrypt(): void { $plain = 'abc'; $hash = HealthUtils::generateHash(DHealthCheckMode::BCRYPT, $plain); $this->assertNotFalse(password_verify($plain, $hash)); } - + public function testGenerateHashThrowsForUnknownHashtype(): void { $this->expectException(HTException::class); HealthUtils::generateHash(999999, 'plain'); } - + public function testGetAttackModeBruteForce(): void { $mode = $this->callPrivateMethod('getAttackMode', DHealthCheckType::BRUTE_FORCE); $this->assertSame(' -a 3', $mode); } - + public function testGetAttackInputMd5BruteForce(): void { $input = $this->callPrivateMethod('getAttackInput', DHealthCheckMode::MD5, DHealthCheckType::BRUTE_FORCE); $this->assertSame(' -1 ?l?u?d ?1?1?1?1?1', $input); } - + public function testGetAttackInputBcryptBruteForce(): void { $input = $this->callPrivateMethod('getAttackInput', DHealthCheckMode::BCRYPT, DHealthCheckType::BRUTE_FORCE); $this->assertSame(' ?l?l?l', $input); } - + public function testGetAttackNumHashesMd5(): void { $num = $this->callPrivateMethod('getAttackNumHashes', DHealthCheckMode::MD5); $this->assertSame(100, $num); } - + public function testGetAttackNumHashesBcrypt(): void { $num = $this->callPrivateMethod('getAttackNumHashes', DHealthCheckMode::BCRYPT); $this->assertSame(10, $num); } - + public function testGetAttackNumHashesUnknown(): void { $num = $this->callPrivateMethod('getAttackNumHashes', 999); $this->assertSame(100, $num); } - + public function testCheckNeededReturnsPendingAgentCheck(): void { $result = HealthUtils::checkNeeded($this->agent); $this->assertInstanceOf(HealthCheckAgent::class, $result); $this->assertSame($this->healthCheckAgent->getId(), $result->getId()); } - + public function testCheckNeededReturnsFalseWhenAgentHasNoPending(): void { $freshAgent = $this->createAgent('hc_fresh'); $result = HealthUtils::checkNeeded($freshAgent); $this->assertFalse($result); } - + public function testCheckNeededReturnsFalseWhenHealthCheckIsAborted(): void { $abortedCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::ABORTED); $isolatedAgent = $this->createAgent('hc_isolated'); $pendingAgent = $this->createHealthCheckAgent($abortedCheck, $isolatedAgent); - + $result = HealthUtils::checkNeeded($isolatedAgent); $this->assertFalse($result); } - + public function testCheckCompletionMarksCompleteWhenAllAgentsDone(): void { $allDoneCheck = $this->createHealthCheck($this->crackerBinary); $this->createHealthCheckAgent($allDoneCheck, $this->agent, DHealthCheckAgentStatus::COMPLETED, 5, 1, 0, 10); $this->createHealthCheckAgent($allDoneCheck, $this->otherAgent, DHealthCheckAgentStatus::FAILED, 0, 0, 0, 0, 'error'); - + HealthUtils::checkCompletion($allDoneCheck); - + $updated = Factory::getHealthCheckFactory()->get($allDoneCheck->getId()); $this->assertSame(DHealthCheckStatus::COMPLETED, $updated->getStatus()); } - + public function testCheckCompletionDoesNotCompleteWhenAgentPending(): void { HealthUtils::checkCompletion($this->healthCheck); - + $updated = Factory::getHealthCheckFactory()->get($this->healthCheck->getId()); $this->assertSame(DHealthCheckStatus::PENDING, $updated->getStatus()); } - + public function testResetAgentCheckResetsPendingAgent(): void { HealthUtils::resetAgentCheck($this->healthCheckAgent->getId()); - + $updated = Factory::getHealthCheckAgentFactory()->get($this->healthCheckAgent->getId()); $this->assertSame(DHealthCheckAgentStatus::PENDING, $updated->getStatus()); $this->assertSame(0, $updated->getStart()); @@ -150,45 +150,60 @@ public function testResetAgentCheckResetsPendingAgent(): void { $this->assertSame(0, $updated->getCracked()); $this->assertSame(0, $updated->getNumGpus()); } - + + public function testResetAgentCheckResetsCompletedAgentUnderPendingCheck(): void { + HealthUtils::resetAgentCheck($this->completedAgent->getId()); + + $updated = Factory::getHealthCheckAgentFactory()->get($this->completedAgent->getId()); + $this->assertSame(DHealthCheckAgentStatus::PENDING, $updated->getStatus()); + $this->assertSame(0, $updated->getCracked()); + $this->assertSame(0, $updated->getNumGpus()); + $this->assertSame(0, $updated->getStart()); + $this->assertSame(0, $updated->getEnd()); + $this->assertSame('', $updated->getErrors()); + + $parentCheck = Factory::getHealthCheckFactory()->get($this->healthCheck->getId()); + $this->assertSame(DHealthCheckStatus::PENDING, $parentCheck->getStatus()); + } + public function testResetAgentCheckReopensCompletedHealthCheck(): void { $completedCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::COMPLETED); $agentCheck = $this->createHealthCheckAgent($completedCheck, $this->agent, DHealthCheckAgentStatus::COMPLETED, 5, 1, 0, 10); - + HealthUtils::resetAgentCheck($agentCheck->getId()); - + $updatedCheck = Factory::getHealthCheckFactory()->get($completedCheck->getId()); $this->assertSame(DHealthCheckStatus::PENDING, $updatedCheck->getStatus()); } - + public function testResetAgentCheckThrowsForAbortedHealthCheck(): void { $abortedCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::ABORTED); $agentCheck = $this->createHealthCheckAgent($abortedCheck, $this->agent, DHealthCheckAgentStatus::FAILED, 5, 1, 0, 10); - + $this->expectException(HTException::class); HealthUtils::resetAgentCheck($agentCheck->getId()); } - + public function testResetAgentCheckThrowsForInvalidId(): void { $this->expectException(HTException::class); HealthUtils::resetAgentCheck(-1); } - + public function testDeleteHealthCheckRemovesCheckAndAgents(): void { HealthUtils::deleteHealthCheck($this->healthCheck->getId()); - + $this->assertNull(Factory::getHealthCheckFactory()->get($this->healthCheck->getId())); - + $qF = new QueryFilter(HealthCheckAgent::HEALTH_CHECK_ID, $this->healthCheck->getId(), '='); $remaining = Factory::getHealthCheckAgentFactory()->filter([Factory::FILTER => $qF]); $this->assertSame([], $remaining); } - + public function testDeleteHealthCheckThrowsForInvalidId(): void { $this->expectException(HTException::class); HealthUtils::deleteHealthCheck(-1); } - + private function callPrivateMethod(string $name, ...$args): mixed { $ref = new ReflectionClass(HealthUtils::class); $method = $ref->getMethod($name); diff --git a/ci/phpunit/inc/utils/JwtTokenUtilsTest.php b/ci/phpunit/inc/utils/JwtTokenUtilsTest.php index ed8015531..cdd0201bc 100644 --- a/ci/phpunit/inc/utils/JwtTokenUtilsTest.php +++ b/ci/phpunit/inc/utils/JwtTokenUtilsTest.php @@ -15,19 +15,19 @@ final class JwtTokenUtilsTest extends TestBase { private User $user; - + #[Override] protected function setUp(): void { parent::setUp(); $this->user = $this->createUser('jwt_user'); } - + public function testCreateKeyCreatesValidKey(): void { $start = time(); $end = $start + 3600; - + $key = JwtTokenUtils::createKey($this->user->getId(), $start, $end); - + $this->assertInstanceOf(JwtApiKey::class, $key); $this->assertSame($start, $key->getStartValid()); $this->assertSame($end, $key->getEndValid()); @@ -35,25 +35,25 @@ public function testCreateKeyCreatesValidKey(): void { $this->assertNotNull($key->getId()); $this->registerDatabaseObject(Factory::getJwtApiKeyFactory(), $key); } - + public function testCreateKeyThrowsForInvalidUser(): void { $this->expectException(HttpError::class); JwtTokenUtils::createKey(-1, time(), time() + 3600); } - + public function testDeleteKeyDeletesExpiredKey(): void { $start = time() - 7200; $end = time() - 3600; $key = $this->createJwtApiKey($this->user, $start, $end); - + JwtTokenUtils::deleteKey($key); - + $this->assertNull(Factory::getJwtApiKeyFactory()->get($key->getId())); } - + public function testDeleteKeyThrowsForUnexpiredKey(): void { $key = $this->createJwtApiKey($this->user); - + $this->expectException(HttpForbidden::class); JwtTokenUtils::deleteKey($key); } diff --git a/ci/phpunit/inc/utils/LockUtilsTest.php b/ci/phpunit/inc/utils/LockUtilsTest.php index f4fd36de8..f0528ec66 100644 --- a/ci/phpunit/inc/utils/LockUtilsTest.php +++ b/ci/phpunit/inc/utils/LockUtilsTest.php @@ -10,26 +10,26 @@ final class LockUtilsTest extends TestBase { private const TEST_LOCK = 'phpunit_test.lock'; - private const LOCK_DIR = __DIR__ . '/../../../../src/inc/utils/locks'; - + private const LOCK_DIR = __DIR__ . '/../../../../src/inc/utils/locks'; + #[Override] protected function setUp(): void { parent::setUp(); $this->releaseTestLock(); $this->cleanupLockFiles(); } - + #[Override] protected function tearDown(): void { $this->releaseTestLock(); $this->cleanupLockFiles(); parent::tearDown(); } - + private function releaseTestLock(): void { LockUtils::release(self::TEST_LOCK); } - + private function cleanupLockFiles(): void { $prefixes = [Lock::CHUNKING, self::TEST_LOCK]; foreach ($prefixes as $prefix) { @@ -39,66 +39,66 @@ private function cleanupLockFiles(): void { } } } - + public function testGetCreatesAndAcquiresLock(): void { LockUtils::get(self::TEST_LOCK); $lockFile = self::LOCK_DIR . '/' . self::TEST_LOCK; $this->assertFileExists($lockFile); LockUtils::release(self::TEST_LOCK); } - + public function testGetReturnsCachedInstance(): void { LockUtils::get(self::TEST_LOCK); LockUtils::get(self::TEST_LOCK); LockUtils::release(self::TEST_LOCK); $this->assertTrue(true); } - + public function testReleaseReleasesLockForReacquisition(): void { LockUtils::get(self::TEST_LOCK); LockUtils::release(self::TEST_LOCK); - + LockUtils::get(self::TEST_LOCK); LockUtils::release(self::TEST_LOCK); $this->assertTrue(true); } - + public function testReleaseIsNoopForUnknownLock(): void { LockUtils::release('nonexistent.lock'); $this->assertTrue(true); } - + public function testDeleteLockFileRemovesExistingLockFile(): void { $taskId = 999001; $lockFilePath = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskId; - + touch($lockFilePath); $this->assertFileExists($lockFilePath); - + LockUtils::deleteLockFile($taskId); - + $this->assertFileDoesNotExist($lockFilePath); } - + public function testDeleteLockFileDoesNotThrowForMissingFile(): void { LockUtils::deleteLockFile(999002); $this->assertTrue(true); } - + public function testDeleteLockFileCleansUpOnlySpecifiedTask(): void { $taskIdA = 999003; $taskIdB = 999004; $pathA = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskIdA; $pathB = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskIdB; - + touch($pathA); touch($pathB); - + LockUtils::deleteLockFile($taskIdA); - + $this->assertFileDoesNotExist($pathA); $this->assertFileExists($pathB); - + unlink($pathB); } } From 82511d565c0b6dd1be0e20cae27130853d8a24c6 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 14:38:27 +0200 Subject: [PATCH 3/9] removed phpstan suggestions (asserting true for tests just to pass) --- ci/phpunit/inc/utils/FileDownloadUtilsTest.php | 1 - ci/phpunit/inc/utils/LockUtilsTest.php | 4 ---- 2 files changed, 5 deletions(-) diff --git a/ci/phpunit/inc/utils/FileDownloadUtilsTest.php b/ci/phpunit/inc/utils/FileDownloadUtilsTest.php index c06ba1763..13cf156ac 100644 --- a/ci/phpunit/inc/utils/FileDownloadUtilsTest.php +++ b/ci/phpunit/inc/utils/FileDownloadUtilsTest.php @@ -75,6 +75,5 @@ public function testRemoveFileDeletesDownloads(): void { public function testRemoveFileIsNoopForNonExistent(): void { FileDownloadUtils::removeFile(-1); - $this->assertTrue(true); } } diff --git a/ci/phpunit/inc/utils/LockUtilsTest.php b/ci/phpunit/inc/utils/LockUtilsTest.php index f0528ec66..8267b71b5 100644 --- a/ci/phpunit/inc/utils/LockUtilsTest.php +++ b/ci/phpunit/inc/utils/LockUtilsTest.php @@ -51,7 +51,6 @@ public function testGetReturnsCachedInstance(): void { LockUtils::get(self::TEST_LOCK); LockUtils::get(self::TEST_LOCK); LockUtils::release(self::TEST_LOCK); - $this->assertTrue(true); } public function testReleaseReleasesLockForReacquisition(): void { @@ -60,12 +59,10 @@ public function testReleaseReleasesLockForReacquisition(): void { LockUtils::get(self::TEST_LOCK); LockUtils::release(self::TEST_LOCK); - $this->assertTrue(true); } public function testReleaseIsNoopForUnknownLock(): void { LockUtils::release('nonexistent.lock'); - $this->assertTrue(true); } public function testDeleteLockFileRemovesExistingLockFile(): void { @@ -82,7 +79,6 @@ public function testDeleteLockFileRemovesExistingLockFile(): void { public function testDeleteLockFileDoesNotThrowForMissingFile(): void { LockUtils::deleteLockFile(999002); - $this->assertTrue(true); } public function testDeleteLockFileCleansUpOnlySpecifiedTask(): void { From a6378d522a6309b20c1dada7e2a5fa25ae03179f Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 14:52:23 +0200 Subject: [PATCH 4/9] fixing tests and asserting some risky tests --- ci/phpunit/inc/utils/LockUtilsTest.php | 12 +++++++++--- ci/phpunit/inc/utils/PreprocessorUtilsTest.php | 18 +++++++++--------- src/inc/Util.php | 7 ++++--- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/ci/phpunit/inc/utils/LockUtilsTest.php b/ci/phpunit/inc/utils/LockUtilsTest.php index 8267b71b5..e3c2f91a1 100644 --- a/ci/phpunit/inc/utils/LockUtilsTest.php +++ b/ci/phpunit/inc/utils/LockUtilsTest.php @@ -17,6 +17,7 @@ protected function setUp(): void { parent::setUp(); $this->releaseTestLock(); $this->cleanupLockFiles(); + $this->lockFile = self::LOCK_DIR . '/' . self::TEST_LOCK; } #[Override] @@ -42,8 +43,7 @@ private function cleanupLockFiles(): void { public function testGetCreatesAndAcquiresLock(): void { LockUtils::get(self::TEST_LOCK); - $lockFile = self::LOCK_DIR . '/' . self::TEST_LOCK; - $this->assertFileExists($lockFile); + $this->assertFileExists($this->lockFile); LockUtils::release(self::TEST_LOCK); } @@ -51,6 +51,7 @@ public function testGetReturnsCachedInstance(): void { LockUtils::get(self::TEST_LOCK); LockUtils::get(self::TEST_LOCK); LockUtils::release(self::TEST_LOCK); + $this->assertFileExists($this->lockFile); } public function testReleaseReleasesLockForReacquisition(): void { @@ -59,10 +60,12 @@ public function testReleaseReleasesLockForReacquisition(): void { LockUtils::get(self::TEST_LOCK); LockUtils::release(self::TEST_LOCK); + $this->assertFileExists($this->lockFile); } public function testReleaseIsNoopForUnknownLock(): void { LockUtils::release('nonexistent.lock'); + $this->assertFileDoesNotExist(self::LOCK_DIR . '/nonexistent.lock'); } public function testDeleteLockFileRemovesExistingLockFile(): void { @@ -78,7 +81,10 @@ public function testDeleteLockFileRemovesExistingLockFile(): void { } public function testDeleteLockFileDoesNotThrowForMissingFile(): void { - LockUtils::deleteLockFile(999002); + $taskId = 999002; + LockUtils::deleteLockFile($taskId); + $lockFilePath = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskId; + $this->assertFileExists($lockFilePath); } public function testDeleteLockFileCleansUpOnlySpecifiedTask(): void { diff --git a/ci/phpunit/inc/utils/PreprocessorUtilsTest.php b/ci/phpunit/inc/utils/PreprocessorUtilsTest.php index 8b125d219..48de0df46 100644 --- a/ci/phpunit/inc/utils/PreprocessorUtilsTest.php +++ b/ci/phpunit/inc/utils/PreprocessorUtilsTest.php @@ -137,7 +137,7 @@ public function testAddPreprocessorThrowsForBlacklistedLimit(): void { $this->expectException(HttpError::class); PreprocessorUtils::addPreprocessor( 'name', 'binary', 'https://example.com/b.zip', - '--keyspace', '--skip', '--limit`test`' + '--keyspace', '--skip', '--limit&test&' ); } @@ -255,7 +255,7 @@ public function testEditLimitCommandUpdates(): void { public function testEditLimitCommandThrowsForBlacklistedChars(): void { $this->expectException(HTException::class); - PreprocessorUtils::editLimitCommand($this->preprocessor->getId(), 'limit`test`'); + PreprocessorUtils::editLimitCommand($this->preprocessor->getId(), 'limit&test&'); } public function testEditPreprocessorUpdatesAllFields(): void { @@ -299,7 +299,7 @@ public function testEditPreprocessorThrowsForEmptyName(): void { public function testEditPreprocessorThrowsForEmptyBinaryName(): void { $this->expectException(HTException::class); PreprocessorUtils::editPreprocessor( - $this->preprocessor->getId(), 'name', '', 'https://example.com/f.zip', + $this->preprocessor->getId(), 'name' . uniqid(), '', 'https://example.com/f.zip', '', '', '' ); } @@ -307,7 +307,7 @@ public function testEditPreprocessorThrowsForEmptyBinaryName(): void { public function testEditPreprocessorThrowsForEmptyUrl(): void { $this->expectException(HTException::class); PreprocessorUtils::editPreprocessor( - $this->preprocessor->getId(), 'name', 'binary', '', + $this->preprocessor->getId(), 'name' . uniqid(), 'binary', '', '', '', '' ); } @@ -315,7 +315,7 @@ public function testEditPreprocessorThrowsForEmptyUrl(): void { public function testEditPreprocessorThrowsForBlacklistedBinaryName(): void { $this->expectException(HTException::class); PreprocessorUtils::editPreprocessor( - $this->preprocessor->getId(), 'name', 'bad|binary', 'https://example.com/f.zip', + $this->preprocessor->getId(), 'name' . uniqid(), 'bad|binary', 'https://example.com/f.zip', '', '', '' ); } @@ -323,7 +323,7 @@ public function testEditPreprocessorThrowsForBlacklistedBinaryName(): void { public function testEditPreprocessorThrowsForBlacklistedKeyspace(): void { $this->expectException(HTException::class); PreprocessorUtils::editPreprocessor( - $this->preprocessor->getId(), 'name', 'binary', 'https://example.com/f.zip', + $this->preprocessor->getId(), 'name' . uniqid(), 'binary', 'https://example.com/f.zip', 'keyspace;rm', '', '' ); } @@ -331,7 +331,7 @@ public function testEditPreprocessorThrowsForBlacklistedKeyspace(): void { public function testEditPreprocessorThrowsForBlacklistedSkip(): void { $this->expectException(HTException::class); PreprocessorUtils::editPreprocessor( - $this->preprocessor->getId(), 'name', 'binary', 'https://example.com/f.zip', + $this->preprocessor->getId(), 'name' . uniqid(), 'binary', 'https://example.com/f.zip', '', 'skip$test', '' ); } @@ -339,14 +339,14 @@ public function testEditPreprocessorThrowsForBlacklistedSkip(): void { public function testEditPreprocessorThrowsForBlacklistedLimit(): void { $this->expectException(HTException::class); PreprocessorUtils::editPreprocessor( - $this->preprocessor->getId(), 'name', 'binary', 'https://example.com/f.zip', + $this->preprocessor->getId(), 'name' . uniqid(), 'binary', 'https://example.com/f.zip', '', '', 'limit`test`' ); } public function testEditPreprocessorConvertsEmptyCommandsToNull(): void { PreprocessorUtils::editPreprocessor( - $this->preprocessor->getId(), 'name', 'binary', 'https://example.com/f.zip', + $this->preprocessor->getId(), 'name' . uniqid(), 'binary', 'https://example.com/f.zip', '', '', '' ); diff --git a/src/inc/Util.php b/src/inc/Util.php index c0850aca7..3449c2cd2 100755 --- a/src/inc/Util.php +++ b/src/inc/Util.php @@ -967,9 +967,10 @@ public static function escapeSpecial($string) { * @param $string string * @return bool true if at least one character is in the blacklist */ - public static function containsBlacklistedChars($string) { - for ($i = 0; $i < strlen(SConfig::getInstance()->getVal(DConfig::BLACKLIST_CHARS)); $i++) { - if (strpos($string, SConfig::getInstance()->getVal(DConfig::BLACKLIST_CHARS)[$i]) !== false) { + public static function containsBlacklistedChars(string $string): bool { + $blacklisted = SConfig::getInstance()->getVal(DConfig::BLACKLIST_CHARS); + for ($i = 0; $i < strlen($blacklisted); $i++) { + if (str_contains($string, $blacklisted[$i])) { return true; } } From c67c9de2319f2dbc322c8df09c4fd2abbbdc55a5 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 15:02:44 +0200 Subject: [PATCH 5/9] fixed assertions, the backtick test will still fail due to postgres --- ci/phpunit/inc/utils/LockUtilsTest.php | 7 ++++--- ci/phpunit/inc/utils/PreprocessorUtilsTest.php | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ci/phpunit/inc/utils/LockUtilsTest.php b/ci/phpunit/inc/utils/LockUtilsTest.php index e3c2f91a1..5aa4b04ac 100644 --- a/ci/phpunit/inc/utils/LockUtilsTest.php +++ b/ci/phpunit/inc/utils/LockUtilsTest.php @@ -11,6 +11,7 @@ final class LockUtilsTest extends TestBase { private const TEST_LOCK = 'phpunit_test.lock'; private const LOCK_DIR = __DIR__ . '/../../../../src/inc/utils/locks'; + private string $lockFile; #[Override] protected function setUp(): void { @@ -51,7 +52,7 @@ public function testGetReturnsCachedInstance(): void { LockUtils::get(self::TEST_LOCK); LockUtils::get(self::TEST_LOCK); LockUtils::release(self::TEST_LOCK); - $this->assertFileExists($this->lockFile); + $this->assertFileDoesNotExist($this->lockFile); } public function testReleaseReleasesLockForReacquisition(): void { @@ -60,7 +61,7 @@ public function testReleaseReleasesLockForReacquisition(): void { LockUtils::get(self::TEST_LOCK); LockUtils::release(self::TEST_LOCK); - $this->assertFileExists($this->lockFile); + $this->assertFileDoesNotExist($this->lockFile); } public function testReleaseIsNoopForUnknownLock(): void { @@ -84,7 +85,7 @@ public function testDeleteLockFileDoesNotThrowForMissingFile(): void { $taskId = 999002; LockUtils::deleteLockFile($taskId); $lockFilePath = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskId; - $this->assertFileExists($lockFilePath); + $this->assertFileDoesNotExist($lockFilePath); } public function testDeleteLockFileCleansUpOnlySpecifiedTask(): void { diff --git a/ci/phpunit/inc/utils/PreprocessorUtilsTest.php b/ci/phpunit/inc/utils/PreprocessorUtilsTest.php index 48de0df46..87f959aab 100644 --- a/ci/phpunit/inc/utils/PreprocessorUtilsTest.php +++ b/ci/phpunit/inc/utils/PreprocessorUtilsTest.php @@ -340,7 +340,7 @@ public function testEditPreprocessorThrowsForBlacklistedLimit(): void { $this->expectException(HTException::class); PreprocessorUtils::editPreprocessor( $this->preprocessor->getId(), 'name' . uniqid(), 'binary', 'https://example.com/f.zip', - '', '', 'limit`test`' + '', '', 'limit`test&' ); } From 3cefa0da59b7fc5c27122331c7a1406e090a18df Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 15:08:48 +0200 Subject: [PATCH 6/9] added migration to add backtick to postgres default blacklist characters if needed --- .../mysql/20260617130352_blacklist-chars-sync.sql | 1 + .../postgres/20260617130352_blacklist-chars-sync.sql | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 src/migrations/mysql/20260617130352_blacklist-chars-sync.sql create mode 100644 src/migrations/postgres/20260617130352_blacklist-chars-sync.sql diff --git a/src/migrations/mysql/20260617130352_blacklist-chars-sync.sql b/src/migrations/mysql/20260617130352_blacklist-chars-sync.sql new file mode 100644 index 000000000..d91c8d51b --- /dev/null +++ b/src/migrations/mysql/20260617130352_blacklist-chars-sync.sql @@ -0,0 +1 @@ +This migration is only a placeholder to keep migrations parallel \ No newline at end of file diff --git a/src/migrations/postgres/20260617130352_blacklist-chars-sync.sql b/src/migrations/postgres/20260617130352_blacklist-chars-sync.sql new file mode 100644 index 000000000..472658bbe --- /dev/null +++ b/src/migrations/postgres/20260617130352_blacklist-chars-sync.sql @@ -0,0 +1,6 @@ +-- the backtick as default blacklisted character got lost for postgres, so for the cases where people still have the default, we add it +UPDATE Config +SET value = '&|"'{}()[]$<>;`' +WHERE item = 'blacklistChars' + AND configSectionId=1 + AND value = '&|"'{}()[]$<>;'; \ No newline at end of file From 0aa1a5e7cd4d12073a66c36b20a98b68f916fa13 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 15:10:53 +0200 Subject: [PATCH 7/9] make one tests only having back ticks so that is specifically tested on postgres to see if migration is fixed --- ci/phpunit/inc/utils/PreprocessorUtilsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/phpunit/inc/utils/PreprocessorUtilsTest.php b/ci/phpunit/inc/utils/PreprocessorUtilsTest.php index 87f959aab..48de0df46 100644 --- a/ci/phpunit/inc/utils/PreprocessorUtilsTest.php +++ b/ci/phpunit/inc/utils/PreprocessorUtilsTest.php @@ -340,7 +340,7 @@ public function testEditPreprocessorThrowsForBlacklistedLimit(): void { $this->expectException(HTException::class); PreprocessorUtils::editPreprocessor( $this->preprocessor->getId(), 'name' . uniqid(), 'binary', 'https://example.com/f.zip', - '', '', 'limit`test&' + '', '', 'limit`test`' ); } From a80b1e0bd9b7c176b84e68ff7dde7a6bf1784464 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 15:35:54 +0200 Subject: [PATCH 8/9] fixed unescaped character --- .../postgres/20260617130352_blacklist-chars-sync.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/migrations/postgres/20260617130352_blacklist-chars-sync.sql b/src/migrations/postgres/20260617130352_blacklist-chars-sync.sql index 472658bbe..f94cdad5c 100644 --- a/src/migrations/postgres/20260617130352_blacklist-chars-sync.sql +++ b/src/migrations/postgres/20260617130352_blacklist-chars-sync.sql @@ -1,6 +1,6 @@ -- the backtick as default blacklisted character got lost for postgres, so for the cases where people still have the default, we add it UPDATE Config -SET value = '&|"'{}()[]$<>;`' +SET value = '&|"''{}()[]$<>;`' WHERE item = 'blacklistChars' AND configSectionId=1 - AND value = '&|"'{}()[]$<>;'; \ No newline at end of file + AND value = '&|"''{}()[]$<>;'; \ No newline at end of file From 17cda1d32ec1831c88c47a61a8de3c00c92d3d14 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 15:47:11 +0200 Subject: [PATCH 9/9] forgot the comments prefix for placeholder --- src/migrations/mysql/20260617130352_blacklist-chars-sync.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/migrations/mysql/20260617130352_blacklist-chars-sync.sql b/src/migrations/mysql/20260617130352_blacklist-chars-sync.sql index d91c8d51b..5b011d454 100644 --- a/src/migrations/mysql/20260617130352_blacklist-chars-sync.sql +++ b/src/migrations/mysql/20260617130352_blacklist-chars-sync.sql @@ -1 +1 @@ -This migration is only a placeholder to keep migrations parallel \ No newline at end of file +-- This migration is only a placeholder to keep migrations parallel \ No newline at end of file