WebapiAbstract.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\TestFramework\TestCase;
  7. use Magento\Framework\App\Filesystem\DirectoryList;
  8. use Magento\Framework\Filesystem;
  9. use Magento\Webapi\Model\Soap\Fault;
  10. use Magento\TestFramework\Helper\Bootstrap;
  11. /**
  12. * Test case for Web API functional tests for REST and SOAP.
  13. *
  14. * @SuppressWarnings(PHPMD.NumberOfChildren)
  15. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  16. */
  17. abstract class WebapiAbstract extends \PHPUnit\Framework\TestCase
  18. {
  19. /** TODO: Reconsider implementation of fixture-management methods after implementing several tests */
  20. /**#@+
  21. * Auto tear down options in setFixture
  22. */
  23. const AUTO_TEAR_DOWN_DISABLED = 0;
  24. const AUTO_TEAR_DOWN_AFTER_METHOD = 1;
  25. const AUTO_TEAR_DOWN_AFTER_CLASS = 2;
  26. /**#@-*/
  27. /**#@+
  28. * Web API adapters that are used to perform actual calls.
  29. */
  30. const ADAPTER_SOAP = 'soap';
  31. const ADAPTER_REST = 'rest';
  32. /**#@-*/
  33. /**
  34. * Application cache model.
  35. *
  36. * @var \Magento\Framework\App\Cache
  37. */
  38. protected $_appCache;
  39. /**
  40. * The list of models to be deleted automatically in tearDown().
  41. *
  42. * @var array
  43. */
  44. protected $_modelsToDelete = [];
  45. /**
  46. * Namespace for fixtures is different for each test case.
  47. *
  48. * @var string
  49. */
  50. protected static $_fixturesNamespace;
  51. /**
  52. * The list of registered fixtures.
  53. *
  54. * @var array
  55. */
  56. protected static $_fixtures = [];
  57. /**
  58. * Fixtures to be deleted in tearDown().
  59. *
  60. * @var array
  61. */
  62. protected static $_methodLevelFixtures = [];
  63. /**
  64. * Fixtures to be deleted in tearDownAfterClass().
  65. *
  66. * @var array
  67. */
  68. protected static $_classLevelFixtures = [];
  69. /**
  70. * Original Magento config values.
  71. *
  72. * @var array
  73. */
  74. protected $_origConfigValues = [];
  75. /**
  76. * The list of instantiated Web API adapters.
  77. *
  78. * @var \Magento\TestFramework\TestCase\Webapi\AdapterInterface[]
  79. */
  80. protected $_webApiAdapters;
  81. /**
  82. * The list of available Web API adapters.
  83. *
  84. * @var array
  85. */
  86. protected $_webApiAdaptersMap = [
  87. self::ADAPTER_SOAP => \Magento\TestFramework\TestCase\Webapi\Adapter\Soap::class,
  88. self::ADAPTER_REST => \Magento\TestFramework\TestCase\Webapi\Adapter\Rest::class,
  89. ];
  90. /**
  91. * Initialize fixture namespaces.
  92. */
  93. public static function setUpBeforeClass()
  94. {
  95. parent::setUpBeforeClass();
  96. self::_setFixtureNamespace();
  97. }
  98. /**
  99. * Run garbage collector for cleaning memory
  100. *
  101. * @return void
  102. */
  103. public static function tearDownAfterClass()
  104. {
  105. //clear garbage in memory
  106. gc_collect_cycles();
  107. $fixtureNamespace = self::_getFixtureNamespace();
  108. if (isset(self::$_classLevelFixtures[$fixtureNamespace])
  109. && count(self::$_classLevelFixtures[$fixtureNamespace])
  110. ) {
  111. self::_deleteFixtures(self::$_classLevelFixtures[$fixtureNamespace]);
  112. }
  113. //ever disable secure area on class down
  114. self::_enableSecureArea(false);
  115. self::_unsetFixtureNamespace();
  116. parent::tearDownAfterClass();
  117. }
  118. /**
  119. * Call safe delete for models which added to delete list
  120. * Restore config values changed during the test
  121. *
  122. * @return void
  123. */
  124. protected function tearDown()
  125. {
  126. $fixtureNamespace = self::_getFixtureNamespace();
  127. if (isset(self::$_methodLevelFixtures[$fixtureNamespace])
  128. && count(self::$_methodLevelFixtures[$fixtureNamespace])
  129. ) {
  130. self::_deleteFixtures(self::$_methodLevelFixtures[$fixtureNamespace]);
  131. }
  132. $this->_callModelsDelete();
  133. $this->_restoreAppConfig();
  134. parent::tearDown();
  135. }
  136. /**
  137. * Perform Web API call to the system under test.
  138. *
  139. * @see \Magento\TestFramework\TestCase\Webapi\AdapterInterface::call()
  140. * @param array $serviceInfo
  141. * @param array $arguments
  142. * @param string|null $webApiAdapterCode
  143. * @param string|null $storeCode
  144. * @param \Magento\Integration\Model\Integration|null $integration
  145. * @return array|int|string|float|bool Web API call results
  146. */
  147. protected function _webApiCall(
  148. $serviceInfo,
  149. $arguments = [],
  150. $webApiAdapterCode = null,
  151. $storeCode = null,
  152. $integration = null
  153. ) {
  154. if ($webApiAdapterCode === null) {
  155. /** Default adapter code is defined in PHPUnit configuration */
  156. $webApiAdapterCode = strtolower(TESTS_WEB_API_ADAPTER);
  157. }
  158. return $this->_getWebApiAdapter($webApiAdapterCode)->call($serviceInfo, $arguments, $storeCode, $integration);
  159. }
  160. /**
  161. * Mark test to be executed for SOAP adapter only.
  162. */
  163. protected function _markTestAsSoapOnly($message = null)
  164. {
  165. if (TESTS_WEB_API_ADAPTER != self::ADAPTER_SOAP) {
  166. $this->markTestSkipped($message ? $message : "The test is intended to be executed for SOAP adapter only.");
  167. }
  168. }
  169. /**
  170. * Mark test to be executed for REST adapter only.
  171. */
  172. protected function _markTestAsRestOnly($message = null)
  173. {
  174. if (TESTS_WEB_API_ADAPTER != self::ADAPTER_REST) {
  175. $this->markTestSkipped($message ? $message : "The test is intended to be executed for REST adapter only.");
  176. }
  177. }
  178. /**
  179. * Set fixture to registry
  180. *
  181. * @param string $key
  182. * @param mixed $fixture
  183. * @param int $tearDown
  184. * @return void
  185. */
  186. public static function setFixture($key, $fixture, $tearDown = self::AUTO_TEAR_DOWN_AFTER_METHOD)
  187. {
  188. $fixturesNamespace = self::_getFixtureNamespace();
  189. if (!isset(self::$_fixtures[$fixturesNamespace])) {
  190. self::$_fixtures[$fixturesNamespace] = [];
  191. }
  192. self::$_fixtures[$fixturesNamespace][$key] = $fixture;
  193. if ($tearDown == self::AUTO_TEAR_DOWN_AFTER_METHOD) {
  194. if (!isset(self::$_methodLevelFixtures[$fixturesNamespace])) {
  195. self::$_methodLevelFixtures[$fixturesNamespace] = [];
  196. }
  197. self::$_methodLevelFixtures[$fixturesNamespace][] = $key;
  198. } else {
  199. if ($tearDown == self::AUTO_TEAR_DOWN_AFTER_CLASS) {
  200. if (!isset(self::$_classLevelFixtures[$fixturesNamespace])) {
  201. self::$_classLevelFixtures[$fixturesNamespace] = [];
  202. }
  203. self::$_classLevelFixtures[$fixturesNamespace][] = $key;
  204. }
  205. }
  206. }
  207. /**
  208. * Get fixture by key
  209. *
  210. * @param string $key
  211. * @return mixed
  212. */
  213. public static function getFixture($key)
  214. {
  215. $fixturesNamespace = self::_getFixtureNamespace();
  216. if (array_key_exists($key, self::$_fixtures[$fixturesNamespace])) {
  217. return self::$_fixtures[$fixturesNamespace][$key];
  218. }
  219. return null;
  220. }
  221. /**
  222. * Call safe delete for model
  223. *
  224. * @param \Magento\Framework\Model\AbstractModel $model
  225. * @param bool $secure
  226. * @return \Magento\TestFramework\TestCase\WebapiAbstract
  227. */
  228. public static function callModelDelete($model, $secure = false)
  229. {
  230. if ($model instanceof \Magento\Framework\Model\AbstractModel && $model->getId()) {
  231. if ($secure) {
  232. self::_enableSecureArea();
  233. }
  234. $model->delete();
  235. if ($secure) {
  236. self::_enableSecureArea(false);
  237. }
  238. }
  239. }
  240. /**
  241. * Call safe delete for model
  242. *
  243. * @param \Magento\Framework\Model\AbstractModel $model
  244. * @param bool $secure
  245. * @return \Magento\TestFramework\TestCase\WebapiAbstract
  246. */
  247. public function addModelToDelete($model, $secure = false)
  248. {
  249. $this->_modelsToDelete[] = ['model' => $model, 'secure' => $secure];
  250. return $this;
  251. }
  252. /**
  253. * Get Web API adapter (create if requested one does not exist).
  254. *
  255. * @param string $webApiAdapterCode
  256. * @return \Magento\TestFramework\TestCase\Webapi\AdapterInterface
  257. * @throws \LogicException When requested Web API adapter is not declared
  258. */
  259. protected function _getWebApiAdapter($webApiAdapterCode)
  260. {
  261. if (!isset($this->_webApiAdapters[$webApiAdapterCode])) {
  262. if (!isset($this->_webApiAdaptersMap[$webApiAdapterCode])) {
  263. throw new \LogicException(
  264. sprintf('Declaration of the requested Web API adapter "%s" was not found.', $webApiAdapterCode)
  265. );
  266. }
  267. $this->_webApiAdapters[$webApiAdapterCode] = Bootstrap::getObjectManager()->get(
  268. $this->_webApiAdaptersMap[$webApiAdapterCode]
  269. );
  270. }
  271. return $this->_webApiAdapters[$webApiAdapterCode];
  272. }
  273. /**
  274. * Set fixtures namespace
  275. *
  276. * @throws \RuntimeException
  277. */
  278. protected static function _setFixtureNamespace()
  279. {
  280. if (self::$_fixturesNamespace !== null) {
  281. throw new \RuntimeException('Fixture namespace is already set.');
  282. }
  283. self::$_fixturesNamespace = uniqid();
  284. }
  285. /**
  286. * Unset fixtures namespace
  287. */
  288. protected static function _unsetFixtureNamespace()
  289. {
  290. $fixturesNamespace = self::_getFixtureNamespace();
  291. unset(self::$_fixtures[$fixturesNamespace]);
  292. self::$_fixturesNamespace = null;
  293. }
  294. /**
  295. * Get fixtures namespace
  296. *
  297. * @throws \RuntimeException
  298. * @return string
  299. */
  300. protected static function _getFixtureNamespace()
  301. {
  302. $fixtureNamespace = self::$_fixturesNamespace;
  303. if ($fixtureNamespace === null) {
  304. throw new \RuntimeException('Fixture namespace must be set.');
  305. }
  306. return $fixtureNamespace;
  307. }
  308. /**
  309. * Enable secure/admin area
  310. *
  311. * @param bool $flag
  312. * @return void
  313. */
  314. protected static function _enableSecureArea($flag = true)
  315. {
  316. /** @var $objectManager \Magento\TestFramework\ObjectManager */
  317. $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
  318. $objectManager->get(\Magento\Framework\Registry::class)->unregister('isSecureArea');
  319. if ($flag) {
  320. $objectManager->get(\Magento\Framework\Registry::class)->register('isSecureArea', $flag);
  321. }
  322. }
  323. /**
  324. * Call delete models from list
  325. *
  326. * @return \Magento\TestFramework\TestCase\WebapiAbstract
  327. */
  328. protected function _callModelsDelete()
  329. {
  330. if ($this->_modelsToDelete) {
  331. foreach ($this->_modelsToDelete as $key => $modelData) {
  332. /** @var $model \Magento\Framework\Model\AbstractModel */
  333. $model = $modelData['model'];
  334. $this->callModelDelete($model, $modelData['secure']);
  335. unset($this->_modelsToDelete[$key]);
  336. }
  337. }
  338. return $this;
  339. }
  340. /**
  341. * Check if all error messages are expected ones
  342. *
  343. * @param array $expectedMessages
  344. * @param array $receivedMessages
  345. */
  346. protected function _assertMessagesEqual($expectedMessages, $receivedMessages)
  347. {
  348. foreach ($receivedMessages as $message) {
  349. $this->assertContains($message, $expectedMessages, "Unexpected message: '{$message}'");
  350. }
  351. $expectedErrorsCount = count($expectedMessages);
  352. $this->assertCount($expectedErrorsCount, $receivedMessages, 'Invalid messages quantity received');
  353. }
  354. /**
  355. * Delete array of fixtures
  356. *
  357. * @param array $fixtures
  358. */
  359. protected static function _deleteFixtures($fixtures)
  360. {
  361. foreach ($fixtures as $fixture) {
  362. self::deleteFixture($fixture, true);
  363. }
  364. }
  365. /**
  366. * Delete fixture by key
  367. *
  368. * @param string $key
  369. * @param bool $secure
  370. * @return void
  371. */
  372. public static function deleteFixture($key, $secure = false)
  373. {
  374. $fixturesNamespace = self::_getFixtureNamespace();
  375. if (array_key_exists($key, self::$_fixtures[$fixturesNamespace])) {
  376. self::callModelDelete(self::$_fixtures[$fixturesNamespace][$key], $secure);
  377. unset(self::$_fixtures[$fixturesNamespace][$key]);
  378. }
  379. }
  380. /** TODO: Remove methods below if not used, otherwise fix them (after having some tests implemented)*/
  381. /**
  382. * Get application cache model
  383. *
  384. * @return \Magento\Framework\App\Cache
  385. */
  386. protected function _getAppCache()
  387. {
  388. if (null === $this->_appCache) {
  389. //set application path
  390. $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
  391. /** @var \Magento\Framework\App\Config\ScopeConfigInterface $config */
  392. $config = $objectManager->get(\Magento\Framework\App\Config\ScopeConfigInterface::class);
  393. $options = $config->getOptions();
  394. $currentCacheDir = $options->getCacheDir();
  395. $currentEtcDir = $options->getEtcDir();
  396. /** @var Filesystem $filesystem */
  397. $filesystem = $objectManager->get(\Magento\Framework\Filesystem::class);
  398. $options->setCacheDir($filesystem->getDirectoryRead(DirectoryList::CACHE)->getAbsolutePath());
  399. $options->setEtcDir($filesystem->getDirectoryRead(DirectoryList::CONFIG)->getAbsolutePath());
  400. $this->_appCache = $objectManager->get(\Magento\Framework\App\Cache::class);
  401. //revert paths options
  402. $options->setCacheDir($currentCacheDir);
  403. $options->setEtcDir($currentEtcDir);
  404. }
  405. return $this->_appCache;
  406. }
  407. /**
  408. * Clean config cache of application
  409. *
  410. * @return bool
  411. */
  412. protected function _cleanAppConfigCache()
  413. {
  414. return $this->_getAppCache()->clean(\Magento\Framework\App\Config::CACHE_TAG);
  415. }
  416. /**
  417. * Update application config data
  418. *
  419. * @param string $path Config path with the form "section/group/node"
  420. * @param string|int|null $value Value of config item
  421. * @param bool $cleanAppCache If TRUE application cache will be refreshed
  422. * @param bool $updateLocalConfig If TRUE local config object will be updated too
  423. * @param bool $restore If TRUE config value will be restored after test run
  424. * @return \Magento\TestFramework\TestCase\WebapiAbstract
  425. * @throws \RuntimeException
  426. */
  427. protected function _updateAppConfig(
  428. $path,
  429. $value,
  430. $cleanAppCache = true,
  431. $updateLocalConfig = false,
  432. $restore = false
  433. ) {
  434. list($section, $group, $node) = explode('/', $path);
  435. if (!$section || !$group || !$node) {
  436. throw new \RuntimeException(
  437. sprintf('Config path must have view as "section/group/node" but now it "%s"', $path)
  438. );
  439. }
  440. $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
  441. /** @var $config \Magento\Config\Model\Config */
  442. $config = $objectManager->create(\Magento\Config\Model\Config::class);
  443. $data[$group]['fields'][$node]['value'] = $value;
  444. $config->setSection($section)->setGroups($data)->save();
  445. if ($restore && !isset($this->_origConfigValues[$path])) {
  446. $this->_origConfigValues[$path] = (string)$objectManager->get(
  447. \Magento\Framework\App\Config\ScopeConfigInterface::class
  448. )->getNode(
  449. $path,
  450. 'default'
  451. );
  452. }
  453. //refresh local cache
  454. if ($cleanAppCache) {
  455. if ($updateLocalConfig) {
  456. $objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class)->reinit();
  457. $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->reinitStores();
  458. }
  459. if (!$this->_cleanAppConfigCache()) {
  460. throw new \RuntimeException('Application configuration cache cannot be cleaned.');
  461. }
  462. }
  463. return $this;
  464. }
  465. /**
  466. * Restore config values changed during tests
  467. */
  468. protected function _restoreAppConfig()
  469. {
  470. foreach ($this->_origConfigValues as $configPath => $origValue) {
  471. $this->_updateAppConfig($configPath, $origValue, true, true);
  472. }
  473. }
  474. /**
  475. * @param \Exception $e
  476. * @return array
  477. * <pre> ex.
  478. * 'message' => "No such entity with %fieldName1 = %value1, %fieldName2 = %value2"
  479. * 'parameters' => [
  480. * "fieldName1" => "email",
  481. * "value1" => "dummy@example.com",
  482. * "fieldName2" => "websiteId",
  483. * "value2" => 0
  484. * ]
  485. *
  486. * </pre>
  487. */
  488. public function processRestExceptionResult(\Exception $e)
  489. {
  490. $error = json_decode($e->getMessage(), true);
  491. //Remove line breaks and replace with space
  492. $error['message'] = trim(preg_replace('/\s+/', ' ', $error['message']));
  493. // remove trace and type, will only be present if server is in dev mode
  494. unset($error['trace']);
  495. unset($error['type']);
  496. return $error;
  497. }
  498. /**
  499. * Verify that SOAP fault contains necessary information.
  500. *
  501. * @param \SoapFault $soapFault
  502. * @param string $expectedMessage
  503. * @param string $expectedFaultCode
  504. * @param array $expectedErrorParams
  505. * @param array $expectedWrappedErrors
  506. * @param string $traceString
  507. */
  508. protected function checkSoapFault(
  509. $soapFault,
  510. $expectedMessage,
  511. $expectedFaultCode,
  512. $expectedErrorParams = [],
  513. $expectedWrappedErrors = [],
  514. $traceString = null
  515. ) {
  516. $this->assertContains($expectedMessage, $soapFault->getMessage(), "Fault message is invalid.");
  517. $errorDetailsNode = 'GenericFault';
  518. $errorDetails = isset($soapFault->detail->$errorDetailsNode) ? $soapFault->detail->$errorDetailsNode : null;
  519. if (!empty($expectedErrorParams) || !empty($expectedWrappedErrors)) {
  520. /** Check SOAP fault details */
  521. $this->assertNotNull($errorDetails, "Details must be present.");
  522. $this->_checkFaultParams($expectedErrorParams, $errorDetails);
  523. $this->_checkWrappedErrors($expectedWrappedErrors, $errorDetails);
  524. }
  525. if ($traceString) {
  526. /** Check error trace */
  527. $traceNode = Fault::NODE_DETAIL_TRACE;
  528. $mode = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
  529. ->get(\Magento\Framework\App\State::class)
  530. ->getMode();
  531. if ($mode == \Magento\Framework\App\State::MODE_DEVELOPER) {
  532. /** Developer mode changes tested behavior and it cannot properly be tested for now */
  533. $this->assertContains(
  534. $traceString,
  535. $errorDetails->$traceNode,
  536. 'Trace Information is incorrect.'
  537. );
  538. } else {
  539. $this->assertNull($errorDetails, "Details are not expected.");
  540. }
  541. }
  542. /** Check SOAP fault code */
  543. $this->assertNotNull($soapFault->faultcode, "Fault code must not be empty.");
  544. $this->assertEquals($expectedFaultCode, $soapFault->faultcode, "Fault code is invalid.");
  545. }
  546. /**
  547. * Check additional error parameters.
  548. *
  549. * @param array $expectedErrorParams
  550. * @param \stdClass $errorDetails
  551. */
  552. protected function _checkFaultParams($expectedErrorParams, $errorDetails)
  553. {
  554. $paramsNode = Fault::NODE_DETAIL_PARAMETERS;
  555. if ($expectedErrorParams) {
  556. $paramNode = Fault::NODE_DETAIL_PARAMETER;
  557. $paramKey = Fault::NODE_DETAIL_PARAMETER_KEY;
  558. $paramValue = Fault::NODE_DETAIL_PARAMETER_VALUE;
  559. $actualParams = [];
  560. if (isset($errorDetails->$paramsNode->$paramNode)) {
  561. if (is_array($errorDetails->$paramsNode->$paramNode)) {
  562. foreach ($errorDetails->$paramsNode->$paramNode as $param) {
  563. $actualParams[$param->$paramKey] = $param->$paramValue;
  564. }
  565. } else {
  566. $param = $errorDetails->$paramsNode->$paramNode;
  567. $actualParams[$param->$paramKey] = $param->$paramValue;
  568. }
  569. }
  570. $this->assertEquals(
  571. $expectedErrorParams,
  572. $actualParams,
  573. "Parameters in fault details are invalid."
  574. );
  575. } else {
  576. $this->assertFalse(isset($errorDetails->$paramsNode), "Parameters are not expected in fault details.");
  577. }
  578. }
  579. /**
  580. * Check additional wrapped errors.
  581. *
  582. * @param array $expectedWrappedErrors
  583. * @param \stdClass $errorDetails
  584. */
  585. protected function _checkWrappedErrors($expectedWrappedErrors, $errorDetails)
  586. {
  587. $wrappedErrorsNode = Fault::NODE_DETAIL_WRAPPED_ERRORS;
  588. if ($expectedWrappedErrors) {
  589. $wrappedErrorNode = Fault::NODE_DETAIL_WRAPPED_ERROR;
  590. $actualWrappedErrors = [];
  591. if (isset($errorDetails->$wrappedErrorsNode->$wrappedErrorNode)) {
  592. $errorNode = $errorDetails->$wrappedErrorsNode->$wrappedErrorNode;
  593. if (is_array($errorNode)) {
  594. foreach ($errorNode as $error) {
  595. $actualWrappedErrors[] = $this->getActualWrappedErrors($error);
  596. }
  597. } else {
  598. $actualWrappedErrors[] = $this->getActualWrappedErrors($errorNode);
  599. }
  600. }
  601. $this->assertEquals(
  602. $expectedWrappedErrors,
  603. $actualWrappedErrors,
  604. "Wrapped errors in fault details are invalid."
  605. );
  606. } else {
  607. $this->assertFalse(
  608. isset($errorDetails->$wrappedErrorsNode),
  609. "Wrapped errors are not expected in fault details."
  610. );
  611. }
  612. }
  613. /**
  614. * @param \stdClass $errorNode
  615. * @return array
  616. */
  617. private function getActualWrappedErrors(\stdClass $errorNode)
  618. {
  619. $actualParameters = [];
  620. $parameterNode = $errorNode->parameters->parameter;
  621. if (is_array($parameterNode)) {
  622. foreach ($parameterNode as $parameter) {
  623. $actualParameters[$parameter->key] = $parameter->value;
  624. }
  625. } else {
  626. $actualParameters[$parameterNode->key] = $parameterNode->value;
  627. }
  628. return [
  629. 'message' => $errorNode->message,
  630. // Can not rename on parameters due to Backward Compatibility
  631. 'params' => $actualParameters,
  632. ];
  633. }
  634. }