Skip to content

Commit 5cc43a7

Browse files
committed
fix: copying a question into an empty form failed
Copying appends at the end by reading the order of the target form's last question, and end() of an empty list is false rather than a question, so the call on it was fatal. That could not happen while copying was limited to the same form, which always held at least the source question. Now that the source may be another form, copying into a new form with nothing in it yet -- the ordinary case -- failed with a server error. It now starts at 1, as adding a new question to an empty form already does. Adds tests for copying from another form, into an empty form, and from a form the user cannot edit, which also covers the permission check the first commit added. Signed-off-by: global-prog <raqeeb@uosamarra.edu.iq>
1 parent 2ea0955 commit 5cc43a7

2 files changed

Lines changed: 100 additions & 1 deletion

File tree

lib/Controller/ApiController.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,11 @@ public function newQuestion(int $formId, ?string $type = null, ?string $subtype
597597
$position = $this->shiftQuestionsForInsert($allQuestions, $position);
598598
$questionData['order'] = $position;
599599
} else {
600-
$questionData['order'] = end($allQuestions)->getOrder() + 1;
600+
// Append at the end. The target form may have no questions yet -- the usual
601+
// case when copying into a new form -- and end() of an empty list is false,
602+
// not a question.
603+
$lastQuestion = end($allQuestions);
604+
$questionData['order'] = $lastQuestion ? $lastQuestion->getOrder() + 1 : 1;
601605
}
602606

603607
$newQuestion = Question::fromParams($questionData);

tests/Unit/Controller/ApiControllerTest.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,6 +1375,101 @@ public function testCloneFormWithConfirmationEmailQuestionId(): void {
13751375
$this->assertEquals(11, $clonedForm->getConfirmationEmailQuestionId());
13761376
}
13771377

1378+
/**
1379+
* @param int $formId the form a copied question comes from
1380+
* @return Question the source question, with one text to recognise it by
1381+
*/
1382+
private function sourceQuestionInForm(int $formId): Question {
1383+
return Question::fromParams([
1384+
'id' => 10,
1385+
'formId' => $formId,
1386+
'order' => 1,
1387+
'type' => 'short',
1388+
'text' => 'Source question',
1389+
'description' => '',
1390+
'isRequired' => false,
1391+
]);
1392+
}
1393+
1394+
public function testCloneQuestionFromAnotherForm(): void {
1395+
$targetForm = Form::fromParams(['id' => 1, 'ownerId' => 'currentUser']);
1396+
$sourceForm = Form::fromParams(['id' => 2, 'ownerId' => 'currentUser']);
1397+
1398+
// Editing rights are checked on both forms: the one being added to, and the one
1399+
// the question is read out of.
1400+
$this->formsService->expects($this->exactly(2))
1401+
->method('getFormIfAllowed')
1402+
->willReturnCallback(fn (int $id, string $permission) => match ([$id, $permission]) {
1403+
[1, Constants::PERMISSION_EDIT] => $targetForm,
1404+
[2, Constants::PERMISSION_EDIT] => $sourceForm,
1405+
});
1406+
1407+
$this->questionMapper->method('findById')->with(10)->willReturn($this->sourceQuestionInForm(2));
1408+
$this->optionMapper->method('findByQuestion')->with(10)->willReturn([]);
1409+
$this->questionMapper->method('findByForm')->with(1)->willReturn([
1410+
Question::fromParams(['id' => 20, 'formId' => 1, 'order' => 3, 'type' => 'short']),
1411+
]);
1412+
1413+
$inserted = null;
1414+
$this->questionMapper->expects($this->once())
1415+
->method('insert')
1416+
->with($this->callback(function (Question $question) use (&$inserted) {
1417+
$inserted = $question;
1418+
return true;
1419+
}));
1420+
1421+
$this->apiController->newQuestion(1, fromId: 10);
1422+
1423+
// Created in the form it was copied into, not back in the one it came from.
1424+
$this->assertEquals(1, $inserted->getFormId());
1425+
$this->assertEquals(4, $inserted->getOrder());
1426+
$this->assertEquals('Source question', $inserted->getText());
1427+
}
1428+
1429+
public function testCloneQuestionIntoEmptyForm(): void {
1430+
$targetForm = Form::fromParams(['id' => 1, 'ownerId' => 'currentUser']);
1431+
$sourceForm = Form::fromParams(['id' => 2, 'ownerId' => 'currentUser']);
1432+
$this->formsService->method('getFormIfAllowed')
1433+
->willReturnCallback(fn (int $id) => $id === 1 ? $targetForm : $sourceForm);
1434+
1435+
$this->questionMapper->method('findById')->with(10)->willReturn($this->sourceQuestionInForm(2));
1436+
$this->optionMapper->method('findByQuestion')->with(10)->willReturn([]);
1437+
// A new form with nothing in it yet: the usual target when copying questions over.
1438+
$this->questionMapper->method('findByForm')->with(1)->willReturn([]);
1439+
1440+
$inserted = null;
1441+
$this->questionMapper->expects($this->once())
1442+
->method('insert')
1443+
->with($this->callback(function (Question $question) use (&$inserted) {
1444+
$inserted = $question;
1445+
return true;
1446+
}));
1447+
1448+
$this->apiController->newQuestion(1, fromId: 10);
1449+
1450+
$this->assertEquals(1, $inserted->getFormId());
1451+
$this->assertEquals(1, $inserted->getOrder());
1452+
}
1453+
1454+
public function testCloneQuestionFromFormWithoutEditRights(): void {
1455+
$targetForm = Form::fromParams(['id' => 1, 'ownerId' => 'currentUser']);
1456+
$this->formsService->method('getFormIfAllowed')
1457+
->willReturnCallback(function (int $id) use ($targetForm) {
1458+
if ($id === 1) {
1459+
return $targetForm;
1460+
}
1461+
throw new NoSuchFormException('User has no permissions to get this form');
1462+
});
1463+
1464+
$this->questionMapper->method('findById')->with(10)->willReturn($this->sourceQuestionInForm(2));
1465+
// Nothing may be read out of a form the user cannot edit, let alone copied.
1466+
$this->optionMapper->expects($this->never())->method('findByQuestion');
1467+
$this->questionMapper->expects($this->never())->method('insert');
1468+
1469+
$this->expectException(NoSuchFormException::class);
1470+
$this->apiController->newQuestion(1, fromId: 10);
1471+
}
1472+
13781473
public function testTransferOwnerNotOwner() {
13791474
$form = new Form();
13801475
$form->setId(1);

0 commit comments

Comments
 (0)