From f92c504292283ec84b3029e5955740c44120fcc6 Mon Sep 17 00:00:00 2001 From: Jonas Rittershofer Date: Tue, 29 Mar 2022 20:12:53 +0200 Subject: [PATCH 1/2] Move API to v2 Signed-off-by: Jonas Rittershofer --- CHANGELOG.md | 7 + appinfo/routes.php | 40 ++--- docs/API.md | 162 ++++++++++++------ docs/DataStructure.md | 76 ++++---- lib/Capabilities.php | 5 +- lib/Controller/ApiController.php | 33 +--- src/Forms.vue | 8 +- src/components/AppNavigationForm.vue | 2 +- src/components/Questions/AnswerInput.vue | 4 +- src/components/Questions/QuestionDropdown.vue | 2 +- src/components/Questions/QuestionMultiple.vue | 2 +- src/mixins/QuestionMixin.js | 2 +- src/mixins/ViewsMixin.js | 4 +- src/views/Create.vue | 6 +- src/views/Results.vue | 10 +- src/views/Submit.vue | 2 +- 16 files changed, 203 insertions(+), 162 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29710e9c6..5ff35b1ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [v3.0 0] +### Breaking +- Moving completely from API v1 to v2. With this, we fundamentally change the way how the forms sharing works, now much more flexible and closer to how it is done in server. + +### Deprecated + + ## [v2.5.0](https://github.com/nextcloud/forms/tree/v2.5.0) (2022-04-08) [Full Changelog](https://github.com/nextcloud/forms/compare/v2.4.0...v2.5.0) diff --git a/appinfo/routes.php b/appinfo/routes.php index 5c89533d7..b6a8319bb 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -59,7 +59,7 @@ 'url' => '/api/{apiVersion}/forms', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -67,7 +67,7 @@ 'url' => '/api/{apiVersion}/form', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -75,7 +75,7 @@ 'url' => '/api/{apiVersion}/form/{id}', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -83,7 +83,7 @@ 'url' => '/api/{apiVersion}/form/clone/{id}', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -91,7 +91,7 @@ 'url' => '/api/{apiVersion}/form/update', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -99,7 +99,7 @@ 'url' => '/api/{apiVersion}/form/{id}', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -115,7 +115,7 @@ 'url' => '/api/{apiVersion}/shared_forms', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], @@ -125,7 +125,7 @@ 'url' => '/api/{apiVersion}/question', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -133,7 +133,7 @@ 'url' => '/api/{apiVersion}/question/update', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -141,7 +141,7 @@ 'url' => '/api/{apiVersion}/question/reorder', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -149,7 +149,7 @@ 'url' => '/api/{apiVersion}/question/{id}', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], @@ -159,7 +159,7 @@ 'url' => '/api/{apiVersion}/option', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -167,7 +167,7 @@ 'url' => '/api/{apiVersion}/option/update', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -175,7 +175,7 @@ 'url' => '/api/{apiVersion}/option/{id}', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], @@ -203,7 +203,7 @@ 'url' => '/api/{apiVersion}/submissions/{hash}', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -211,7 +211,7 @@ 'url' => '/api/{apiVersion}/submissions/export/{hash}', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -219,7 +219,7 @@ 'url' => '/api/{apiVersion}/submissions/export', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -227,7 +227,7 @@ 'url' => '/api/{apiVersion}/submissions/{formId}', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -235,7 +235,7 @@ 'url' => '/api/{apiVersion}/submission/insert', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], [ @@ -243,7 +243,7 @@ 'url' => '/api/{apiVersion}/submission/{id}', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v1(\.1)?' + 'apiVersion' => 'v2' ] ], ] diff --git a/docs/API.md b/docs/API.md index 7da63e125..e84d65734 100644 --- a/docs/API.md +++ b/docs/API.md @@ -19,14 +19,15 @@ This file contains the API-Documentation. For more information on the returned D "data": } ``` +## Breaking Changes on API v2 +- The `mandatory` property of questions has been removed. It is replaced by `isRequired`. +- Completely new way of handling access & shares. -## Deprecation warning ⚠️ -Starting with API version 1.1, the `mandatory` property of questions is deprecated and replaced by `isRequired`. It will be removed in API version 2. ## Form Endpoints ### List owned Forms Returns condensed objects of all Forms beeing owned by the authenticated user. -- Endpoint: `/api/v1.1/forms` +- Endpoint: `/api/v2/forms` - Method: `GET` - Parameters: None - Response: Array of condensed Form Objects, sorted as newest first. @@ -37,6 +38,11 @@ Returns condensed objects of all Forms beeing owned by the authenticated user. "hash": "yWeMwcwCwoqRs8T2", "title": "Form 2", "expires": 0, + "permissions": [ + "edit", + "results", + "submit" + ], "partial": true }, { @@ -44,14 +50,19 @@ Returns condensed objects of all Forms beeing owned by the authenticated user. "hash": "em4djk8B9BpXnkYG", "title": "Form 1", "expires": 0, + "permissions": [ + "edit", + "results", + "submit" + ], "partial": true } ] ``` ### List shared Forms -Returns condensed objects of all Forms, that are shared to the authenticated user via instance ([access-type](DataStructure.md#share-types) `registered` or `selected`) and have not expired yet. -- Endpoint: `/api/v1.1/shared_forms` +Returns condensed objects of all Forms, that are shared & shown to the authenticated user and that have not expired yet. +- Endpoint: `/api/v2/shared_forms` - Method: `GET` - Parameters: None - Response: Array of condensed Form Objects, sorted as newest first, similar to [List owned Forms](#list-owned-forms). @@ -59,33 +70,40 @@ Returns condensed objects of all Forms, that are shared to the authenticated use See above, 'List owned forms' ``` +### Get a partial Form +Returns a single partial form object, corresponding to owned/shared form-listings. +- Endpoint: `/api/v2/partial_form/{hash}` +- Method: `GET` +- Url-Parameter: + | Parameter | Type | Description | + |-----------|---------|-------------| + | _hash_ | String | Hash of the form to request | +- Response: Partial form object, similar to form-list elements. +``` +"data": { + "id": 6, + "hash": "yWeMwcwCwoqRs8T2", + "title": "Form 2", + "expires": 0, + "permissions": [ + "submit" + ], + "partial": true +} +``` + ### Create a new Form -- Endpoint: `/api/v1.1/form` +- Endpoint: `/api/v2/form` - Method: `POST` - Parameters: None -- Response: The new form object. +- Response: The new form object, similar to requesting an existing form. ``` -"data": { - "id": 7, - "hash": "L2HWf8ixX9rNzKnX", - "title": "", - "description": "", - "ownerId": "jonas", - "created": 1611257716, - "access": { - "type": "public" - }, - "expires": null, - "isAnonymous": null, - "submitOnce": true, - "questions": [], - "canSubmit": true -} +See next section, 'Request full data of a form' ``` ### Request full data of a form Returns the full-depth object of the requested form (without submissions). -- Endpoint: `/api/v1.1/form/{id}` +- Endpoint: `/api/v2/form/{id}` - Url-Parameter: | Parameter | Type | Description | |-----------|---------|-------------| @@ -101,21 +119,24 @@ Returns the full-depth object of the requested form (without submissions). "ownerId": "jonas", "created": 1611240961, "access": { - "users": [], - "groups": [], - "type": "public" + "permitAllUsers": false, + "showToAllUsers": false }, "expires": 0, "isAnonymous": false, "submitOnce": false, "canSubmit": true, + "permissions": [ + "edit", + "results", + "submit" + ], "questions": [ { "id": 1, "formId": 3, "order": 1, "type": "dropdown", - "mandatory": false, // deprecated, will be removed in API v2 "isRequired": false, "text": "Question 1", "options": [ @@ -136,18 +157,33 @@ Returns the full-depth object of the requested form (without submissions). "formId": 3, "order": 2, "type": "short", - "mandatory": true, // deprecated, will be removed in API v2 "isRequired": true, "text": "Question 2", "options": [] } + ], + "shares": [ + { + "id": 1, + "formId": 3, + "shareType": 0, + "shareWith": "user1", + "displayName": "User 1 Displayname" + }, + { + "id": 2, + "formId": 3, + "shareType": 3, + "shareWith": "dYcTWjrSsxjMFFQzFAywzq5J", + "displayName": "" + } ] } ``` ### Clone a form Creates a clone of a form (without submissions). -- Endpoint: `/api/v1.1/form/clone/{id}` +- Endpoint: `/api/v2/form/clone/{id}` - Url-Parameter: | Parameter | Type | Description | |-----------|---------|-------------| @@ -160,7 +196,7 @@ See section 'Request full data of a form'. ### Update form properties Update a single or multiple properties of a form-object. Concerns **only** the Form-Object, properties of Questions, Options and Submissions, as well as their creation or deletion, are handled separately. -- Endpoint: `/api/v1.1/form/update` +- Endpoint: `/api/v2/form/update` - Method: `POST` - Parameters: | Parameter | Type | Description | @@ -174,7 +210,7 @@ Update a single or multiple properties of a form-object. Concerns **only** the F ``` ### Delete a form -- Endpoint: `/api/v1.1/form/{id}` +- Endpoint: `/api/v2/form/{id}` - Url-Parameter: | Parameter | Type | Description | |-----------|---------|-------------| @@ -189,7 +225,7 @@ Update a single or multiple properties of a form-object. Concerns **only** the F Contains only manipulative question-endpoints. To retrieve questions, request the full form data. ### Create a new question -- Endpoint: `/api/v1.1/question` +- Endpoint: `/api/v2/question` - Method: `POST` - Parameters: | Parameter | Type | Optional | Description | @@ -204,7 +240,6 @@ Contains only manipulative question-endpoints. To retrieve questions, request th "formId": 3, "order": 3, "type": "short", - "mandatory": false, // deprecated, will be removed in API v2 "isRequired": false, "text": "", "options": [] @@ -213,7 +248,7 @@ Contains only manipulative question-endpoints. To retrieve questions, request th ### Update question properties Update a single or multiple properties of a question-object. -- Endpoint: `/api/v1.1/question/update` +- Endpoint: `/api/v2/question/update` - Method: `POST` - Parameters: | Parameter | Type | Description | @@ -228,7 +263,7 @@ Update a single or multiple properties of a question-object. ### Reorder questions Reorders all Questions of a single form -- Endpoint: `/api/v1.1/question/reorder` +- Endpoint: `/api/v2/question/reorder` - Method: `POST` - Parameters: | Parameter | Type | Description | @@ -252,7 +287,7 @@ Reorders all Questions of a single form ``` ### Delete a question -- Endpoint: `/api/v1.1/question/{id}` +- Endpoint: `/api/v2/question/{id}` - Url-Parameter: | Parameter | Type | Description | |-----------|---------|-------------| @@ -267,7 +302,7 @@ Reorders all Questions of a single form Contains only manipulative question-endpoints. To retrieve options, request the full form data. ### Create a new Option -- Endpoint: `/api/v1.1/option` +- Endpoint: `/api/v2/option` - Method: `POST` - Parameters: | Parameter | Type | Description | @@ -284,7 +319,7 @@ Contains only manipulative question-endpoints. To retrieve options, request the ``` ### Update option properties -- Endpoint: `/api/v1.1/option/update` +- Endpoint: `/api/v2/option/update` - Method: `POST` - Parameters: | Parameter | Type | Description | @@ -298,7 +333,7 @@ Contains only manipulative question-endpoints. To retrieve options, request the ``` ### Delete an option -- Endpoint: `/api/v1.1/option/{id}` +- Endpoint: `/api/v2/option/{id}` - Url-Parameter: | Parameter | Type | Description | |-----------|---------|-------------| @@ -309,11 +344,44 @@ Contains only manipulative question-endpoints. To retrieve options, request the "data": 7 ``` +## Sharing Endpoints +### Add a new Share +- Endpoint: `/api/v2/share` +- Method: `POST` +- Parameters: + | Parameter | Type | Description | + |-------------|---------|-------------| + | _formId_ | Integer | Id of the form to share | + | _shareType_ | String | NC-shareType, out of the used shareTypes. | + | _shareWith_ | String | User/Group for the share. Not used for link-shares. | +- Response: **Status-Code OK**, as well as the new share object. +``` +"data": { + "id": 3, + "formId": 3, + "shareType": 0, + "shareWith": "user3", + "displayName": "User 3 Displayname" +} +``` + +### Delete a Share +- Endpoint: `/api/v2/share/{id}` +- Url-Parameter: + | Parameter | Type | Description | + |-----------|---------|-------------| + | _id_ | Integer | ID of the share to delete | +- Method: `DELETE` +- Response: **Status-Code OK**, as well as the id of the deleted share. +``` +"data": 5 +``` + ## Submission Endpoints ### Get Form Submissions Get all Submissions to a Form -- Endpoint: `/api/v1.1/submissions/{hash}` +- Endpoint: `/api/v2/submissions/{hash}` - Url-Parameter: | Parameter | Type | Description | |-----------|---------|-------------| @@ -372,7 +440,6 @@ Get all Submissions to a Form "formId": 3, "order": 1, "type": "dropdown", - "mandatory": false, // deprecated, will be removed in API v2 "isRequired": false, "text": "Question 1", "options": [ @@ -398,7 +465,6 @@ Get all Submissions to a Form "formId": 3, "order": 2, "type": "short", - "mandatory": true, // deprecated will be removed in API v2 "isRequired": true, "text": "Question 2", "options": [] @@ -409,7 +475,7 @@ Get all Submissions to a Form ### Get Submissions as csv (Download) Returns all submissions to the form in form of a csv-file. -- Endpoint: `/api/v1.1/submissions/export/{hash}` +- Endpoint: `/api/v2/submissions/export/{hash}` - Url-Parameter: | Parameter | Type | Description | |-----------|---------|-------------| @@ -424,7 +490,7 @@ Returns all submissions to the form in form of a csv-file. ### Export Submissions to Cloud (Files-App) Creates a csv file and stores it to the cloud, resp. Files-App. -- Endpoint: `/api/v1.1/submissions/export` +- Endpoint: `/api/v2/submissions/export` - Method: `POST` - Parameters: | Parameter | Type | Description | @@ -438,7 +504,7 @@ Creates a csv file and stores it to the cloud, resp. Files-App. ### Delete Submissions Delete all Submissions to a form -- Endpoint: `/api/v1.1/submissions/{formId}` +- Endpoint: `/api/v2/submissions/{formId}` - Url-Parameter: | Parameter | Type | Description | |-----------|---------|-------------| @@ -451,7 +517,7 @@ Delete all Submissions to a form ### Insert a Submission Store Submission to Database -- Endpoint: `/api/v1.1/submission/insert` +- Endpoint: `/api/v2/submission/insert` - Method: `POST` - Parameters: | Parameter | Type | Description | @@ -471,7 +537,7 @@ Store Submission to Database - Response: **Status-Code OK**. ### Delete a single Submission -- Endpoint: `/api/v1.1/submission/{id}` +- Endpoint: `/api/v2/submission/{id}` - Url-Parameter: | Parameter | Type | Description | |-----------|---------|-------------| diff --git a/docs/DataStructure.md b/docs/DataStructure.md index 776563409..82ffa9934 100644 --- a/docs/DataStructure.md +++ b/docs/DataStructure.md @@ -1,7 +1,7 @@ # Forms Data Structure -**State: Forms v2.3.0 - 09.04.2021** +**State: Forms v3.0.0 - 23.03.2022** -This document describes the Object-Structure, that is used within the Forms App and on Forms API v1.1. It does partially **not** equal the actual database structure behind. +This document describes the Object-Structure, that is used within the Forms App and on Forms API v2. It does partially **not** equal the actual database structure behind. ## Data Structures ### Form @@ -17,9 +17,11 @@ This document describes the Object-Structure, that is used within the Forms App | expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ | | isAnonymous | Boolean | | If Answers will be stored anonymously | | submitOnce | Boolean | | If users are only allowed to submit once to the form | -| questions | Array of [Questions](#question) | | Array of questions belonging to the form | -| submissions | Array of [Submissions](#submissions) | | Array of submissions belonging to the form | | canSubmit | Boolean | | If the user can Submit to the form, i.e. calculated information out of `submitOnce` and existing submissions. | +| permissions | Array of [Permissions](#permissions) | Array of permissions regarding the form | +| questions | Array of [Questions](#question) | | Array of questions belonging to the form | +| shares | Array of [Shares](#share) | | Array of shares of the form | +| submissions | Array of [Submissions](#submission) | | Array of submissions belonging to the form | ``` { @@ -33,9 +35,15 @@ This document describes the Object-Structure, that is used within the Forms App "expires": 0, "isAnonymous": false, "submitOnce": false, + "canSubmit": true, + "permissions": [ + "edit", + "results", + "submit" + ], "questions": [], "submissions": [], - "canSubmit": true + "shares": [] } ``` @@ -46,7 +54,6 @@ This document describes the Object-Structure, that is used within the Forms App | formId | Integer | | The id of the form, the question belongs to | | order | Integer | unique within form; *not* `0` | The order of the question within that form. Value `0` indicates deleted questions within database (typ. not visible outside) | | type | [Question-Type](#question-types) | | Type of the question | -| _mandatory_ | _Boolean_ | | _deprecated: will be removed in API v2, replaced by `isRequired`_ | | isRequired | Boolean | | If the question is required to fill the form | | text | String | max. 2048 ch. | The question-text | | options | Array of [Options](#option) | | Array of options belonging to the question. Only relevant for question-type with predefined options. | @@ -79,6 +86,16 @@ Options are predefined answer-possibilities corresponding to questions with appr } ``` +### Share +A share-object describes a single share of the form. +| Property | Type | Restrictions | Description | +|-------------|-----------------|--------------|-------------| +| id | Integer | unique | An instance-wide unique id of the share | +| formId | Integer | | The id of the form, the share belongs to | +| shareType | NC-IShareType (Int) | `IShare::TYPE_USER = 0`, `IShare::TYPE_GROUP = 1`, `IShare::TYPE_LINK = 3` | Type of the share. Thus also describes how to interpret shareWith. | +| shareWith | String | | User/Group/Hash - depending on the shareType | +| displayName | String | | Display name of share-target. | + ### Submission A submission-object describes a single submission by a user to a form. | Property | Type | Restrictions | Description | @@ -120,44 +137,25 @@ The actual answers of users on submission. } ``` +## Permmissions +Array of permissions, the user has on the form. Permissions are named by resp. routes on frontend. +| Permission | Description | +| -----------|-------------| +| edit | User is allowed to edit the form | +| results | User is allowed to access the form results | +| submit | User is allowed to submit to the form | ## Access Object -Defines how users are allowed to access the form. -| Property | Type | Description | -|-------------|-----------------|-------------| -| users | Array of [userShares](#share-objects) | Only relevant if `type=selected` | -| groups | Array of [groupShares](#share-objects) | Only relevant if `type=selected` | -| type | [ShareType](#share-types) | Share Type of the form. - -``` -{ - "users": [], - "groups": [], - "type": "public" -} -``` +Defines some extended options of sharing / access +| Property | Type | Description | +|------------------|-----------|-------------| +| permitAllUsers | Boolean | All logged in users of this instance are allowed to submit to the form | +| showToAllUsers | Boolean | Only active, if permitAllUsers is true - Show the form to all users on appNavigation | -### Share Types -Three types of sharing options are currently available, which define the access to the form. Independent of Share-Type, the form is currently only accessible via its share-link. -| Type-ID | Description | -|------------|-------------| -| `public` | Everybody is allowed to access the form. Anonymous users can fill the form on its public page. | -| `registered` | Only registered & logged-in users on this instance can fill the form. | -| `selected` | Only selected users are allowed to fill the form. Allowed users are defined in the Access-object. | - -### Share Objects -Objects of userShares or groupShares. - -| Property | Type | Description | -|-------------|-----------------|-------------| -| shareWith | String | Nextcloud userId or groupId of the sharee | -| displayName | String | Nextcloud Display Name of the Sharee | -| shareType | NC-IShareType (Int) | Nextcloud `IShare`-Type. Currently `IShare::TYPE_USER = 0` and `IShare::TYPE_GROUP = 1` ``` { - "shareWith": "user1", - "displayName": "User No. 1", - "shareType": 0 + "permitAllUsers": false, + "showToAllUsers": false } ``` diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 261a1e5bf..5c31abe26 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -42,10 +42,7 @@ public function getCapabilities() { return [ 'forms' => [ 'version' => $this->appManager->getAppVersion('forms'), - 'apiVersions' => [ - 'v1', - 'v1.1' - ] + 'apiVersions' => ['v2'] ] ]; } diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 5bdcda3da..f88f5e868 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -211,6 +211,7 @@ public function getPartialForm(string $hash): DataResponse { * * Read all information to edit a Form (form, questions, options, except submissions/answers). * + * @param int $id FormId * @return DataResponse * @throws OCSBadRequestException * @throws OCSForbiddenException @@ -228,12 +229,6 @@ public function getForm(int $id): DataResponse { throw new OCSForbiddenException(); } - // TODO remove after removal of API v1.1 - // Backward compatibility for mandatory - foreach ($form['questions'] as $index => $question) { - $form['questions'][$index]['mandatory'] = $question['isRequired']; - } - return new DataResponse($form); } @@ -266,12 +261,8 @@ public function newForm(): DataResponse { $this->formMapper->insert($form); - // Return like getForm(), just without loading Questions (as there are none). - $result = $form->read(); - $result['questions'] = []; - $result['shares'] = []; - - return new DataResponse($result); + // Return like getForm() + return $this->getForm($form->getId()); } /** @@ -491,10 +482,6 @@ public function newQuestion(int $formId, string $type, string $text = ''): DataR $response = $question->read(); $response['options'] = []; - // TODO remove after removal of API v1.1 - // Backward compatibility for mandatory - $response['mandatory'] = $response['isRequired']; - return new DataResponse($response); } @@ -639,14 +626,6 @@ public function updateQuestion(int $id, array $keyValuePairs): DataResponse { throw new OCSForbiddenException('Please use reorderQuestions() to change order'); } - // TODO remove after removal of API v1.1 - // Rename deprecated mandatory key to isRequired - if (key_exists('mandatory', $keyValuePairs)) { - $this->logger->info('Key \'mandatory\' is deprecated, please use \'isRequired\'.'); - $keyValuePairs['isRequired'] = $keyValuePairs['mandatory']; - unset($keyValuePairs['mandatory']); - } - // Create QuestionEntity with given Params & Id. $question = Question::fromParams($keyValuePairs); $question->setId($id); @@ -916,12 +895,6 @@ public function getSubmissions(string $hash): DataResponse { // Load currently active questions $questions = $this->formsService->getQuestions($form->getId()); - // TODO remove after removal of API v1.1 - // Backward compatibility for mandatory - foreach ($questions as $index => $question) { - $questions[$index]['mandatory'] = $question['isRequired']; - } - $response = [ 'submissions' => $submissions, 'questions' => $questions, diff --git a/src/Forms.vue b/src/Forms.vue index c9f294fc0..e9660ed22 100644 --- a/src/Forms.vue +++ b/src/Forms.vue @@ -222,7 +222,7 @@ export default { // Load Owned forms try { - const response = await axios.get(generateOcsUrl('apps/forms/api/v1.1/forms')) + const response = await axios.get(generateOcsUrl('apps/forms/api/v2/forms')) this.forms = OcsResponse2Data(response) } catch (error) { showError(t('forms', 'An error occurred while loading the forms list')) @@ -231,7 +231,7 @@ export default { // Load shared forms try { - const response = await axios.get(generateOcsUrl('apps/forms/api/v1.1/shared_forms')) + const response = await axios.get(generateOcsUrl('apps/forms/api/v2/shared_forms')) this.sharedForms = OcsResponse2Data(response) } catch (error) { showError(t('forms', 'An error occurred while loading the forms list')) @@ -271,7 +271,7 @@ export default { async onNewForm() { try { // Request a new empty form - const response = await axios.post(generateOcsUrl('apps/forms/api/v1.1/form')) + const response = await axios.post(generateOcsUrl('apps/forms/api/v2/form')) const newForm = OcsResponse2Data(response) this.forms.unshift(newForm) this.$router.push({ name: 'edit', params: { hash: newForm.hash } }) @@ -289,7 +289,7 @@ export default { */ async onCloneForm(id) { try { - const response = await axios.post(generateOcsUrl('apps/forms/api/v1.1/form/clone/{id}', { id })) + const response = await axios.post(generateOcsUrl('apps/forms/api/v2/form/clone/{id}', { id })) const newForm = OcsResponse2Data(response) this.forms.unshift(newForm) this.$router.push({ name: 'edit', params: { hash: newForm.hash } }) diff --git a/src/components/AppNavigationForm.vue b/src/components/AppNavigationForm.vue index bb9300767..c3d1b6df7 100644 --- a/src/components/AppNavigationForm.vue +++ b/src/components/AppNavigationForm.vue @@ -152,7 +152,7 @@ export default { // All good, let's delete this.loading = true try { - await axios.delete(generateOcsUrl('apps/forms/api/v1.1/form/{id}', { id: this.form.id })) + await axios.delete(generateOcsUrl('apps/forms/api/v2/form/{id}', { id: this.form.id })) this.$emit('delete', this.form.id) } catch (error) { showError(t('forms', 'Error while deleting {title}', { title: this.formTitle })) diff --git a/src/components/Questions/AnswerInput.vue b/src/components/Questions/AnswerInput.vue index cdaa1d384..7dc97f829 100644 --- a/src/components/Questions/AnswerInput.vue +++ b/src/components/Questions/AnswerInput.vue @@ -146,7 +146,7 @@ export default { */ async createAnswer(answer) { try { - const response = await axios.post(generateOcsUrl('apps/forms/api/v1.1/option'), { + const response = await axios.post(generateOcsUrl('apps/forms/api/v2/option'), { questionId: answer.questionId, text: answer.text, }) @@ -174,7 +174,7 @@ export default { */ async updateAnswer(answer) { try { - await axios.post(generateOcsUrl('apps/forms/api/v1.1/option/update'), { + await axios.post(generateOcsUrl('apps/forms/api/v2/option/update'), { id: this.answer.id, keyValuePairs: { text: answer.text, diff --git a/src/components/Questions/QuestionDropdown.vue b/src/components/Questions/QuestionDropdown.vue index dd59675ac..ca21e0ad3 100644 --- a/src/components/Questions/QuestionDropdown.vue +++ b/src/components/Questions/QuestionDropdown.vue @@ -283,7 +283,7 @@ export default { if (!option.local) { // let's not await, deleting in background - axios.delete(generateOcsUrl('apps/forms/api/v1.1/option/{id}', { id: option.id })) + axios.delete(generateOcsUrl('apps/forms/api/v2/option/{id}', { id: option.id })) .catch(error => { showError(t('forms', 'There was an issue deleting this option')) console.error(error) diff --git a/src/components/Questions/QuestionMultiple.vue b/src/components/Questions/QuestionMultiple.vue index c4cff959e..aad539564 100644 --- a/src/components/Questions/QuestionMultiple.vue +++ b/src/components/Questions/QuestionMultiple.vue @@ -315,7 +315,7 @@ export default { if (!option.local) { // let's not await, deleting in background - axios.delete(generateOcsUrl('apps/forms/api/v1.1/option/{id}', { id: option.id })) + axios.delete(generateOcsUrl('apps/forms/api/v2/option/{id}', { id: option.id })) .catch(error => { showError(t('forms', 'There was an issue deleting this option')) console.error(error) diff --git a/src/mixins/QuestionMixin.js b/src/mixins/QuestionMixin.js index e3d1aecf3..dad54d31e 100644 --- a/src/mixins/QuestionMixin.js +++ b/src/mixins/QuestionMixin.js @@ -189,7 +189,7 @@ export default { async saveQuestionProperty(key, value) { try { // TODO: add loading status feedback ? - await axios.post(generateOcsUrl('apps/forms/api/v1.1/question/update'), { + await axios.post(generateOcsUrl('apps/forms/api/v2/question/update'), { id: this.id, keyValuePairs: { [key]: value, diff --git a/src/mixins/ViewsMixin.js b/src/mixins/ViewsMixin.js index 42f35a98c..a70a09e0a 100644 --- a/src/mixins/ViewsMixin.js +++ b/src/mixins/ViewsMixin.js @@ -89,7 +89,7 @@ export default { this.cancelFetchFullForm = cancel try { - const response = await request(generateOcsUrl('apps/forms/api/v1.1/form/{id}', { id })) + const response = await request(generateOcsUrl('apps/forms/api/v2/form/{id}', { id })) this.$emit('update:form', OcsResponse2Data(response)) this.isLoadingForm = false } catch (error) { @@ -109,7 +109,7 @@ export default { async saveFormProperty(key) { try { // TODO: add loading status feedback ? - await axios.post(generateOcsUrl('apps/forms/api/v1.1/form/update'), { + await axios.post(generateOcsUrl('apps/forms/api/v2/form/update'), { id: this.form.id, keyValuePairs: { [key]: this.form[key], diff --git a/src/views/Create.vue b/src/views/Create.vue index 30784dbb5..0a41d7106 100644 --- a/src/views/Create.vue +++ b/src/views/Create.vue @@ -278,7 +278,7 @@ export default { this.isLoadingQuestions = true try { - const response = await axios.post(generateOcsUrl('apps/forms/api/v1.1/question'), { + const response = await axios.post(generateOcsUrl('apps/forms/api/v2/question'), { formId: this.form.id, type, text, @@ -316,7 +316,7 @@ export default { this.isLoadingQuestions = true try { - await axios.delete(generateOcsUrl('apps/forms/api/v1.1/question/{id}', { id })) + await axios.delete(generateOcsUrl('apps/forms/api/v2/question/{id}', { id })) const index = this.form.questions.findIndex(search => search.id === id) this.form.questions.splice(index, 1) } catch (error) { @@ -335,7 +335,7 @@ export default { const newOrder = this.form.questions.map(question => question.id) try { - await axios.post(generateOcsUrl('apps/forms/api/v1.1/question/reorder'), { + await axios.post(generateOcsUrl('apps/forms/api/v2/question/reorder'), { formId: this.form.id, newOrder, }) diff --git a/src/views/Results.vue b/src/views/Results.vue index 56c6c4f7b..a445c8877 100644 --- a/src/views/Results.vue +++ b/src/views/Results.vue @@ -194,7 +194,7 @@ export default { * @return {string} */ downloadUrl() { - return generateOcsUrl('apps/forms/api/v1.1/submissions/export/{hash}', { hash: this.form.hash }) + return generateOcsUrl('apps/forms/api/v2/submissions/export/{hash}', { hash: this.form.hash }) }, }, @@ -229,7 +229,7 @@ export default { console.debug('Loading results for form', this.form.hash) try { - const response = await axios.get(generateOcsUrl('apps/forms/api/v1.1/submissions/{hash}', { hash: this.form.hash })) + const response = await axios.get(generateOcsUrl('apps/forms/api/v2/submissions/{hash}', { hash: this.form.hash })) let loadedSubmissions = OcsResponse2Data(response).submissions const loadedQuestions = OcsResponse2Data(response).questions @@ -253,7 +253,7 @@ export default { picker.pick() .then(async (path) => { try { - const response = await axios.post(generateOcsUrl('apps/forms/api/v1.1/submissions/export'), { + const response = await axios.post(generateOcsUrl('apps/forms/api/v2/submissions/export'), { hash: this.form.hash, path, }) @@ -269,7 +269,7 @@ export default { this.loadingResults = true try { - await axios.delete(generateOcsUrl('apps/forms/api/v1.1/submission/{id}', { id })) + await axios.delete(generateOcsUrl('apps/forms/api/v2/submission/{id}', { id })) const index = this.form.submissions.findIndex(search => search.id === id) this.form.submissions.splice(index, 1) } catch (error) { @@ -287,7 +287,7 @@ export default { this.loadingResults = true try { - await axios.delete(generateOcsUrl('apps/forms/api/v1.1/submissions/{formId}', { formId: this.form.id })) + await axios.delete(generateOcsUrl('apps/forms/api/v2/submissions/{formId}', { formId: this.form.id })) this.form.submissions = [] } catch (error) { console.error(error) diff --git a/src/views/Submit.vue b/src/views/Submit.vue index 0f0b857dd..d8bcea163 100644 --- a/src/views/Submit.vue +++ b/src/views/Submit.vue @@ -231,7 +231,7 @@ export default { this.loading = true try { - await axios.post(generateOcsUrl('apps/forms/api/v1.1/submission/insert'), { + await axios.post(generateOcsUrl('apps/forms/api/v2/submission/insert'), { formId: this.form.id, answers: this.answers, shareHash: this.shareHash, From e90412e910eefb7775d005d823c565577ee84128 Mon Sep 17 00:00:00 2001 From: Jonas Rittershofer Date: Tue, 29 Mar 2022 20:12:27 +0200 Subject: [PATCH 2/2] Some basic API tests Signed-off-by: Jonas Rittershofer --- .github/workflows/phpunit.yml | 27 +- composer.json | 3 +- composer.lock | 598 ++++++++++++- tests/Integration/Api/ApiV2Test.php | 1219 +++++++++++++++++++++++++++ 4 files changed, 1840 insertions(+), 7 deletions(-) create mode 100644 tests/Integration/Api/ApiV2Test.php diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 9a277df11..8a9c11038 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -54,9 +54,11 @@ jobs: - name: Set up Nextcloud env: DB_PORT: 4444 + OC_PASS: test run: | mkdir data ./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password + ./occ user:add --password-from-env --display-name="Test Displayname" test ./occ app:enable --force ${{ env.APP_NAME }} php -S localhost:8080 & @@ -68,14 +70,23 @@ jobs: working-directory: apps/${{ env.APP_NAME }} run: composer test:integration - - name: Upload coverage + - name: Upload Unit coverage if: matrix.php-versions == matrix.coverage-on uses: codecov/codecov-action@v1 with: root_dir: apps/${{ env.APP_NAME }} files: apps/${{ env.APP_NAME }}/tests/clover.unit.xml fail_ci_if_error: true - flags: unittests + flags: unit + + - name: Upload Integration coverage + if: matrix.php-versions == matrix.coverage-on + uses: codecov/codecov-action@v1 + with: + root_dir: apps/${{ env.APP_NAME }} + files: apps/${{ env.APP_NAME }}/tests/clover.integration.xml + fail_ci_if_error: true + flags: integration mysql: runs-on: ubuntu-latest @@ -127,9 +138,11 @@ jobs: - name: Set up Nextcloud env: DB_PORT: 4444 + OC_PASS: test run: | mkdir data ./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password + ./occ user:add --password-from-env --display-name="Test Displayname" test ./occ app:enable --force ${{ env.APP_NAME }} php -S localhost:8080 & @@ -193,9 +206,11 @@ jobs: - name: Set up Nextcloud env: DB_PORT: 4444 + OC_PASS: test run: | mkdir data ./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password + ./occ user:add --password-from-env --display-name="Test Displayname" test ./occ app:enable --force ${{ env.APP_NAME }} php -S localhost:8080 & @@ -224,7 +239,7 @@ jobs: oracle: image: deepdiver/docker-oracle-xe-11g # "wnameless/oracle-xe-11g-r2" ports: - - "1521:1521" + - 4444:1521/tcp steps: - name: Checkout server @@ -252,10 +267,14 @@ jobs: run: composer i - name: Set up Nextcloud + env: + DB_PORT: 4444 + OC_PASS: test run: | mkdir data - ./occ maintenance:install --verbose --database=oci --database-name=XE --database-host=127.0.0.1 --database-port=1521 --database-user=autotest --database-pass=owncloud --admin-user admin --admin-pass admin + ./occ maintenance:install --verbose --database=oci --database-name=XE --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=autotest --database-pass=owncloud --admin-user admin --admin-pass admin php -f index.php + ./occ user:add --password-from-env --display-name="Test Displayname" test ./occ app:enable --force ${{ env.APP_NAME }} - name: PHPUnit diff --git a/composer.json b/composer.json index 300813ff8..75c174924 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.5", "christophwurst/nextcloud": "^23.0", - "phpunit/phpunit": "^9" + "phpunit/phpunit": "^9", + "guzzlehttp/guzzle": "^7.4" }, "require": { "league/csv": "^9.7.3" diff --git a/composer.lock b/composer.lock index 38d23258b..26caf24b8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cce56b3ebe3471b0b16b1dcf88865be4", + "content-hash": "62733788069011c2d49faa517001cf3a", "packages": [ { "name": "league/csv", @@ -255,6 +255,329 @@ ], "time": "2022-03-03T08:28:38+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.4.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "ac1ec1cd9b5624694c3a40be801d94137afb12b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ac1ec1cd9b5624694c3a40be801d94137afb12b4", + "reference": "ac1ec1cd9b5624694c3a40be801d94137afb12b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5", + "guzzlehttp/psr7": "^1.8.3 || ^2.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.4-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.4.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-03-20T14:16:28+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-22T20:56:57+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c94a94f120803a18554c1805ef2e539f8285f9a2", + "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.2.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2022-03-20T21:55:58+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.11.0", @@ -1227,6 +1550,166 @@ }, "time": "2019-01-08T18:20:26+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, { "name": "psr/log", "version": "1.1.4", @@ -1277,6 +1760,50 @@ }, "time": "2021-05-03T11:20:27+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "sebastian/cli-parser", "version": "1.0.1", @@ -2241,6 +2768,73 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.25.0", @@ -2442,5 +3036,5 @@ "platform-overrides": { "php": "7.3" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.0.0" } diff --git a/tests/Integration/Api/ApiV2Test.php b/tests/Integration/Api/ApiV2Test.php new file mode 100644 index 000000000..2edccbe9c --- /dev/null +++ b/tests/Integration/Api/ApiV2Test.php @@ -0,0 +1,1219 @@ + + * + * @author Jonas Rittershofer + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Forms\Tests\Integration\Api; + +use OCA\Forms\Db\Form; +use OCA\Forms\Db\FormMapper; + +use OCP\DB\QueryBuilder\IQueryBuilder; + +use GuzzleHttp\Client; +use GuzzleHttp\Exception\ClientException; +use Test\TestCase; + +class ApiV2Test extends TestCase { + /** @var GuzzleHttp\Client */ + private $http; + + /** @var FormMapper */ + private $formMapper; + + /** @var Array */ + private $testForms = [ + [ + 'hash' => 'abcdefg', + 'title' => 'Title of a Form', + 'description' => 'Just a simple form.', + 'owner_id' => 'test', + 'access_json' => [ + 'permitAllUsers' => false, + 'showToAllUsers' => false + ], + 'created' => 12345, + 'expires' => 0, + 'is_anonymous' => false, + 'submit_once' => true, + 'questions' => [ + [ + 'type' => 'short', + 'text' => 'First Question?', + 'isRequired' => true, + 'order' => 1, + 'options' => [] + ], + [ + 'type' => 'multiple_unique', + 'text' => 'Second Question?', + 'isRequired' => false, + 'order' => 2, + 'options' => [ + [ + 'text' => 'Option 1' + ], + [ + 'text' => 'Option 2' + ] + ] + ] + ], + 'shares' => [ + [ + 'shareType' => 0, + 'shareWith' => 'user1', + ], + [ + 'shareType' => 3, + 'shareWith' => 'shareHash', + ], + ], + 'submissions' => [ + [ + 'userId' => 'user1', + 'timestamp' => 123456, + 'answers' => [ + [ + 'questionIndex' => 0, + 'text' => 'This is a short answer.' + ], + [ + 'questionIndex' => 1, + 'text' => 'Option 1' + ] + ] + ], + [ + 'userId' => 'user2', + 'timestamp' => 12345, + 'answers' => [ + [ + 'questionIndex' => 0, + 'text' => 'This is another short answer.' + ], + [ + 'questionIndex' => 1, + 'text' => 'Option 2' + ] + ] + ] + ] + ], + [ + 'hash' => 'abcdefghij', + 'title' => 'Title of a second Form', + 'description' => '', + 'owner_id' => 'someUser', + 'access_json' => [ + 'permitAllUsers' => true, + 'showToAllUsers' => true + ], + 'created' => 12345, + 'expires' => 0, + 'is_anonymous' => false, + 'submit_once' => true, + 'questions' => [ + [ + 'type' => 'short', + 'text' => 'Third Question?', + 'isRequired' => false, + 'order' => 1, + 'options' => [] + ], + ], + 'shares' => [ + [ + 'shareType' => 0, + 'shareWith' => 'user2', + ], + ], + 'submissions' => [] + ] + ]; + + /** + * Set up test environment. + * Writing testforms into db, preparing http request + */ + public function setUp(): void { + parent::setUp(); + + $qb = TestCase::$realDatabase->getQueryBuilder(); + + // Write our test forms into db + foreach ($this->testForms as $index => $form) { + $qb->insert('forms_v2_forms') + ->values([ + 'hash' => $qb->createNamedParameter($form['hash'], IQueryBuilder::PARAM_STR), + 'title' => $qb->createNamedParameter($form['title'], IQueryBuilder::PARAM_STR), + 'description' => $qb->createNamedParameter($form['description'], IQueryBuilder::PARAM_STR), + 'owner_id' => $qb->createNamedParameter($form['owner_id'], IQueryBuilder::PARAM_STR), + 'access_json' => $qb->createNamedParameter(json_encode($form['access_json']), IQueryBuilder::PARAM_STR), + 'created' => $qb->createNamedParameter($form['created'], IQueryBuilder::PARAM_INT), + 'expires' => $qb->createNamedParameter($form['expires'], IQueryBuilder::PARAM_INT), + 'is_anonymous' => $qb->createNamedParameter($form['is_anonymous'], IQueryBuilder::PARAM_BOOL), + 'submit_once' => $qb->createNamedParameter($form['submit_once'], IQueryBuilder::PARAM_BOOL) + ]); + $qb->execute(); + $formId = $qb->getLastInsertId(); + $this->testForms[$index]['id'] = $formId; + + // Insert Questions into DB + foreach ($form['questions'] as $qIndex => $question) { + $qb->insert('forms_v2_questions') + ->values([ + 'form_id' => $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT), + 'order' => $qb->createNamedParameter($question['order'], IQueryBuilder::PARAM_INT), + 'type' => $qb->createNamedParameter($question['type'], IQueryBuilder::PARAM_STR), + 'is_required' => $qb->createNamedParameter($question['isRequired'], IQueryBuilder::PARAM_BOOL), + 'text' => $qb->createNamedParameter($question['text'], IQueryBuilder::PARAM_STR) + ]); + $qb->execute(); + $questionId = $qb->getLastInsertId(); + $this->testForms[$index]['questions'][$qIndex]['id'] = $questionId; + + // Insert Options into DB + foreach ($question['options'] as $oIndex => $option) { + $qb->insert('forms_v2_options') + ->values([ + 'question_id' => $qb->createNamedParameter($questionId, IQueryBuilder::PARAM_INT), + 'text' => $qb->createNamedParameter($option['text'], IQueryBuilder::PARAM_STR) + ]); + $qb->execute(); + $this->testForms[$index]['questions'][$qIndex]['options'][$oIndex]['id'] = $qb->getLastInsertId(); + } + } + + // Insert Shares into DB + foreach ($form['shares'] as $sIndex => $share) { + $qb->insert('forms_v2_shares') + ->values([ + 'form_id' => $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT), + 'share_type' => $qb->createNamedParameter($share['shareType'], IQueryBuilder::PARAM_STR), + 'share_with' => $qb->createNamedParameter($share['shareWith'], IQueryBuilder::PARAM_STR) + ]); + $qb->execute(); + $this->testForms[$index]['shares'][$sIndex]['id'] = $qb->getLastInsertId(); + } + + // Insert Submissions into DB + foreach ($form['submissions'] as $suIndex => $submission) { + $qb->insert('forms_v2_submissions') + ->values([ + 'form_id' => $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT), + 'user_id' => $qb->createNamedParameter($submission['userId'], IQueryBuilder::PARAM_STR), + 'timestamp' => $qb->createNamedParameter($submission['timestamp'], IQueryBuilder::PARAM_INT) + ]); + $qb->execute(); + $submissionId = $qb->getLastInsertId(); + $this->testForms[$index]['submissions'][$suIndex]['id'] = $submissionId; + + foreach ($submission['answers'] as $aIndex => $answer) { + $qb->insert('forms_v2_answers') + ->values([ + 'submission_id' => $qb->createNamedParameter($submissionId, IQueryBuilder::PARAM_INT), + 'question_id' => $qb->createNamedParameter($this->testForms[$index]['questions'][$answer['questionIndex']]['id'], IQueryBuilder::PARAM_INT), + 'text' => $qb->createNamedParameter($answer['text'], IQueryBuilder::PARAM_STR) + ]); + $qb->execute(); + $this->testForms[$index]['submissions'][$suIndex]['answers'][$aIndex]['id'] = $qb->getLastInsertId(); + } + } + } + + // Set up http Client + $this->http = new Client([ + 'base_uri' => 'http://localhost:8080/ocs/v2.php/apps/forms/', + 'auth' => ['test', 'test'], + 'headers' => [ + 'OCS-ApiRequest' => 'true', + 'Accept' => 'application/json' + ], + ]); + } + + /** Clean up database from testforms */ + public function tearDown(): void { + $qb = TestCase::$realDatabase->getQueryBuilder(); + + foreach ($this->testForms as $form) { + $qb->delete('forms_v2_forms') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($form['id'], IQueryBuilder::PARAM_INT))); + $qb->execute(); + + foreach ($form['questions'] as $question) { + $qb->delete('forms_v2_questions') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($question['id'], IQueryBuilder::PARAM_INT))); + $qb->execute(); + + foreach ($question['options'] as $option) { + $qb->delete('forms_v2_options') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($option['id'], IQueryBuilder::PARAM_INT))); + $qb->execute(); + } + } + + foreach ($form['shares'] as $share) { + $qb->delete('forms_v2_shares') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share['id'], IQueryBuilder::PARAM_INT))); + $qb->execute(); + } + + if (isset($form['submissions'])) { + foreach ($form['submissions'] as $submission) { + $qb->delete('forms_v2_submissions') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($submission['id'], IQueryBuilder::PARAM_INT))); + $qb->execute(); + + foreach ($submission['answers'] as $answer) { + $qb->delete('forms_v2_answers') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($answer['id'], IQueryBuilder::PARAM_INT))); + $qb->execute(); + } + } + } + } + + parent::tearDown(); + } + + // Small Wrapper for OCS-Response + private function OcsResponse2Data($resp) { + $arr = json_decode($resp->getBody()->getContents(), true); + return $arr['ocs']['data']; + } + + // Unset Id, as we can not control it on the tests. + private function arrayUnsetId(array $arr): array { + foreach ($arr as $index => $elem) { + unset($arr[$index]['id']); + } + return $arr; + } + + public function dataGetForms() { + return [ + 'getTestforms' => [ + 'expected' => [[ + 'hash' => 'abcdefg', + 'title' => 'Title of a Form', + 'expires' => 0, + 'permissions' => [ + 'edit', + 'results', + 'submit' + ], + 'partial' => true + ]] + ] + ]; + } + /** + * @dataProvider dataGetForms + * + * @param array $expected + */ + public function testGetForms(array $expected): void { + $resp = $this->http->request('GET', 'api/v2/forms'); + + $data = $this->OcsResponse2Data($resp); + $data = $this->arrayUnsetId($data); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($expected, $data); + } + + public function dataGetSharedForms() { + return [ + 'getTestforms' => [ + 'expected' => [[ + 'hash' => 'abcdefghij', + 'title' => 'Title of a second Form', + 'expires' => 0, + 'permissions' => [ + 'submit' + ], + 'partial' => true + ]] + ] + ]; + } + /** + * @dataProvider dataGetSharedForms + * + * @param array $expected + */ + public function testGetSharedForms(array $expected): void { + $resp = $this->http->request('GET', 'api/v2/shared_forms'); + + $data = $this->OcsResponse2Data($resp); + $data = $this->arrayUnsetId($data); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($expected, $data); + } + + public function dataGetPartialForm() { + return [ + 'getPartialForm' => [ + 'expected' => [ + 'hash' => 'abcdefghij', + 'title' => 'Title of a second Form', + 'expires' => 0, + 'permissions' => [ + 'submit' + ], + 'partial' => true + ] + ] + ]; + } + /** + * @dataProvider dataGetPartialForm + * + * @param array $expected + */ + public function testGetPartialForm(array $expected): void { + $resp = $this->http->request('GET', "api/v2/partial_form/{$this->testForms[1]['hash']}"); + + $data = $this->OcsResponse2Data($resp); + unset($data['id']); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($expected, $data); + } + + public function dataGetNewForm() { + return [ + 'getNewForm' => [ + 'expected' => [ + // 'hash' => Some random, cannot be checked. + 'title' => '', + 'description' => '', + 'ownerId' => 'test', + // 'created' => Hard to check exactly. + 'access' => [ + 'permitAllUsers' => false, + 'showToAllUsers' => false + ], + 'expires' => 0, + 'isAnonymous' => false, + 'submitOnce' => true, + 'canSubmit' => true, + 'permissions' => [ + 'edit', + 'results', + 'submit' + ], + 'questions' => [], + 'shares' => [], + ] + ] + ]; + } + /** + * @dataProvider dataGetNewForm + * + * @param array $expected + */ + public function testGetNewForm(array $expected): void { + $resp = $this->http->request('POST', 'api/v2/form'); + $data = $this->OcsResponse2Data($resp); + + // Store for deletion on tearDown + $this->testForms[] = $data; + + // Cannot control id + unset($data['id']); + // Check general behaviour of hash + $this->assertMatchesRegularExpression('/^[a-zA-Z0-9]{16}$/', $data['hash']); + unset($data['hash']); + // Check general behaviour of created (Created in the last 10 seconds) + $this->assertTrue(time() - $data['created'] < 10); + unset($data['created']); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($expected, $data); + } + + public function dataGetFullForm() { + return [ + 'getFullForm' => [ + 'expected' => [ + 'hash' => 'abcdefg', + 'title' => 'Title of a Form', + 'description' => 'Just a simple form.', + 'ownerId' => 'test', + 'created' => 12345, + 'access' => [ + 'permitAllUsers' => false, + 'showToAllUsers' => false + ], + 'expires' => 0, + 'isAnonymous' => false, + 'submitOnce' => true, + 'canSubmit' => true, + 'permissions' => [ + 'edit', + 'results', + 'submit' + ], + 'questions' => [ + [ + 'type' => 'short', + 'text' => 'First Question?', + 'isRequired' => true, + 'order' => 1, + 'options' => [] + ], + [ + 'type' => 'multiple_unique', + 'text' => 'Second Question?', + 'isRequired' => false, + 'order' => 2, + 'options' => [ + [ + 'text' => 'Option 1' + ], + [ + 'text' => 'Option 2' + ] + ] + ] + ], + 'shares' => [ + [ + 'shareType' => 0, + 'shareWith' => 'user1', + 'displayName' => '' + ], + [ + 'shareType' => 3, + 'shareWith' => 'shareHash', + 'displayName' => '' + ], + ], + ] + ] + ]; + } + /** + * @dataProvider dataGetFullForm + * + * @param array $expected + */ + public function testGetFullForm(array $expected): void { + $resp = $this->http->request('GET', "api/v2/form/{$this->testForms[0]['id']}"); + $data = $this->OcsResponse2Data($resp); + + // Cannot control ids, but check general consistency. + foreach ($data['questions'] as $qIndex => $question) { + $this->assertEquals($data['id'], $question['formId']); + unset($data['questions'][$qIndex]['formId']); + + foreach ($question['options'] as $oIndex => $option) { + $this->assertEquals($question['id'], $option['questionId']); + unset($data['questions'][$qIndex]['options'][$oIndex]['questionId']); + unset($data['questions'][$qIndex]['options'][$oIndex]['id']); + } + unset($data['questions'][$qIndex]['id']); + } + foreach ($data['shares'] as $sIndex => $share) { + $this->assertEquals($data['id'], $share['formId']); + unset($data['shares'][$sIndex]['formId']); + unset($data['shares'][$sIndex]['id']); + } + unset($data['id']); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($expected, $data); + } + + public function dataCloneForm() { + $fullFormExpected = $this->dataGetFullForm()['getFullForm']['expected']; + // Compared to full form expected, update changed properties + $fullFormExpected['title'] = 'Title of a Form - Copy'; + $fullFormExpected['shares'] = []; + // Compared to full form expected, unset unpredictable properties. These will be checked logically. + unset($fullFormExpected['id']); + unset($fullFormExpected['hash']); + unset($fullFormExpected['created']); + foreach ($fullFormExpected['questions'] as $qIndex => $question) { + unset($fullFormExpected['questions'][$qIndex]['formId']); + } + + return [ + 'updateFormProps' => [ + 'expected' => $fullFormExpected + ] + ]; + } + /** + * @dataProvider dataCloneForm + * + * @param array $expected + */ + public function testCloneForm(array $expected): void { + $resp = $this->http->request('POST', "api/v2/form/clone/{$this->testForms[0]['id']}"); + $data = $this->OcsResponse2Data($resp); + + // Store for deletion on tearDown + $this->testForms[] = $data; + + // Cannot control ids, but check general consistency. + foreach ($data['questions'] as $qIndex => $question) { + $this->assertEquals($data['id'], $question['formId']); + unset($data['questions'][$qIndex]['formId']); + + foreach ($question['options'] as $oIndex => $option) { + $this->assertEquals($question['id'], $option['questionId']); + unset($data['questions'][$qIndex]['options'][$oIndex]['questionId']); + unset($data['questions'][$qIndex]['options'][$oIndex]['id']); + } + unset($data['questions'][$qIndex]['id']); + } + foreach ($data['shares'] as $sIndex => $share) { + $this->assertEquals($data['id'], $share['formId']); + unset($data['shares'][$sIndex]['formId']); + unset($data['shares'][$sIndex]['id']); + } + // Check not just returning source-form (id must differ). + $this->assertGreaterThan($this->testForms[0]['id'], $data['id']); + unset($data['id']); + + // Check general behaviour of hash + $this->assertMatchesRegularExpression('/^[a-zA-Z0-9]{16}$/', $data['hash']); + unset($data['hash']); + // Check general behaviour of created (Created in the last 10 seconds) + $this->assertTrue(time() - $data['created'] < 10); + unset($data['created']); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($expected, $data); + } + + public function dataUpdateFormProperties() { + $fullFormExpected = $this->dataGetFullForm()['getFullForm']['expected']; + $fullFormExpected['title'] = 'This is my NEW Title!'; + $fullFormExpected['access'] = [ + 'permitAllUsers' => true, + 'showToAllUsers' => true + ]; + return [ + 'updateFormProps' => [ + 'expected' => $fullFormExpected + ] + ]; + } + /** + * @dataProvider dataUpdateFormProperties + * + * @param array $expected + */ + public function testUpdateFormProperties(array $expected): void { + $resp = $this->http->request('POST', 'api/v2/form/update', [ + 'json' => [ + 'id' => $this->testForms[0]['id'], + 'keyValuePairs' => [ + 'title' => 'This is my NEW Title!', + 'access' => [ + 'permitAllUsers' => true, + 'showToAllUsers' => true + ] + ] + ] + ]); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($this->testForms[0]['id'], $data); + + // Check if form equals updated form. + $this->testGetFullForm($expected); + } + + public function testDeleteForm() { + $resp = $this->http->request('DELETE', "api/v2/form/{$this->testForms[0]['id']}"); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($this->testForms[0]['id'], $data); + + // Check if not existent anymore. + try { + $this->http->request('GET', "api/v2/form/{$this->testForms[0]['id']}"); + } catch (ClientException $e) { + $resp = $e->getResponse(); + } + $this->assertEquals(400, $resp->getStatusCode()); + } + + public function dataCreateNewQuestion() { + return [ + 'newQuestion' => [ + 'expected' => [ + // 'formId' => 3, // Checked during test + // 'order' => 3, // Checked during test + 'type' => 'short', + 'isRequired' => false, + 'text' => 'Already some Question?', + 'options' => [] + ] + ] + ]; + } + /** + * @dataProvider dataCreateNewQuestion + * + * @param array $expected + */ + public function testCreateNewQuestion(array $expected): void { + $resp = $this->http->request('POST', 'api/v2/question', [ + 'json' => [ + 'formId' => $this->testForms[0]['id'], + 'type' => 'short', + 'text' => 'Already some Question?' + ] + ]); + $data = $this->OcsResponse2Data($resp); + + // Store for deletion on tearDown + $this->testForms[0]['questions'][] = $data; + + // Check formId & order + $this->assertEquals($this->testForms[0]['id'], $data['formId']); + unset($data['formId']); + $this->assertEquals(sizeof($this->testForms[0]['questions']), $data['order']); + unset($data['order']); + unset($data['id']); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($expected, $data); + } + + public function dataUpdateQuestionProperties() { + $fullFormExpected = $this->dataGetFullForm()['getFullForm']['expected']; + $fullFormExpected['questions'][0]['text'] = 'Still first Question!'; + $fullFormExpected['questions'][0]['isRequired'] = false; + + return [ + 'updateQuestionProps' => [ + 'fullFormExpected' => $fullFormExpected + ] + ]; + } + /** + * @dataProvider dataUpdateQuestionProperties + * + * @param array $fullFormExpected + */ + public function testUpdateQuestionProperties(array $fullFormExpected): void { + $resp = $this->http->request('POST', 'api/v2/question/update', [ + 'json' => [ + 'id' => $this->testForms[0]['questions'][0]['id'], + 'keyValuePairs' => [ + 'isRequired' => false, + 'text' => 'Still first Question!' + ] + ] + ]); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($this->testForms[0]['questions'][0]['id'], $data); + + // Check if form equals updated form. + $this->testGetFullForm($fullFormExpected); + } + + public function dataReorderQuestions() { + $fullFormExpected = $this->dataGetFullForm()['getFullForm']['expected']; + $fullFormExpected['questions'][0]['order'] = 2; + $fullFormExpected['questions'][1]['order'] = 1; + + // Exchange questions, as they will be returned in new order. + $tmp = $fullFormExpected['questions'][0]; + $fullFormExpected['questions'][0] = $fullFormExpected['questions'][1]; + $fullFormExpected['questions'][1] = $tmp; + + return [ + 'updateQuestionProps' => [ + 'fullFormExpected' => $fullFormExpected + ] + ]; + } + /** + * @dataProvider dataReorderQuestions + * + * @param array $fullFormExpected + */ + public function testReorderQuestions(array $fullFormExpected): void { + $resp = $this->http->request('POST', 'api/v2/question/reorder', [ + 'json' => [ + 'formId' => $this->testForms[0]['id'], + 'newOrder' => [ + $this->testForms[0]['questions'][1]['id'], + $this->testForms[0]['questions'][0]['id'] + ] + ] + ]); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals([ + $this->testForms[0]['questions'][0]['id'] => [ 'order' => 2 ], + $this->testForms[0]['questions'][1]['id'] => [ 'order' => 1 ] + ], $data); + + // Check if form equals updated form. + $this->testGetFullForm($fullFormExpected); + } + + public function dataDeleteQuestion() { + $fullFormExpected = $this->dataGetFullForm()['getFullForm']['expected']; + array_splice($fullFormExpected['questions'], 0, 1); + $fullFormExpected['questions'][0]['order'] = 1; + + return [ + 'deleteQuestion' => [ + 'fullFormExpected' => $fullFormExpected + ] + ]; + } + /** + * @dataProvider dataDeleteQuestion + * + * @param array $fullFormExpected + */ + public function testDeleteQuestion(array $fullFormExpected) { + $resp = $this->http->request('DELETE', "api/v2/question/{$this->testForms[0]['questions'][0]['id']}"); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($this->testForms[0]['questions'][0]['id'], $data); + + $this->testGetFullForm($fullFormExpected); + } + + public function dataCreateNewOption() { + return [ + 'newOption' => [ + 'expected' => [ + // 'questionId' => Done dynamically below. + 'text' => 'A new Option.' + ] + ] + ]; + } + /** + * @dataProvider dataCreateNewOption + * + * @param array $expected + */ + public function testCreateNewOption(array $expected): void { + $resp = $this->http->request('POST', 'api/v2/option', [ + 'json' => [ + 'questionId' => $this->testForms[0]['questions'][1]['id'], + 'text' => 'A new Option.' + ] + ]); + $data = $this->OcsResponse2Data($resp); + + // Store for deletion on tearDown + $this->testForms[0]['questions'][1]['options'][] = $data; + + // Check questionId + $this->assertEquals($this->testForms[0]['questions'][1]['id'], $data['questionId']); + unset($data['questionId']); + unset($data['id']); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($expected, $data); + } + + public function dataUpdateOptionProperties() { + $fullFormExpected = $this->dataGetFullForm()['getFullForm']['expected']; + $fullFormExpected['questions'][1]['options'][0]['text'] = 'New option Text.'; + + return [ + 'updateOptionProps' => [ + 'fullFormExpected' => $fullFormExpected + ] + ]; + } + /** + * @dataProvider dataUpdateOptionProperties + * + * @param array $fullFormExpected + */ + public function testUpdateOptionProperties(array $fullFormExpected): void { + $resp = $this->http->request('POST', 'api/v2/option/update', [ + 'json' => [ + 'id' => $this->testForms[0]['questions'][1]['options'][0]['id'], + 'keyValuePairs' => [ + 'text' => 'New option Text.' + ] + ] + ]); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($this->testForms[0]['questions'][1]['options'][0]['id'], $data); + + // Check if form equals updated form. + $this->testGetFullForm($fullFormExpected); + } + + public function dataDeleteOption() { + $fullFormExpected = $this->dataGetFullForm()['getFullForm']['expected']; + array_splice($fullFormExpected['questions'][1]['options'], 0, 1); + + return [ + 'deleteOption' => [ + 'fullFormExpected' => $fullFormExpected + ] + ]; + } + /** + * @dataProvider dataDeleteOption + * + * @param array $fullFormExpected + */ + public function testDeleteOption(array $fullFormExpected) { + $resp = $this->http->request('DELETE', "api/v2/option/{$this->testForms[0]['questions'][1]['options'][0]['id']}"); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($this->testForms[0]['questions'][1]['options'][0]['id'], $data); + + $this->testGetFullForm($fullFormExpected); + } + + public function dataAddShare() { + return [ + 'addAShare' => [ + 'expected' => [ + // 'formId' => Checked dynamically + 'shareType' => 0, + 'shareWith' => 'test', + 'displayName' => 'Test Displayname' + ] + ] + ]; + } + /** + * @dataProvider dataAddShare + * + * @param array $expected + */ + public function testAddShare(array $expected) { + $resp = $this->http->request('POST', 'api/v2/share', [ + 'json' => [ + 'formId' => $this->testForms[0]['id'], + 'shareType' => 0, + 'shareWith' => 'test' + ] + ]); + $data = $this->OcsResponse2Data($resp); + + // Store for cleanup + $this->testForms[0]['shares'][] = $data; + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($this->testForms[0]['id'], $data['formId']); + unset($data['formId']); + unset($data['id']); + $this->assertEquals($expected, $data); + } + + public function dataDeleteShare() { + $fullFormExpected = $this->dataGetFullForm()['getFullForm']['expected']; + array_splice($fullFormExpected['shares'], 0, 1); + + return [ + 'deleteShare' => [ + 'fullFormExpected' => $fullFormExpected + ] + ]; + } + /** + * @dataProvider dataDeleteShare + * + * @param array $fullFormExpected + */ + public function testDeleteShare(array $fullFormExpected) { + $resp = $this->http->request('DELETE', "api/v2/share/{$this->testForms[0]['shares'][0]['id']}"); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($this->testForms[0]['shares'][0]['id'], $data); + + $this->testGetFullForm($fullFormExpected); + } + + public function dataGetSubmissions() { + return [ + 'getSubmissions' => [ + 'expected' => [ + 'submissions' => [ + [ + // 'formId' => Checked dynamically + 'userId' => 'user1', + 'userDisplayName' => 'user1', + 'timestamp' => 123456, + 'answers' => [ + [ + // 'submissionId' => Checked dynamically + // 'questionId' => Checked dynamically + 'text' => 'This is a short answer.' + ], + [ + // 'submissionId' => Checked dynamically + // 'questionId' => Checked dynamically + 'text' => 'Option 1' + ] + ] + ], + [ + // 'formId' => Checked dynamically + 'userId' => 'user2', + 'userDisplayName' => 'user2', + 'timestamp' => 12345, + 'answers' => [ + [ + // 'submissionId' => Checked dynamically + // 'questionId' => Checked dynamically + 'text' => 'This is another short answer.' + ], + [ + // 'submissionId' => Checked dynamically + // 'questionId' => Checked dynamically + 'text' => 'Option 2' + ] + ] + ] + ], + 'questions' => $this->dataGetFullForm()['getFullForm']['expected']['questions'] + ] + ] + ]; + } + /** + * @dataProvider dataGetSubmissions + * + * @param array $expected + */ + public function testGetSubmissions(array $expected) { + $resp = $this->http->request('GET', "api/v2/submissions/{$this->testForms[0]['hash']}"); + $data = $this->OcsResponse2Data($resp); + + // Cannot control ids, but check general consistency. + foreach ($data['submissions'] as $sIndex => $submission) { + $this->assertEquals($this->testForms[0]['id'], $submission['formId']); + unset($data['submissions'][$sIndex]['formId']); + + foreach ($submission['answers'] as $aIndex => $answer) { + $this->assertEquals($submission['id'], $answer['submissionId']); + $this->assertEquals($this->testForms[0]['questions'][ + $this->testForms[0]['submissions'][$sIndex]['answers'][$aIndex]['questionIndex'] + ]['id'], $answer['questionId']); + unset($data['submissions'][$sIndex]['answers'][$aIndex]['submissionId']); + unset($data['submissions'][$sIndex]['answers'][$aIndex]['questionId']); + unset($data['submissions'][$sIndex]['answers'][$aIndex]['id']); + } + unset($data['submissions'][$sIndex]['id']); + } + foreach ($data['questions'] as $qIndex => $question) { + $this->assertEquals($this->testForms[0]['id'], $question['formId']); + unset($data['questions'][$qIndex]['formId']); + + foreach ($question['options'] as $oIndex => $option) { + $this->assertEquals($question['id'], $option['questionId']); + unset($data['questions'][$qIndex]['options'][$oIndex]['questionId']); + unset($data['questions'][$qIndex]['options'][$oIndex]['id']); + } + unset($data['questions'][$qIndex]['id']); + } + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($expected, $data); + } + + public function dataExportSubmissions() { + return [ + 'exportSubmissions' => [ + 'expected' => ' + "User display name","Timestamp","First Question?","Second Question?" + "user1","Friday, January 2, 1970 at 10:17:36 AM GMT+0:00","This is a short answer.","Option 1" + "user2","Thursday, January 1, 1970 at 3:25:45 AM GMT+0:00","This is another short answer.","Option 2"' + ] + ]; + } + /** + * @dataProvider dataExportSubmissions + * + * @param array $expected + */ + public function testExportSubmissions(string $expected) { + $resp = $this->http->request('GET', "api/v2/submissions/export/{$this->testForms[0]['hash']}"); + $data = substr($resp->getBody()->getContents(), 3); // Some strange Character removed at the beginning + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals('attachment; filename="Title of a Form (responses).csv"', $resp->getHeaders()['Content-Disposition'][0]); + $this->assertEquals('text/csv;charset=UTF-8', $resp->getHeaders()['Content-type'][0]); + $arr_txt_expected = preg_split('/,/', str_replace(["\t", "\n"], '', $expected)); + $arr_txt_data = preg_split('/,/', str_replace(["\t", "\n"], '', $data)); + $this->assertEquals($arr_txt_expected, $arr_txt_data); + } + + public function testExportToCloud() { + $resp = $this->http->request('POST', 'api/v2/submissions/export', [ + 'json' => [ + 'hash' => $this->testForms[0]['hash'], + 'path' => '' + ]] + ); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals('Title of a Form (responses).csv', $data); + } + + public function dataDeleteSubmissions() { + $submissionsExpected = $this->dataGetSubmissions()['getSubmissions']['expected']; + $submissionsExpected['submissions'] = []; + + return [ + 'deleteSubmissions' => [ + 'submissionsExpected' => $submissionsExpected + ] + ]; + } + /** + * @dataProvider dataDeleteSubmissions + * + * @param array $submissionsExpected + */ + public function testDeleteSubmissions(array $submissionsExpected) { + $resp = $this->http->request('DELETE', "api/v2/submissions/{$this->testForms[0]['id']}"); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($this->testForms[0]['id'], $data); + + $this->testGetSubmissions($submissionsExpected); + } + + public function dataInsertSubmission() { + $submissionsExpected = $this->dataGetSubmissions()['getSubmissions']['expected']; + $submissionsExpected['submissions'][] = [ + 'userId' => 'test' + ]; + + return [ + 'insertSubmission' => [ + 'submissionsExpected' => $submissionsExpected + ] + ]; + } + /** + * @dataProvider dataInsertSubmission + * + * @param array $submissionsExpected + */ + public function testInsertSubmission(array $submissionsExpected) { + $resp = $this->http->request('POST', 'api/v2/submission/insert', [ + 'json' => [ + 'formId' => $this->testForms[0]['id'], + 'answers' => [ + $this->testForms[0]['questions'][0]['id'] => ['ShortAnswer!'], + $this->testForms[0]['questions'][1]['id'] => [ + $this->testForms[0]['questions'][1]['options'][0]['id'] + ] + ] + ] + ]); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + + // Check stored submissions + $resp = $this->http->request('GET', "api/v2/submissions/{$this->testForms[0]['hash']}"); + $data = $this->OcsResponse2Data($resp); + + // Store for deletion + $this->testForms[0]['submissions'][] = $data['submissions'][0]; + + // Check Ids + foreach ($data['submissions'][0]['answers'] as $aIndex => $answer) { + $this->assertEquals($data['submissions'][0]['id'], $answer['submissionId']); + unset($data['submissions'][0]['answers'][$aIndex]['id']); + unset($data['submissions'][0]['answers'][$aIndex]['submissionId']); + } + unset($data['submissions'][0]['id']); + // Check general behaviour of timestamp (Insert in the last 10 seconds) + $this->assertTrue(time() - $data['submissions'][0]['timestamp'] < 10); + unset($data['submissions'][0]['timestamp']); + + $this->assertEquals([ + 'userId' => 'test', + 'userDisplayName' => 'Test Displayname', + 'formId' => $this->testForms[0]['id'], + 'answers' => [ + [ + 'questionId' => $this->testForms[0]['questions'][0]['id'], + 'text' => 'ShortAnswer!' + ], + [ + 'questionId' => $this->testForms[0]['questions'][1]['id'], + 'text' => 'Option 1' + ] + ] + ], $data['submissions'][0]); + } + + public function dataDeleteSingleSubmission() { + $submissionsExpected = $this->dataGetSubmissions()['getSubmissions']['expected']; + array_splice($submissionsExpected['submissions'], 0, 1); + + return [ + 'deleteSingleSubmission' => [ + 'submissionsExpected' => $submissionsExpected + ] + ]; + } + /** + * @dataProvider dataDeleteSingleSubmission + * + * @param array $submissionsExpected + */ + public function testDeleteSingleSubmission(array $submissionsExpected) { + $resp = $this->http->request('DELETE', "api/v2/submission/{$this->testForms[0]['submissions'][0]['id']}"); + $data = $this->OcsResponse2Data($resp); + + $this->assertEquals(200, $resp->getStatusCode()); + $this->assertEquals($this->testForms[0]['submissions'][0]['id'], $data); + + $this->testGetSubmissions($submissionsExpected); + } +};