Repository: getmoto/moto

Issue:
lambda ListFunctions incorrectly returns published versions by default
The ListFunctions Lambda API indicates that versions should be returned if `FunctionVersion` is set to `ALL`. (https://docs.aws.amazon.com/lambda/latest/dg/API_ListFunctions.html)

Moto currently returns the latest plus all versions _by default_ and ignores `FunctionVersion`.

## Reproducing the issue

```
zf = lambda_zip_file()
role_name = next(lambda_role_name())
with moto.mock_lambda():
    lambda_client = boto3.client('lambda')
    lambda_client.create_function(
        FunctionName='MotoTest',
        Runtime='nodejs12.x',
        Code={'ZipFile': zf},
        Handler='handler.handle',
        Role=role_name,
        Timeout=30,
        MemorySize=3008,
        Tags={},
        Publish=True
    )
    fns = lambda_client.list_functions()['Functions']  # NOTE: No FunctionVersion='ALL'
    print([(_['FunctionArn']) for _ in fns])
```
gives the following output:
```
['arn:aws:lambda:eu-west-1:123456789012:function:MotoTest', 'arn:aws:lambda:eu-west-1:123456789012:function:MotoTest:1']
```

The same process against AWS Lambda gives:
```
['arn:aws:lambda:eu-west-1:999999999999:function:MotoTest']
```

and with `list_functions(FunctionVersion='ALL')`
```
['arn:aws:lambda:eu-west-1:999999999999:function:MotoTest:$LATEST', 'arn:aws:lambda:eu-west-1:999999999999:function:MotoTest:2']
```


Discussion:
Thanks for raising this @eoinsha!

Resolving patch:
diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py
--- a/moto/awslambda/models.py
+++ b/moto/awslambda/models.py
@@ -964,7 +964,7 @@ def put_function(self, fn):
         fn.policy = Policy(fn)
         self._arns[fn.function_arn] = fn
 
-    def publish_function(self, name_or_arn):
+    def publish_function(self, name_or_arn, description=""):
         function = self.get_function_by_name_or_arn(name_or_arn)
         name = function.function_name
         if name not in self._functions:
@@ -975,6 +975,8 @@ def publish_function(self, name_or_arn):
         new_version = len(self._functions[name]["versions"]) + 1
         fn = copy.copy(self._functions[name]["latest"])
         fn.set_version(new_version)
+        if description:
+            fn.description = description
 
         self._functions[name]["versions"].append(fn)
         self._arns[fn.function_arn] = fn
@@ -1028,13 +1030,26 @@ def all(self):
         result = []
 
         for function_group in self._functions.values():
-            if function_group["latest"] is not None:
-                result.append(function_group["latest"])
+            latest = copy.deepcopy(function_group["latest"])
+            latest.function_arn = "{}:$LATEST".format(latest.function_arn)
+            result.append(latest)
 
             result.extend(function_group["versions"])
 
         return result
 
+    def latest(self):
+        """
+        Return the list of functions with version @LATEST
+        :return:
+        """
+        result = []
+        for function_group in self._functions.values():
+            if function_group["latest"] is not None:
+                result.append(function_group["latest"])
+
+        return result
+
 
 class LayerStorage(object):
     def __init__(self):
@@ -1091,6 +1106,9 @@ def create_function(self, spec):
 
         if spec.get("Publish"):
             ver = self.publish_function(function_name)
+            fn = copy.deepcopy(
+                fn
+            )  # We don't want to change the actual version - just the return value
             fn.version = ver.version
         return fn
 
@@ -1162,8 +1180,8 @@ def get_layer_versions(self, layer_name):
     def layers_versions_by_arn(self, layer_version_arn):
         return self._layers.get_layer_version_by_arn(layer_version_arn)
 
-    def publish_function(self, function_name):
-        return self._lambdas.publish_function(function_name)
+    def publish_function(self, function_name, description=""):
+        return self._lambdas.publish_function(function_name, description)
 
     def get_function(self, function_name_or_arn, qualifier=None):
         return self._lambdas.get_function_by_name_or_arn(
@@ -1210,8 +1228,10 @@ def get_function_by_arn(self, function_arn):
     def delete_function(self, function_name, qualifier=None):
         return self._lambdas.del_function(function_name, qualifier)
 
-    def list_functions(self):
-        return self._lambdas.all()
+    def list_functions(self, func_version=None):
+        if func_version == "ALL":
+            return self._lambdas.all()
+        return self._lambdas.latest()
 
     def send_sqs_batch(self, function_arn, messages, queue_arn):
         success = True
diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py
--- a/moto/awslambda/responses.py
+++ b/moto/awslambda/responses.py
@@ -236,11 +236,12 @@ def _invoke_async(self, request, full_url):
             return 404, response_headers, "{}"
 
     def _list_functions(self, request, full_url, headers):
+        querystring = self.querystring
+        func_version = querystring.get("FunctionVersion", [None])[0]
         result = {"Functions": []}
 
-        for fn in self.lambda_backend.list_functions():
+        for fn in self.lambda_backend.list_functions(func_version):
             json_data = fn.get_configuration()
-            json_data["Version"] = "$LATEST"
             result["Functions"].append(json_data)
 
         return 200, {}, json.dumps(result)
@@ -298,8 +299,9 @@ def _delete_event_source_mapping(self, uuid):
 
     def _publish_function(self, request, full_url, headers):
         function_name = self.path.rsplit("/", 2)[-2]
+        description = self._get_param("Description")
 
-        fn = self.lambda_backend.publish_function(function_name)
+        fn = self.lambda_backend.publish_function(function_name, description)
         if fn:
             config = fn.get_configuration()
             return 201, {}, json.dumps(config)


Test patch:
diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py
--- a/tests/test_awslambda/test_lambda.py
+++ b/tests/test_awslambda/test_lambda.py
@@ -121,8 +121,40 @@ def test_lambda_regions(region):
 @mock_lambda
 def test_list_functions():
     conn = boto3.client("lambda", _lambda_region)
-    result = conn.list_functions()
-    result["Functions"].should.have.length_of(0)
+    conn.list_functions()["Functions"].should.have.length_of(0)
+
+    function_name = "testFunction"
+
+    conn.create_function(
+        FunctionName=function_name,
+        Runtime="python3.7",
+        Role=get_role_name(),
+        Handler="lambda_function.lambda_handler",
+        Code={"ZipFile": get_test_zip_file1()},
+    )
+    conn.list_functions()["Functions"].should.have.length_of(1)
+    conn.publish_version(FunctionName=function_name, Description="v2")
+    conn.list_functions()["Functions"].should.have.length_of(1)
+
+    # FunctionVersion=ALL means we should get a list of all versions
+    result = conn.list_functions(FunctionVersion="ALL")["Functions"]
+    result.should.have.length_of(2)
+
+    v1 = [f for f in result if f["Version"] == "1"][0]
+    v1["Description"].should.equal("v2")
+    v1["FunctionArn"].should.equal(
+        "arn:aws:lambda:{}:{}:function:{}:1".format(
+            _lambda_region, ACCOUNT_ID, function_name
+        )
+    )
+
+    latest = [f for f in result if f["Version"] == "$LATEST"][0]
+    latest["Description"].should.equal("")
+    latest["FunctionArn"].should.equal(
+        "arn:aws:lambda:{}:{}:function:{}:$LATEST".format(
+            _lambda_region, ACCOUNT_ID, function_name
+        )
+    )
 
 
 @pytest.mark.network
@@ -772,14 +804,14 @@ def test_publish():
         Publish=False,
     )
 
-    function_list = conn.list_functions()
+    function_list = conn.list_functions(FunctionVersion="ALL")
     function_list["Functions"].should.have.length_of(1)
     latest_arn = function_list["Functions"][0]["FunctionArn"]
 
     res = conn.publish_version(FunctionName="testFunction")
     assert res["ResponseMetadata"]["HTTPStatusCode"] == 201
 
-    function_list = conn.list_functions()
+    function_list = conn.list_functions(FunctionVersion="ALL")
     function_list["Functions"].should.have.length_of(2)
 
     # #SetComprehension ;-)
@@ -815,8 +847,9 @@ def test_list_create_list_get_delete_list():
 
     conn.list_functions()["Functions"].should.have.length_of(0)
 
+    function_name = "testFunction"
     conn.create_function(
-        FunctionName="testFunction",
+        FunctionName=function_name,
         Runtime="python2.7",
         Role=get_role_name(),
         Handler="lambda_function.lambda_handler",
@@ -837,10 +870,7 @@ def test_list_create_list_get_delete_list():
             "CodeSha256": hashlib.sha256(zip_content).hexdigest(),
             "CodeSize": len(zip_content),
             "Description": "test lambda function",
-            "FunctionArn": "arn:aws:lambda:{}:{}:function:testFunction".format(
-                _lambda_region, ACCOUNT_ID
-            ),
-            "FunctionName": "testFunction",
+            "FunctionName": function_name,
             "Handler": "lambda_function.lambda_handler",
             "MemorySize": 128,
             "Role": get_role_name(),
@@ -853,16 +883,48 @@ def test_list_create_list_get_delete_list():
         },
         "ResponseMetadata": {"HTTPStatusCode": 200},
     }
-    func = conn.list_functions()["Functions"][0]
-    func.pop("LastModified")
-    func.should.equal(expected_function_result["Configuration"])
+    functions = conn.list_functions()["Functions"]
+    functions.should.have.length_of(1)
+    functions[0]["FunctionArn"].should.equal(
+        "arn:aws:lambda:{}:{}:function:{}".format(
+            _lambda_region, ACCOUNT_ID, function_name
+        )
+    )
+    functions = conn.list_functions(FunctionVersion="ALL")["Functions"]
+    functions.should.have.length_of(2)
+
+    latest = [f for f in functions if f["Version"] == "$LATEST"][0]
+    latest["FunctionArn"].should.equal(
+        "arn:aws:lambda:{}:{}:function:{}:$LATEST".format(
+            _lambda_region, ACCOUNT_ID, function_name
+        )
+    )
+    latest.pop("FunctionArn")
+    latest.pop("LastModified")
+    latest.should.equal(expected_function_result["Configuration"])
+
+    published = [f for f in functions if f["Version"] != "$LATEST"][0]
+    published["Version"].should.equal("1")
+    published["FunctionArn"].should.equal(
+        "arn:aws:lambda:{}:{}:function:{}:1".format(
+            _lambda_region, ACCOUNT_ID, function_name
+        )
+    )
+
+    func = conn.get_function(FunctionName=function_name)
+
+    func["Configuration"]["FunctionArn"].should.equal(
+        "arn:aws:lambda:{}:{}:function:{}".format(
+            _lambda_region, ACCOUNT_ID, function_name
+        )
+    )
 
-    func = conn.get_function(FunctionName="testFunction")
     # this is hard to match against, so remove it
     func["ResponseMetadata"].pop("HTTPHeaders", None)
     # Botocore inserts retry attempts not seen in Python27
     func["ResponseMetadata"].pop("RetryAttempts", None)
     func["Configuration"].pop("LastModified")
+    func["Configuration"].pop("FunctionArn")
 
     func.should.equal(expected_function_result)
     conn.delete_function(FunctionName="testFunction")