Repository: Kinto/kinto

Issue:
If id, schema or last_modified are marked as required, record can't be validated
Because we pop those fields before validating them with the collection schema.

We can either:
* prevent those fields to be mentioned as `required` when defining the collection schema
* or not pop them if they are present in the schema before validating
If id, schema or last_modified are marked as required, record can't be validated
Because we pop those fields before validating them with the collection schema.

We can either:
* prevent those fields to be mentioned as `required` when defining the collection schema
* or not pop them if they are present in the schema before validating


Discussion:
Yes we should prevent them from being defined in the first level ``required``.
Yes we should prevent them from being defined in the first level ``required``.

Resolving patch:
diff --git a/kinto/views/records.py b/kinto/views/records.py
--- a/kinto/views/records.py
+++ b/kinto/views/records.py
@@ -1,5 +1,3 @@
-import copy
-
 import jsonschema
 from kinto.core import resource, utils
 from kinto.core.errors import raise_invalid
@@ -55,15 +53,21 @@ def process_record(self, new, old=None):
         if not schema or not asbool(settings.get(schema_validation)):
             return new
 
-        collection_timestamp = self._collection[self.model.modified_field]
-
+        # Remove internal and auto-assigned fields from schema and record.
+        internal_fields = (self.model.id_field,
+                           self.model.modified_field,
+                           self.schema_field,
+                           self.model.permissions_field)
+        required_fields = [f for f in schema.get('required', []) if f not in internal_fields]
+        if required_fields:
+            schema = {**schema, 'required': required_fields}
+        else:
+            schema = {f: v for f, v in new.items() if f != 'required'}
+        data = {f: v for f, v in new.items() if f not in internal_fields}
+
+        # Validate or fail with 400.
         try:
-            stripped = copy.deepcopy(new)
-            stripped.pop(self.model.id_field, None)
-            stripped.pop(self.model.modified_field, None)
-            stripped.pop(self.model.permissions_field, None)
-            stripped.pop(self.schema_field, None)
-            jsonschema.validate(stripped, schema)
+            jsonschema.validate(data, schema)
         except jsonschema_exceptions.ValidationError as e:
             if e.validator_value:
                 field = e.validator_value[-1]
@@ -71,7 +75,10 @@ def process_record(self, new, old=None):
                 field = e.schema_path[-1]
             raise_invalid(self.request, name=field, description=e.message)
 
+        # Assign the schema version (collection object timestamp) to the record.
+        collection_timestamp = self._collection[self.model.modified_field]
         new[self.schema_field] = collection_timestamp
+
         return new
 
     def collection_get(self):


Test patch:
diff --git a/tests/test_views_collections_schema.py b/tests/test_views_collections_schema.py
--- a/tests/test_views_collections_schema.py
+++ b/tests/test_views_collections_schema.py
@@ -240,3 +240,29 @@ def test_additional_properties_are_rejected(self):
                                  headers=self.headers,
                                  status=400)
         assert "'extra' was unexpected)" in resp.json['message']
+
+
+class InternalRequiredProperties(BaseWebTestWithSchema, unittest.TestCase):
+    def setUp(self):
+        super().setUp()
+        # See bug Kinto/kinto#1244
+        schema = {**SCHEMA, 'required': ['id', 'schema', 'last_modified']}
+        self.app.put_json(COLLECTION_URL,
+                          {'data': {'schema': schema}},
+                          headers=self.headers)
+
+    def test_record_can_be_validated_with_minimum_fields(self):
+        self.app.post_json(RECORDS_URL,
+                           {'data': {}},
+                           headers=self.headers)
+
+    def test_record_can_be_validated_with_every_fields(self):
+        self.app.post_json(RECORDS_URL,
+                           {'data': {'id': 'abc', 'last_modified': 1234,
+                                     'schema': 42, 'title': 'b', 'name': 'n'}},
+                           headers=self.headers)
+
+    def test_record_can_be_validated_with_id_and_last_modified(self):
+        self.app.post_json(RECORDS_URL,
+                           {'data': {'id': 'abc', 'last_modified': 1234}},
+                           headers=self.headers)