From aae415acc1393e654be5c203f132d3dc14c1c43c Mon Sep 17 00:00:00 2001 From: Carles Bruguera Date: Fri, 22 Jan 2021 17:29:02 +0100 Subject: [PATCH 1/2] Fix unique field query construction when field is nested Add test for different unique field locations --- eve/io/mongo/validation.py | 30 +++++++++------ eve/tests/methods/post.py | 78 +++++++++++++++++++++++++++++++++----- eve/tests/test_settings.py | 41 ++++++++++++++++++-- 3 files changed, 125 insertions(+), 24 deletions(-) diff --git a/eve/io/mongo/validation.py b/eve/io/mongo/validation.py index ad18ec1eb..0ef9f08b5 100644 --- a/eve/io/mongo/validation.py +++ b/eve/io/mongo/validation.py @@ -94,19 +94,27 @@ def _is_value_unique(self, unique, field, value, query): .. versionadded:: 0.6 """ if unique: - schema = self.schema - attribute_path = self.document_path + (field,) - temp_path = [attribute_path[0]] - for i, path in enumerate(attribute_path[:-1]): - schema = schema[path] - if schema["type"] != "list": - temp_path.append(attribute_path[i + 1]) - - final_path = ".".join(temp_path) - - query[final_path] = value + # In order to create the right query to check for unique values + # We need to obtain the schema path for the current field + # excluding any list fields in between. + schema = self.root_schema + document_field_path = list(self.document_path) + [field] + field_schema_path = [] + + while document_field_path: + current_schema_path_type = schema.get("type") + path = document_field_path.pop(0) + if current_schema_path_type == "dict": + schema = schema["schema"][path] + field_schema_path.append(path) + elif schema.get("type") == "list": + schema = schema["schema"] + else: + schema = schema[path] + field_schema_path.append(path) + query[".".join(field_schema_path)] = value resource_config = config.DOMAIN[self.resource] # exclude soft deleted documents if applicable diff --git a/eve/tests/methods/post.py b/eve/tests/methods/post.py index 8844c8527..b2cbfc956 100644 --- a/eve/tests/methods/post.py +++ b/eve/tests/methods/post.py @@ -1020,17 +1020,77 @@ def test_unique_within_resource_value_different_resources(self): self.assert201(status) def test_unique_within_resource_in_resource_without_filter(self): - r, status = self.post( - "test_unique", data={"unique_within_resource_attribute": "unique_value"} - ) + def make_payload(unique_value): + return {"unique_within_resource_attribute": unique_value} + + r, status = self.post("test_unique", data=make_payload("unique_value")) self.assert201(status) - r, status = self.post( - "test_unique", data={"unique_within_resource_attribute": "unique_value"} - ) + r, status = self.post("test_unique", data=make_payload("unique_value")) self.assert422(status) - r, status = self.post( - "test_unique", data={"unique_within_resource_attribute": "unique_value 2"} - ) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) + self.assert201(status) + + def test_unique_in_root_attribute(self): + def make_payload(unique_value): + return {"unique_attribute": unique_value} + + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert201(status) + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert422(status) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) + self.assert201(status) + + def test_unique_in_dict_attribute(self): + def make_payload(unique_value): + return {"unique_in_dict_attribute": {"unique_attribute": unique_value}} + + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert201(status) + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert422(status) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) + self.assert201(status) + + def test_unique_in_list_attribute(self): + def make_payload(unique_value): + return {"unique_in_list_attribute": [{"unique_attribute": unique_value}]} + + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert201(status) + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert422(status) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) + self.assert201(status) + + def test_unique_in_deep_dict_attribute(self): + def make_payload(unique_value): + return { + "unique_in_deep_dict_attribute": { + "dict_attribute": {"unique_attribute": unique_value} + } + } + + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert201(status) + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert422(status) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) + self.assert201(status) + + def test_unique_in_deep_list_attribute(self): + def make_payload(unique_value): + return { + "unique_in_deep_list_attribute": { + "list_attribute": [{"unique_attribute": unique_value}] + } + } + + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert201(status) + r, status = self.post("test_unique", data=make_payload("unique_value")) + self.assert422(status) + r, status = self.post("test_unique", data=make_payload("unique_value_2")) self.assert201(status) def perform_post(self, data, valid_items=[0]): diff --git a/eve/tests/test_settings.py b/eve/tests/test_settings.py index ca7c249eb..84d462cc4 100644 --- a/eve/tests/test_settings.py +++ b/eve/tests/test_settings.py @@ -302,6 +302,40 @@ "datasource": {"source": "test_unique"}, "schema": { "unique_attribute": {"type": "string", "unique": True}, + "unique_in_dict_attribute": { + "type": "dict", + "schema": {"unique_attribute": {"type": "string", "unique": True}}, + }, + "unique_in_list_attribute": { + "type": "list", + "schema": { + "type": "dict", + "schema": {"unique_attribute": {"type": "string", "unique": True}}, + }, + }, + "unique_in_deep_dict_attribute": { + "type": "dict", + "schema": { + "dict_attribute": { + "type": "dict", + "schema": {"unique_attribute": {"type": "string", "unique": True}}, + } + }, + }, + "unique_in_deep_list_attribute": { + "type": "dict", + "schema": { + "list_attribute": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "unique_attribute": {"type": "string", "unique": True} + }, + }, + } + }, + }, "unique_within_resource_attribute": { "type": "string", "unique_within_resource": True, @@ -309,6 +343,8 @@ }, } +test_unique_nested = {"datasource": {"source": "test_unique_nested"}, "schema": {}} + credit_rules = { "allow_unknown": True, "schema": { @@ -331,10 +367,7 @@ brands = { "item_title": "brand", - "schema": { - "name": {"type": "string"}, - "address": "string", - }, + "schema": {"name": {"type": "string"}, "address": "string"}, } components = { From 90f4b3d981baf9894773ad814888c544bd250299 Mon Sep 17 00:00:00 2001 From: Carles Bruguera Date: Sat, 23 Jan 2021 11:26:01 +0100 Subject: [PATCH 2/2] Rerun CI