diff --git a/ci/apiv2/test_http_methods.py b/ci/apiv2/test_http_methods.py index d70e2d47b..f6b952534 100644 --- a/ci/apiv2/test_http_methods.py +++ b/ci/apiv2/test_http_methods.py @@ -1,13 +1,20 @@ import requests +import json +import time from hashtopolis import HashtopolisConnector, HashtopolisConfig from utils import BaseTest + class HttpMethodsTest(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.config = HashtopolisConfig() + def test_empty_body(self): - config = HashtopolisConfig() - conn = HashtopolisConnector('/ui/users', config) + conn = HashtopolisConnector('/ui/users', self.config) conn.authenticate() headers = conn._headers @@ -19,4 +26,69 @@ def test_empty_body(self): self.assertGreaterEqual(len(values), 1) # TODO: Test for non-empty body which should fail - # TODO: Test for invalid parameters \ No newline at end of file + # TODO: Test for invalid parameters + + def test_get_one_response_should_not_duplicate_self_reference_in_data(self): + conn = HashtopolisConnector('/ui/users', self.config) + conn.authenticate() + resource_path = conn._model_uri + "/1" + uri = conn._api_endpoint + resource_path + + response = requests.get(uri, headers=conn._headers) + r = response.json() + + self.assertIsNotNone(r['links']['self'], "Top level self reference should be present in all responses.") + self.assertIn(response.request.path_url, r['links']['self'], "Self reference for a single resource should be its path.") + self.assertTrue(isinstance(r['data'], dict), "A single resource should be represented as an object.") + self.assertNotIn('self', r['data'].get('links', {}), "A single resource should not include a self reference.") + + def test_get_many_response_should_include_self_reference_for_every_resource(self): + conn = HashtopolisConnector('/ui/hashtypes', self.config) + conn.authenticate() + resource_path = conn._model_uri + uri = conn._api_endpoint + resource_path + + response = requests.get(uri, headers=conn._headers) + r = response.json() + + self.assertIsNotNone(r['links']['self']) + self.assertIn(response.request.path_url, r['links']['self'], "Self reference for a resource collection should be its path.") + + resources = r.get('data') + self.assertIsInstance(resources, list) + self.assertGreater(len(resources), 0) + + for resource in resources: + self.assertIsInstance(resource, dict) + self.assertIn('self', resource.get('links', {}), "A resource in a collection should contain a self refrence") + self.assertEqual(f"{response.request.path_url}/{resource.get('id')}", resource['links']['self']) + + def test_post_user_should_return_getone_uri_in_location(self): + conn = HashtopolisConnector('/ui/users', self.config) + conn.authenticate() + uri = f"{conn._api_endpoint}{conn._model_uri}" + stamp = int(time.time() * 1000) + + payload = { + "data": { + "type": "user", + "attributes": { + "name": f"test-{stamp}", + "email": f"test-{stamp}@example.com", + "globalPermissionGroupId": 1, + "isValid": True, + "sessionLifetime": 3600, + }, + }, + } + + headers = dict(conn._headers) + headers["Content-Type"] = "application/json" + response = requests.post(uri, headers=headers, data=json.dumps(payload)) + + self.assertEqual(response.status_code, 201) + self.assertIsNotNone(response.headers.get('Location'), "Missing Location header in created resource") + + resource_response = requests.get(f"{conn._hashtopolis_uri}{response.headers.get('Location')}", headers=conn._headers) + self.assertEqual(resource_response.status_code, 200, "Unable to find the created resource") + diff --git a/src/inc/apiv2/common/AbstractBaseAPI.php b/src/inc/apiv2/common/AbstractBaseAPI.php index 49671e6ca..7bcea0357 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.php @@ -760,9 +760,6 @@ protected function obj2Resource(object $obj, array &$expandResult = [], ?array $ "type" => $this->getObjectTypeName($obj), "id" => $obj->getId(), "attributes" => $attributes, - "links" => [ - "self" => $linkSelf, - ], ]; if (sizeof($relationships) > 0) { @@ -1657,6 +1654,9 @@ protected static function getOneResource(object $apiClass, object $object, Reque $linksSelf = $request->getUri()->getPath() . ((!empty($linksQuery)) ? '?' . $linksQuery : ''); $links = ["self" => $linksSelf]; + + $resourceApiClass = $apiClass->container->get('classMapper')->get(get_class($object)); + $resourceLocation = $apiClass->routeParser->urlFor($resourceApiClass . ':getOne', ['id' => $object->getId()]); $metaData = []; if ($apiClass->permissionErrors !== null) { @@ -1669,10 +1669,8 @@ protected static function getOneResource(object $apiClass, object $object, Reque $body->write($apiClass->ret2json($ret)); return $response->withHeader("Content-Type", "application/vnd.api+json") - ->withHeader("Location", $dataResources[0]["links"]["self"]) + ->withHeader("Location", $resourceLocation) ->withStatus($statusCode); - //for location we use links value from $dataresources because if we use $linksSelf, the wrong location gets returned in - //case of a POST request } //Meta response for helper functions that do not respond with resource records diff --git a/src/inc/apiv2/common/AbstractModelAPI.php b/src/inc/apiv2/common/AbstractModelAPI.php index 5d15a0754..7f1091c1e 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.php +++ b/src/inc/apiv2/common/AbstractModelAPI.php @@ -766,6 +766,10 @@ public static function getManyResources(object $apiClass, Request $request, Resp foreach ($objects as $object) { // Create object $newObject = $apiClass->obj2Resource($object, $expandResult, $request->getQueryParams()['fields'] ?? null, $request->getQueryParams()['aggregate'] ?? null); + // Resource path + $resourceApiClass = $apiClass->container->get('classMapper')->get(get_class($object)); + $resourceLocation = $apiClass->routeParser->urlFor($resourceApiClass . ':getOne', ['id' => $object->getId()]); + $newObject["links"]["self"] = $resourceLocation; $includedResources = $apiClass->processExpands($apiClass, $expands, $object, $expandResult, $includedResources, $request->getQueryParams()['fields'] ?? null, $request->getQueryParams()['aggregate'] ?? null); // Add to result output