diff --git a/lang/en.yml b/lang/en.yml index ea3ab5e6..37f9f86e 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -1,17 +1,7 @@ en: SilverStripe\LinkField\Controllers\LinkFieldController: - BAD_DATA: 'Bad data' CREATE_LINK: 'Create link' - EMPTY_DATA: 'Empty data' - INVALID_ID: 'Invalid ID' - INVALID_OWNER: 'Invalid Owner' - INVALID_OWNER_CLASS: 'Invalid ownerClass' - INVALID_OWNER_ID: 'Invalid ownerID' - INVALID_OWNER_RELATION: 'Invalid ownerRelation' - INVALID_TOKEN: 'Invalid CSRF token' - INVALID_TYPEKEY: 'Invalid typeKey' - MENUTITLE: SilverStripe\LinkField\Controllers\LinkFieldController - UNAUTHORIZED: Unauthorized + MENUTITLE: 'Link fields' UPDATE_LINK: 'Update link' SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait: INVALID_TYPECLASS: '"{class}": {typeclass} is not a valid Link Type' diff --git a/src/Controllers/LinkFieldController.php b/src/Controllers/LinkFieldController.php index afa6f948..553f8e22 100644 --- a/src/Controllers/LinkFieldController.php +++ b/src/Controllers/LinkFieldController.php @@ -74,17 +74,17 @@ public function linkForm(): Form if ($id) { $link = Link::get()->byID($id); if (!$link) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_ID', 'Invalid ID')); + $this->jsonError(404); } $operation = 'edit'; if (!$link->canView()) { - $this->jsonError(403, _t(__CLASS__ . '.UNAUTHORIZED', 'Unauthorized')); + $this->jsonError(403); } } else { $typeKey = $this->typeKeyFromRequest(); $link = LinkTypeService::create()->byKey($typeKey); if (!$link) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_TYPEKEY', 'Invalid typeKey')); + $this->jsonError(404); } $operation = 'create'; } @@ -107,17 +107,13 @@ public function linkData(HTTPRequest $request): HTTPResponse $data[$link->ID] = $this->getLinkData($link); } } - - $response = $this->getResponse(); - $response->addHeader('Content-type', 'application/json'); - $response->setBody(json_encode($data)); - return $response; + return $this->jsonSuccess(200, $data); } private function getLinkData(Link $link): array { if (!$link->canView()) { - $this->jsonError(403, _t(__CLASS__ . '.UNAUTHORIZED', 'Unauthorized')); + $this->jsonError(403); } $data = $link->jsonSerialize(); $data['canDelete'] = $link->canDelete(); @@ -134,11 +130,11 @@ public function linkDelete(): HTTPResponse { $link = $this->linkFromRequest(); if (!$link->canDelete()) { - $this->jsonError(403, _t(__CLASS__ . '.UNAUTHORIZED', 'Unauthorized')); + $this->jsonError(403); } // Check security token on destructive operation if (!SecurityToken::inst()->checkRequest($this->getRequest())) { - $this->jsonError(400, _t(__CLASS__ . '.INVALID_TOKEN', 'Invalid CSRF token')); + $this->jsonError(400); } // delete() will also delete any published version immediately $link->delete(); @@ -151,10 +147,7 @@ public function linkDelete(): HTTPResponse $owner->write(); } // Send response - $response = $this->getResponse(); - $response->addHeader('Content-type', 'application/json'); - $response->setBody(json_encode(['success' => true])); - return $response; + return $this->jsonSuccess(201); } /** @@ -174,9 +167,8 @@ public function getLinkForm(): Form public function save(array $data, Form $form): HTTPResponse { if (empty($data)) { - $this->jsonError(400, _t(__CLASS__ . '.EMPTY_DATA', 'Empty data')); + $this->jsonError(400); } - /** @var Link $link */ $id = $this->itemIDFromRequest(); if ($id) { @@ -184,10 +176,10 @@ public function save(array $data, Form $form): HTTPResponse $operation = 'edit'; $link = Link::get()->byID($id); if (!$link) { - $this->jsonErorr(404, _t(__CLASS__ . '.INVALID_ID', 'Invalid ID')); + $this->jsonError(404); } if (!$link->canEdit()) { - $this->jsonError(403, _t(__CLASS__ . '.UNAUTHORIZED', 'Unauthorized')); + $this->jsonError(403); } } else { // Creating a new Link @@ -195,11 +187,11 @@ public function save(array $data, Form $form): HTTPResponse $typeKey = $this->typeKeyFromRequest(); $className = LinkTypeService::create()->byKey($typeKey) ?? ''; if (!$className) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_TYPEKEY', 'Invalid typeKey')); + $this->jsonError(404); } $link = $className::create(); if (!$link->canCreate()) { - $this->jsonError(403, _t(__CLASS__ . '.UNAUTHORIZED', 'Unauthorized')); + $this->jsonError(403); } } @@ -210,7 +202,7 @@ public function save(array $data, Form $form): HTTPResponse if ((isset($data['ID']) && ((int) $data['ID'] !== $id)) || isset($data['Sort']) ) { - $this->jsonError(400, _t(__CLASS__ . '.BAD_DATA', 'Bad data')); + $this->jsonError(400); } // Update DataObject from form data @@ -274,13 +266,13 @@ public function linkSort() $request = $this->getRequest(); // Check security token if (!SecurityToken::inst()->checkRequest($request)) { - $this->jsonError(400, _t(__CLASS__ . '.INVALID_TOKEN', 'Invalid CSRF token')); + $this->jsonError(400); } $json = json_decode($request->getBody() ?? ''); $newLinkIDs = $json?->newLinkIDs; // If someone's passing a JSON object or other non-array here, they're doing something wrong if (!is_array($newLinkIDs) || empty($newLinkIDs)) { - $this->jsonError(400, _t('LinkField.BAD_DATA', 'Bad data')); + $this->jsonError(400); } // Fetch and validate links $links = Link::get()->filter(['ID' => $newLinkIDs])->toArray(); @@ -295,7 +287,7 @@ public function linkSort() $ownerRelation = $link->OwnerRelation; } if ($link->OwnerID !== $ownerID || $link->OwnerRelation !== $ownerRelation) { - $this->jsonError(400, _t('LinkField.BAD_DATA', 'Bad data')); + $this->jsonError(400); } $linkIDToLink[$link->ID] = $link; } @@ -307,7 +299,7 @@ public function linkSort() // There's also corresponding logic in Link::onBeforeWrite() to also have a minimum of 1 $sort = $i + 1; if ($link->Sort !== $sort && !$link->canEdit()) { - $this->jsonError(403, _t(__CLASS__ . '.UNAUTHORIZED', 'Unauthorized')); + $this->jsonError(403); } } // Update Sort field on links @@ -321,10 +313,7 @@ public function linkSort() } } // Send response - $response = $this->getResponse(); - $response->addHeader('Content-type', 'application/json'); - $response->setBody(json_encode(['success' => true])); - return $response; + return $this->jsonSuccess(201); } /** @@ -413,11 +402,11 @@ private function linkFromRequest(): Link { $itemID = $this->itemIDFromRequest(); if (!$itemID) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_ID', 'Invalid ID')); + $this->jsonError(404); } $link = Link::get()->byID($itemID); if (!$link) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_ID', 'Invalid ID')); + $this->jsonError(404); } return $link; } @@ -429,11 +418,11 @@ private function linksFromRequest(): DataList { $itemIDs = $this->itemIDsFromRequest(); if (empty($itemIDs)) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_ID', 'Invalid ID')); + $this->jsonError(404); } $links = Link::get()->byIDs($itemIDs); if (!$links->exists()) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_ID', 'Invalid ID')); + $this->jsonError(404); } return $links; } @@ -446,7 +435,7 @@ private function itemIDFromRequest(): int $request = $this->getRequest(); $itemID = (string) $request->param('ItemID'); if (!ctype_digit($itemID)) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_ID', 'Invalid ID')); + $this->jsonError(404); } return (int) $itemID; } @@ -460,13 +449,13 @@ private function itemIDsFromRequest(): array $itemIDs = $request->getVar('itemIDs'); if (!is_array($itemIDs)) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_ID', 'Invalid ID')); + $this->jsonError(404); } $idsAsInt = []; foreach ($itemIDs as $id) { if (!is_int($id) && !ctype_digit($id)) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_ID', 'Invalid ID')); + $this->jsonError(404); } $idsAsInt[] = (int) $id; } @@ -482,7 +471,7 @@ private function typeKeyFromRequest(): string $request = $this->getRequest(); $typeKey = (string) $request->getVar('typeKey'); if (strlen($typeKey) === 0 || !preg_match('#^[a-z\-]+$#', $typeKey)) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_TYPEKEY', 'Invalid typeKey')); + $this->jsonError(404); } return $typeKey; } @@ -495,7 +484,7 @@ private function getOwnerClassFromRequest(): string $request = $this->getRequest(); $ownerClass = $request->getVar('ownerClass') ?: $request->postVar('OwnerClass'); if (!is_a($ownerClass, DataObject::class, true)) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_OWNER_CLASS', 'Invalid ownerClass')); + $this->jsonError(404); } return $ownerClass; @@ -509,7 +498,7 @@ private function getOwnerIDFromRequest(): int $request = $this->getRequest(); $ownerID = (int) ($request->getVar('ownerID') ?: $request->postVar('OwnerID')); if ($ownerID === 0) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_OWNER_ID', 'Invalid ownerID')); + $this->jsonError(404); } return $ownerID; @@ -546,7 +535,7 @@ private function getOwnerFromRequest(): DataObject return $owner; } } - $this->jsonError(404, _t(__CLASS__ . '.INVALID_OWNER', 'Invalid Owner')); + $this->jsonError(404); } /** @@ -558,7 +547,7 @@ private function getOwnerRelationFromRequest(): string $request = $this->getRequest(); $ownerRelation = $request->getVar('ownerRelation') ?: $request->postVar('OwnerRelation'); if (!$ownerRelation) { - $this->jsonError(404, _t(__CLASS__ . '.INVALID_OWNER_RELATION', 'Invalid ownerRelation')); + $this->jsonError(404); } return $ownerRelation; diff --git a/tests/php/Controllers/LinkFieldControllerTest.php b/tests/php/Controllers/LinkFieldControllerTest.php index 18d55a96..08efa606 100644 --- a/tests/php/Controllers/LinkFieldControllerTest.php +++ b/tests/php/Controllers/LinkFieldControllerTest.php @@ -55,8 +55,7 @@ public function testLinkFormGetSchema( string $typeKey, string $fail, int $expectedCode, - ?string $expectedValue, - string $expectedMessage + ?string $expectedValue ): void { TestPhoneLink::$fail = $fail; $owner = $this->getFixtureLinkOwner(); @@ -75,10 +74,7 @@ public function testLinkFormGetSchema( $response = $this->get($url, null, $headers); $this->assertSame('application/json', $response->getHeader('Content-type')); $this->assertSame($expectedCode, $response->getStatusCode()); - if ($expectedCode !== 200) { - $jsonError = json_decode($response->getBody(), true); - $this->assertSame($expectedMessage, $jsonError['errors'][0]['value']); - } else { + if ($expectedCode === 200) { $formSchema = json_decode($response->getBody(), true); $this->assertSame("admin/linkfield/schema/linkForm/$id", $formSchema['id']); $expectedAction = "admin/linkfield/linkForm/$id?typeKey=testphone&ownerID=$ownerID&ownerClass=$ownerClass" @@ -114,7 +110,6 @@ public function provideLinkFormGetSchema(): array 'fail' => '', 'expectedCode' => 200, 'expectedValue' => '0123456789', - 'expectedMessage' => '', ], 'Valid new record' => [ 'idType' => 'new-record', @@ -122,7 +117,6 @@ public function provideLinkFormGetSchema(): array 'fail' => '', 'expectedCode' => 200, 'expectedValue' => null, - 'expectedMessage' => '', ], 'Reject invalid ID' => [ 'idType' => 'invalid', @@ -130,7 +124,6 @@ public function provideLinkFormGetSchema(): array 'fail' => '', 'expectedCode' => 404, 'expectedValue' => null, - 'expectedMessage' => 'Invalid ID', ], 'Reject missing ID' => [ 'idType' => 'missing', @@ -138,7 +131,6 @@ public function provideLinkFormGetSchema(): array 'fail' => '', 'expectedCode' => 404, 'expectedValue' => null, - 'expectedMessage' => 'Invalid ID', ], 'Reject non-numeric ID' => [ 'idType' => 'non-numeric', @@ -146,7 +138,6 @@ public function provideLinkFormGetSchema(): array 'fail' => '', 'expectedCode' => 404, 'expectedValue' => null, - 'expectedMessage' => 'Invalid ID', ], 'Reject invalid typeKey for new record' => [ 'idType' => 'new-record', @@ -154,7 +145,6 @@ public function provideLinkFormGetSchema(): array 'fail' => '', 'expectedCode' => 404, 'expectedValue' => null, - 'expectedMessage' => 'Invalid typeKey', ], 'Reject fail canView() check' => [ 'idType' => 'existing', @@ -162,7 +152,6 @@ public function provideLinkFormGetSchema(): array 'fail' => 'can-view', 'expectedCode' => 403, 'expectedValue' => null, - 'expectedMessage' => 'Unauthorized', ], ]; } @@ -176,7 +165,6 @@ public function testLinkFormPost( string $dataType, string $fail, int $expectedCode, - string $expectedMessage, string $expectedLinkType ): void { TestPhoneLink::$fail = $fail; @@ -215,11 +203,9 @@ public function testLinkFormPost( $this->assertStringContainsString('Silverstripe - Bad Request', $response->getBody()); return; } + $this->assertSame($expectedCode, $response->getStatusCode()); $this->assertSame('application/json', $response->getHeader('Content-type')); - if ($expectedCode !== 200) { - $jsonError = json_decode($response->getBody(), true); - $this->assertSame($expectedMessage, $jsonError['errors'][0]['value']); - } else { + if ($expectedCode === 200) { $formSchema = json_decode($response->getBody(), true); $newID = $this->getIDAfterPost($expectedLinkType); if ($expectedLinkType === 'new-record') { @@ -243,7 +229,6 @@ public function testLinkFormPost( // state node is flattened, unlike schema node $this->assertSame('9876543210', $formSchema['state']['fields'][3]['value']); if ($fail) { - $this->assertSame($expectedMessage, $formSchema['errors'][0]['value']); // Phone was note updated on PhoneLink dataobject $link = TestPhoneLink::get()->byID($newID); $this->assertSame($link->Phone, '0123456789'); @@ -279,7 +264,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => '', 'expectedCode' => 200, - 'expectedMessage' => '', 'expectedLinkType' => 'existing', ], 'Valid create new record' => [ @@ -288,7 +272,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => '', 'expectedCode' => 200, - 'expectedMessage' => '', 'expectedLinkType' => 'new-record', ], 'Invalid validate()' => [ @@ -297,7 +280,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => 'validate', 'expectedCode' => 200, - 'expectedMessage' => 'Fail was validate', 'expectedLinkType' => 'existing', ], 'Invalid getCMSCompositeValidator()' => [ @@ -306,7 +288,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => 'cms-composite-validator', 'expectedCode' => 200, - 'expectedMessage' => 'Fail was cms-composite-validator', 'expectedLinkType' => 'existing', ], 'Reject invalid ID' => [ @@ -315,7 +296,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => '', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', 'expectedLinkType' => '', ], 'Reject missing ID' => [ @@ -324,7 +304,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => '', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', 'expectedLinkType' => '', ], 'Reject non-numeric ID' => [ @@ -333,7 +312,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => '', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', 'expectedLinkType' => '', ], 'Reject invalid typeKey for new record' => [ @@ -342,7 +320,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => '', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid typeKey', 'expectedLinkType' => '', ], 'Reject empty data' => [ @@ -351,7 +328,6 @@ public function provideLinkFormPost(): array 'dataType' => 'empty', 'fail' => '', 'expectedCode' => 400, - 'expectedMessage' => 'Empty data', 'expectedLinkType' => '', ], 'Reject invalid-id data' => [ @@ -360,7 +336,6 @@ public function provideLinkFormPost(): array 'dataType' => 'invalid-id', 'fail' => '', 'expectedCode' => 400, - 'expectedMessage' => 'Bad data', 'expectedLinkType' => '', ], 'Reject fail csrf-token' => [ @@ -369,7 +344,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => 'csrf-token', 'expectedCode' => 400, - 'expectedMessage' => 'Invalid CSRF token', 'expectedLinkType' => '', ], 'Reject fail canEdit() check existing record' => [ @@ -378,7 +352,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => 'can-edit', 'expectedCode' => 403, - 'expectedMessage' => 'Unauthorized', 'expectedLinkType' => '', ], 'Reject fail canCreate() check new record' => [ @@ -387,7 +360,6 @@ public function provideLinkFormPost(): array 'dataType' => 'valid', 'fail' => 'can-create', 'expectedCode' => 403, - 'expectedMessage' => 'Unauthorized', 'expectedLinkType' => '', ], ]; @@ -446,7 +418,6 @@ public function provideLinkFormReadOnly(): array public function testLinkData( string $idType, int $expectedCode, - string $expectedMessage, ): void { $id = $this->getID($idType); if ($id === -1) { @@ -457,10 +428,7 @@ public function testLinkData( $response = $this->get($url); $this->assertSame('application/json', $response->getHeader('Content-type')); $this->assertSame($expectedCode, $response->getStatusCode()); - if ($expectedCode !== 200) { - $jsonError = json_decode($response->getBody(), true); - $this->assertSame($expectedMessage, $jsonError['errors'][0]['value']); - } else { + if ($expectedCode === 200) { $data = json_decode($response->getBody(), true); $this->assertSame($id, $data['ID']); $this->assertSame('0123456789', $data['Phone']); @@ -475,27 +443,22 @@ public function provideLinkData(): array 'Valid' => [ 'idType' => 'existing', 'expectedCode' => 200, - 'expectedMessage' => '', ], 'Reject invalid ID' => [ 'idType' => 'invalid', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', ], 'Reject missing ID' => [ 'idType' => 'missing', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', ], 'Reject non-numeric ID' => [ 'idType' => 'non-numeric', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', ], 'Reject new record ID' => [ 'idType' => 'new-record', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', ], ]; } @@ -506,8 +469,7 @@ public function provideLinkData(): array public function testLinkDelete( string $idType, string $fail, - int $expectedCode, - string $expectedMessage + int $expectedCode ): void { TestPhoneLink::$fail = $fail; $owner = $this->getFixtureLinkOwner(); @@ -529,9 +491,7 @@ public function testLinkDelete( $response = $this->mainSession->sendRequest('DELETE', $url, [], $headers); $this->assertSame('application/json', $response->getHeader('Content-type')); $this->assertSame($expectedCode, $response->getStatusCode()); - if ($expectedCode !== 200) { - $jsonError = json_decode($response->getBody(), true); - $this->assertSame($expectedMessage, $jsonError['errors'][0]['value']); + if ($expectedCode >= 400) { $this->assertNotNull(TestPhoneLink::get()->byID($fixtureID)); $owner = $this->getFixtureLinkOwner(); $this->assertSame($ownerLinkID, $owner->LinkID); @@ -549,44 +509,37 @@ public function provideLinkDelete(): array 'Valid' => [ 'idType' => 'existing', 'fail' => '', - 'expectedCode' => 200, - 'expectedMessage' => '', + 'expectedCode' => 201, ], 'Reject fail canDelete()' => [ 'idType' => 'existing', 'fail' => 'can-delete', 'expectedCode' => 403, - 'expectedMessage' => 'Unauthorized', ], 'Reject fail csrf-token' => [ 'idType' => 'existing', 'fail' => 'csrf-token', 'expectedCode' => 400, - 'expectedMessage' => 'Invalid CSRF token', ], 'Reject invalid ID' => [ 'idType' => 'invalid', 'fail' => '', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', ], 'Reject missing ID' => [ 'idType' => 'missing', 'fail' => '', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', ], 'Reject non-numeric ID' => [ 'idType' => 'non-numeric', 'fail' => '', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', ], 'Reject new record ID' => [ 'idType' => 'new-record', 'fail' => '', 'expectedCode' => 404, - 'expectedMessage' => 'Invalid ID', ], ]; } @@ -598,7 +551,6 @@ public function testLinkSort( array $newTitleOrder, string $fail, int $expectedCode, - string $expectedMessage, array $expectedTitles ): void { TestPhoneLink::$fail = $fail; @@ -622,10 +574,6 @@ public function testLinkSort( } $response = $this->post($url, null, $headers, null, $body); $this->assertSame($expectedCode, $response->getStatusCode()); - if ($expectedCode !== 200) { - $jsonError = json_decode($response->getBody(), true); - $this->assertSame($expectedMessage, $jsonError['errors'][0]['value']); - } $this->assertSame( $this->getExpectedTitles($expectedTitles), TestPhoneLink::get()->filter(['OwnerRelation' => 'LinkList'])->column('Title') @@ -638,50 +586,43 @@ public function provideLinkSort(): array 'Success' => [ 'newTitleOrder' => [4, 2, 3], 'fail' => '', - 'expectedCode' => 200, - 'expectedMessage' => '', + 'expectedCode' => 201, 'expectedTitles' => [4, 2, 3], ], 'Emtpy data' => [ 'newTitleOrder' => [], 'fail' => '', 'expectedCode' => 400, - 'expectedMessage' => 'Bad data', 'expectedTitles' => [2, 3, 4], ], 'Fail can edit' => [ 'newTitleOrder' => [4, 2, 3], 'fail' => 'can-edit', 'expectedCode' => 403, - 'expectedMessage' => 'Unauthorized', 'expectedTitles' => [2, 3, 4], ], 'Fail object data' => [ 'newTitleOrder' => [], 'fail' => 'object-data', 'expectedCode' => 400, - 'expectedMessage' => 'Bad data', 'expectedTitles' => [2, 3, 4], ], 'Fail csrf token' => [ 'newTitleOrder' => [4, 2, 3], 'fail' => 'csrf-token', 'expectedCode' => 400, - 'expectedMessage' => 'Invalid CSRF token', 'expectedTitles' => [2, 3, 4], ], 'Mismatched owner' => [ 'newTitleOrder' => [4, 1, 2], 'fail' => '', 'expectedCode' => 400, - 'expectedMessage' => 'Bad data', 'expectedTitles' => [2, 3, 4], ], 'Mismatched owner relation' => [ 'newTitleOrder' => [4, 5, 2], 'fail' => '', 'expectedCode' => 400, - 'expectedMessage' => 'Bad data', 'expectedTitles' => [2, 3, 4], ], ];