Repository: getmoto/moto

Issue:
Moto doesn't check for no-op updates to Lambda functions
from https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html :

<blockquote>Lambda doesn't create a new version if the code in the unpublished version is the same as the previous published version. You need to deploy code changes in $LATEST before you can create a new version.</blockquote>

Moto doesn't appear to perform this check, and will increment the version number regardless. This is at least the case when calling update_function_code with `Publish=True` set - I haven't tested for using publish_version separately.


Discussion:

Resolving patch:
diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py
--- a/moto/awslambda/models.py
+++ b/moto/awslambda/models.py
@@ -1310,7 +1310,14 @@ def publish_function(
         if not self._functions[name]["latest"]:
             return None
 
-        new_version = len(self._functions[name]["versions"]) + 1
+        all_versions = self._functions[name]["versions"]
+        if all_versions:
+            latest_published = all_versions[-1]
+            if latest_published.code_sha_256 == function.code_sha_256:
+                # Nothing has changed, don't publish
+                return latest_published
+
+        new_version = len(all_versions) + 1
         fn = copy.copy(self._functions[name]["latest"])
         fn.set_version(new_version)
         if description:
@@ -1879,6 +1886,7 @@ def update_function_code(
         self, function_name: str, qualifier: str, body: Dict[str, Any]
     ) -> Optional[Dict[str, Any]]:
         fn: LambdaFunction = self.get_function(function_name, qualifier)
+        fn.update_function_code(body)
 
         if body.get("Publish", False):
             fn = self.publish_function(function_name)  # type: ignore[assignment]


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
@@ -18,7 +18,9 @@
     get_role_name,
     get_test_zip_file1,
     get_test_zip_file2,
+    get_test_zip_file3,
     create_invalid_lambda,
+    _process_lambda,
 )
 
 _lambda_region = "us-west-2"
@@ -870,6 +872,7 @@ def test_list_versions_by_function():
         MemorySize=128,
         Publish=True,
     )
+    conn.update_function_code(FunctionName=function_name, ZipFile=get_test_zip_file1())
 
     res = conn.publish_version(FunctionName=function_name)
     assert res["ResponseMetadata"]["HTTPStatusCode"] == 201
@@ -1041,12 +1044,14 @@ def test_update_function_zip(key):
         Publish=True,
     )
     name_or_arn = fxn[key]
+    first_sha = fxn["CodeSha256"]
 
     zip_content_two = get_test_zip_file2()
 
-    conn.update_function_code(
+    update1 = conn.update_function_code(
         FunctionName=name_or_arn, ZipFile=zip_content_two, Publish=True
     )
+    update1["CodeSha256"].shouldnt.equal(first_sha)
 
     response = conn.get_function(FunctionName=function_name, Qualifier="2")
 
@@ -1066,6 +1071,30 @@ def test_update_function_zip(key):
     config.should.have.key("FunctionName").equals(function_name)
     config.should.have.key("Version").equals("2")
     config.should.have.key("LastUpdateStatus").equals("Successful")
+    config.should.have.key("CodeSha256").equals(update1["CodeSha256"])
+
+    most_recent_config = conn.get_function(FunctionName=function_name)
+    most_recent_config["Configuration"]["CodeSha256"].should.equal(
+        update1["CodeSha256"]
+    )
+
+    # Publishing this again, with the same code, gives us the same version
+    same_update = conn.update_function_code(
+        FunctionName=name_or_arn, ZipFile=zip_content_two, Publish=True
+    )
+    same_update["FunctionArn"].should.equal(
+        most_recent_config["Configuration"]["FunctionArn"] + ":2"
+    )
+    same_update["Version"].should.equal("2")
+
+    # Only when updating the code should we have a new version
+    new_update = conn.update_function_code(
+        FunctionName=name_or_arn, ZipFile=get_test_zip_file3(), Publish=True
+    )
+    new_update["FunctionArn"].should.equal(
+        most_recent_config["Configuration"]["FunctionArn"] + ":3"
+    )
+    new_update["Version"].should.equal("3")
 
 
 @mock_lambda
@@ -1191,6 +1220,8 @@ def test_multiple_qualifiers():
     )
 
     for _ in range(10):
+        new_zip = _process_lambda(f"func content {_}")
+        client.update_function_code(FunctionName=fn_name, ZipFile=new_zip)
         client.publish_version(FunctionName=fn_name)
 
     resp = client.list_versions_by_function(FunctionName=fn_name)["Versions"]
diff --git a/tests/test_awslambda/test_lambda_policy.py b/tests/test_awslambda/test_lambda_policy.py
--- a/tests/test_awslambda/test_lambda_policy.py
+++ b/tests/test_awslambda/test_lambda_policy.py
@@ -7,7 +7,7 @@
 from moto import mock_lambda, mock_s3
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from uuid import uuid4
-from .utilities import get_role_name, get_test_zip_file1
+from .utilities import get_role_name, get_test_zip_file1, get_test_zip_file2
 
 _lambda_region = "us-west-2"
 boto3.setup_default_session(region_name=_lambda_region)
@@ -139,7 +139,7 @@ def test_get_policy_with_qualifier():
         Publish=True,
     )
 
-    zip_content_two = get_test_zip_file1()
+    zip_content_two = get_test_zip_file2()
 
     conn.update_function_code(
         FunctionName=function_name, ZipFile=zip_content_two, Publish=True
@@ -250,7 +250,7 @@ def test_remove_function_permission__with_qualifier(key):
     name_or_arn = f[key]
 
     # Ensure Qualifier=2 exists
-    zip_content_two = get_test_zip_file1()
+    zip_content_two = get_test_zip_file2()
     conn.update_function_code(
         FunctionName=function_name, ZipFile=zip_content_two, Publish=True
     )