. */ namespace SP\Tests\Domain\Export\Services; use Defuse\Crypto\Exception\EnvironmentIsBrokenException; use Defuse\Crypto\Key; use Defuse\Crypto\KeyProtectedByPassword; use DOMDocument; use DOMElement; use DOMException; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Constraint\Callback; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\MockObject\MockObject; use RuntimeException; use SP\Core\Context\ContextException; use SP\Domain\Common\Providers\Version; use SP\Domain\Common\Services\ServiceException; use SP\Domain\Core\Crypt\CryptInterface; use SP\Domain\Core\Exceptions\CheckException; use SP\Domain\Core\Exceptions\CryptException; use SP\Domain\Core\Exceptions\SPException; use SP\Domain\Core\PhpExtensionCheckerService; use SP\Domain\Export\Ports\XmlAccountExportService; use SP\Domain\Export\Ports\XmlCategoryExportService; use SP\Domain\Export\Ports\XmlClientExportService; use SP\Domain\Export\Ports\XmlTagExportService; use SP\Domain\Export\Services\XmlExport; use SP\Domain\File\Ports\DirectoryHandlerService; use SP\Domain\User\Dtos\UserDto; use SP\Infrastructure\File\FileException; use SP\Tests\Generators\UserDataGenerator; use SP\Tests\UnitaryTestCase; /** * Class XmlExportTest * */ #[Group('unitary')] class XmlExportTest extends UnitaryTestCase { use XmlTrait; private PhpExtensionCheckerService|MockObject $phpExtensionCheckerService; private MockObject|XmlClientExportService $xmlClientExportService; private XmlAccountExportService|MockObject $xmlAccountExportService; private XmlCategoryExportService|MockObject $xmlCategoryExportService; private XmlTagExportService|MockObject $xmlTagExportService; private CryptInterface|MockObject $crypt; private XmlExport $xmlExport; /** * @throws ServiceException * @throws Exception * @throws FileException * @throws CheckException * @throws EnvironmentIsBrokenException * @throws DOMException * @throws SPException */ public function testExport() { $this->context->setUserData( UserDto::fromModel( UserDataGenerator::factory() ->buildUserData() ->mutate( [ 'login' => 'test_user', 'userGroupName' => 'test_group', ] ) ) ); $exportPath = $this->createMock(DirectoryHandlerService::class); $exportPath->expects(self::once()) ->method('checkOrCreate'); $exportPath->method('getPath') ->willReturn(TMP_PATH); $password = self::$faker->password(); $this->xmlCategoryExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestCategories')); $this->xmlClientExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestClients')); $this->xmlTagExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestTags')); $this->xmlAccountExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestAccounts')); $this->checkCrypt($password); $out = $this->xmlExport->export($exportPath, $password); $this->assertNotEmpty($this->config->getConfigData()->getExportHash()); $this->assertFileExists($out); $xml = new DOMDocument(); $xml->load($out, LIBXML_NOBLANKS); $meta = $xml->documentElement->getElementsByTagName('Meta')->item(0)->childNodes; $this->assertEquals(6, $meta->count()); $this->checkNodes( $meta, [ 'Generator' => 'sysPass', 'Version' => Version::getVersionStringNormalized(), 'Time' => static fn(string $value) => self::assertTrue($value > 0), 'User' => 'test_user', 'Group' => 'test_group', 'Hash' => static fn(string $value) => self::assertNotEmpty($value), ] ); $this->assertNotEmpty($meta->item(5)->attributes->getNamedItem('sign')->nodeValue); $encrypted = $xml->documentElement->getElementsByTagName('Encrypted')->item(0); $this->assertStringStartsWith('$2y$10$', $encrypted->attributes->getNamedItem('hash')->nodeValue); $this->assertEquals(4, $encrypted->childNodes->count()); $this->assertEquals( 'encrypted_data_categories', $encrypted->childNodes->item(0)->childNodes->item(0)->nodeValue ); $this->assertNotEmpty($encrypted->childNodes->item(0)->attributes->getNamedItem('key')->nodeValue); $this->assertEquals( 'encrypted_data_clients', $encrypted->childNodes->item(1)->childNodes->item(0)->nodeValue ); $this->assertNotEmpty($encrypted->childNodes->item(1)->attributes->getNamedItem('key')->nodeValue); $this->assertEquals( 'encrypted_data_tags', $encrypted->childNodes->item(2)->childNodes->item(0)->nodeValue ); $this->assertNotEmpty($encrypted->childNodes->item(2)->attributes->getNamedItem('key')->nodeValue); $this->assertEquals( 'encrypted_data_accounts', $encrypted->childNodes->item(3)->childNodes->item(0)->nodeValue ); $this->assertNotEmpty($encrypted->childNodes->item(3)->attributes->getNamedItem('key')->nodeValue); } /** * @throws DOMException */ private function createNode(string $nodeName): DOMElement { return new DOMElement($nodeName, self::$faker->text()); } /** * @param string $password * @param int $times * @return void * @throws EnvironmentIsBrokenException */ private function checkCrypt(string $password, int $times = 4): void { $securedKey = KeyProtectedByPassword::createRandomPasswordProtectedKey($password); $this->crypt ->expects(self::exactly($times)) ->method('makeSecuredKey') ->with($password, false) ->willReturn($securedKey); $this->crypt ->expects(self::exactly($times)) ->method('encrypt') ->with( self::anything(), new Callback(static function (Key $key) use ($password, $securedKey) { return $key->saveToAsciiSafeString() === $securedKey->unlockKey($password)->saveToAsciiSafeString(); }) ) ->willReturn( 'encrypted_data_categories', 'encrypted_data_clients', 'encrypted_data_tags', 'encrypted_data_accounts' ); } /** * @throws CheckException * @throws Exception * @throws FileException * @throws ServiceException * @throws SPException */ public function testExportWithCheckDirectoryException() { $this->context->setUserData( UserDto::fromModel( UserDataGenerator::factory() ->buildUserData() ->mutate( [ 'login' => 'test_user', 'userGroup.name' => 'test_group', ] ) ) ); $exportPath = $this->createMock(DirectoryHandlerService::class); $exportPath->expects(self::once()) ->method('checkOrCreate') ->willThrowException(CheckException::error('test')); $this->expectException(CheckException::class); $this->expectExceptionMessage('test'); $this->xmlExport->export($exportPath); } /** * @throws CheckException * @throws Exception * @throws FileException * @throws ServiceException * @throws SPException */ public function testExportWithExportCategoryException() { $this->context->setUserData( UserDto::fromModel( UserDataGenerator::factory() ->buildUserData() ->mutate( [ 'login' => 'test_user', 'userGroupName' => 'test_group', ] ) ) ); $exportPath = $this->createMock(DirectoryHandlerService::class); $exportPath->expects(self::once()) ->method('checkOrCreate'); $exportPath->method('getPath') ->willReturn(TMP_PATH); $password = self::$faker->password(); $this->xmlCategoryExportService ->expects(self::once()) ->method('export') ->willThrowException(new RuntimeException('test')); $this->expectException(ServiceException::class); $this->expectExceptionMessage('Error while exporting'); $this->xmlExport->export($exportPath, $password); } /** * @throws CheckException * @throws Exception * @throws FileException * @throws ServiceException * @throws DOMException * @throws EnvironmentIsBrokenException * @throws SPException */ public function testExportWithExportClientException() { $this->context->setUserData( UserDto::fromModel( UserDataGenerator::factory() ->buildUserData() ->mutate( [ 'login' => 'test_user', 'userGroupName' => 'test_group', ] ) ) ); $exportPath = $this->createMock(DirectoryHandlerService::class); $exportPath->expects(self::once()) ->method('checkOrCreate'); $exportPath->method('getPath') ->willReturn(TMP_PATH); $this->xmlCategoryExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestCategories')); $this->xmlClientExportService ->expects(self::once()) ->method('export') ->willThrowException(new RuntimeException('test')); $password = self::$faker->password(); $this->checkCrypt($password, 1); $this->expectException(ServiceException::class); $this->expectExceptionMessage('Error while exporting'); $this->xmlExport->export($exportPath, $password); } /** * @throws CheckException * @throws Exception * @throws FileException * @throws ServiceException * @throws DOMException * @throws EnvironmentIsBrokenException * @throws SPException */ public function testExportWithExportTagException() { $this->context->setUserData( UserDto::fromModel( UserDataGenerator::factory() ->buildUserData() ->mutate( [ 'login' => 'test_user', 'userGroupName' => 'test_group', ] ) ) ); $exportPath = $this->createMock(DirectoryHandlerService::class); $exportPath->expects(self::once()) ->method('checkOrCreate'); $exportPath->method('getPath') ->willReturn(TMP_PATH); $this->xmlCategoryExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestCategories')); $this->xmlClientExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestClients')); $this->xmlTagExportService ->expects(self::once()) ->method('export') ->willThrowException(new RuntimeException('test')); $password = self::$faker->password(); $this->checkCrypt($password, 2); $this->expectException(ServiceException::class); $this->expectExceptionMessage('Error while exporting'); $this->xmlExport->export($exportPath, $password); } /** * @throws CheckException * @throws Exception * @throws FileException * @throws ServiceException * @throws DOMException * @throws EnvironmentIsBrokenException * @throws SPException */ public function testExportWithExportAccountException() { $this->context->setUserData( UserDto::fromModel( UserDataGenerator::factory() ->buildUserData() ->mutate( [ 'login' => 'test_user', 'userGroupName' => 'test_group', ] ) ) ); $exportPath = $this->createMock(DirectoryHandlerService::class); $exportPath->expects(self::once()) ->method('checkOrCreate'); $exportPath->method('getPath') ->willReturn(TMP_PATH); $this->xmlCategoryExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestCategories')); $this->xmlClientExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestClients')); $this->xmlTagExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestTags')); $this->xmlAccountExportService ->expects(self::once()) ->method('export') ->willThrowException(new RuntimeException('test')); $password = self::$faker->password(); $this->checkCrypt($password, 3); $this->expectException(ServiceException::class); $this->expectExceptionMessage('Error while exporting'); $this->xmlExport->export($exportPath, $password); } /** * @throws ServiceException * @throws Exception * @throws FileException * @throws CheckException * @throws DOMException * @throws SPException */ public function testExportWithCryptException() { $this->context->setUserData( UserDto::fromModel( UserDataGenerator::factory() ->buildUserData() ->mutate( [ 'login' => 'test_user', 'userGroupName' => 'test_group', ] ) ) ); $exportPath = $this->createMock(DirectoryHandlerService::class); $exportPath->expects(self::once()) ->method('checkOrCreate'); $exportPath->method('getPath') ->willReturn(TMP_PATH); $password = self::$faker->password(); $this->xmlCategoryExportService ->expects(self::once()) ->method('export') ->willReturn($this->createNode('TestCategories')); $this->crypt ->expects(self::once()) ->method('makeSecuredKey') ->willThrowException(CryptException::error('test')); $this->expectException(ServiceException::class); $this->expectExceptionMessage('test'); $this->xmlExport->export($exportPath, $password); } /** * @throws Exception * @throws ServiceException * @throws ContextException * @throws SPException */ protected function setUp(): void { parent::setUp(); $this->phpExtensionCheckerService = $this->createMock(PhpExtensionCheckerService::class); $this->xmlClientExportService = $this->createMock(XmlClientExportService::class); $this->xmlAccountExportService = $this->createMock(XmlAccountExportService::class); $this->xmlCategoryExportService = $this->createMock(XmlCategoryExportService::class); $this->xmlTagExportService = $this->createMock(XmlTagExportService::class); $this->crypt = $this->createMock(CryptInterface::class); $this->xmlExport = new XmlExport( $this->application, $this->phpExtensionCheckerService, $this->xmlClientExportService, $this->xmlAccountExportService, $this->xmlCategoryExportService, $this->xmlTagExportService, $this->crypt ); } }