diff --git a/appinfo/routes.php b/appinfo/routes.php index 2a7b133e2..488a0f849 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -93,7 +93,6 @@ ['name' => 'row#index', 'url' => '/row/table/{tableId}', 'verb' => 'GET'], ['name' => 'row#show', 'url' => '/row/{id}', 'verb' => 'GET'], ['name' => 'row#indexView', 'url' => '/row/view/{viewId}', 'verb' => 'GET'], - ['name' => 'row#create', 'url' => '/row', 'verb' => 'POST'], ['name' => 'row#update', 'url' => '/row/{id}/column/{columnId}', 'verb' => 'PUT'], ['name' => 'row#updateSet', 'url' => '/row/{id}', 'verb' => 'PUT'], ['name' => 'row#destroyByView', 'url' => '/view/{viewId}/row/{id}', 'verb' => 'DELETE'], diff --git a/lib/Controller/Api1Controller.php b/lib/Controller/Api1Controller.php index 4f630d806..9e1276d06 100644 --- a/lib/Controller/Api1Controller.php +++ b/lib/Controller/Api1Controller.php @@ -10,6 +10,7 @@ namespace OCA\Tables\Controller; use Exception; +use InvalidArgumentException; use OCA\Tables\Api\V1Api; use OCA\Tables\AppInfo\Application; use OCA\Tables\Db\ViewMapper; @@ -376,9 +377,10 @@ public function getView(int $viewId): DataResponse { * * @param int $viewId View ID * @param array{key: 'title'|'emoji'|'description', value: string}|array{key: 'columns', value: int[]}|array{key: 'sort', value: array{columnId: int, mode: 'ASC'|'DESC'}}|array{key: 'filter', value: array{columnId: int, operator: 'begins-with'|'ends-with'|'contains'|'is-equal'|'is-greater-than'|'is-greater-than-or-equal'|'is-lower-than'|'is-lower-than-or-equal'|'is-empty', value: string|int|float}} $data key-value pairs - * @return DataResponse|DataResponse + * @return DataResponse|DataResponse * * 200: View updated + * 400: Invalid data * 403: No permissions * 404: Not found */ @@ -386,9 +388,13 @@ public function updateView(int $viewId, array $data): DataResponse { try { return new DataResponse($this->viewService->update($viewId, $data)->jsonSerialize()); } catch (PermissionError $e) { - $this->logger->warning('A permission error occurred: '.$e->getMessage(), ['exception' => $e]); + $this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_FORBIDDEN); + } catch (InvalidArgumentException $e) { + $this->logger->warning('An invalid request occurred: ' . $e->getMessage(), ['exception' => $e]); + $message = ['message' => $e->getMessage()]; + return new DataResponse($message, Http::STATUS_BAD_REQUEST); } catch (InternalError|Exception $e) { $this->logger->error('An internal error or exception occurred: '.$e->getMessage(), ['exception' => $e]); $message = ['message' => $e->getMessage()]; @@ -1127,6 +1133,7 @@ public function indexViewRows(int $viewId, ?int $limit, ?int $offset): DataRespo * 200: Row returned * 403: No permissions */ + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')] public function createRowInView(int $viewId, $data): DataResponse { if(is_string($data)) { $data = json_decode($data, true); @@ -1173,6 +1180,7 @@ public function createRowInView(int $viewId, $data): DataResponse { * 403: No permissions * 404: Not found */ + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')] public function createRowInTable(int $tableId, $data): DataResponse { if(is_string($data)) { $data = json_decode($data, true); @@ -1360,8 +1368,10 @@ public function deleteRowByView(int $rowId, int $viewId): DataResponse { * 403: No permissions * 404: Not found */ + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')] public function importInTable(int $tableId, string $path, bool $createMissingColumns = true): DataResponse { try { + // minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer return new DataResponse($this->importService->import($tableId, null, $path, $createMissingColumns)); } catch (PermissionError $e) { $this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]); @@ -1393,8 +1403,10 @@ public function importInTable(int $tableId, string $path, bool $createMissingCol * 403: No permissions * 404: Not found */ + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')] public function importInView(int $viewId, string $path, bool $createMissingColumns = true): DataResponse { try { + // minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer return new DataResponse($this->importService->import(null, $viewId, $path, $createMissingColumns)); } catch (PermissionError $e) { $this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]); diff --git a/lib/Controller/Errors.php b/lib/Controller/Errors.php index a4c3003fa..c0acb01de 100644 --- a/lib/Controller/Errors.php +++ b/lib/Controller/Errors.php @@ -8,6 +8,7 @@ namespace OCA\Tables\Controller; use Closure; +use InvalidArgumentException; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; @@ -20,13 +21,17 @@ protected function handleError(Closure $callback): DataResponse { try { return new DataResponse($callback()); } catch (PermissionError $e) { - $this->logger->warning('A permission error accured: '.$e->getMessage(), ['exception' => $e]); + $this->logger->warning('A permission error occurred: '.$e->getMessage(), ['exception' => $e]); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_FORBIDDEN); } catch (NotFoundError $e) { - $this->logger->warning('A not found error accured: '.$e->getMessage(), ['exception' => $e]); + $this->logger->warning('A not found error occurred: ' . $e->getMessage(), ['exception' => $e]); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); + } catch (InvalidArgumentException $e) { + $this->logger->warning('An invalid request occurred: ' . $e->getMessage(), ['exception' => $e]); + $message = ['message' => $e->getMessage()]; + return new DataResponse($message, Http::STATUS_BAD_REQUEST); } catch (InternalError|\Exception $e) { $this->logger->error('An internal error or exception occurred: '.$e->getMessage(), ['exception' => $e]); $message = ['message' => $e->getMessage()]; diff --git a/lib/Controller/ImportController.php b/lib/Controller/ImportController.php index 8aa656f9d..2fd1ffe21 100644 --- a/lib/Controller/ImportController.php +++ b/lib/Controller/ImportController.php @@ -8,6 +8,7 @@ namespace OCA\Tables\Controller; use OCA\Tables\AppInfo\Application; +use OCA\Tables\Middleware\Attribute\RequirePermission; use OCA\Tables\Service\ImportService; use OCA\Tables\UploadException; use OCP\AppFramework\Controller; @@ -65,8 +66,10 @@ public function previewImportTable(int $tableId, String $path): DataResponse { /** * @NoAdminRequired */ + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')] public function importInTable(int $tableId, String $path, bool $createMissingColumns = true, array $columnsConfig = []): DataResponse { return $this->handleError(function () use ($tableId, $path, $createMissingColumns, $columnsConfig) { + // minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer return $this->service->import($tableId, null, $path, $createMissingColumns, $columnsConfig); }); } @@ -83,8 +86,10 @@ public function previewImportView(int $viewId, String $path): DataResponse { /** * @NoAdminRequired */ + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')] public function importInView(int $viewId, String $path, bool $createMissingColumns = true, array $columnsConfig = []): DataResponse { return $this->handleError(function () use ($viewId, $path, $createMissingColumns, $columnsConfig) { + // minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer return $this->service->import(null, $viewId, $path, $createMissingColumns, $columnsConfig); }); } @@ -107,11 +112,13 @@ public function previewUploadImportTable(int $tableId): DataResponse { /** * @NoAdminRequired */ + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')] public function importUploadInTable(int $tableId, bool $createMissingColumns = true, string $columnsConfig = ''): DataResponse { try { $columnsConfigArray = json_decode($columnsConfig, true); $file = $this->getUploadedFile('uploadfile'); return $this->handleError(function () use ($tableId, $file, $createMissingColumns, $columnsConfigArray) { + // minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer return $this->service->import($tableId, null, $file['tmp_name'], $createMissingColumns, $columnsConfigArray); }); } catch (UploadException | NotPermittedException $e) { @@ -138,11 +145,13 @@ public function previewUploadImportView(int $viewId): DataResponse { /** * @NoAdminRequired */ + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')] public function importUploadInView(int $viewId, bool $createMissingColumns = true, string $columnsConfig = ''): DataResponse { try { $columnsConfigArray = json_decode($columnsConfig, true); $file = $this->getUploadedFile('uploadfile'); return $this->handleError(function () use ($viewId, $file, $createMissingColumns, $columnsConfigArray) { + // minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer return $this->service->import(null, $viewId, $file['tmp_name'], $createMissingColumns, $columnsConfigArray); }); } catch (UploadException | NotPermittedException $e) { diff --git a/lib/Controller/RowController.php b/lib/Controller/RowController.php index e6b992d1e..64084f8bb 100644 --- a/lib/Controller/RowController.php +++ b/lib/Controller/RowController.php @@ -63,22 +63,6 @@ public function show(int $id): DataResponse { }); } - /** - * @NoAdminRequired - */ - public function create( - ?int $tableId, - ?int $viewId, - array $data - ): DataResponse { - return $this->handleError(function () use ($tableId, $viewId, $data) { - return $this->service->create( - $tableId, - $viewId, - $data); - }); - } - /** * @NoAdminRequired */ diff --git a/lib/Model/RowDataInput.php b/lib/Model/RowDataInput.php index 47a51bbdd..137bf3e2a 100644 --- a/lib/Model/RowDataInput.php +++ b/lib/Model/RowDataInput.php @@ -8,17 +8,23 @@ namespace OCA\Tables\Model; use ArrayAccess; +use Iterator; +use function current; +use function key; +use function next; +use function reset; /** * @template-implements ArrayAccess + * @template-implements Iterator */ -class RowDataInput implements ArrayAccess { +class RowDataInput implements ArrayAccess, Iterator { protected const DATA_KEY = 'columnId'; protected const DATA_VAL = 'value'; /** @psalm-var array */ protected array $data = []; - public function add(int $columnId, string $value): self { + public function add(int $columnId, string|array $value): self { $this->data[] = [self::DATA_KEY => $columnId, self::DATA_VAL => $value]; return $this; } @@ -45,4 +51,24 @@ public function offsetUnset(mixed $offset): void { unset($this->data[$offset]); } } + + public function current(): mixed { + return current($this->data); + } + + public function next(): void { + next($this->data); + } + + public function key(): mixed { + return key($this->data); + } + + public function valid(): bool { + return $this->key() !== null; + } + + public function rewind(): void { + reset($this->data); + } } diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index 4f9d012de..a30725019 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -187,8 +187,8 @@ private function getPreviewData(Worksheet $worksheet): array { } /** - * @param int|null $tableId - * @param int|null $viewId + * @param ?int $tableId + * @param ?int $viewId * @param string $path * @param bool $createMissingColumns * @return array @@ -208,7 +208,8 @@ public function import(?int $tableId, ?int $viewId, string $path, bool $createMi throw new PermissionError('create columns at the view id = '.$viewId.' is not allowed.'); } $this->viewId = $viewId; - } elseif ($tableId) { + } + if ($tableId) { $table = $this->tableService->find($tableId); if (!$this->permissionsService->canCreateRows($table, 'table')) { throw new PermissionError('create row at the view id = '. (string) $viewId .' is not allowed.'); @@ -217,11 +218,17 @@ public function import(?int $tableId, ?int $viewId, string $path, bool $createMi throw new PermissionError('create columns at the view id = '. (string) $viewId .' is not allowed.'); } $this->tableId = $tableId; - } else { + } + if (!$this->tableId && !$this->viewId) { $e = new \Exception('Neither tableId nor viewId is given.'); $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } + if ($this->tableId && $this->viewId) { + $e = new \LogicException('Both table ID and view ID are provided, but only one of them is allowed'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } if ($this->userId === null || $this->userManager->get($this->userId) === null) { $error = 'No user in context, can not import data. Cancel.'; diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index fa2f07894..18eeafeef 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -11,7 +11,6 @@ use OCA\Tables\Db\ColumnMapper; use OCA\Tables\Db\Row2; use OCA\Tables\Db\Row2Mapper; -use OCA\Tables\Db\Table; use OCA\Tables\Db\TableMapper; use OCA\Tables\Db\View; use OCA\Tables\Db\ViewMapper; @@ -192,7 +191,8 @@ public function create(?int $tableId, ?int $viewId, RowDataInput|array $data): R } $columns = $this->columnMapper->findMultiple($view->getColumnsArray()); - } elseif ($tableId) { + } + if ($tableId) { try { $table = $this->tableMapper->find($tableId); } catch (DoesNotExistException $e) { @@ -209,7 +209,9 @@ public function create(?int $tableId, ?int $viewId, RowDataInput|array $data): R } $columns = $this->columnMapper->findAllByTable($tableId); - } else { + } + + if (!$viewId && !$tableId) { throw new InternalError('Cannot create row without table or view in context'); } diff --git a/lib/Service/ViewService.php b/lib/Service/ViewService.php index 1bed13295..bd7e98bc0 100644 --- a/lib/Service/ViewService.php +++ b/lib/Service/ViewService.php @@ -11,13 +11,12 @@ use DateTime; use Exception; - +use InvalidArgumentException; use OCA\Tables\AppInfo\Application; +use OCA\Tables\Db\Column; use OCA\Tables\Db\Table; use OCA\Tables\Db\View; use OCA\Tables\Db\ViewMapper; - - use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; @@ -74,7 +73,6 @@ public function __construct( $this->contextService = $contextService; } - /** * @param Table $table * @param string|null $userId @@ -246,6 +244,7 @@ public function updateSingle(int $id, string $key, ?string $value, ?string $user * @return View * @throws InternalError * @throws PermissionError + * @throws InvalidArgumentException */ public function update(int $id, array $data, ?string $userId = null, bool $skipTableEnhancement = false): View { $userId = $this->permissionsService->preCheckUserId($userId); @@ -255,26 +254,42 @@ public function update(int $id, array $data, ?string $userId = null, bool $skipT // security if (!$this->permissionsService->canManageView($view, $userId)) { - throw new PermissionError('PermissionError: can not update view with id '.$id); + throw new PermissionError('PermissionError: can not update view with id ' . $id); } $updatableParameter = ['title', 'emoji', 'description', 'columns', 'sort', 'filter']; foreach ($data as $key => $value) { if (!in_array($key, $updatableParameter)) { - throw new InternalError('View parameter '.$key.' can not be updated.'); + throw new InternalError('View parameter ' . $key . ' can not be updated.'); } - $setterMethod = 'set'.ucfirst($key); + + if ($key === 'columns') { + // we have to fetch the service here as ColumnService already depends on the ViewService, i.e. no DI + $columnService = \OCP\Server::get(ColumnService::class); + $columnIds = \json_decode($value, true); + $availableColumns = $columnService->findAllByTable($view->getTableId(), $view->getId(), $this->userId); + $availableColumns = array_map(static fn (Column $column) => $column->getId(), $availableColumns); + foreach ($columnIds as $columnId) { + if (!in_array($columnId, $availableColumns, true)) { + throw new InvalidArgumentException('Invalid column ID provided'); + } + } + } + + $setterMethod = 'set' . ucfirst($key); $view->$setterMethod($value); } $time = new DateTime(); $view->setLastEditBy($userId); $view->setLastEditAt($time->format('Y-m-d H:i:s')); $view = $this->mapper->update($view); - if(!$skipTableEnhancement) { + if (!$skipTableEnhancement) { $this->enhanceView($view, $userId); } return $view; + } catch (InvalidArgumentException $e) { + throw $e; } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError($e->getMessage()); diff --git a/openapi.json b/openapi.json index 7e27390a1..fddafc065 100644 --- a/openapi.json +++ b/openapi.json @@ -1769,6 +1769,24 @@ } } }, + "400": { + "description": "Invalid data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, "500": { "description": "", "content": { diff --git a/src/modules/modals/CreateRow.vue b/src/modules/modals/CreateRow.vue index 5e7566c97..7fa4ac831 100644 --- a/src/modules/modals/CreateRow.vue +++ b/src/modules/modals/CreateRow.vue @@ -118,12 +118,9 @@ export default { } try { - const data = [] + const data = {} for (const [key, value] of Object.entries(this.row)) { - data.push({ - columnId: key, - value, - }) + data[key] = value } await this.$store.dispatch('insertNewRow', { viewId: this.isView ? this.elementId : null, diff --git a/src/store/data.js b/src/store/data.js index cac1df8c5..6229b079f 100644 --- a/src/store/data.js +++ b/src/store/data.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import axios from '@nextcloud/axios' -import { generateUrl } from '@nextcloud/router' +import { generateUrl, generateOcsUrl } from '@nextcloud/router' import displayError from '../shared/utils/displayError.js' import { parseCol } from '../shared/components/ncTable/mixins/columnParser.js' import { MetaColumns } from '../shared/components/ncTable/mixins/metaColumns.js' @@ -209,7 +209,9 @@ export default { let res = null try { - res = await axios.post(generateUrl('/apps/tables/row'), { viewId, tableId, data }) + const collection = viewId == null ? 'tables' : 'views' + const nodeId = viewId == null ? tableId : viewId + res = await axios.post(generateOcsUrl('/apps/tables/api/2/' + collection + '/' + nodeId + '/rows'), { data }) } catch (e) { displayError(e, t('tables', 'Could not insert row.')) return false @@ -217,7 +219,7 @@ export default { const stateId = genStateKey(!!(viewId), viewId ?? tableId) if (stateId) { - const row = res.data + const row = res?.data?.ocs?.data const rows = state.rows[stateId] rows.push(row) commit('setRows', { stateId, rows: [...rows] }) diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 53034bb30..5e1f859bf 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1476,6 +1476,17 @@ export interface operations { readonly "application/json": components["schemas"]["View"]; }; }; + /** @description Invalid data */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly message: string; + }; + }; + }; /** @description No permissions */ readonly 403: { headers: { diff --git a/tests/integration/features/APIv1.feature b/tests/integration/features/APIv1.feature index 500cd9dfe..b70fc3c98 100644 --- a/tests/integration/features/APIv1.feature +++ b/tests/integration/features/APIv1.feature @@ -229,6 +229,31 @@ Feature: APIv1 When user "participant1" deletes view "first-view" Then table "view-test" has the following views for user "participant1" + @api1 @views + Scenario: Column can be added to a view + Given table "Private One" with emoji "🤫" exists for user "participant1" as "table_p1" + Then column "Volatile Notes" exists with following properties + | type | text | + | subtype | line | + | mandatory | 0 | + | description | Note me a thing | + And user "participant1" create view "Simple View" with emoji "🙃" for "table_p1" as "simple-view" + When user "participant1" sets columns "Volatile Notes" to view "simple-view" + Then the reported status is "200" + + @api1 @views + Scenario: Foreign or nonexistent columns cannot be added to a view + Given table "Private One" with emoji "🤫" exists for user "participant1" as "table_p1" + Then column "Volatile Notes" exists with following properties + | type | text | + | subtype | line | + | mandatory | 0 | + | description | Note me a thing | + And table "Private Two" with emoji "🥶" exists for user "participant2" as "table_p2" + And user "participant2" create view "Sneaky View" with emoji "🫣" for "table_p2" as "sneaky-view" + When user "participant2" sets columns "Volatile Notes" to view "sneaky-view" + Then the reported status is "400" + @api1 @contexts @contexts-sharing Scenario: Share an owned context Given table "Table 1 via api v2" with emoji "👋" exists for user "participant1" as "t1" via v2 diff --git a/tests/integration/features/APIv2.feature b/tests/integration/features/APIv2.feature index 3e9eea12c..62ff7f1ae 100644 --- a/tests/integration/features/APIv2.feature +++ b/tests/integration/features/APIv2.feature @@ -786,6 +786,7 @@ Feature: APIv2 | usergroupSelectUsers | true | | usergroupSelectGroups | false | And user "participant1-v2" create view "v1" with emoji "⚡️" for "t1" as "v1" + And user "participant1-v2" sets columns "one,two,three,four,five" to view "v1" When user "participant1-v2" tries to create a row using v2 on "view" "v1" with following values | one | AHA | | two | 161 | @@ -793,6 +794,12 @@ Feature: APIv2 | four | 2023-12-24 | | five | [{"id": "admin", "type": 0}] | Then the reported status is 200 + And the inserted row has the following values + | one | AHA | + | two | 161 | + | three | true | + | four | 2023-12-24 | + | five | [{"id": "admin", "type": 0}] | @api2 @rows @views Scenario: Create rows on a view via v2 with permissions @@ -825,6 +832,7 @@ Feature: APIv2 | usergroupSelectUsers | true | | usergroupSelectGroups | false | And user "participant1-v2" create view "v1" with emoji "⚡️" for "t1" as "v1" + And user "participant1-v2" sets columns "one,two,three,four,five" to view "v1" And user "participant1-v2" shares view "v1" with "participant2-v2" When user "participant2-v2" tries to create a row using v2 on "view" "v1" with following values | one | AHA | @@ -833,6 +841,12 @@ Feature: APIv2 | four | 2023-12-24 | | five | [{"id": "admin", "type": 0}] | Then the reported status is 200 + And the inserted row has the following values + | one | AHA | + | two | 161 | + | three | true | + | four | 2023-12-24 | + | five | [{"id": "admin", "type": 0}] | @api2 @rows @views Scenario: Create rows on a view via v2 without permissions @@ -865,6 +879,7 @@ Feature: APIv2 | usergroupSelectUsers | true | | usergroupSelectGroups | false | And user "participant1-v2" create view "v1" with emoji "⚡️" for "t1" as "v1" + And user "participant1-v2" sets columns "one,two,three,four,five" to view "v1" And user "participant1-v2" shares view "v1" with "participant2-v2" And user "participant1-v2" sets permission "create" to 0 When user "participant2-v2" tries to create a row using v2 on "view" "v1" with following values @@ -906,6 +921,7 @@ Feature: APIv2 | usergroupSelectUsers | true | | usergroupSelectGroups | false | And user "participant1-v2" create view "v1" with emoji "⚡️" for "t1" as "v1" + And user "participant1-v2" sets columns "one,two,three,four,five" to view "v1" When user "participant2-v2" tries to create a row using v2 on "view" "v1" with following values | one | AHA | | two | 161 | diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index ac725145e..5d7891d79 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -832,6 +832,27 @@ public function updateView(string $user, string $viewName, string $title, ?strin Assert::assertEquals($itemToVerify['emoji'], $emoji); } + /** + * @When user :user sets columns :columnList to view :viewAlias + */ + public function applyColumnsToView(string $user, string $columnList, string $viewAlias) { + $this->setCurrentUser($user); + + $columns = explode(',', $columnList); + $columns = array_map(function (string $columnAlias) { + $col = $this->collectionManager->getByAlias('column', $columnAlias); + return $col['id']; + }, $columns); + + $view = $this->collectionManager->getByAlias('view', $viewAlias); + + $this->sendRequest( + 'PUT', + '/apps/tables/api/1/views/' . $view['id'], + [ 'data' => ['columns' => json_encode($columns)] ] + ); + } + /** * @When user :user deletes view :viewName * @@ -1120,6 +1141,8 @@ public function createColumn(string $title, ?TableNode $properties = null): void $columnToVerify = $this->getDataFromResponse($this->response); Assert::assertEquals(200, $this->response->getStatusCode()); Assert::assertEquals($columnToVerify['title'], $title); + + $this->collectionManager->register($newColumn, 'column', $newColumn['id'], $title); } /** @@ -2368,4 +2391,33 @@ public function shareViewWithUser(string $initiator, string $viewAlias, string $ $this->shareId = $share['id']; } } + + /** + * @Given the inserted row has the following values + */ + public function theInsertedRowHasTheFollowingValues(TableNode $columnValues) { + $jsonBody = json_decode($this->response->getBody()->getContents(), true); + $insertedRow = $jsonBody['ocs']['data']; + + $expected = []; + foreach ($columnValues->getRows() as $row) { + $columnId = $this->tableColumns[$row[0]]; + $expected[$columnId] = $row[1]; + } + + foreach ($insertedRow['data'] as $entry) { + if (!isset($expected[$entry['columnId']])) { + throw new \Exception(sprintf('Unexpected column with ID %d was returned', $entry['columnId'])); + } + // intentional weak comparison + if ($expected[$entry['columnId']] != $entry['value']) { + throw new \Exception(sprintf('Unexpected value %s for column with ID %d was returned', $entry['value'], $entry['columnId'])); + } + unset($expected[$entry['columnId']]); + } + + if (!empty($expected)) { + throw new \Exception(sprintf('Some expected columns were not returned: %s ', print_r($expected, true))); + } + } }