From 98f82ee2e9bf8fbd35fe1cb0ac2610760eef40c4 Mon Sep 17 00:00:00 2001 From: Artur Neumann Date: Thu, 10 Mar 2022 13:58:48 +0545 Subject: [PATCH] link file to workpackage backend Signed-off-by: Artur Neumann --- .gitignore | 1 + bootstrap.php | 1 + lib/Controller/OpenProjectAPIController.php | 43 ++- lib/Exception/OpenprojectErrorException.php | 18 + .../OpenprojectResponseException.php | 18 + lib/Service/OpenProjectAPIService.php | 106 +++++- phpunit.xml | 8 +- src/components/tab/SearchInput.vue | 13 +- .../OpenProjectAPIControllerTest.php | 37 +- .../lib/Service/OpenProjectAPIServiceTest.php | 351 +++++++++++++++++- 10 files changed, 557 insertions(+), 39 deletions(-) create mode 100644 lib/Exception/OpenprojectErrorException.php create mode 100644 lib/Exception/OpenprojectResponseException.php diff --git a/.gitignore b/.gitignore index 495d66429..c894178f9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ example/ log/ vendor/ .php-cs-fixer.cache +tests/pact/ diff --git a/bootstrap.php b/bootstrap.php index c10f1812f..e19830421 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -19,6 +19,7 @@ $classLoader->addPsr4("OCA\\Files\\Event\\", $serverPath . '/apps/files/lib/Event', true); $classLoader->addPsr4("OCA\\OpenProject\\AppInfo\\", __DIR__ . '/lib/AppInfo', true); $classLoader->addPsr4("OCA\\OpenProject\\Controller\\", __DIR__ . '/lib/Controller', true); +$classLoader->addPsr4("OCA\\OpenProject\\Exception\\", __DIR__ . '/lib/Exception', true); $classLoader->register(); set_include_path(get_include_path() . PATH_SEPARATOR . '/usr/share/php'); diff --git a/lib/Controller/OpenProjectAPIController.php b/lib/Controller/OpenProjectAPIController.php index 2530308a7..f5ad91e66 100644 --- a/lib/Controller/OpenProjectAPIController.php +++ b/lib/Controller/OpenProjectAPIController.php @@ -11,8 +11,12 @@ namespace OCA\OpenProject\Controller; +use Exception; +use OCA\OpenProject\Exception\OpenprojectErrorException; use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataDownloadResponse; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; use OCP\IConfig; use OCP\IRequest; use OCP\AppFramework\Http; @@ -21,6 +25,7 @@ use OCA\OpenProject\Service\OpenProjectAPIService; use OCA\OpenProject\AppInfo\Application; +use OCP\IURLGenerator; class OpenProjectAPIController extends Controller { @@ -53,10 +58,16 @@ class OpenProjectAPIController extends Controller { */ private $openprojectUrl; + /** + * @var IURLGenerator + */ + private $urlGenerator; + public function __construct(string $appName, IRequest $request, IConfig $config, OpenProjectAPIService $openprojectAPIService, + IURLGenerator $urlGenerator, ?string $userId) { parent::__construct($appName, $request); $this->openprojectAPIService = $openprojectAPIService; @@ -66,6 +77,7 @@ public function __construct(string $appName, $this->clientID = $config->getAppValue(Application::APP_ID, 'client_id'); $this->clientSecret = $config->getAppValue(Application::APP_ID, 'client_secret'); $this->openprojectUrl = $config->getAppValue(Application::APP_ID, 'oauth_instance_url'); + $this->urlGenerator = $urlGenerator; } /** @@ -168,10 +180,37 @@ public function getSearchedWorkPackages(?string $searchQuery = null, ?int $fileI * @NoAdminRequired * @param int $workpackageId * @param int $fileId + * @param string $fileName * @return DataResponse */ - public function linkWorkPackageToFile(int $workpackageId = 0, int $fileId = 0): DataResponse { - return new DataResponse("Fake result, to make UI happy"); + public function linkWorkPackageToFile(int $workpackageId, int $fileId, string $fileName) { + if ($this->accessToken === '' || !OpenProjectAPIService::validateOpenProjectURL($this->openprojectUrl)) { + return new DataResponse('', Http::STATUS_BAD_REQUEST); + } + + $storageUrl = $this->urlGenerator->getBaseUrl(); + + try { + $result = $this->openprojectAPIService->linkWorkPackageToFile( + $this->openprojectUrl, + $this->accessToken, + $this->refreshToken, + $this->clientID, + $this->clientSecret, + $workpackageId, + $fileId, + $fileName, + $storageUrl, + $this->userId, + ); + } catch (OpenprojectErrorException $e) { + return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST); + } catch (NotPermittedException | NotFoundException $e) { + return new DataResponse('fileid not found', Http::STATUS_NOT_FOUND); + } catch (Exception $e) { + return new DataResponse($e->getMessage(), Http::STATUS_INTERNAL_SERVER_ERROR); + } + return new DataResponse($result); } /** diff --git a/lib/Exception/OpenprojectErrorException.php b/lib/Exception/OpenprojectErrorException.php new file mode 100644 index 000000000..595b54417 --- /dev/null +++ b/lib/Exception/OpenprojectErrorException.php @@ -0,0 +1,18 @@ +appName = $appName; $this->userManager = $userManager; $this->avatarManager = $avatarManager; @@ -83,6 +93,7 @@ public function __construct( $this->config = $config; $this->notificationManager = $notificationManager; $this->client = $clientService->newClient(); + $this->storage = $storage; } /** @@ -387,7 +398,8 @@ public function request(string $openprojectUrl, string $accessToken, string $ref $paramsContent .= http_build_query($params); $url .= '?' . $paramsContent; } else { - $options['body'] = $params; + $options['body'] = $params['body']; + $options['headers']['Content-Type'] = 'application/json'; } } @@ -581,4 +593,94 @@ public static function getOpenProjectOauthURL(IConfig $config, IURLGenerator $ur '&redirect_uri=' . urlencode($redirectUri) . '&response_type=code'; } + + /** + * @param string $userId + * @param int $fileId + * @return \OCP\Files\Node|null + * @throws NotPermittedException + * @throws \OC\User\NoUserException + */ + public function getFile($userId, $fileId) { + $userFolder = $this->storage->getUserFolder($userId); + + $file = $userFolder->getById($fileId); + return $file[0]; + } + + /** + * @throws \OCP\Files\InvalidPathException + * @throws NotFoundException + * @throws \OCP\PreConditionNotMetException + * @throws NotPermittedException + * @throws OpenprojectErrorException + * @throws \OC\User\NoUserException + * @throws OpenprojectResponseException + * @return int + */ + public function linkWorkPackageToFile( + string $openprojectUrl, + string $accessToken, + string $refreshToken, + string $clientID, + string $clientSecret, + int $workpackageId, + int $fileId, + string $fileName, + string $storageUrl, + string $userId + ) { + $file = $this->getFile($userId, $fileId); + if ($file instanceof File) { + if (!$file->isReadable()) { + throw new NotPermittedException(); + } + } else { + throw new NotFoundException(); + } + + $body = [ + '_type' => 'Collection', + '_embedded' => [ + 'elements' => [ + [ + 'originData' => [ + 'id' => $fileId, + 'name' => $fileName, + 'mimeType' => $file->getMimeType(), + 'createdAt' => gmdate('Y-m-d\TH:i:s.000\Z', $file->getCreationTime()), + 'lastModifiedAt' => gmdate('Y-m-d\TH:i:s.000\Z', $file->getMTime()), + 'createdByName' => '', + 'lastModifiedByName' => '' + ], + '_links' => [ + 'storageUrl' => [ + 'href' => $storageUrl + ] + ] + ] + ] + ] + ]; + + $params['body'] = \Safe\json_encode($body); + $result = $this->request( + $openprojectUrl, $accessToken, $refreshToken, $clientID, $clientSecret, $userId, 'work_packages/' . $workpackageId. '/file_links', $params, 'POST' + ); + + if (isset($result['error'])) { + throw new OpenprojectErrorException($result['error']); + } + if ( + !isset($result['_type']) || + $result['_type'] !== 'Collection' || + !isset($result['_embedded']) || + !isset($result['_embedded']['elements']) || + !isset($result['_embedded']['elements'][0]) || + !isset($result['_embedded']['elements'][0]['id']) + ) { + throw new OpenprojectResponseException('Malformed response'); + } + return $result['_embedded']['elements'][0]['id']; + } } diff --git a/phpunit.xml b/phpunit.xml index 3dd438d70..c51b08dc5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -34,12 +34,14 @@ - + - - + + + + diff --git a/src/components/tab/SearchInput.vue b/src/components/tab/SearchInput.vue index 110784c75..14192933c 100644 --- a/src/components/tab/SearchInput.vue +++ b/src/components/tab/SearchInput.vue @@ -103,16 +103,19 @@ export default { : null }, async linkWorkPackageToFile(selectedOption) { - const req = { - values: { - workpackageId: selectedOption.id, - fileId: this.fileInfo.id, + const params = new URLSearchParams() + params.append('workpackageId', selectedOption.id) + params.append('fileId', this.fileInfo.id) + params.append('fileName', this.fileInfo.name) + const config = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', }, } const url = generateUrl('/apps/integration_openproject/work-packages') try { - await axios.post(url, req) + await axios.post(url, params, config) this.$emit('saved', selectedOption) this.selectedId.push({ id: selectedOption.id, diff --git a/tests/lib/Controller/OpenProjectAPIControllerTest.php b/tests/lib/Controller/OpenProjectAPIControllerTest.php index 6cb99a3a6..74da9692d 100644 --- a/tests/lib/Controller/OpenProjectAPIControllerTest.php +++ b/tests/lib/Controller/OpenProjectAPIControllerTest.php @@ -13,6 +13,7 @@ use OCP\IConfig; use OCP\IRequest; use OCP\AppFramework\Http; +use OCP\IURLGenerator; use PHPUnit\Framework\TestCase; class OpenProjectAPIControllerTest extends TestCase { @@ -22,6 +23,11 @@ class OpenProjectAPIControllerTest extends TestCase { /** @var IRequest $requestMock */ private $requestMock; + /** + * @var IURLGenerator + */ + private $urlGeneratorMock; + /** * @return void * @before @@ -36,6 +42,10 @@ public function setUpMocks(): void { ['integration_openproject', 'client_secret'], ['integration_openproject', 'oauth_instance_url'], )->willReturnOnConsecutiveCalls('cliendID', 'clientSecret', 'http://openproject.org'); + $this->urlGeneratorMock = $this->getMockBuilder(IURLGenerator::class)->getMock(); + $this->urlGeneratorMock + ->method('getBaseUrl') + ->willReturn('http://nextcloud.org/'); } /** @@ -66,7 +76,7 @@ public function testGetNotifications() { ->willReturn(['some' => 'data']); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test', ); $response = $controller->getNotifications(); $this->assertSame(Http::STATUS_OK, $response->getStatus()); @@ -80,7 +90,7 @@ public function testGetNotificationsNoAccessToken() { $this->getUserValueMock(''); $service = $this->createMock(OpenProjectAPIService::class); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getNotifications(); $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus()); @@ -99,7 +109,7 @@ public function testGetNotificationsErrorResponse() { ->willReturn(['error' => 'something went wrong']); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getNotifications(); $this->assertSame(Http::STATUS_UNAUTHORIZED, $response->getStatus()); @@ -130,6 +140,7 @@ public function testGetOpenProjectAvatar() { $this->requestMock, $this->configMock, $service, + $this->urlGeneratorMock, 'test' ); $response = $controller->getOpenProjectAvatar('id', 'name'); @@ -164,7 +175,7 @@ public function testGetOpenProjectAvatarNoType() { ) ->willReturn(['avatar' => 'some image data']); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getOpenProjectAvatar('id', 'name'); $this->assertSame('some image data', $response->render()); @@ -214,7 +225,7 @@ public function testGetSearchedWorkPackages($searchQuery, $fileId, array $expect ->willReturn($expectedResponse); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getSearchedWorkPackages($searchQuery, $fileId); $this->assertSame(Http::STATUS_OK, $response->getStatus()); @@ -229,7 +240,7 @@ public function testGetSearchedWorkPackagesNoAccessToken(): void { $this->getUserValueMock(''); $service = $this->createMock(OpenProjectAPIService::class); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getSearchedWorkPackages('test'); $this->assertSame(Http::STATUS_UNAUTHORIZED, $response->getStatus()); @@ -248,7 +259,7 @@ public function testGetSearchedWorkPackagesErrorResponse(): void { ->willReturn(['error' => 'something went wrong']); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getSearchedWorkPackages('test'); $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus()); @@ -272,7 +283,7 @@ public function testGetOpenProjectWorkPackageStatus(): void { ]); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getOpenProjectWorkPackageStatus('7'); $this->assertSame(Http::STATUS_OK, $response->getStatus()); @@ -289,7 +300,7 @@ public function testGetOpenProjectWorkPackageStatusErrorResponse(): void { $this->getUserValueMock(''); $service = $this->createMock(OpenProjectAPIService::class); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getOpenProjectWorkPackageStatus('7'); $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus()); @@ -308,7 +319,7 @@ public function testGetOpenProjectWorkPackageStatusNoAccessToken(): void { ->willReturn(['error' => 'something went wrong']); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getOpenProjectWorkPackageStatus('7'); $this->assertSame(Http::STATUS_UNAUTHORIZED, $response->getStatus()); @@ -330,7 +341,7 @@ public function testGetOpenProjectWorkPackageType(): void { "color" => "#CC5DE8", "position" => 4, "isDefault" => true, "isMilestone" => false, "createdAt" => "2022-01-12T08:53:15Z", "updatedAt" => "2022-01-12T08:53:34Z"]); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getOpenProjectWorkPackageType('3'); $this->assertSame(Http::STATUS_OK, $response->getStatus()); @@ -345,7 +356,7 @@ public function testGetOpenProjectWorkPackageTypeErrorResponse(): void { $this->getUserValueMock(''); $service = $this->createMock(OpenProjectAPIService::class); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getOpenProjectWorkPackageType('3'); $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus()); @@ -364,7 +375,7 @@ public function testGetOpenProjectWorkPackageTypeNoAccessToken(): void { ->willReturn(['error' => 'something went wrong']); $controller = new OpenProjectAPIController( - 'integration_openproject', $this->requestMock, $this->configMock, $service, 'test' + 'integration_openproject', $this->requestMock, $this->configMock, $service, $this->urlGeneratorMock, 'test' ); $response = $controller->getOpenProjectWorkPackageType('3'); $this->assertSame(Http::STATUS_UNAUTHORIZED, $response->getStatus()); diff --git a/tests/lib/Service/OpenProjectAPIServiceTest.php b/tests/lib/Service/OpenProjectAPIServiceTest.php index d473e587d..1408c7f28 100644 --- a/tests/lib/Service/OpenProjectAPIServiceTest.php +++ b/tests/lib/Service/OpenProjectAPIServiceTest.php @@ -16,7 +16,8 @@ use GuzzleHttp\Exception\ConnectException; use OC\Avatar\GuestAvatar; use OC\Http\Client\Client; -use OCP\ICertificateManager; +use OCA\OpenProject\Exception\OpenprojectErrorException; +use OCP\Files\IRootFolder; use OCP\IConfig; use OCP\ILogger; use OCP\IURLGenerator; @@ -24,7 +25,6 @@ use PhpPact\Consumer\Model\ConsumerRequest; use PhpPact\Consumer\Model\ProviderResponse; use PhpPact\Standalone\MockService\MockServerEnvConfig; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use OCP\AppFramework\Http; @@ -61,10 +61,31 @@ class OpenProjectAPIServiceTest extends TestCase { private $workPackagesPath = '/api/v3/work_packages'; /** - * @var \OCP\IAvatarManager|MockObject + * @var array */ - private $avatarManagerMock; - + private $validFileLinkRequestBody = [ + '_type' => 'Collection', + '_embedded' => [ + 'elements' => [ + [ + 'originData' => [ + 'id' => 5503, + 'name' => 'logo.png', + 'mimeType' => 'image/png', + 'createdAt' => '2021-12-19T09:42:10.000Z', + 'lastModifiedAt' => '2021-12-20T14:00:13.000Z', + 'createdByName' => '', + 'lastModifiedByName' => '' + ], + '_links' => [ + 'storageUrl' => [ + 'href' => 'http://nextcloud.org' + ] + ] + ] + ] + ] + ]; /** * @return void * @before @@ -80,11 +101,44 @@ public function setupMockServer(): void { * @before */ public function setUpMocks(): void { - /** @var IConfig $config */ + $this->service = $this->getOpenProjectAPIService(); + } + + /** + * @return \OCP\Files\File + */ + private function getFileMock() { + $fileMock = $this->getMockBuilder('\OCP\Files\File')->getMock(); + $fileMock->method('isReadable')->willReturn(true); + + $fileMock->method('getName')->willReturn('logo.png'); + $fileMock->method('getMimeType')->willReturn('image/png'); + $fileMock->method('getCreationTime')->willReturn(1639906930); + $fileMock->method('getMTime')->willReturn(1640008813); + return $fileMock; + } + + /** + * @return IRootFolder + */ + private function getStorageMock() { + $fileMock = $this->getFileMock(); + + $folderMock = $this->getMockBuilder('\OCP\Files\Folder')->getMock(); + $folderMock->method('getById')->willReturn([$fileMock]); + + $storageMock = $this->getMockBuilder('\OCP\Files\IRootFolder')->getMock(); + $storageMock->method('getUserFolder')->willReturn($folderMock); + return $storageMock; + } + + /** + * @param IRootFolder|null $storageMock + * @return OpenProjectAPIService + */ + private function getOpenProjectAPIService($storageMock = null) { $config = $this->createMock(IConfig::class); - /** @var ICertificateManager $certificateManager */ $certificateManager = $this->getMockBuilder('\OCP\ICertificateManager')->getMock(); - // @phpstan-ignore-next-line $certificateManager->method('getAbsoluteBundlePath')->willReturn('/'); $logger = $this->createMock(ILogger::class); @@ -99,9 +153,9 @@ public function setUpMocks(): void { $clientService = $this->getMockBuilder('\OCP\Http\Client\IClientService')->getMock(); $clientService->method('newClient')->willReturn($ocClient); - $this->avatarManagerMock = $this->getMockBuilder('\OCP\IAvatarManager') + $avatarManagerMock = $this->getMockBuilder('\OCP\IAvatarManager') ->getMock(); - $this->avatarManagerMock + $avatarManagerMock ->method('getGuestAvatar') ->willReturn( new GuestAvatar( @@ -109,15 +163,20 @@ public function setUpMocks(): void { $this->createMock(\Psr\Log\LoggerInterface::class) ) ); - $this->service = new OpenProjectAPIService( + if ($storageMock === null) { + $storageMock = $this->createMock(\OCP\Files\IRootFolder::class); + } + + return new OpenProjectAPIService( 'integration_openproject', $this->createMock(\OCP\IUserManager::class), - $this->avatarManagerMock, + $avatarManagerMock, $this->createMock(\Psr\Log\LoggerInterface::class), $this->createMock(\OCP\IL10N::class), $this->createMock(\OCP\IConfig::class), $this->createMock(\OCP\Notification\IManager::class), - $clientService + $clientService, + $storageMock ); } @@ -821,6 +880,31 @@ public function connectExpectionDataProvider() { ]; } + /** + * @return void + */ + public function testLinkWorkPackageToFileRequest(): void { + $service = $this->getServiceMock(['request', 'getFile']); + + $service->method('getFile') + ->willReturn($this->getFileMock()); + $service->method('request') + ->willReturn(['_type' => 'Collection', '_embedded' => ['elements' => [['id' => 2456]]]]); + + $service->expects($this->once()) + ->method('request') + ->with( + 'url', 'token', 'refresh', 'id', 'secret', 'user', 'work_packages/123/file_links', + ['body' => \Safe\json_encode($this->validFileLinkRequestBody)] + ); + + $result = $service->linkWorkPackageToFile( + 'url', 'token', 'refresh', 'id', 'secret', + 123, 5503, 'logo.png', 'http://nextcloud.org', 'user' + ); + $this->assertSame(2456, $result); + } + /** * @return void * @param \Exception $exception @@ -848,11 +932,250 @@ public function testRequestException( $this->createMock(\OCP\IL10N::class), $this->createMock(\OCP\IConfig::class), $this->createMock(\OCP\Notification\IManager::class), - $clientService + $clientService, + $this->createMock(\OCP\Files\IRootFolder::class), ); $response = $service->request('', '', '', '', '', '', '', []); $this->assertSame($expectedError, $response['error']); $this->assertSame($expectedHttpStatusCode, $response['statusCode']); } + + public function testLinkWorkPackageToFilePact(): void { + $consumerRequest = new ConsumerRequest(); + $consumerRequest + ->setMethod('POST') + ->setPath($this->workPackagesPath . '/123/file_links') + ->setHeaders(['Authorization' => 'Bearer 1234567890']) + ->setBody($this->validFileLinkRequestBody); + $providerResponse = new ProviderResponse(); + $providerResponse + ->setStatus(Http::STATUS_OK) + ->addHeader('Content-Type', 'application/json') + ->setBody(['_type' => 'Collection', '_embedded' => ['elements' => [['id' => 1337]]]]); + + $this->builder + ->uponReceiving('a POST request to /work_packages') + ->with($consumerRequest) + ->willRespondWith($providerResponse); + + $storageMock = $this->getStorageMock(); + + $service = $this->getOpenProjectAPIService($storageMock); + + $result = $service->linkWorkPackageToFile( + $this->mockServerBaseUri, + '1234567890', + '', + $this->clientId, + $this->clientSecret, + 123, + 5503, + 'logo.png', + 'http://nextcloud.org', + 'admin' + ); + + $this->assertSame(1337, $result); + } + + /** + * @return void + */ + public function testLinkWorkPackageToFileEmptyStorageUrlPact(): void { + $consumerRequest = new ConsumerRequest(); + $consumerRequest + ->setMethod('POST') + ->setPath($this->workPackagesPath . '/123/file_links') + ->setHeaders(['Authorization' => 'Bearer 1234567890']) + ->setBody([ + '_type' => 'Collection', + '_embedded' => [ + 'elements' => [ + [ + 'originData' => $this->validFileLinkRequestBody['_embedded']['elements'][0]['originData'], + '_links' => [ + 'storageUrl' => [ + 'href' => '' + ] + ] + ] + ] + ] + ]); + $providerResponse = new ProviderResponse(); + $providerResponse + ->setStatus(Http::STATUS_BAD_REQUEST) + ->addHeader('Content-Type', 'application/json') + ->setBody([ + '_type' => 'Error', + 'errorIdentifier' => 'urn:openproject-org:api:v3:errors:InvalidRequestBody', + 'message' => 'The request body was invalid.' + ]); + + $this->builder + ->uponReceiving('a POST request to /work_packages with empty storage URL') + ->with($consumerRequest) + ->willRespondWith($providerResponse); + + $storageMock = $this->getStorageMock(); + $service = $this->getOpenProjectAPIService($storageMock); + + $this->expectException(OpenprojectErrorException::class); + $service->linkWorkPackageToFile( + $this->mockServerBaseUri, + '1234567890', + '', + $this->clientId, + $this->clientSecret, + 123, + 5503, + 'logo.png', + '', + 'admin' + ); + } + + /** + * @return void + */ + public function testLinkWorkPackageToFileNotAvailableStorageUrlPact(): void { + $consumerRequest = new ConsumerRequest(); + $consumerRequest + ->setMethod('POST') + ->setPath($this->workPackagesPath . '/123/file_links') + ->setHeaders(['Authorization' => 'Bearer 1234567890']) + ->setBody([ + '_type' => 'Collection', + '_embedded' => [ + 'elements' => [ + [ + 'originData' => $this->validFileLinkRequestBody['_embedded']['elements'][0]['originData'], + '_links' => [ + 'storageUrl' => [ + 'href' => 'http://not-existing' + ] + ] + ] + ] + ] + ]); + $providerResponse = new ProviderResponse(); + $providerResponse + ->setStatus(Http::STATUS_UNPROCESSABLE_ENTITY) + ->addHeader('Content-Type', 'application/json') + ->setBody([ + '_type' => 'Error', + 'errorIdentifier' => 'urn:openproject-org:api:v3:errors:PropertyConstraintViolation', + 'message' => 'The request was invalid. File Link logo.png - Storage was invalid.' + ]); + + $this->builder + ->uponReceiving('a POST request to /work_packages with a not available storage URL') + ->with($consumerRequest) + ->willRespondWith($providerResponse); + + $storageMock = $this->getStorageMock(); + $service = $this->getOpenProjectAPIService($storageMock); + + $this->expectException(OpenprojectErrorException::class); + $service->linkWorkPackageToFile( + $this->mockServerBaseUri, + '1234567890', + '', + $this->clientId, + $this->clientSecret, + 123, + 5503, + 'logo.png', + 'http://not-existing', + 'admin' + ); + } + + /** + * @return void + */ + public function testLinkWorkPackageToFileMissingPermissionPact(): void { + $consumerRequest = new ConsumerRequest(); + $consumerRequest + ->setMethod('POST') + ->setPath($this->workPackagesPath . '/123/file_links') + ->setHeaders(['Authorization' => 'Bearer MissingPermission']) + ->setBody($this->validFileLinkRequestBody); + $providerResponse = new ProviderResponse(); + $providerResponse + ->setStatus(Http::STATUS_FORBIDDEN) + ->addHeader('Content-Type', 'application/json') + ->setBody([ + '_type' => 'Error', + 'errorIdentifier' => 'urn:openproject-org:api:v3:errors:MissingPermission', + 'message' => 'You are not authorized to access this resource.' + ]); + + $this->builder + ->uponReceiving('a POST request to /work_packages but missing permission') + ->with($consumerRequest) + ->willRespondWith($providerResponse); + + $storageMock = $this->getStorageMock(); + $service = $this->getOpenProjectAPIService($storageMock); + + $this->expectException(OpenprojectErrorException::class); + $service->linkWorkPackageToFile( + $this->mockServerBaseUri, + 'MissingPermission', + '', + $this->clientId, + $this->clientSecret, + 123, + 5503, + 'logo.png', + 'http://nextcloud.org', + 'admin' + ); + } + + /** + * @return void + */ + public function testLinkWorkPackageToFileNotFoundPact(): void { + $consumerRequest = new ConsumerRequest(); + $consumerRequest + ->setMethod('POST') + ->setPath($this->workPackagesPath . '/999999/file_links') + ->setHeaders(['Authorization' => 'Bearer 1234567890']) + ->setBody($this->validFileLinkRequestBody); + $providerResponse = new ProviderResponse(); + $providerResponse + ->setStatus(Http::STATUS_NOT_FOUND) + ->addHeader('Content-Type', 'application/json') + ->setBody([ + '_type' => 'Error', + 'errorIdentifier' => 'urn:openproject-org:api:v3:errors:NotFound', + 'message' => 'The requested resource could not be found.' + ]); + + $this->builder + ->uponReceiving('a POST request to /work_packages but not existing workpackage') + ->with($consumerRequest) + ->willRespondWith($providerResponse); + + $storageMock = $this->getStorageMock(); + $service = $this->getOpenProjectAPIService($storageMock); + + $this->expectException(OpenprojectErrorException::class); + $service->linkWorkPackageToFile( + $this->mockServerBaseUri, + '1234567890', + '', + $this->clientId, + $this->clientSecret, + 999999, + 5503, + 'logo.png', + 'http://nextcloud.org', + 'admin' + ); + } }