From f774f68ede3c84b566cbb64699898125c54c03a5 Mon Sep 17 00:00:00 2001 From: Luciano Righetti Date: Tue, 11 Jan 2022 12:33:34 +0100 Subject: [PATCH] add: add api tests for tags and orgs, extend openapi spec, fix routes for tags plugin --- config/routes.php | 15 +- tests/Fixture/TagsTaggedsFixture.php | 40 ++++ tests/Fixture/TagsTagsFixture.php | 87 ++++++++ .../DeleteOrganisationApiTest.php | 59 ++++++ .../IndexOrganisationsApiTest.php | 45 ++++ .../Organisations/TagOrganisationApiTest.php | 86 ++++++++ .../UntagOrganisationApiTest.php | 86 ++++++++ tests/TestCase/Api/Tags/IndexTagsApiTest.php | 47 +++++ tests/TestCase/Api/Users/EditUserApiTest.php | 1 - webroot/docs/openapi.yaml | 194 +++++++++++++++++- 10 files changed, 657 insertions(+), 3 deletions(-) create mode 100644 tests/Fixture/TagsTaggedsFixture.php create mode 100644 tests/Fixture/TagsTagsFixture.php create mode 100644 tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php create mode 100644 tests/TestCase/Api/Organisations/IndexOrganisationsApiTest.php create mode 100644 tests/TestCase/Api/Organisations/TagOrganisationApiTest.php create mode 100644 tests/TestCase/Api/Organisations/UntagOrganisationApiTest.php create mode 100644 tests/TestCase/Api/Tags/IndexTagsApiTest.php diff --git a/config/routes.php b/config/routes.php index e467725..d51121a 100644 --- a/config/routes.php +++ b/config/routes.php @@ -101,5 +101,18 @@ $routes->scope('/api', function (RouteBuilder $routes) { // Generic API route $routes->connect('/{controller}/{action}/*'); + + // Tags plugin routes + $routes->plugin( + 'tags', + ['path' => '/tags'], + function ($routes) { + $routes->setRouteClass(DashedRoute::class); + $routes->connect( + '/{action}/*', + ['controller' => 'Tags'] + ); + } + ); }); -}); \ No newline at end of file +}); diff --git a/tests/Fixture/TagsTaggedsFixture.php b/tests/Fixture/TagsTaggedsFixture.php new file mode 100644 index 0000000..e2bf70f --- /dev/null +++ b/tests/Fixture/TagsTaggedsFixture.php @@ -0,0 +1,40 @@ +records = [ + [ + 'tag_id' => TagsTagsFixture::TAG_ORG_A_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'fk_model' => 'Organisations', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'tag_id' => TagsTagsFixture::TAG_ORG_B_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_B_ID, + 'fk_model' => 'Organisations', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + ]; + parent::init(); + } +} diff --git a/tests/Fixture/TagsTagsFixture.php b/tests/Fixture/TagsTagsFixture.php new file mode 100644 index 0000000..002bc9e --- /dev/null +++ b/tests/Fixture/TagsTagsFixture.php @@ -0,0 +1,87 @@ +records = [ + [ + 'id' => self::TAG_RED_ID, + 'name' => 'red', + 'namespace' => null, + 'predicate' => null, + 'value' => null, + 'colour' => 'FF0000', + 'counter' => 0, + 'text_colour' => 'red', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::TAG_GREEN_ID, + 'name' => 'green', + 'namespace' => null, + 'predicate' => null, + 'value' => null, + 'colour' => '00FF00', + 'counter' => 0, + 'text_colour' => 'green', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::TAG_BLUE_ID, + 'name' => 'blue', + 'namespace' => null, + 'predicate' => null, + 'value' => null, + 'colour' => '0000FF', + 'counter' => 0, + 'text_colour' => 'blue', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::TAG_ORG_A_ID, + 'name' => 'org-a', + 'namespace' => null, + 'predicate' => null, + 'value' => null, + 'colour' => '000000', + 'counter' => 0, + 'text_colour' => 'black', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::TAG_ORG_B_ID, + 'name' => 'org-b', + 'namespace' => null, + 'predicate' => null, + 'value' => null, + 'colour' => '000000', + 'counter' => 0, + 'text_colour' => 'black', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ] + ]; + parent::init(); + } +} diff --git a/tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php b/tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php new file mode 100644 index 0000000..9840faf --- /dev/null +++ b/tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php @@ -0,0 +1,59 @@ +initializeValidator(APP . '../webroot/docs/openapi.yaml'); + } + + public function testDeleteOrganisation(): void + { + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_B_ID); + $this->delete($url); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists('Organisations', ['id' => OrganisationsFixture::ORGANISATION_B_ID]); + //TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec($url, 'delete'); + $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); + } + + public function testDeleteOrganisationNotAllowedToRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_B_ID); + $this->delete($url); + + $this->assertResponseCode(405); + $this->assertDbRecordExists('Organisations', ['id' => OrganisationsFixture::ORGANISATION_B_ID]); + //TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec($url, 'delete'); + $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); + } +} diff --git a/tests/TestCase/Api/Organisations/IndexOrganisationsApiTest.php b/tests/TestCase/Api/Organisations/IndexOrganisationsApiTest.php new file mode 100644 index 0000000..ba7c255 --- /dev/null +++ b/tests/TestCase/Api/Organisations/IndexOrganisationsApiTest.php @@ -0,0 +1,45 @@ +initializeValidator(APP . '../webroot/docs/openapi.yaml'); + } + + public function testIndexOrganisations(): void + { + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', OrganisationsFixture::ORGANISATION_A_ID)); + $this->assertResponseContains(sprintf('"id": %d', OrganisationsFixture::ORGANISATION_B_ID)); + // TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec(self::ENDPOINT); + } +} diff --git a/tests/TestCase/Api/Organisations/TagOrganisationApiTest.php b/tests/TestCase/Api/Organisations/TagOrganisationApiTest.php new file mode 100644 index 0000000..e23fcbc --- /dev/null +++ b/tests/TestCase/Api/Organisations/TagOrganisationApiTest.php @@ -0,0 +1,86 @@ +initializeValidator(APP . '../webroot/docs/openapi.yaml'); + } + + public function testTagOrganisation(): void + { + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_A_ID); + $this->post( + $url, + [ + 'tag_list' => "[\"red\"]" + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordExists( + 'TagsTagged', + [ + 'tag_id' => TagsTagsFixture::TAG_RED_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'fk_model' => 'Organisations' + ] + ); + //TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec($url, 'post'); + } + + public function testTagOrganisationNotAllowedToRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_A_ID); + $this->post( + $url, + [ + 'tag_list' => "[\"green\"]" + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists( + 'TagsTagged', + [ + 'tag_id' => TagsTagsFixture::TAG_GREEN_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'fk_model' => 'Organisations' + ] + ); + //TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec($url, 'post'); + } +} diff --git a/tests/TestCase/Api/Organisations/UntagOrganisationApiTest.php b/tests/TestCase/Api/Organisations/UntagOrganisationApiTest.php new file mode 100644 index 0000000..0c23ca4 --- /dev/null +++ b/tests/TestCase/Api/Organisations/UntagOrganisationApiTest.php @@ -0,0 +1,86 @@ +initializeValidator(APP . '../webroot/docs/openapi.yaml'); + } + + public function testUntagOrganisation(): void + { + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_A_ID); + $this->post( + $url, + [ + 'tag_list' => "[\"org-a\"]" + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists( + 'TagsTagged', + [ + 'tag_id' => TagsTagsFixture::TAG_ORG_A_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'fk_model' => 'Organisations' + ] + ); + //TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec($url, 'post'); + } + + public function testUntagOrganisationNotAllowedToRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_A_ID); + $this->post( + $url, + [ + 'tag_list' => "[\"org-a\"]" + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordExists( + 'TagsTagged', + [ + 'tag_id' => TagsTagsFixture::TAG_ORG_A_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'fk_model' => 'Organisations' + ] + ); + //TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec($url, 'post'); + } +} diff --git a/tests/TestCase/Api/Tags/IndexTagsApiTest.php b/tests/TestCase/Api/Tags/IndexTagsApiTest.php new file mode 100644 index 0000000..137151f --- /dev/null +++ b/tests/TestCase/Api/Tags/IndexTagsApiTest.php @@ -0,0 +1,47 @@ +initializeValidator(APP . '../webroot/docs/openapi.yaml'); + } + + public function testIndexTags(): void + { + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + + $this->assertResponseOk(); + $this->assertResponseContains('"name": "red"'); + $this->assertResponseContains('"name": "green"'); + $this->assertResponseContains('"name": "blue"'); + // TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec(self::ENDPOINT); + } +} diff --git a/tests/TestCase/Api/Users/EditUserApiTest.php b/tests/TestCase/Api/Users/EditUserApiTest.php index 5ac568a..6154c00 100644 --- a/tests/TestCase/Api/Users/EditUserApiTest.php +++ b/tests/TestCase/Api/Users/EditUserApiTest.php @@ -8,7 +8,6 @@ use Cake\TestSuite\IntegrationTestTrait; use Cake\TestSuite\TestCase; use App\Test\Fixture\AuthKeysFixture; use App\Test\Fixture\UsersFixture; -use App\Test\Fixture\OrganisationsFixture; use App\Test\Fixture\RolesFixture; use App\Test\Helper\ApiTestTrait; diff --git a/webroot/docs/openapi.yaml b/webroot/docs/openapi.yaml index a2a161e..f76a861 100644 --- a/webroot/docs/openapi.yaml +++ b/webroot/docs/openapi.yaml @@ -13,6 +13,8 @@ tags: description: "Users enrolled in this Cerebrate instance." - name: Organisations description: "Organisations can be equivalent to legal entities or specific individual teams within such entities. Their purpose is to relate individuals to their affiliations and for release control of information using the Trust Circles." + - name: Tags + description: "Tags can be attached to entity to quickly classify them, allowing further filtering and searches." paths: /api/v1/users/index: @@ -177,6 +179,96 @@ paths: default: $ref: "#/components/responses/ApiErrorResponse" + /api/v1/organisations/index: + get: + summary: "Get organisations" + operationId: getOrganisations + tags: + - Organisations + responses: + "200": + $ref: "#/components/responses/OrganisationListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /api/v1/organisations/delete/{organisationId}: + delete: + summary: "Delete organisation by ID" + operationId: deleteOrganisationById + tags: + - Organisations + parameters: + - $ref: "#/components/parameters/organisationId" + responses: + "200": + $ref: "#/components/responses/OrganisationResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /api/v1/organisations/tag/{organisationId}: + post: + summary: "Tag organisation by ID" + operationId: tagOrganisationById + tags: + - Organisations + parameters: + - $ref: "#/components/parameters/organisationId" + requestBody: + $ref: "#/components/requestBodies/TagOrganisationRequest" + responses: + "200": + $ref: "#/components/responses/OrganisationResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /api/v1/organisations/untag/{organisationId}: + post: + summary: "Remove organisation tag by ID" + operationId: untagOrganisationById + tags: + - Organisations + parameters: + - $ref: "#/components/parameters/organisationId" + requestBody: + $ref: "#/components/requestBodies/UntagOrganisationRequest" + responses: + "200": + $ref: "#/components/responses/OrganisationResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /api/v1/tags/index: + get: + summary: "Get tags list" + operationId: getTags + tags: + - Tags + responses: + "200": + $ref: "#/components/responses/TagListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + components: schemas: # General @@ -281,9 +373,60 @@ components: aligments: $ref: "#/components/schemas/AligmentList" + OrganisationList: + type: array + items: + $ref: "#/components/schemas/Organisation" + # Tags + TagName: + type: string + example: "white" + + TagNamespace: + type: string + nullable: true + example: "tlp" + + TagPredicate: + type: string + nullable: true + + TagValue: + type: string + nullable: true + + TagColour: + type: string + example: "FFFFFF" + + TagTextColour: + type: string + example: "white" + Tag: type: object + properties: + id: + $ref: "#/components/schemas/ID" + name: + $ref: "#/components/schemas/TagName" + namespace: + $ref: "#/components/schemas/TagNamespace" + predicate: + $ref: "#/components/schemas/TagPredicate" + value: + $ref: "#/components/schemas/TagValue" + colour: + $ref: "#/components/schemas/TagColour" + text_colour: + $ref: "#/components/schemas/TagTextColour" + counter: + type: integer + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" TagList: type: array @@ -417,6 +560,7 @@ components: Authorization: YOUR_API_KEY requestBodies: + # Users AddUserRequest: required: true content: @@ -457,6 +601,7 @@ components: password: type: string + # Organisations AddOrganisationRequest: required: true content: @@ -501,8 +646,32 @@ components: contacts: $ref: "#/components/schemas/OrganisationContacts" + TagOrganisationRequest: + required: true + content: + application/json: + schema: + type: object + properties: + tag_list: + type: string + description: "Stringified JSON array of the tag names to add." + example: '["red"]' + + UntagOrganisationRequest: + required: true + content: + application/json: + schema: + type: object + properties: + tag_list: + type: string + description: "Stringified JSON array of the tag names to remove." + example: '["red"]' + responses: - # User + # Users UserResponse: description: "User response" content: @@ -517,6 +686,7 @@ components: schema: $ref: "#/components/schemas/UserList" + # Organisations OrganisationResponse: description: "Organisation response" content: @@ -524,6 +694,28 @@ components: schema: $ref: "#/components/schemas/Organisation" + OrganisationListResponse: + description: "Organisations list response" + content: + application/json: + schema: + $ref: "#/components/schemas/OrganisationList" + + # Tags + TagResponse: + description: "Tag response" + content: + application/json: + schema: + $ref: "#/components/schemas/Tag" + + TagListResponse: + description: "Tags list response" + content: + application/json: + schema: + $ref: "#/components/schemas/TagList" + # Errors ApiErrorResponse: description: "Unexpected API error"