Repository: python-scim/scim2-models

Issue:
[bug] ListResponse does not impose to have totalResults

as of today for /schemas  , the current models requires to have totalResults set 

otherwise it complains about 

```python
  File "D:\tools\python3.10\lib\site-packages\scim2_models\rfc7644\list_response.py", line 117, in check_results_number
    if obj.total_results > 0 and not obj.resources:
TypeError: '>' not supported between instances of 'NoneType' and 'int'
```


the specification is a bit more complex , totalResult is mandatory only when it is subject to filter , as a show case all request no subject to filtering such as /Schema , totalResults is not mandatory 

all sample in the specification follows this rules cf 
https://datatracker.ietf.org/doc/html/rfc7643#section-8.7.1

Hints from issue comments:
@azmeuk: Thank you for your bug report.

[RFC7644 §3.4.2](https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2) indicates that `totalResults` is *REQUIRED*.

```
totalResults  The total number of results returned by the list or
      query operation.  The value may be larger than the number of
      resources returned, such as when returning a single page (see
      [Section 3.4.2.4](https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.4)) of results where multiple pages are available.
      REQUIRED.
```

However our code does not enforce this:

https://github.com/python-scim/scim2-models/blob/935d6051af75d07dd7ab5ac839dfeca638bcb469/scim2_models/rfc7644/list_response.py#L86-L87

I guess this is a bug, but this is not what you describe.

> the specification is a bit more complex , totalResult is mandatory only when it is subject to filter , as a show case all request no subject to filtering such as /Schema , totalResults is not mandatory

I am not sure what you mean.
- A request like `/Schemas` returns a `ListResponse` object, and thus needs to include the `totalResults` parameter.
- A request like `/Schemas/urn:ietf:params:scim:schemas:core:2.0:User` does not return a `ListResponse` object, but a `Schema` object, so `totalResults` would not be valid here.

> all sample in the specification follows this rules cf
https://datatracker.ietf.org/doc/html/rfc7643#section-8.7.1

You cite RFC7643 but there is no mention of `ListResponse` in this spec. `ListResponse` is defined and illustrated in RFC7644. What you cite in RFC7643 §8.7.1 is a payload that could be part of a `ListResponse`, but not a `ListResponse` per se.

I included all samples of code seen in RFC7643 and RFC7644 [in the sample directory](https://github.com/python-scim/scim2-models/tree/main/samples).
You can see that every code snippet from the spces mentioning a `ListResponse` alse include the `totalResults` parameter.
@LasneF: sorry for the mess,  i thought it was enforced some how (and so that enforcement was wrong) 

it looks only bug in some case @LasneF: nb : not sure it is fair to close as the issue is there , @azmeuk: I think I  don't understand what issue you are referring to.
Do you mean that `totalResults` is currently not marked as required by scim2-models while it actually should?@LasneF: Let s be clean : 

How to reproduce 
 Command used  using the source code from the main branch as of today
python scim2_tester/checker.py http://127.0.0.1:8010/

result 
  File "D:\tools\python3.10\lib\site-packages\scim2_models\rfc7644\list_response.py", line 117, in check_results_number
    if obj.total_results > 0 and not obj.resources:
TypeError: '>' not supported between instances of 'NoneType' and 'int'


looking on the trace of my server it looks likes it is when evaluating the /schema

if i add a TotalResults to it the test succeed pass 


here is the payload that my server returns ,that fails validation 
```
{

    "schemas": [
      "urn:ietf:params:scim:api:messages:2.0:ListResponse"
    ],
    "Resources": [
      {
        "schemas": [
          "urn:ietf:params:scim:schemas:core:2.0:Schema"
        ],
        "id": "urn:ietf:params:scim:schemas:core:2.0:User",
        "meta": {
          "resourceType": "Schema",
          "created": "2001-01-01T00:00:00+00:00",
          "lastModified": "2001-01-01T00:00:00+00:00",
          "version": "1.3",
          "location": "https://api.scim.dev/scim/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:User"
        },
        "name": "User",
        "attributes": [
          {
            "name": "userName",
            "type": "string",
            "mutability": "readWrite",
            "returned": "default",
            "uniqueness": "server",
            "required": true,
            "multiValued": false,
            "caseExact": false
          },
          {
            "name": "externalId",
            "type": "string",
            "mutability": "readWrite",
            "returned": "default",
            "uniqueness": "server",
            "required": false,
            "multiValued": false,
            "caseExact": false
          },
          {
            "name": "name",
            "type": "string",
            "mutability": "readWrite",
            "returned": "default",
            "uniqueness": "server",
            "required": false,
            "multiValued": false,
            "caseExact": false,
            "subAttributes": [
              {
                "name": "formatted",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": false,
                "multiValued": false,
                "caseExact": false
              },
              {
                "name": "familyName",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": false,
                "multiValued": false,
                "caseExact": false
              },
              {
                "name": "givenName",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": false,
                "multiValued": false,
                "caseExact": false
              }
            ]
          },
          {
            "name": "active",
            "type": "string",
            "mutability": "readWrite",
            "returned": "default",
            "uniqueness": "server",
            "required": false,
            "multiValued": false,
            "caseExact": false
          },
          {
            "name": "password",
            "type": "string",
            "mutability": "readWrite",
            "returned": "default",
            "uniqueness": "server",
            "required": false,
            "multiValued": false,
            "caseExact": false
          },
          {
            "name": "emails",
            "type": "string",
            "mutability": "readWrite",
            "returned": "default",
            "uniqueness": "server",
            "required": true,
            "multiValued": true,
            "caseExact": false,
            "subAttributes": [
              {
                "name": "value",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": true,
                "multiValued": false,
                "caseExact": false
              },
              {
                "name": "type",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": false,
                "multiValued": false,
                "caseExact": false
              },
              {
                "name": "primary",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": false,
                "multiValued": false,
                "caseExact": false
              }
            ]
          },
          {
            "name": "groups",
            "type": "string",
            "mutability": "readWrite",
            "returned": "default",
            "uniqueness": "server",
            "required": false,
            "multiValued": true,
            "caseExact": false,
            "subAttributes": [
              {
                "name": "value",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": false,
                "multiValued": false,
                "caseExact": false
              },
              {
                "name": "$ref",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": false,
                "multiValued": false,
                "caseExact": false
              },
              {
                "name": "display",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": false,
                "multiValued": false,
                "caseExact": false
              }
            ]
          }
        ]
      },
      {
        "schemas": [
          "urn:ietf:params:scim:schemas:core:2.0:Schema"
        ],
        "id": "urn:ietf:params:scim:schemas:core:2.0:Group",
        "meta": {
          "resourceType": "Schema",
          "created": "2001-01-01T00:00:00+00:00",
          "lastModified": "2001-01-01T00:00:00+00:00",
          "version": "1.2",
          "location": "https://api.scim.dev/scim/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:Group"
        },
        "name": "Group",
        "attributes": [
          {
            "name": "displayName",
            "type": "string",
            "mutability": "readWrite",
            "returned": "default",
            "uniqueness": "server",
            "required": true,
            "multiValued": false,
            "caseExact": false
          },
          {
            "name": "externalId",
            "type": "string",
            "mutability": "readWrite",
            "returned": "default",
            "uniqueness": "server",
            "required": false,
            "multiValued": false,
            "caseExact": false
          },
          {
            "name": "members",
            "type": "string",
            "mutability": "readWrite",
            "returned": "default",
            "uniqueness": "server",
            "required": false,
            "multiValued": true,
            "caseExact": false,
            "subAttributes": [
              {
                "name": "value",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": true,
                "multiValued": false,
                "caseExact": false
              },
              {
                "name": "$ref",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": false,
                "multiValued": false,
                "caseExact": false
              },
              {
                "name": "display",
                "type": "string",
                "mutability": "readWrite",
                "returned": "default",
                "uniqueness": "server",
                "required": false,
                "multiValued": false,
                "caseExact": false
              }
            ]
          }
        ]
      }
    ]
  }
```@azmeuk: I released scim2-models 0.3.1 with the fix from #90 that enforces the presence of `totalResult` in `ListResponse` payloads, and display a better error message when failing.

Solution patch:
diff --git a/doc/changelog.rst b/doc/changelog.rst
index 9a0460f..9adac30 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -7,6 +7,7 @@ Changelog
 Fixed
 ^^^^^
 - Fix :attr:`~SearchRequest.start_index` and :attr:`~SearchRequest.count` limits. :issue:`84`
+- :attr:`~ListResponse.total_resuls` is required.
 
 [0.3.0] - 2024-12-11
 --------------------
diff --git a/scim2_models/rfc7644/list_response.py b/scim2_models/rfc7644/list_response.py
index 099bee8..af55ec6 100644
--- a/scim2_models/rfc7644/list_response.py
+++ b/scim2_models/rfc7644/list_response.py
@@ -104,7 +104,13 @@ class ListResponse(Message, Generic[AnyResource], metaclass=ListResponseMetaclas
     def check_results_number(
         cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
     ) -> Self:
-        """:rfc:`RFC7644 §3.4.2 <7644#section-3.4.2.4>` indicates that 'resources' must be set if 'totalResults' is non-zero."""
+        """Validate result numbers.
+
+        :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2.4>` indicates that:
+
+        - 'totalResults' is required
+        - 'resources' must be set if 'totalResults' is non-zero.
+        """
         obj = handler(value)
 
         if (
@@ -114,6 +120,12 @@ def check_results_number(
         ):
             return obj
 
+        if obj.total_results is None:
+            raise PydanticCustomError(
+                "required_error",
+                "Field 'total_results' is required but value is missing or null",
+            )
+
         if obj.total_results > 0 and not obj.resources:
             raise PydanticCustomError(
                 "no_resource_error",


Test patch:
diff --git a/tests/test_list_response.py b/tests/test_list_response.py
index 071cb5d..cf9419e 100644
--- a/tests/test_list_response.py
+++ b/tests/test_list_response.py
@@ -217,3 +217,26 @@ def test_list_response_schema_ordering():
         ],
     }
     ListResponse[Union[User[EnterpriseUser], Group]].model_validate(payload)
+
+
+def test_total_results_required():
+    """ListResponse.total_results is required."""
+    payload = {
+        "Resources": [
+            {
+                "schemas": [
+                    "urn:ietf:params:scim:schemas:core:2.0:User",
+                ],
+                "userName": "bjensen@example.com",
+                "id": "foobar",
+            }
+        ],
+    }
+
+    with pytest.raises(
+        ValidationError,
+        match="Field 'total_results' is required but value is missing or null",
+    ):
+        ListResponse[User].model_validate(
+            payload, scim_ctx=Context.RESOURCE_QUERY_RESPONSE
+        )