Skip to content

Commit

Permalink
fix: Add brute force protection to form endpoints
Browse files Browse the repository at this point in the history
* fix: Add brute force protection to form endpoints

Endpoints that query for forms are now protected against brute force
attacks to find valid forms, invalid hashes or IDs.

---------

Signed-off-by: Ferdinand Thiessen <[email protected]>
Signed-off-by: Christian Hartmann <[email protected]>
Co-authored-by: Christian Hartmann <[email protected]>
susnux and Chartman123 authored Jan 23, 2025
1 parent 04bea4c commit 6b8fdad
Showing 6 changed files with 160 additions and 97 deletions.
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
use OCA\Forms\FormsMigrator;
use OCA\Forms\Listener\AnalyticsDatasourceListener;
use OCA\Forms\Listener\UserDeletedListener;
use OCA\Forms\Middleware\ThrottleFormAccessMiddleware;
use OCA\Forms\Search\SearchProvider;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -43,6 +44,7 @@ public function register(IRegistrationContext $context): void {
$context->registerCapability(Capabilities::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(DatasourceEvent::class, AnalyticsDatasourceListener::class);
$context->registerMiddleware(ThrottleFormAccessMiddleware::class);
$context->registerSearchProvider(SearchProvider::class);
$context->registerUserMigrator(FormsMigrator::class);
}
154 changes: 73 additions & 81 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@
use OCA\Forms\Db\SubmissionMapper;
use OCA\Forms\Db\UploadedFile;
use OCA\Forms\Db\UploadedFileMapper;
use OCA\Forms\Exception\NoSuchFormException;
use OCA\Forms\ResponseDefinitions;
use OCA\Forms\Service\ConfigService;
use OCA\Forms\Service\FormsService;
@@ -31,6 +32,7 @@
use OCP\AppFramework\Db\IMapperException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
@@ -147,6 +149,7 @@ public function getForms(string $type = 'owned'): DataResponse {
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'POST', url: '/api/v3/forms')]
public function newForm(?int $fromId = null): DataResponse {
// Check if user is allowed
@@ -226,19 +229,10 @@ public function newForm(?int $fromId = null): DataResponse {
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'GET', url: '/api/v3/forms/{formId}')]
public function getForm(int $formId): DataResponse {
try {
$form = $this->formMapper->findById($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find form');
throw new OCSBadRequestException('Could not find form');
}

if (!$this->formsService->hasUserAccess($form)) {
$this->logger->debug('User has no permissions to get this form');
throw new OCSForbiddenException('User has no permissions to get this form');
}
$form = $this->getFormIfAllowed($formId, Constants::PERMISSION_SUBMIT);

return new DataResponse($this->formsService->getForm($form));
}
@@ -259,6 +253,7 @@ public function getForm(int $formId): DataResponse {
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'PATCH', url: '/api/v3/forms/{formId}')]
public function updateForm(int $formId, array $keyValuePairs): DataResponse {
$this->logger->debug('Updating form: formId: {formId}, values: {keyValuePairs}', [
@@ -359,6 +354,7 @@ public function updateForm(int $formId, array $keyValuePairs): DataResponse {
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'DELETE', url: '/api/v3/forms/{formId}')]
public function deleteForm(int $formId): DataResponse {
$this->logger->debug('Delete Form: {formId}', [
@@ -472,6 +468,7 @@ public function getQuestion(int $formId, int $questionId): DataResponse {
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'POST', url: '/api/v3/forms/{formId}/questions')]
public function newQuestion(int $formId, ?string $type = null, string $text = '', ?int $fromId = null): DataResponse {
$form = $this->getFormIfAllowed($formId);
@@ -594,6 +591,7 @@ public function newQuestion(int $formId, ?string $type = null, string $text = ''
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'PATCH', url: '/api/v3/forms/{formId}/questions/{questionId}')]
public function updateQuestion(int $formId, int $questionId, array $keyValuePairs): DataResponse {
$this->logger->debug('Updating question: formId: {formId}, questionId: {questionId}, values: {keyValuePairs}', [
@@ -602,6 +600,14 @@ public function updateQuestion(int $formId, int $questionId, array $keyValuePair
'keyValuePairs' => $keyValuePairs
]);

// Make sure we query the form first to check the user has permissions
// So the user does not get information about "questions" if they do not even have permissions to the form
$form = $this->getFormIfAllowed($formId);
if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException('This form is archived and can not be modified');
}

try {
$question = $this->questionMapper->findById($questionId);
} catch (IMapperException $e) {
@@ -613,12 +619,6 @@ public function updateQuestion(int $formId, int $questionId, array $keyValuePair
throw new OCSBadRequestException('Question doesn\'t belong to given Form');
}

$form = $this->getFormIfAllowed($formId);
if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException('This form is archived and can not be modified');
}

// Don't allow empty array
if (sizeof($keyValuePairs) === 0) {
$this->logger->info('Empty keyValuePairs, will not update.');
@@ -668,12 +668,20 @@ public function updateQuestion(int $formId, int $questionId, array $keyValuePair
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'DELETE', url: '/api/v3/forms/{formId}/questions/{questionId}')]
public function deleteQuestion(int $formId, int $questionId): DataResponse {
$this->logger->debug('Mark question as deleted: {questionId}', [
'questionId' => $questionId,
]);


$form = $this->getFormIfAllowed($formId);
if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException('This form is archived and can not be modified');
}

try {
$question = $this->questionMapper->findById($questionId);
} catch (IMapperException $e) {
@@ -685,12 +693,6 @@ public function deleteQuestion(int $formId, int $questionId): DataResponse {
throw new OCSBadRequestException('Question doesn\'t belong to given Form');
}

$form = $this->getFormIfAllowed($formId);
if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException('This form is archived and can not be modified');
}

// Store Order of deleted Question
$deletedOrder = $question->getOrder();

@@ -732,6 +734,7 @@ public function deleteQuestion(int $formId, int $questionId): DataResponse {
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'PATCH', url: '/api/v3/forms/{formId}/questions')]
public function reorderQuestions(int $formId, array $newOrder): DataResponse {
$this->logger->debug('Reordering Questions on Form {formId} as Question-Ids {newOrder}', [
@@ -829,6 +832,7 @@ public function reorderQuestions(int $formId, array $newOrder): DataResponse {
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'POST', url: '/api/v3/forms/{formId}/questions/{questionId}/options')]
public function newOption(int $formId, int $questionId, array $optionTexts): DataResponse {
$this->logger->debug('Adding new options: formId: {formId}, questionId: {questionId}, text: {text}', [
@@ -909,6 +913,7 @@ public function newOption(int $formId, int $questionId, array $optionTexts): Dat
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'PATCH', url: '/api/v3/forms/{formId}/questions/{questionId}/options/{optionId}')]
public function updateOption(int $formId, int $questionId, int $optionId, array $keyValuePairs): DataResponse {
$this->logger->debug('Updating option: form: {formId}, question: {questionId}, option: {optionId}, values: {keyValuePairs}', [
@@ -978,6 +983,7 @@ public function updateOption(int $formId, int $questionId, int $optionId, array
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'DELETE', url: '/api/v3/forms/{formId}/questions/{questionId}/options/{optionId}')]
public function deleteOption(int $formId, int $questionId, int $optionId): DataResponse {
$this->logger->debug('Deleting option: {optionId}', [
@@ -1037,6 +1043,7 @@ public function deleteOption(int $formId, int $questionId, int $optionId): DataR
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'PATCH', url: '/api/v3/forms/{formId}/questions/{questionId}/options')]
public function reorderOptions(int $formId, int $questionId, array $newOrder) {
$form = $this->getFormIfAllowed($formId);
@@ -1134,19 +1141,10 @@ public function reorderOptions(int $formId, int $questionId, array $newOrder) {
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'GET', url: '/api/v3/forms/{formId}/submissions')]
public function getSubmissions(int $formId, ?string $fileFormat = null): DataResponse|DataDownloadResponse {
try {
$form = $this->formMapper->findById($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find form');
throw new OCSNotFoundException('Could not find form');
}

if (!$this->formsService->canSeeResults($form)) {
$this->logger->debug('The current user has no permission to get the results for this form');
throw new OCSForbiddenException('The current user has no permission to get the results for this form');
}
$form = $this->getFormIfAllowed($formId, Constants::PERMISSION_RESULTS);

if ($fileFormat !== null) {
$submissionsData = $this->submissionService->getSubmissionsData($form, $fileFormat);
@@ -1205,24 +1203,10 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'DELETE', url: '/api/v3/forms/{formId}/submissions')]
public function deleteAllSubmissions(int $formId): DataResponse {
$this->logger->debug('Delete all submissions to form: {formId}', [
'formId' => $formId,
]);

try {
$form = $this->formMapper->findById($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find form');
throw new OCSNotFoundException('Could not find form');
}

// The current user has permissions to remove submissions
if (!$this->formsService->canDeleteResults($form)) {
$this->logger->debug('This form is not owned by the current user and user has no `results_delete` permission');
throw new OCSForbiddenException('This form is not owned by the current user and user has no `results_delete` permission');
}
$form = $this->getFormIfAllowed($formId, Constants::PERMISSION_RESULTS_DELETE);

// Delete all submissions (incl. Answers)
$this->submissionMapper->deleteByForm($formId);
@@ -1337,31 +1321,26 @@ public function newSubmission(int $formId, array $answers, string $shareHash = '
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'DELETE', url: '/api/v3/forms/{formId}/submissions/{submissionId}')]
public function deleteSubmission(int $formId, int $submissionId): DataResponse {
$this->logger->debug('Delete Submission: {submissionId}', [
'submissionId' => $submissionId,
]);

$form = $this->getFormIfAllowed($formId, Constants::PERMISSION_RESULTS_DELETE);
try {
$submission = $this->submissionMapper->findById($submissionId);
$form = $this->formMapper->findById($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find form or submission');
throw new OCSNotFoundException('Could not find form or submission');
$this->logger->debug('Could not find submission');
throw new OCSNotFoundException('Could not find submission');
}

if ($formId !== $submission->getFormId()) {
$this->logger->debug('Submission doesn\'t belong to given form');
throw new OCSBadRequestException('Submission doesn\'t belong to given form');
}

// The current user has permissions to remove submissions
if (!$this->formsService->canDeleteResults($form)) {
$this->logger->debug('This form is not owned by the current user and user has no `results_delete` permission');
throw new OCSForbiddenException('This form is not owned by the current user and user has no `results_delete` permission');
}

// Delete submission (incl. Answers)
$this->submissionMapper->deleteById($submissionId);
$this->formMapper->update($form);
@@ -1383,20 +1362,10 @@ public function deleteSubmission(int $formId, int $submissionId): DataResponse {
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'POST', url: '/api/v3/forms/{formId}/submissions/export')]
public function exportSubmissionsToCloud(int $formId, string $path, string $fileFormat = Constants::DEFAULT_FILE_FORMAT) {
try {
$form = $this->formMapper->findById($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find form');
throw new OCSNotFoundException('Could not find form');
}

if (!$this->formsService->canSeeResults($form)) {
$this->logger->debug('The current user has no permission to get the results for this form');
throw new OCSForbiddenException('The current user has no permission to get the results for this form');
}

$form = $this->getFormIfAllowed($formId, Constants::PERMISSION_RESULTS);
$file = $this->submissionService->writeFileToCloud($form, $path, $fileFormat);

return new DataResponse($file->getName());
@@ -1421,8 +1390,9 @@ public function exportSubmissionsToCloud(int $formId, string $path, string $file
* 200: the file id and name of the uploaded file
*/
#[CORS()]
#[NoAdminRequired()]
#[PublicPage()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'POST', url: '/api/v3/forms/{formId}/submissions/files/{questionId}')]
public function uploadFiles(int $formId, int $questionId, string $shareHash = ''): DataResponse {
$this->logger->debug('Uploading files for formId: {formId}, questionId: {questionId}', [
@@ -1614,7 +1584,7 @@ private function loadFormForSubmission(int $formId, string $shareHash): Form {
$form = $this->formMapper->findById($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find form');
throw new OCSNotFoundException('Could not find form');
throw new NoSuchFormException('Could not find form');
}

// Does the user have access to the form (Either by logged-in user, or by providing public share-hash.)
@@ -1634,7 +1604,7 @@ private function loadFormForSubmission(int $formId, string $shareHash): Form {
} finally {
// Now forbid, if no public share and no direct share.
if (!$isPublicShare && !$this->formsService->hasUserAccess($form)) {
throw new OCSForbiddenException('Not allowed to access this form');
throw new NoSuchFormException('Not allowed to access this form', Http::STATUS_FORBIDDEN);
}
}

@@ -1650,20 +1620,42 @@ private function loadFormForSubmission(int $formId, string $shareHash): Form {
* Helper that retrieves a form if the current user is allowed to edit it
* This throws an exception in case either the form is not found or permissions are missing.
* @param int $formId The form ID to retrieve
* @throws OCSNotFoundException If the form was not found
* @throws OCSForbiddenException If the current user has no permission to edit
* @throws NoSuchFormException If the form was not found or the current user has no permission to edit
*/
private function getFormIfAllowed(int $formId): Form {
private function getFormIfAllowed(int $formId, string $permissions = 'all'): Form {
try {
$form = $this->formMapper->findById($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find form');
throw new OCSNotFoundException('Could not find form');
throw new NoSuchFormException('Could not find form');
}

if ($form->getOwnerId() !== $this->currentUser->getUID()) {
$this->logger->debug('This form is not owned by the current user');
throw new OCSForbiddenException('This form is not owned by the current user');
switch ($permissions) {
case Constants::PERMISSION_SUBMIT:
if (!$this->formsService->hasUserAccess($form)) {
$this->logger->debug('User has no permissions to get this form');
throw new NoSuchFormException('User has no permissions to get this form', Http::STATUS_FORBIDDEN);
}
break;
case Constants::PERMISSION_RESULTS:
if (!$this->formsService->canSeeResults($form)) {
$this->logger->debug('The current user has no permission to get the results for this form');
throw new NoSuchFormException('The current user has no permission to get the results for this form', Http::STATUS_FORBIDDEN);
}
break;
case Constants::PERMISSION_RESULTS_DELETE:
if (!$this->formsService->canDeleteResults($form)) {
$this->logger->debug('This form is not owned by the current user and user has no `results_delete` permission');
throw new NoSuchFormException('This form is not owned by the current user and user has no `results_delete` permission', Http::STATUS_FORBIDDEN);
}
break;
default:
// By default we request full permissions
if ($form->getOwnerId() !== $this->currentUser->getUID()) {
$this->logger->debug('This form is not owned by the current user');
throw new NoSuchFormException('This form is not owned by the current user', Http::STATUS_FORBIDDEN);
}
break;
}
return $form;
}
19 changes: 19 additions & 0 deletions lib/Exception/NoSuchFormException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Forms\Exception;

use OCP\AppFramework\Http;

class NoSuchFormException extends \Exception {

public function __construct($message = '', int $errorCode = Http::STATUS_NOT_FOUND) {
parent::__construct($message, $errorCode);
}

}
34 changes: 34 additions & 0 deletions lib/Middleware/ThrottleFormAccessMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Forms\Middleware;

use Exception;
use OCA\Forms\Exception\NoSuchFormException;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Middleware;

/**
* Simple middleware to throttle requests after invalid form access
*/
class ThrottleFormAccessMiddleware extends Middleware {

public function afterException(Controller $controller, string $methodName, Exception $exception) {
if (!($exception instanceof NoSuchFormException)) {
throw $exception;
}

$response = new DataResponse(
$exception->getMessage(),
$exception->getCode(),
);
$response->throttle(['action' => 'form']);
return $response;
}
}
2 changes: 1 addition & 1 deletion tests/Integration/Api/ApiV3Test.php
Original file line number Diff line number Diff line change
@@ -664,7 +664,7 @@ public function testDeleteForm() {
} catch (ClientException $e) {
$resp = $e->getResponse();
}
$this->assertEquals(400, $resp->getStatusCode());
$this->assertEquals(404, $resp->getStatusCode());
}

public function dataCreateNewQuestion() {
46 changes: 31 additions & 15 deletions tests/Unit/Controller/ApiControllerTest.php
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@ function is_uploaded_file(string|bool|null $filename) {
use OCA\Forms\Db\Submission;
use OCA\Forms\Db\SubmissionMapper;
use OCA\Forms\Db\UploadedFileMapper;
use OCA\Forms\Exception\NoSuchFormException;
use OCA\Forms\Service\ConfigService;
use OCA\Forms\Service\FormsService;
use OCA\Forms\Service\SubmissionService;
@@ -200,7 +201,7 @@ public function testGetSubmissions_invalidForm() {
->method('findById')
->with(1)
->willThrowException($exception);
$this->expectException(OCSNotFoundException::class);
$this->expectException(NoSuchFormException::class);
$this->apiController->getSubmissions(1);
}

@@ -219,7 +220,7 @@ public function testGetSubmissions_noPermissions() {
->with($form)
->willReturn(false);

$this->expectException(OCSForbiddenException::class);
$this->expectException(NoSuchFormException::class);
$this->apiController->getSubmissions(1);
}

@@ -305,7 +306,7 @@ public function testExportSubmissions_invalidForm() {
->method('findById')
->with(99)
->willThrowException($exception);
$this->expectException(OCSNotFoundException::class);
$this->expectException(NoSuchFormException::class);
$this->apiController->getSubmissions(99, 'csv');
}

@@ -324,7 +325,7 @@ public function testExportSubmissions_noPermissions() {
->with($form)
->willReturn(false);

$this->expectException(OCSForbiddenException::class);
$this->expectException(NoSuchFormException::class);
$this->apiController->getSubmissions(1, 'csv');
}

@@ -364,7 +365,7 @@ public function testExportSubmissionsToCloud_invalidForm() {
->method('findById')
->with(1)
->willThrowException($exception);
$this->expectException(OCSNotFoundException::class);
$this->expectException(NoSuchFormException::class);
$this->apiController->exportSubmissionsToCloud(1, '');
}

@@ -437,7 +438,7 @@ public function dataCloneForm_exceptions() {
'not found' => [
'canCreate' => true,
'callback' => fn ($id): Form => $this->throwMockedException(MockedMapperException::class),
'exception' => OCSNotFoundException::class
'exception' => NoSuchFormException::class
],
'not owned' => [
'canCreate' => true,
@@ -447,7 +448,7 @@ public function dataCloneForm_exceptions() {
$form->setOwnerId('otherUser');
return $form;
},
'exception' => OCSForbiddenException::class
'exception' => NoSuchFormException::class
]
];
}
@@ -766,7 +767,7 @@ public function testNewSubmission_formNotFound() {
->method('findById')
->with(1)
->willThrowException($exception);
$this->expectException(OCSNotFoundException::class);
$this->expectException(NoSuchFormException::class);
$this->apiController->newSubmission(1, [], '');
}

@@ -775,16 +776,16 @@ public function testNewSubmission_formNotFound() {
*/
public function dataForCheckForbiddenException() {
return [
'user_dont_have_access_to_form' => [false, true, true],
'form_expired' => [true, true, true],
'not_allowed_to_submit' => [true, false, false],
'user_dont_have_access_to_form' => [false, true, true, NoSuchFormException::class],
'form_expired' => [true, true, true, OCSForbiddenException::class],
'not_allowed_to_submit' => [true, false, false, OCSForbiddenException::class],
];
}

/**
* @dataProvider dataForCheckForbiddenException()
*/
public function testNewSubmission_forbiddenException($hasUserAccess, $hasFormExpired, $canSubmit) {
public function testNewSubmission_forbiddenException($hasUserAccess, $hasFormExpired, $canSubmit, $exception) {
$form = new Form();
$form->setId(1);
$form->setOwnerId('admin');
@@ -800,7 +801,7 @@ public function testNewSubmission_forbiddenException($hasUserAccess, $hasFormExp

$this->formAccess($hasUserAccess, $hasFormExpired, $canSubmit);

$this->expectException(OCSForbiddenException::class);
$this->expectException($exception);

$this->apiController->newSubmission(1, [], '');
}
@@ -834,12 +835,27 @@ public function testNewSubmission_validateSubmission() {
public function testDeleteSubmissionNotFound() {
$exception = $this->createMock(MapperException::class);

$form = new Form();
$form->setId(1);
$form->setOwnerId('currentUser');

$this->formMapper->expects(self::once())
->method('findById')
->with(1)
->willReturn($form);

$this->formsService->expects(self::once())
->method('canDeleteResults')
->with($form)
->willReturn(true);

$this->submissionMapper
->expects($this->once())
->method('findById')
->with(42)
->willThrowException($exception);

// Not found as this is about the submission, not the form
$this->expectException(OCSNotFoundException::class);
$this->apiController->deleteSubmission(1, 42);
}
@@ -867,7 +883,7 @@ public function testDeleteSubmissionNoPermission($submissionData, $formData) {
->with($form)
->willReturn(false);

$this->expectException(OCSForbiddenException::class);
$this->expectException(NoSuchFormException::class);
$this->apiController->deleteSubmission(1, 42);
}

@@ -938,7 +954,7 @@ public function testTransferOwnerNotOwner() {
->with(1)
->willReturn($form);

$this->expectException(OCSForbiddenException::class);
$this->expectException(NoSuchFormException::class);
$this->apiController->updateForm(1, ['ownerId' => 'newOwner']);
}

0 comments on commit 6b8fdad

Please sign in to comment.