Repository: getmoto/moto

Issue:
logs client describe_log_groups nextToken is int instead of str
Hi,

I was mocking some code of mine when I realised that the value returned for nextToken when calling `logs.describe_log_groups` is an int, however `logs.describe_log_groups` expects an str. Here's a minimal reproduction case:

```python
import boto3
from moto import mock_logs

with mock_logs():
    client = boto3.client('logs')

    for i in range(55):
        client.create_log_group(logGroupName=f'my-log-group-{i}')

    result = client.describe_log_groups()
    groups = result['logGroups']

    result = client.describe_log_groups(nextToken=result['nextToken'])
```

This throws:

```
Traceback (most recent call last):
  File "foo.py", line 13, in <module>
    client.describe_log_groups(nextToken=result['nextToken'])
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/client.py", line 357, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/client.py", line 648, in _make_api_call
    request_dict = self._convert_to_request_dict(
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/client.py", line 696, in _convert_to_request_dict
    request_dict = self._serializer.serialize_to_request(
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/validate.py", line 297, in serialize_to_request
    raise ParamValidationError(report=report.generate_report())
botocore.exceptions.ParamValidationError: Parameter validation failed:
Invalid type for parameter nextToken, value: 50, type: <class 'int'>, valid types: <class 'str'>
```

I tried casting the nextToken value to an `str` in the hopes of working aroud this issue, however this generates another error:

```
Traceback (most recent call last):
  File "foo.py", line 13, in <module>
    client.describe_log_groups(nextToken=str(result['nextToken']))
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/client.py", line 357, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/client.py", line 662, in _make_api_call
    http, parsed_response = self._make_request(
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/client.py", line 682, in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/endpoint.py", line 102, in make_request
    return self._send_request(request_dict, operation_model)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/endpoint.py", line 136, in _send_request
    while self._needs_retry(attempts, operation_model, request_dict,
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/endpoint.py", line 253, in _needs_retry
    responses = self._event_emitter.emit(
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/hooks.py", line 356, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/hooks.py", line 228, in emit
    return self._emit(event_name, kwargs)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/hooks.py", line 211, in _emit
    response = handler(**kwargs)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/retryhandler.py", line 183, in __call__
    if self._checker(attempts, response, caught_exception):
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/retryhandler.py", line 250, in __call__
    should_retry = self._should_retry(attempt_number, response,
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/retryhandler.py", line 269, in _should_retry
    return self._checker(attempt_number, response, caught_exception)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/retryhandler.py", line 316, in __call__
    checker_response = checker(attempt_number, response,
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/retryhandler.py", line 222, in __call__
    return self._check_caught_exception(
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/retryhandler.py", line 359, in _check_caught_exception
    raise caught_exception
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/endpoint.py", line 197, in _do_get_response
    responses = self._event_emitter.emit(event_name, request=request)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/hooks.py", line 356, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/hooks.py", line 228, in emit
    return self._emit(event_name, kwargs)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/botocore/hooks.py", line 211, in _emit
    response = handler(**kwargs)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/moto/core/models.py", line 322, in __call__
    status, headers, body = response_callback(
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/moto/core/responses.py", line 202, in dispatch
    return cls()._dispatch(*args, **kwargs)
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/moto/core/responses.py", line 312, in _dispatch
    return self.call_action()
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/moto/core/responses.py", line 397, in call_action
    response = method()
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/moto/logs/responses.py", line 42, in describe_log_groups
    groups, next_token = self.logs_backend.describe_log_groups(
  File "/home/ubuntu/.venvs/moto-test-case/lib/python3.8/site-packages/moto/logs/models.py", line 497, in describe_log_groups
    groups_page = groups[next_token : next_token + limit]
TypeError: can only concatenate str (not "int") to str
```

This occurs with Python 3.8 on moto 1.3.16. I'll provide a PR if I get around to it, just wanted to log the issue to keep track of it.


Discussion:
Thanks @teotwaki! PR's are always welcome :) 

Resolving patch:
diff --git a/moto/logs/models.py b/moto/logs/models.py
--- a/moto/logs/models.py
+++ b/moto/logs/models.py
@@ -485,20 +485,39 @@ def delete_log_group(self, log_group_name):
     def describe_log_groups(self, limit, log_group_name_prefix, next_token):
         if log_group_name_prefix is None:
             log_group_name_prefix = ""
-        if next_token is None:
-            next_token = 0
 
         groups = [
             group.to_describe_dict()
             for name, group in self.groups.items()
             if name.startswith(log_group_name_prefix)
         ]
-        groups = sorted(groups, key=lambda x: x["creationTime"], reverse=True)
-        groups_page = groups[next_token : next_token + limit]
-
-        next_token += limit
-        if next_token >= len(groups):
-            next_token = None
+        groups = sorted(groups, key=lambda x: x["logGroupName"])
+
+        index_start = 0
+        if next_token:
+            try:
+                index_start = (
+                    next(
+                        index
+                        for (index, d) in enumerate(groups)
+                        if d["logGroupName"] == next_token
+                    )
+                    + 1
+                )
+            except StopIteration:
+                index_start = 0
+                # AWS returns an empty list if it receives an invalid token.
+                groups = []
+
+        index_end = index_start + limit
+        if index_end > len(groups):
+            index_end = len(groups)
+
+        groups_page = groups[index_start:index_end]
+
+        next_token = None
+        if groups_page and index_end < len(groups):
+            next_token = groups_page[-1]["logGroupName"]
 
         return groups_page, next_token
 
diff --git a/moto/logs/responses.py b/moto/logs/responses.py
--- a/moto/logs/responses.py
+++ b/moto/logs/responses.py
@@ -42,7 +42,10 @@ def describe_log_groups(self):
         groups, next_token = self.logs_backend.describe_log_groups(
             limit, log_group_name_prefix, next_token
         )
-        return json.dumps({"logGroups": groups, "nextToken": next_token})
+        result = {"logGroups": groups}
+        if next_token:
+            result["nextToken"] = next_token
+        return json.dumps(result)
 
     def create_log_stream(self):
         log_group_name = self._get_param("logGroupName")


Test patch:
diff --git a/tests/test_logs/test_logs.py b/tests/test_logs/test_logs.py
--- a/tests/test_logs/test_logs.py
+++ b/tests/test_logs/test_logs.py
@@ -458,3 +458,39 @@ def test_describe_subscription_filters_errors():
     ex.response["Error"]["Message"].should.equal(
         "The specified log group does not exist"
     )
+
+
+@mock_logs
+def test_describe_log_groups_paging():
+    client = boto3.client("logs", "us-east-1")
+
+    group_names = [
+        "/aws/lambda/lowercase-dev",
+        "/aws/lambda/FileMonitoring",
+        "/aws/events/GetMetricData",
+        "/aws/lambda/fileAvailable",
+    ]
+
+    for name in group_names:
+        client.create_log_group(logGroupName=name)
+
+    resp = client.describe_log_groups()
+    resp["logGroups"].should.have.length_of(4)
+    resp.should_not.have.key("nextToken")
+
+    resp = client.describe_log_groups(limit=2)
+    resp["logGroups"].should.have.length_of(2)
+    resp["nextToken"].should.equal("/aws/lambda/FileMonitoring")
+
+    resp = client.describe_log_groups(nextToken=resp["nextToken"], limit=1)
+    resp["logGroups"].should.have.length_of(1)
+    resp["nextToken"].should.equal("/aws/lambda/fileAvailable")
+
+    resp = client.describe_log_groups(nextToken=resp["nextToken"])
+    resp["logGroups"].should.have.length_of(1)
+    resp["logGroups"][0]["logGroupName"].should.equal("/aws/lambda/lowercase-dev")
+    resp.should_not.have.key("nextToken")
+
+    resp = client.describe_log_groups(nextToken="invalid-token")
+    resp["logGroups"].should.have.length_of(0)
+    resp.should_not.have.key("nextToken")