diff --git a/composer.json b/composer.json index 939210928..8a4c37398 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "require": { "php": ">=8.3", "bedita/i18n": "^5.1.0", - "bedita/web-tools": "^5.1.0", + "bedita/web-tools": "^5.3", "cakephp/authentication": "^2.9", "cakephp/cakephp": "~4.5.0", "cakephp/plugin-installer": "^1.3", diff --git a/config/app_local.example.php b/config/app_local.example.php index e8708f584..4cd85f4a5 100644 --- a/config/app_local.example.php +++ b/config/app_local.example.php @@ -66,6 +66,22 @@ // //], // ], + /** + * Api Proxy configuration, for ApiController. + * This refers to `/api/{endpoint}` calls. + * Contains an array of setting to use for API proxy configuration. + * + * ## Options + * + * - `blocked` - Array of blocked methods per endpoint. + */ + // 'ApiProxy' => [ + // 'blocked' => [ + // 'objects' => ['GET', 'POST', 'PATCH', 'DELETE'], + // 'users' => ['GET', 'POST', 'PATCH', 'DELETE'], + // ], + // ], + /** * Clone configuration. * This adds custom rules to clone objects. diff --git a/config/routes.php b/config/routes.php index 353679145..a6b6d0147 100644 --- a/config/routes.php +++ b/config/routes.php @@ -423,6 +423,23 @@ ['pass' => ['id', 'relation', 'format', 'query'], '_name' => 'export:related:filtered'] ); + $routes->get( + '/history/objects', + ['controller' => 'History', 'action' => 'objects'], + 'history:objects', + ); + + $routes->get( + '/users/list', + ['controller' => 'Modules', 'action' => 'users'], + 'users:list', + ); + $routes->connect( + '/resources/get/{id}', + ['controller' => 'Modules', 'action' => 'get'], + ['pass' => ['id'], '_name' => 'resource:get'], + ); + // Download stream $routes->get( '/download/{id}', diff --git a/resources/js/app/components/objects-history/objects-history.vue b/resources/js/app/components/objects-history/objects-history.vue index a9696966a..0576bff2e 100644 --- a/resources/js/app/components/objects-history/objects-history.vue +++ b/resources/js/app/components/objects-history/objects-history.vue @@ -122,7 +122,7 @@ export default { return fetch(`/api/history?${query}`).then((r) => r.json()); }, async getObjects(ids) { - return fetch(`/api/objects?filter[id]=${ids.join(',')}`).then((r) => r.json()); + return fetch(`/history/objects?filter[id]=${ids.join(',')}`).then((r) => r.json()); }, loadHistory(pageSize = 20, page = 1) { this.loading = true; diff --git a/resources/js/app/components/placeholder-list/placeholder-list.vue b/resources/js/app/components/placeholder-list/placeholder-list.vue index 71a4b49dd..c9a3cbf38 100644 --- a/resources/js/app/components/placeholder-list/placeholder-list.vue +++ b/resources/js/app/components/placeholder-list/placeholder-list.vue @@ -103,7 +103,7 @@ export default { return this.cache[id]; } const baseUrl = new URL(BEDITA.base).pathname; - const response = await fetch(`${baseUrl}api/objects/${id}`, { + const response = await fetch(`${baseUrl}resources/get/${id}`, { credentials: 'same-origin', headers: { accept: 'application/json', diff --git a/resources/js/app/components/property-view/property-view.js b/resources/js/app/components/property-view/property-view.js index af688f937..dfbf115df 100644 --- a/resources/js/app/components/property-view/property-view.js +++ b/resources/js/app/components/property-view/property-view.js @@ -191,7 +191,7 @@ export default { const creatorId = this.object?.meta?.created_by; const modifierId = this.object?.meta?.modified_by; const usersId = [creatorId, modifierId]; - const userRes = await fetch(`${API_URL}api/users?filter[id]=${usersId.join(',')}&fields[users]=name,surname,username`, API_OPTIONS); + const userRes = await fetch(`${API_URL}users/list?filter[id]=${usersId.join(',')}`, API_OPTIONS); const userJson = await userRes.json(); const users = userJson.data; diff --git a/resources/js/app/components/recent-activity/recent-activity.vue b/resources/js/app/components/recent-activity/recent-activity.vue index ffea9851b..ff577b547 100644 --- a/resources/js/app/components/recent-activity/recent-activity.vue +++ b/resources/js/app/components/recent-activity/recent-activity.vue @@ -153,7 +153,7 @@ export default { this.currentPage = json.meta.pagination.page; this.activities = [...(json.data || [])]; const ids = this.activities.filter(item => item.meta.resource_id).map(item => item.meta.resource_id).filter((v, i, a) => a.indexOf(v) === i).map(i=>Number(i)); - const objectResponse = await fetch(`${API_URL}api/objects?filter[id]=${ids.join(',')}&page_size=${pageSize}`, API_OPTIONS); + const objectResponse = await fetch(`${API_URL}history/objects?filter[id]=${ids.join(',')}&page_size=${pageSize}`, API_OPTIONS); const objectJson = await objectResponse.json(); for (const item of this.activities) { const object = objectJson.data.find(obj => obj.id === item.meta.resource_id); diff --git a/resources/js/app/components/user-accesses/user-accesses.vue b/resources/js/app/components/user-accesses/user-accesses.vue index f2d9ee3b0..0aff8345c 100644 --- a/resources/js/app/components/user-accesses/user-accesses.vue +++ b/resources/js/app/components/user-accesses/user-accesses.vue @@ -76,7 +76,7 @@ export default { async getAccesses(pageSize = 20, page = 1) { const filterDate = this.filterDate ? new Date(this.filterDate).toISOString() : ''; - return fetch(`/api/users?page_size=${pageSize}&page=${page}&filter[last_login][gt]=${filterDate}&sort=-last_login`).then((r) => r.json()); + return fetch(`/users/list?page_size=${pageSize}&page=${page}&filter[last_login][gt]=${filterDate}&sort=-last_login`).then((r) => r.json()); }, loadAccesses(pageSize = 20, page = 1) { this.loading = true; diff --git a/resources/js/app/plugins/tinymce/placeholders.js b/resources/js/app/plugins/tinymce/placeholders.js index b6dabe2d4..3af9defc5 100644 --- a/resources/js/app/plugins/tinymce/placeholders.js +++ b/resources/js/app/plugins/tinymce/placeholders.js @@ -16,11 +16,7 @@ const options = { function fetchData(id) { if (!cache[id]) { - let fetchType = fetch(`${baseUrl}api/objects/${id}`, options) - .then((response) => response.json()) - .then((json) => json.data.type); - cache[id] = fetchType - .then((type) => fetch(`${baseUrl}api/${type}/${id}`, options)) + cache[id] = fetch(`${baseUrl}resources/get/${id}`, options) .then((response) => response.json()); } @@ -40,6 +36,7 @@ function loadPreview(editor, node, id) { if (!data) { return; } + console.log(data); let domElements = editor.getBody().querySelectorAll(`[data-placeholder="${data.id}"]`); [...domElements].forEach((dom) => { diff --git a/src/Controller/Admin/AdministrationBaseController.php b/src/Controller/Admin/AdministrationBaseController.php index 25e2739f2..e24ba0483 100644 --- a/src/Controller/Admin/AdministrationBaseController.php +++ b/src/Controller/Admin/AdministrationBaseController.php @@ -14,6 +14,7 @@ use App\Controller\AppController; use BEdita\SDK\BEditaClientException; +use BEdita\WebTools\Utility\ApiTools; use Cake\Event\EventInterface; use Cake\Http\Exception\UnauthorizedException; use Cake\Http\Response; @@ -215,17 +216,22 @@ protected function loadData(): array $query = $this->getRequest()->getQueryParams(); $resourceEndpoint = sprintf('%s/%s', $this->endpoint, $this->resourceType); $endpoint = $this->resourceType === 'roles' ? 'roles' : $resourceEndpoint; - $resultResponse = []; - $pagination = ['page' => 0]; - while (Hash::get($pagination, 'page') === 0 || Hash::get($pagination, 'page', -1) < Hash::get($pagination, 'page_count', -1)) { - $query['page'] = $pagination['page'] + 1; - $response = (array)$this->apiClient->get($endpoint, $query); - $pagination = (array)Hash::get($response, 'meta.pagination'); - foreach ((array)Hash::get($response, 'data') as $data) { - $resultResponse['data'][] = $data; - } + $resultResponse = ['data' => []]; + $pageCount = $page = 1; + $total = 0; + $limit = 500; + while ($limit > $total && $page <= $pageCount) { + $response = (array)$this->apiClient->get($endpoint, compact('page') + ['page_size' => 100]); + $response = ApiTools::cleanResponse($response); + $resultResponse['data'] = array_merge( + $resultResponse['data'], + (array)Hash::get($response, 'data'), + ); $resultResponse['meta'] = Hash::get($response, 'meta'); - $resultResponse['links'] = Hash::get($response, 'links'); + $pageCount = (int)Hash::get($response, 'meta.pagination.page_count'); + $count = (int)Hash::get($response, 'meta.pagination.page_items'); + $total += $count; + $page++; } return $resultResponse; diff --git a/src/Controller/ApiController.php b/src/Controller/ApiController.php index 046203738..4ff8df887 100644 --- a/src/Controller/ApiController.php +++ b/src/Controller/ApiController.php @@ -1,4 +1,6 @@ allowed()) { + throw new UnauthorizedException(__('You are not authorized to access this resource')); + } $this->Security->setConfig('unlockedActions', ['post', 'patch', 'delete']); return null; } + + /** + * Check if the request is allowed. + * + * @return bool + */ + protected function allowed(): bool + { + // block requests from browser address bar + $sameOrigin = (string)Hash::get((array)$this->request->getHeader('Sec-Fetch-Site'), 0) === 'same-origin'; + $noReferer = empty((array)$this->request->getHeader('Referer')); + $isNavigate = in_array('navigate', (array)$this->request->getHeader('Sec-Fetch-Mode')); + if (!$sameOrigin || $noReferer || $isNavigate) { + return false; + } + /** @var \Authentication\Identity|null $user */ + $user = $this->Authentication->getIdentity(); + $roles = empty($user) ? [] : (array)$user->get('roles'); + if (empty($roles)) { + return false; + } + if (in_array('admin', $roles)) { + return true; + } + $method = $this->request->getMethod(); + $action = $this->request->getParam('pass')[0] ?? null; + $blockedMethods = (array)Configure::read('ApiProxy.blocked', [ + 'objects' => ['GET', 'POST', 'PATCH', 'DELETE'], + 'users' => ['GET', 'POST', 'PATCH', 'DELETE'], + ]); + $blocked = in_array($method, $blockedMethods[$action] ?? []); + $modules = $this->viewBuilder()->getVar('modules'); + $modules = array_values($modules); + $modules = (array)Hash::combine($modules, '{n}.name', '{n}.hints.allow'); + $modules = array_merge( + $modules, + [ + 'history' => ['GET'], + 'model' => ['GET'], + ], + ); + $allowedMethods = (array)Hash::get($modules, $action, []); + $allowed = in_array($method, $allowedMethods); + + return $allowed && !$blocked; + } } diff --git a/src/Controller/Component/ModulesComponent.php b/src/Controller/Component/ModulesComponent.php index a69c16d9a..d8185983e 100644 --- a/src/Controller/Component/ModulesComponent.php +++ b/src/Controller/Component/ModulesComponent.php @@ -20,6 +20,8 @@ use Cake\Controller\Component; use Cake\Core\Configure; use Cake\Event\Event; +use Cake\Event\EventInterface; +use Cake\Http\Client\Response; use Cake\Http\Exception\BadRequestException; use Cake\Http\Exception\InternalErrorException; use Cake\I18n\I18n; @@ -84,6 +86,20 @@ class ModulesComponent extends Component ], ]; + /** + * @inheritDoc + */ + public function beforeFilter(EventInterface $event): ?Response + { + /** @var \Authentication\Identity|null $user */ + $user = $this->Authentication->getIdentity(); + if (!empty($user)) { + $this->getController()->set('modules', $this->getModules()); + } + + return null; + } + /** * Read modules and project info from `/home' endpoint. * @@ -103,12 +119,12 @@ public function startup(): void Cache::delete(sprintf('home_%d', $user->get('id'))); } - $modules = $this->getModules(); $project = $this->getProject(); $uploadable = (array)Hash::get($this->Schema->objectTypesFeatures(), 'uploadable'); - $this->getController()->set(compact('modules', 'project', 'uploadable')); + $this->getController()->set(compact('project', 'uploadable')); $currentModuleName = $this->getConfig('currentModuleName'); + $modules = (array)$this->getController()->viewBuilder()->getVar('modules'); if (!empty($currentModuleName)) { $currentModule = Hash::get($modules, $currentModuleName); } diff --git a/src/Controller/HistoryController.php b/src/Controller/HistoryController.php index 30c587553..97fefa3e1 100644 --- a/src/Controller/HistoryController.php +++ b/src/Controller/HistoryController.php @@ -1,6 +1,9 @@ loadComponent('Schema'); } + /** + * Get objects list: id, title, uname only / no relationships, no links. + * + * @return void + */ + public function objects(): void + { + $this->viewBuilder()->setClassName('Json'); + $this->getRequest()->allowMethod('get'); + $query = array_merge( + $this->getRequest()->getQueryParams(), + ['fields' => 'id,title,uname'] + ); + $response = ApiTools::cleanResponse((array)$this->apiClient->get('objects', $query)); + $data = $response['data']; + $meta = $response['meta']; + $this->set(compact('data', 'meta')); + $this->setSerialize(['data', 'meta']); + } + /** * Get history data by ID * diff --git a/src/Controller/ModulesController.php b/src/Controller/ModulesController.php index af074bbb1..58c033a90 100644 --- a/src/Controller/ModulesController.php +++ b/src/Controller/ModulesController.php @@ -16,6 +16,7 @@ use App\Utility\Message; use App\Utility\PermissionsTrait; use BEdita\SDK\BEditaClientException; +use BEdita\WebTools\Utility\ApiTools; use Cake\Core\Configure; use Cake\Event\Event; use Cake\Event\EventInterface; @@ -611,4 +612,48 @@ private function setupViewRelations(array $relations): void $this->set('schemasByType', $schemasByType); $this->set('filtersByType', $this->Properties->filtersByType($rightTypes)); } + + /** + * Get list of users / no email, no relationships, no links, no schema, no included. + * + * @return void + */ + public function users(): void + { + $this->viewBuilder()->setClassName('Json'); + $this->getRequest()->allowMethod('get'); + $query = array_merge( + $this->getRequest()->getQueryParams(), + ['fields' => 'id,title,username,name,surname'] + ); + $response = (array)$this->apiClient->get('users', $query); + $response = ApiTools::cleanResponse($response); + $data = (array)Hash::get($response, 'data'); + $meta = (array)Hash::get($response, 'meta'); + $this->set(compact('data', 'meta')); + $this->setSerialize(['data', 'meta']); + } + + /** + * Get single resource, minimal data / no relationships, no links, no schema, no included. + * + * @param string $id The object ID + * @return void + */ + public function get(string $id): void + { + $this->viewBuilder()->setClassName('Json'); + $this->getRequest()->allowMethod('get'); + $response = (array)$this->apiClient->getObject($id, 'objects'); + $query = array_merge( + $this->getRequest()->getQueryParams(), + ['fields' => 'id,title,description,uname,status,media_url'] + ); + $response = (array)$this->apiClient->getObject($id, $response['data']['type'], $query); + $response = ApiTools::cleanResponse($response); + $data = (array)Hash::get($response, 'data'); + $meta = (array)Hash::get($response, 'meta'); + $this->set(compact('data', 'meta')); + $this->setSerialize(['data', 'meta']); + } } diff --git a/tests/TestCase/Controller/ApiControllerTest.php b/tests/TestCase/Controller/ApiControllerTest.php new file mode 100644 index 000000000..035d6d49e --- /dev/null +++ b/tests/TestCase/Controller/ApiControllerTest.php @@ -0,0 +1,247 @@ + for more details. + */ + +namespace App\Test\TestCase\Controller; + +use App\Controller\ApiController; +use Authentication\AuthenticationServiceInterface; +use Authentication\Identity; +use Authentication\IdentityInterface; +use Cake\Http\Exception\UnauthorizedException; +use Cake\Http\ServerRequest; +use Cake\TestSuite\TestCase; +use Cake\Utility\Hash; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * {@see \App\Controller\ApiController} Test Case + * + * @coversDefaultClass \App\Controller\ApiController + * @uses \App\Controller\ApiController + */ +class ApiControllerTest extends TestCase +{ + /** + * @inheritDoc + */ + public function setUp(): void + { + parent::setUp(); + $this->loadRoutes(); + } + + /** + * Test subject + * + * @var \App\Controller\ApiController + */ + protected $ApiController; + + /** + * Setup controller to test with request config + * + * @param array $config configuration for controller setup + * @return void + */ + protected function setupController($config = null): void + { + $request = null; + if ($config != null) { + $headers = (array)Hash::get($config, 'headers', []); + unset($config['headers']); + $request = new ServerRequest($config); + foreach ($headers as $name => $value) { + $request = $request->withHeader($name, $value); + } + } + $this->ApiController = new ApiController($request); + } + + /** + * Data provider for `testUnauthorizedException` test case. + * + * @return array + */ + public function unauthorizedExceptionProvider(): array + { + return [ + 'no same origin' => [ + [ + 'headers' => [ + 'Referer' => 'http://example.com', + ], + ], + ], + 'no referer' => [ + [ + 'headers' => [ + 'Sec-Fetch-Site' => 'same-origin', + ], + ], + ], + 'navigate' => [ + [ + 'headers' => [ + 'Sec-Fetch-Site' => 'same-origin', + 'Referer' => 'http://example.com', + 'Sec-Fetch-Mode' => 'navigate', + ], + ], + ], + 'user empty roles' => [ + [ + 'headers' => [ + 'Sec-Fetch-Site' => 'same-origin', + 'Referer' => 'http://example.com', + ], + ], + ], + ]; + } + + /** + * Test for unauthorized exception + * + * @param array $config Request configuration. + * @return void + * @dataProvider unauthorizedExceptionProvider + * @covers ::beforeFilter() + * @covers ::allowed() + */ + public function testUnauthorizedException(array $config): void + { + $expected = new UnauthorizedException(__('You are not authorized to access this resource')); + $this->expectException(get_class($expected)); + $this->expectExceptionCode($expected->getCode()); + $this->expectExceptionMessage($expected->getMessage()); + $this->setupController($config); + $this->ApiController->setRequest($this->ApiController->getRequest()->withAttribute('authentication', $this->getAuthenticationServiceMock())); + $this->ApiController->dispatchEvent('Controller.initialize'); + } + + /** + * Test unauthorized role + * + * @return void + * @covers ::beforeFilter() + * @covers ::allowed() + */ + public function testUnauthorizedRole(): void + { + $expected = new UnauthorizedException(__('You are not authorized to access this resource')); + $this->expectException(get_class($expected)); + $this->expectExceptionCode($expected->getCode()); + $this->expectExceptionMessage($expected->getMessage()); + $this->setupController([ + 'headers' => [ + 'Sec-Fetch-Site' => 'same-origin', + 'Referer' => 'http://example.com', + ], + ]); + $user = new Identity([ + 'id' => 1, + 'username' => 'dummy', + 'roles' => ['readers'], + ]); + $this->ApiController->setRequest($this->ApiController->getRequest()->withAttribute('authentication', $this->getAuthenticationServiceMock())); + $this->ApiController->Authentication->setIdentity($user); + $this->ApiController->dispatchEvent('Controller.initialize'); + } + + /** + * Test for authorized admin + * + * @return void + * @covers ::beforeFilter() + * @covers ::allowed() + */ + public function testAuthorizeAdmin(): void + { + $this->setupController([ + 'headers' => [ + 'Sec-Fetch-Site' => 'same-origin', + 'Referer' => 'http://example.com', + ], + ]); + $user = new Identity([ + 'id' => 1, + 'username' => 'admin', + 'roles' => ['admin'], + ]); + $this->ApiController->setRequest($this->ApiController->getRequest()->withAttribute('authentication', $this->getAuthenticationServiceMock())); + $this->ApiController->Authentication->setIdentity($user); + $this->ApiController->dispatchEvent('Controller.initialize'); + $actual = $this->ApiController->Security->getConfig('unlockedActions'); + $expected = ['post', 'patch', 'delete']; + static::assertEquals($expected, $actual); + } + + /** + * Test for authorized user + * + * @return void + * @covers ::beforeFilter() + * @covers ::allowed() + */ + public function testUserAllowed(): void + { + $this->setupController([ + 'params' => ['pass' => ['events']], + 'headers' => [ + 'Sec-Fetch-Site' => 'same-origin', + 'Referer' => 'http://example.com', + ], + ]); + $user = new Identity([ + 'id' => 1, + 'username' => 'dummy', + 'roles' => ['manager'], + ]); + $this->ApiController->setRequest($this->ApiController->getRequest()->withAttribute('authentication', $this->getAuthenticationServiceMock())); + $this->ApiController->Authentication->setIdentity($user); + $this->ApiController->dispatchEvent('Controller.initialize'); + $actual = $this->ApiController->Security->getConfig('unlockedActions'); + $expected = ['post', 'patch', 'delete']; + static::assertEquals($expected, $actual); + } + + /** + * Get mocked AuthenticationService. + * + * @return AuthenticationServiceInterface + */ + protected function getAuthenticationServiceMock(): AuthenticationServiceInterface + { + $authenticationService = $this->getMockBuilder(AuthenticationServiceInterface::class) + ->getMock(); + $authenticationService->method('clearIdentity') + ->willReturnCallback(function (ServerRequestInterface $request, ResponseInterface $response): array { + return [ + 'request' => $request->withoutAttribute('identity'), + 'response' => $response, + ]; + }); + $authenticationService->method('persistIdentity') + ->willReturnCallback(function (ServerRequestInterface $request, ResponseInterface $response, IdentityInterface $identity): array { + return [ + 'request' => $request->withAttribute('identity', $identity), + 'response' => $response, + ]; + }); + + return $authenticationService; + } +} diff --git a/tests/TestCase/Controller/Component/ModulesComponentTest.php b/tests/TestCase/Controller/Component/ModulesComponentTest.php index 401ae232f..193c8b35e 100644 --- a/tests/TestCase/Controller/Component/ModulesComponentTest.php +++ b/tests/TestCase/Controller/Component/ModulesComponentTest.php @@ -28,6 +28,7 @@ use Cake\Cache\Cache; use Cake\Controller\Controller; use Cake\Core\Configure; +use Cake\Event\Event; use Cake\Http\Exception\BadRequestException; use Cake\Http\Exception\InternalErrorException; use Cake\TestSuite\TestCase; @@ -276,6 +277,7 @@ public function testIsAbstract($expected, $data): void // Mock Authentication component $controller->setRequest($controller->getRequest()->withAttribute('authentication', $this->getAuthenticationServiceMock())); $this->Modules->Authentication->setIdentity(new Identity(['id' => 1, 'roles' => ['guest']])); + $this->Modules->beforeFilter(new Event('Module.beforeFilter')); $this->Modules->startup(); $actual = $this->Modules->isAbstract($data); @@ -348,6 +350,7 @@ public function testObjectTypes($expected, $data): void $this->Modules->Authentication->setIdentity(new Identity(['id' => 1, 'roles' => ['guest']])); if (!empty($expected)) { + $this->Modules->beforeFilter(new Event('Module.beforeFilter')); $this->Modules->startup(); } $actual = $this->Modules->objectTypes($data); @@ -767,6 +770,7 @@ public function startupProvider(): array * @return void * @dataProvider startupProvider() * @covers ::startup() + * @covers ::beforeFilter() */ public function testBeforeRender($userId, $modules, ?string $currentModule, array $project, array $meta, array $config = [], ?string $currentModuleName = null): void { @@ -800,6 +804,7 @@ public function testBeforeRender($userId, $modules, ?string $currentModule, arra $clearHomeCache = true; $this->Modules->setConfig(compact('apiClient', 'currentModuleName', 'clearHomeCache')); + $this->Modules->beforeFilter(new Event('Module.beforeFilter')); $this->Modules->startup(); $viewVars = $controller->viewBuilder()->getVars(); diff --git a/tests/TestCase/Controller/HistoryControllerTest.php b/tests/TestCase/Controller/HistoryControllerTest.php index 2ff366937..58b103369 100644 --- a/tests/TestCase/Controller/HistoryControllerTest.php +++ b/tests/TestCase/Controller/HistoryControllerTest.php @@ -213,4 +213,33 @@ public function testSetHistory(array $data, string $expected): void $actual = $this->HistoryController->getRequest()->getSession()->read(sprintf('history.%s.attributes', $id)); static::assertEquals($actual, $expected); } + + /** + * Test `objects` method + * + * @return void + * @covers ::objects() + */ + public function testObjects(): void + { + $this->HistoryController->objects(); + $vars = ['data', 'meta']; + foreach ($vars as $var) { + static::assertNotEmpty($this->HistoryController->viewBuilder()->getVar($var)); + } + $actual = $this->HistoryController->viewBuilder()->getVar('data'); + foreach ($actual as $item) { + static::assertArrayHasKey('id', $item); + static::assertArrayHasKey('type', $item); + static::assertArrayHasKey('attributes', $item); + $attributes = $item['attributes']; + static::assertArrayHasKey('uname', $attributes); + static::assertArrayHasKey('title', $attributes); + static::assertArrayNotHasKey('links', $item); + static::assertArrayNotHasKey('relationships', $item); + } + $actual = $this->HistoryController->viewBuilder()->getVar('meta'); + static::assertArrayHasKey('pagination', $actual); + static::assertArrayNotHasKey('schema', $actual); + } } diff --git a/tests/TestCase/Controller/ModulesControllerTest.php b/tests/TestCase/Controller/ModulesControllerTest.php index a7050d7ab..154407a71 100644 --- a/tests/TestCase/Controller/ModulesControllerTest.php +++ b/tests/TestCase/Controller/ModulesControllerTest.php @@ -23,6 +23,7 @@ use Authentication\IdentityInterface; use Cake\Cache\Cache; use Cake\Core\Configure; +use Cake\Event\Event; use Cake\Http\ServerRequest; use Cake\Utility\Hash; use Psr\Http\Message\ResponseInterface; @@ -76,6 +77,7 @@ protected function setupController(?array $requestConfig = []): void $this->controller->Authentication->setIdentity(new Identity(['id' => 'dummy'])); // Mock GET /config using cache Cache::write(CacheTools::cacheKey('config.Modules'), []); + $this->controller->Modules->beforeFilter(new Event('Module.beforeFilter')); $this->controller->Modules->startup(); $this->setupApi(); $this->createTestObject(); @@ -1068,4 +1070,52 @@ public function testGetSetObjectType(): void $actual = $this->controller->getObjectType(); static::assertSame($expected, $actual); } + + /** + * Test list users + * + * @return void + * @covers ::users() + */ + public function testListUsers(): void + { + $this->setupController(); + // Get list of users / no email, no relationships, no links, no schema, no included. + $this->controller->users(); + $actual = $this->controller->viewBuilder()->getVars(); + // check data has only id,title,username,name,surname + $data = $actual['data']; + foreach ($data as $item) { + $itemKeys = array_keys($item); + sort($itemKeys); + static::assertSame(['attributes', 'id', 'meta', 'type'], $itemKeys); + $keys = array_keys($item['attributes']); + sort($keys); + static::assertSame($keys, ['name', 'surname', 'title', 'username']); + } + } + + /** + * Test get single resource minimal data + * + * @return void + * @covers ::get() + */ + public function testResourceGet(): void + { + $this->setupController(); + // get object ID for test + $id = $this->getTestId(); + // Get single resource, minimal data / no relationships, no links, no schema, no included. + $this->controller->get($id); + $actual = $this->controller->viewBuilder()->getVars(); + // check data has only id,title,description,uname,status + $item = $actual['data']; + $itemKeys = array_keys($item); + sort($itemKeys); + static::assertSame(['attributes', 'id', 'type'], $itemKeys); + $keys = array_keys($item['attributes']); + sort($keys); + static::assertSame($keys, ['description', 'status', 'title', 'uname']); + } }