From cb1e5f9e93dd4ac009fc6acc67b58ca6ce54ff1f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rub=C3=A9n=20D?=
+ * An offset to check for.
+ *
+ * The return value will be casted to boolean if non-boolean was returned. + */ + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->properties); + } + + /** + * Unset a non-class property + * + * @param mixed $offset + * + * @return void + */ + public function offsetUnset(mixed $offset): void + { + unset($this->properties[$offset]); } } diff --git a/lib/SP/Domain/Common/Services/Service.php b/lib/SP/Domain/Common/Services/Service.php index 5675918a..64c4226d 100644 --- a/lib/SP/Domain/Common/Services/Service.php +++ b/lib/SP/Domain/Common/Services/Service.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -64,7 +64,7 @@ abstract class Service if ($this->context instanceof SessionContext) { $key = Session::getSessionKey($this->context); } else { - $key = $this->context->getTrasientKey('_masterpass'); + $key = $this->context->getTrasientKey(ContextInterface::MASTER_PASSWORD_KEY); } if (empty($key)) { diff --git a/lib/SP/Infrastructure/Account/Repositories/PublicLinkRepository.php b/lib/SP/Infrastructure/Account/Repositories/PublicLinkRepository.php index c2c492ef..9ebf790a 100644 --- a/lib/SP/Infrastructure/Account/Repositories/PublicLinkRepository.php +++ b/lib/SP/Infrastructure/Account/Repositories/PublicLinkRepository.php @@ -29,6 +29,7 @@ use SP\Core\Exceptions\QueryException; use SP\Core\Exceptions\SPException; use SP\DataModel\ItemSearchData; use SP\DataModel\PublicLinkData; +use SP\Domain\Account\Ports\PublicLinkRepositoryInterface; use SP\Infrastructure\Common\Repositories\DuplicatedItemException; use SP\Infrastructure\Common\Repositories\Repository; use SP\Infrastructure\Common\Repositories\RepositoryItemTrait; @@ -41,7 +42,7 @@ use function SP\__u; * * @package SP\Infrastructure\Common\Repositories\PublicLink */ -final class PublicLinkRepository extends Repository implements \SP\Domain\Account\Ports\PublicLinkRepositoryInterface +final class PublicLinkRepository extends Repository implements PublicLinkRepositoryInterface { use RepositoryItemTrait; diff --git a/lib/SP/Infrastructure/Database/QueryResult.php b/lib/SP/Infrastructure/Database/QueryResult.php index 811a3368..6e5aeb79 100644 --- a/lib/SP/Infrastructure/Database/QueryResult.php +++ b/lib/SP/Infrastructure/Database/QueryResult.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -94,7 +94,7 @@ class QueryResult private function checkDataType(?string $dataType = null): void { if (null !== $dataType && $this->dataType !== null && $dataType !== $this->dataType) { - throw new SPException(sprintf(__u('Invalid data\'s type: %s - Expected: %s'), $dataType, $this->dataType)); + throw new SPException(sprintf(__u('Invalid data\'s type: %s - Current: %s'), $dataType, $this->dataType)); } } diff --git a/tests/SP/Domain/Account/Services/PublicLinkServiceTest.php b/tests/SP/Domain/Account/Services/PublicLinkServiceTest.php new file mode 100644 index 00000000..b98aa7c1 --- /dev/null +++ b/tests/SP/Domain/Account/Services/PublicLinkServiceTest.php @@ -0,0 +1,582 @@ +. + */ + +namespace SP\Tests\Domain\Account\Services; + +use PHPUnit\Framework\Constraint\Callback; +use PHPUnit\Framework\MockObject\MockObject; +use SP\Core\Context\ContextInterface; +use SP\Core\Crypt\CryptInterface; +use SP\DataModel\ItemSearchData; +use SP\DataModel\PublicLinkData; +use SP\Domain\Account\Ports\AccountServiceInterface; +use SP\Domain\Account\Ports\PublicLinkRepositoryInterface; +use SP\Domain\Account\Services\PublicLinkService; +use SP\Domain\Common\Models\Simple; +use SP\Domain\Common\Services\ServiceException; +use SP\Http\RequestInterface; +use SP\Infrastructure\Common\Repositories\NoSuchItemException; +use SP\Infrastructure\Database\QueryResult; +use SP\Tests\Generators\PublicLinkDataGenerator; +use SP\Tests\UnitaryTestCase; + +/** + * Class PublicLinkServiceTest + * + * @group unitary + */ +class PublicLinkServiceTest extends UnitaryTestCase +{ + + private PublicLinkRepositoryInterface|MockObject $publicLinkRepository; + private RequestInterface|MockObject $request; + private MockObject|PublicLinkService $publicLinkService; + private CryptInterface|MockObject $crypt; + private MockObject|AccountServiceInterface $accountService; + + /** + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Domain\Common\Services\ServiceException + */ + public function testAddLinkView() + { + $publicLinkData = new PublicLinkData(); + $publicLinkData->setHash(self::$faker->sha1); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('addLinkView') + ->with( + new Callback(function (PublicLinkData $publicLinkData) { + $useInfo = unserialize($publicLinkData->getUseInfo(), ['allowed_classes' => false]); + + return is_array($useInfo) && count($useInfo) === 1; + }) + ); + + $this->publicLinkService->addLinkView($publicLinkData); + } + + /** + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Domain\Common\Services\ServiceException + */ + public function testAddLinkViewWithoutHash() + { + $publicLinkData = new PublicLinkData(); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Public link hash not set'); + + $this->publicLinkService->addLinkView($publicLinkData); + } + + /** + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Domain\Common\Services\ServiceException + */ + public function testAddLinkViewWithUseInfo() + { + $publicLinkData = new PublicLinkData(); + $publicLinkData->setHash(self::$faker->sha1); + $publicLinkData->setUseInfo([ + [ + 'who' => self::$faker->ipv4, + 'time' => time(), + 'hash' => self::$faker->sha1, + 'agent' => self::$faker->userAgent, + 'https' => self::$faker->boolean, + ], + ]); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('addLinkView') + ->with( + new Callback(function (PublicLinkData $publicLinkData) { + $useInfo = unserialize($publicLinkData->getUseInfo(), ['allowed_classes' => false]); + + return is_array($useInfo) && count($useInfo) === 2; + }) + ); + + $this->publicLinkService->addLinkView($publicLinkData); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testGetByHash() + { + $hash = self::$faker->sha1; + $publicLink = PublicLinkDataGenerator::factory()->buildPublicLink(); + $result = new QueryResult([new Simple($publicLink->toArray())]); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('getByHash') + ->with($hash) + ->willReturn($result); + + $actual = $this->publicLinkService->getByHash($hash); + + $this->assertEquals($publicLink, $actual); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testGetByHashNotFound() + { + $hash = self::$faker->sha1; + + $this->publicLinkRepository + ->expects(self::once()) + ->method('getByHash') + ->with($hash) + ->willReturn(new QueryResult([])); + + $this->expectException(NoSuchItemException::class); + $this->expectExceptionMessage('Link not found'); + + $this->publicLinkService->getByHash($hash); + } + + /** + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Domain\Common\Services\ServiceException + */ + public function testDeleteByIdBatch() + { + $ids = array_map(fn() => self::$faker->randomNumber(), range(0, 9)); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('deleteByIdBatch') + ->with($ids) + ->willReturn(10); + + $actual = $this->publicLinkService->deleteByIdBatch($ids); + + $this->assertEquals(count($ids), $actual); + } + + /** + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Domain\Common\Services\ServiceException + */ + public function testDeleteByIdBatchWithCountMismatch() + { + $ids = array_map(fn() => self::$faker->randomNumber(), range(0, 9)); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('deleteByIdBatch') + ->with($ids) + ->willReturn(1); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Error while removing the links'); + + $this->publicLinkService->deleteByIdBatch($ids); + } + + public function testCreateLinkHash() + { + $this->assertNotEmpty(PublicLinkService::createLinkHash()); + } + + /** + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\SPException + */ + public function testUpdate() + { + $publicLinkList = PublicLinkDataGenerator::factory()->buildPublicLinkList(); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('update') + ->with($publicLinkList); + + $this->publicLinkService->update($publicLinkList); + } + + /** + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testDelete() + { + $id = self::$faker->randomNumber(); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('delete') + ->with($id); + + $this->publicLinkService->delete($id); + } + + public function testSearch() + { + $itemSearchData = new ItemSearchData(self::$faker->colorName); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('search') + ->with($itemSearchData); + + $this->publicLinkService->search($itemSearchData); + } + + /** + * @throws \SP\Infrastructure\Common\Repositories\NoSuchItemException + * @throws \SP\Core\Exceptions\SPException + */ + public function testGetHashForItem() + { + $itemId = self::$faker->randomNumber(); + $publicLinkData = PublicLinkDataGenerator::factory()->buildPublicLink(); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('getHashForItem') + ->with($itemId) + ->willReturn(new QueryResult([new Simple($publicLinkData->toArray())])); + + $actual = $this->publicLinkService->getHashForItem($itemId); + + $this->assertEquals($publicLinkData, $actual); + } + + /** + * @throws \SP\Infrastructure\Common\Repositories\NoSuchItemException + * @throws \SP\Core\Exceptions\SPException + */ + public function testGetHashForItemNotFound() + { + $itemId = self::$faker->randomNumber(); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('getHashForItem') + ->with($itemId) + ->willReturn(new QueryResult([])); + + $this->expectException(NoSuchItemException::class); + $this->expectExceptionMessage('Link not found'); + + $this->publicLinkService->getHashForItem($itemId); + } + + /** + * @throws \Defuse\Crypto\Exception\CryptoException + * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException + * @throws \SP\Infrastructure\Common\Repositories\NoSuchItemException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\SPException + */ + public function testRefresh() + { + $id = self::$faker->randomNumber(); + $publicLinkData = PublicLinkDataGenerator::factory()->buildPublicLink(); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('getById') + ->with($id) + ->willReturn(new QueryResult([new Simple($publicLinkData->toArray())])); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('refresh') + ->with( + new Callback(function (PublicLinkData $actual) use ($publicLinkData) { + $filter = ['hash', 'dateExpire', 'maxCountViews', 'data']; + + return $actual->toArray(null, $filter) === $publicLinkData->toArray(null, $filter) + && !empty($actual->getHash()) + && !empty($actual->getDateExpire()) + && !empty($actual->getMaxCountViews()) + && !empty($actual->getData()); + }) + ) + ->willReturn(true); + + $passData = ['pass' => self::$faker->password, 'key' => self::$faker->sha1]; + + $this->accountService + ->expects(self::once()) + ->method('getDataForLink') + ->with($publicLinkData->getItemId()) + ->willReturn(new Simple($passData)); + + $this->crypt + ->expects(self::once()) + ->method('decrypt') + ->with( + $passData['pass'], + $passData['key'], + $this->context->getTrasientKey(ContextInterface::MASTER_PASSWORD_KEY) + ) + ->willReturn(self::$faker->password); + + $actual = $this->publicLinkService->refresh($id); + + $this->assertTrue($actual); + } + + /** + * @throws \Defuse\Crypto\Exception\CryptoException + * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException + * @throws \SP\Infrastructure\Common\Repositories\NoSuchItemException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\SPException + */ + public function testRefreshNotFound() + { + $id = self::$faker->randomNumber(); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('getById') + ->with($id) + ->willReturn(new QueryResult([])); + + $this->expectException(NoSuchItemException::class); + $this->expectExceptionMessage('Link not found'); + + $this->publicLinkService->refresh($id); + } + + /** + * @return void + * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException + */ + public function testGetPublicLinkKey() + { + $hash = self::$faker->sha1; + + $actual = $this->publicLinkService->getPublicLinkKey($hash); + + $this->assertEquals($hash, $actual->getHash()); + $this->assertNotEmpty($actual->getKey()); + } + + /** + * @return void + * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException + */ + public function testGetPublicLinkKeyWithoutHash() + { + $actual = $this->publicLinkService->getPublicLinkKey(); + + $this->assertNotEmpty($actual->getHash()); + $this->assertNotEmpty($actual->getKey()); + } + + /** + * @return void + * @throws \SP\Core\Exceptions\SPException + * @throws \SP\Infrastructure\Common\Repositories\NoSuchItemException + */ + public function testGetById() + { + $itemId = self::$faker->randomNumber(); + $builPublicLinkList = PublicLinkDataGenerator::factory()->buildPublicLinkList(); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('getById') + ->with($itemId) + ->willReturn(new QueryResult([new Simple($builPublicLinkList->toArray())])); + + $actual = $this->publicLinkService->getById($itemId); + + $this->assertEquals($builPublicLinkList->toArray(null, ['clientName']), $actual->toArray()); + } + + /** + * @return void + * @throws \SP\Core\Exceptions\SPException + * @throws \SP\Infrastructure\Common\Repositories\NoSuchItemException + */ + public function testGetByIdNotFound() + { + $itemId = self::$faker->randomNumber(); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('getById') + ->with($itemId) + ->willReturn(new QueryResult([])); + + $this->expectException(NoSuchItemException::class); + $this->expectExceptionMessage('Link not found'); + + $this->publicLinkService->getById($itemId); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testGetAllBasic() + { + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Not implemented'); + + $this->publicLinkService->getAllBasic(); + } + + public function testGetUseInfo() + { + $hash = self::$faker->sha1; + $who = self::$faker->ipv4; + $userAgent = self::$faker->userAgent; + + $request = $this->createMock(RequestInterface::class); + + $request->expects(self::once()) + ->method('getClientAddress') + ->with(true) + ->willReturn($who); + + $request->expects(self::once()) + ->method('getHeader') + ->with('User-Agent') + ->willReturn($userAgent); + + $request->expects(self::once()) + ->method('isHttps') + ->willReturn(true); + + $actual = PublicLinkService::getUseInfo($hash, $request); + + $this->assertArrayHasKey('who', $actual); + $this->assertArrayHasKey('time', $actual); + $this->assertArrayHasKey('hash', $actual); + $this->assertArrayHasKey('agent', $actual); + $this->assertArrayHasKey('https', $actual); + $this->assertEquals($who, $actual['who']); + $this->assertEquals($hash, $actual['hash']); + $this->assertEquals($userAgent, $actual['agent']); + $this->assertTrue($actual['https']); + } + + /** + * @return void + * @throws \Defuse\Crypto\Exception\CryptoException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreate() + { + $publicLinkData = PublicLinkDataGenerator::factory()->buildPublicLink(); + $result = new QueryResult(); + $result->setLastId(self::$faker->randomNumber()); + + $this->publicLinkRepository + ->expects(self::once()) + ->method('create') + ->with( + new Callback(function (PublicLinkData $actual) use ($publicLinkData) { + $filter = ['hash', 'dateExpire', 'maxCountViews', 'data']; + + return $actual->toArray(null, $filter) === $publicLinkData->toArray(null, $filter) + && !empty($actual->getHash()) + && !empty($actual->getDateExpire()) + && !empty($actual->getMaxCountViews()) + && !empty($actual->getData()); + }) + ) + ->willReturn($result); + + $passData = ['pass' => self::$faker->password, 'key' => self::$faker->sha1]; + + $this->accountService + ->expects(self::once()) + ->method('getDataForLink') + ->with($publicLinkData->getItemId()) + ->willReturn(new Simple($passData)); + + $this->crypt + ->expects(self::once()) + ->method('decrypt') + ->with( + $passData['pass'], + $passData['key'], + $this->context->getTrasientKey(ContextInterface::MASTER_PASSWORD_KEY) + ) + ->willReturn(self::$faker->password); + + $actual = $this->publicLinkService->create($publicLinkData); + + $this->assertEquals($result->getLastId(), $actual); + } + + public function testCalcDateExpire() + { + $expireDate = time() + $this->config->getConfigData()->getPublinksMaxTime(); + + $this->assertEqualsWithDelta($expireDate, PublicLinkService::calcDateExpire($this->config), 2); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->publicLinkRepository = $this->createMock(PublicLinkRepositoryInterface::class); + $this->request = $this->createMock(RequestInterface::class); + $this->request->method('getClientAddress') + ->willReturn(self::$faker->ipv4); + $this->request->method('getHeader') + ->willReturn(self::$faker->userAgent); + $this->request->method('isHttps') + ->willReturn(self::$faker->boolean); + + $this->accountService = $this->createMock(AccountServiceInterface::class); + $this->crypt = $this->createMock(CryptInterface::class); + + $this->publicLinkService = + new PublicLinkService( + $this->application, + $this->publicLinkRepository, + $this->request, + $this->accountService, + $this->crypt + ); + } +} diff --git a/tests/SP/Generators/PublicLinkDataGenerator.php b/tests/SP/Generators/PublicLinkDataGenerator.php new file mode 100644 index 00000000..3514233a --- /dev/null +++ b/tests/SP/Generators/PublicLinkDataGenerator.php @@ -0,0 +1,85 @@ +. + */ + +namespace SP\Tests\Generators; + +use SP\DataModel\PublicLinkData; +use SP\DataModel\PublicLinkListData; + +/** + * Class PublicLinkDataGenerator + */ +final class PublicLinkDataGenerator extends DataGenerator +{ + public function buildPublicLink(): PublicLinkData + { + return new PublicLinkData($this->getPublicLinkProperties()); + } + + private function getPublicLinkProperties(): array + { + return [ + 'id' => $this->faker->randomNumber(), + 'itemId' => $this->faker->randomNumber(), + 'hash' => $this->faker->randomNumber(), + 'userId' => $this->faker->randomNumber(), + 'typeId' => $this->faker->randomNumber(), + 'notify' => $this->faker->boolean, + 'dateAdd' => $this->faker->unixTime(), + 'dateUpdate' => $this->faker->unixTime(), + 'dateExpire' => $this->faker->unixTime(), + 'countViews' => $this->faker->randomNumber(), + 'totalCountViews' => $this->faker->randomNumber(), + 'maxCountViews' => $this->faker->randomNumber(), + 'useInfo' => serialize($this->getUseInfo()), + 'data' => $this->faker->text, + ]; + } + + private function getUseInfo(): array + { + return array_map( + fn() => [ + 'who' => $this->faker->ipv4, + 'time' => $this->faker->unixTime, + 'hash' => $this->faker->sha1, + 'agent' => $this->faker->userAgent, + 'https' => $this->faker->boolean, + ], + range(0, 9) + ); + } + + public function buildPublicLinkList(): PublicLinkListData + { + return new PublicLinkListData( + array_merge($this->getPublicLinkProperties(), [ + 'userName' => $this->faker->name, + 'userLogin' => $this->faker->userName, + 'accountName' => $this->faker->colorName, + 'clientName' => $this->faker->company, + ]) + ); + } +} diff --git a/tests/SP/UnitaryTestCase.php b/tests/SP/UnitaryTestCase.php index 5ebd53e1..0b20c8a3 100644 --- a/tests/SP/UnitaryTestCase.php +++ b/tests/SP/UnitaryTestCase.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -86,6 +86,7 @@ abstract class UnitaryTestCase extends TestCase $this->context->initialize(); $this->context->setUserData($userLogin); $this->context->setUserProfile(new ProfileData()); + $this->context->setTrasientKey(ContextInterface::MASTER_PASSWORD_KEY, self::$faker->password); $configData = ConfigDataGenerator::factory()->buildConfigData();