Repository: getmoto/moto (python)

Issue:
Lambda update_function_code not incrementing Version when Publish=True
When running the following beneath moto
```python
import boto3
client = boto3.client('lambda', region_name='us-east-2')
res = client.update_function_code(FunctionName='test-function', ZipFile=<somearchive>, Publish=True)
print(res["Version"])
```
After the first invocation this prints out `1`, but this also prints out  `1` for both results if the following is run:
```python
import boto3
client = boto3.client('lambda', region_name='us-east-2')
res = client.update_function_code(FunctionName='test-function', ZipFile=<somearchive>, Publish=True)
print(res["Version"])
res = client.update_function_code(FunctionName='test-function', ZipFile=<somearchive>, Publish=True)
print(res["Version"])
```
I would expect the second invocation to print out `2` since a new version is getting published.


Pull request:
AWSLambda: Publish when update_fn has been called
Fixes #8337 

Partially reverts #5734 

The docs for `publish_function` state:

> Lambda publishes a new function version only if the code has never been published, or if the code has changed from the last published version. If there is no change, the function version remains at the last published version.

The actual behaviour is that Lambda publishes a new function version only if [..] the `update_function_code` or `update_function_configuration` functions have been called.

If you call the 'update'-function, AWS does not check whether the provided code is different - they just assume it is different, and will publish a new version.

The new tests for this behaviour are verified against AWS.

Patch:
diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py
index f39f00c70..59020c64e 100644
--- a/moto/awslambda/models.py
+++ b/moto/awslambda/models.py
@@ -651,16 +651,15 @@ class LambdaFunction(CloudFormationModel, DockerModel):
 
         self.logs_group_name = f"/aws/lambda/{self.function_name}"
 
-        # this isn't finished yet. it needs to find out the VpcId value
-        self._vpc_config = spec.get(
-            "VpcConfig", {"SubnetIds": [], "SecurityGroupIds": []}
-        )
+        # VpcConfig has no default, if not supplied
+        self._vpc_config = spec.get("VpcConfig")
 
         # auto-generated
         self.version = version
         self.last_modified = iso_8601_datetime_with_nanoseconds()
 
         self._set_function_code(self.code)
+        self.code_or_config_updated = False
 
         self.function_arn: str = make_function_arn(
             self.region, self.account_id, self.function_name
@@ -718,7 +717,9 @@ class LambdaFunction(CloudFormationModel, DockerModel):
         self._ephemeral_storage = ephemeral_storage
 
     @property
-    def vpc_config(self) -> Dict[str, Any]:  # type: ignore[misc]
+    def vpc_config(self) -> Optional[Dict[str, Any]]:  # type: ignore[misc]
+        if not self._vpc_config:
+            return None
         config = self._vpc_config.copy()
         if config["SecurityGroupIds"]:
             config.update({"VpcId": "vpc-123abc"})
@@ -794,7 +795,7 @@ class LambdaFunction(CloudFormationModel, DockerModel):
         resp = {"Configuration": self.get_configuration()}
         if "S3Key" in self.code:
             resp["Code"] = {
-                "Location": f"s3://awslambda-{self.region}-tasks.s3-{self.region}.amazonaws.com/{self.code['S3Key']}",
+                "Location": f"https://awslambda-{self.region}-tasks.s3.{self.region}.amazonaws.com/{self.code['S3Key']}",
                 "RepositoryType": "S3",
             }
         elif "ImageUri" in self.code:
@@ -838,6 +839,8 @@ class LambdaFunction(CloudFormationModel, DockerModel):
             elif key == "Layers":
                 self.layers = self._get_layers_data(value)
 
+        self.code_or_config_updated = True
+
         return self.get_configuration()
 
     def _set_function_code(self, updated_spec: Dict[str, Any]) -> None:
@@ -935,6 +938,7 @@ class LambdaFunction(CloudFormationModel, DockerModel):
 
     def update_function_code(self, updated_spec: Dict[str, Any]) -> Dict[str, Any]:
         self._set_function_code(updated_spec)
+        self.code_or_config_updated = True
         return self.get_configuration()
 
     @staticmethod
@@ -1688,9 +1692,11 @@ class LambdaStorage(object):
         all_versions = self._functions[name]["versions"]
         if all_versions:
             latest_published = all_versions[-1]
-            if latest_published.code_sha_256 == function.code_sha_256:
+            if not function.code_or_config_updated:
                 # Nothing has changed, don't publish
                 return latest_published
+            else:
+                function.code_or_config_updated = False
 
         new_version = len(all_versions) + 1
         fn = copy.copy(self._functions[name]["latest"])


Test patch:
diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/__init__.py
index 5e856146e..5dfd89f8a 100644
--- a/tests/test_awslambda/__init__.py
+++ b/tests/test_awslambda/__init__.py
@@ -25,7 +25,7 @@ def lambda_aws_verified(func):
     """
 
     @wraps(func)
-    def pagination_wrapper():
+    def pagination_wrapper(**kwargs):
         role_name = "moto_test_role_" + str(uuid4())[0:6]
 
         allow_aws_request = (
@@ -33,12 +33,12 @@ def lambda_aws_verified(func):
         )
 
         if allow_aws_request:
-            return create_role_and_test(role_name)
+            return create_role_and_test(kwargs, role_name)
         else:
             with mock_aws():
-                return create_role_and_test(role_name)
+                return create_role_and_test(kwargs, role_name)
 
-    def create_role_and_test(role_name):
+    def create_role_and_test(kwargs, role_name):
         from .test_lambda import _lambda_region
 
         policy_doc = {
@@ -63,7 +63,8 @@ def lambda_aws_verified(func):
             # Role is not immediately available
             check_iam_role_is_ready_to_use(iam_role_arn, role_name, _lambda_region)
 
-            resp = func(iam_role_arn)
+            kwargs["iam_role_arn"] = iam_role_arn
+            resp = func(**kwargs)
         finally:
             iam.delete_role(RoleName=role_name)
 
diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py
index e22c99584..8c60ae1c3 100644
--- a/tests/test_awslambda/test_lambda.py
+++ b/tests/test_awslambda/test_lambda.py
@@ -15,6 +15,7 @@ from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from moto.utilities.distutils_version import LooseVersion
 from tests.test_ecr.test_ecr_helpers import _create_image_manifest
+from tests.test_s3 import s3_aws_verified
 
 from . import lambda_aws_verified
 from .utilities import (
@@ -23,7 +24,6 @@ from .utilities import (
     get_role_name,
     get_test_zip_file1,
     get_test_zip_file2,
-    get_test_zip_file3,
 )
 
 PYTHON_VERSION = "python3.11"
@@ -245,7 +245,6 @@ def test_create_function_from_zipfile():
             "utf-8"
         ),
         "Version": "1",
-        "VpcConfig": {"SecurityGroupIds": [], "SubnetIds": []},
         "ResponseMetadata": {"HTTPStatusCode": 201},
         "State": "Active",
         "Layers": [],
@@ -582,25 +581,26 @@ def test_create_function_from_mocked_ecr_missing_image(
     )
 
 
-@mock_aws
-@freeze_time("2015-01-01 00:00:00")
-def test_get_function():
-    bucket_name = str(uuid4())
-    s3_conn = boto3.client("s3", _lambda_region)
-    s3_conn.create_bucket(
-        Bucket=bucket_name,
-        CreateBucketConfiguration={"LocationConstraint": _lambda_region},
-    )
+@pytest.mark.aws_verified
+@lambda_aws_verified
+@s3_aws_verified
+def test_get_function(iam_role_arn=None, bucket_name=None):
+    region = "us-east-1"
+
+    sts = boto3.client("sts", region)
+    account_id = sts.get_caller_identity()["Account"]
+
+    s3_conn = boto3.client("s3", region)
 
     zip_content = get_test_zip_file1()
     s3_conn.put_object(Bucket=bucket_name, Key="test.zip", Body=zip_content)
-    conn = boto3.client("lambda", _lambda_region)
+    conn = boto3.client("lambda", region)
     function_name = str(uuid4())[0:6]
 
     conn.create_function(
         FunctionName=function_name,
         Runtime=PYTHON_VERSION,
-        Role=get_role_name(),
+        Role=iam_role_arn,
         Handler="lambda_function.lambda_handler",
         Code={"S3Bucket": bucket_name, "S3Key": "test.zip"},
         Description="test lambda function",
@@ -611,16 +611,7 @@ def test_get_function():
     )
 
     result = conn.get_function(FunctionName=function_name)
-    # this is hard to match against, so remove it
-    result["ResponseMetadata"].pop("HTTPHeaders", None)
-    # Botocore inserts retry attempts not seen in Python27
-    result["ResponseMetadata"].pop("RetryAttempts", None)
-    result["Configuration"].pop("LastModified")
 
-    assert (
-        result["Code"]["Location"]
-        == f"s3://awslambda-{_lambda_region}-tasks.s3-{_lambda_region}.amazonaws.com/test.zip"
-    )
     assert result["Code"]["RepositoryType"] == "S3"
 
     assert result["Configuration"]["CodeSha256"] == base64.b64encode(
@@ -632,13 +623,11 @@ def test_get_function():
     assert result["Configuration"]["FunctionName"] == function_name
     assert result["Configuration"]["Handler"] == "lambda_function.lambda_handler"
     assert result["Configuration"]["MemorySize"] == 128
-    assert result["Configuration"]["Role"] == get_role_name()
+    assert result["Configuration"]["Role"] == iam_role_arn
     assert result["Configuration"]["Runtime"] == PYTHON_VERSION
     assert result["Configuration"]["Timeout"] == 3
     assert result["Configuration"]["Version"] == "$LATEST"
-    assert "VpcConfig" in result["Configuration"]
-    assert "Environment" in result["Configuration"]
-    assert "Variables" in result["Configuration"]["Environment"]
+    assert "VpcConfig" not in result["Configuration"]
     assert result["Configuration"]["Environment"]["Variables"] == {
         "test_variable": "test_value"
     }
@@ -648,7 +637,7 @@ def test_get_function():
     assert result["Configuration"]["Version"] == "$LATEST"
     assert (
         result["Configuration"]["FunctionArn"]
-        == f"arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:{function_name}:$LATEST"
+        == f"arn:aws:lambda:{region}:{account_id}:function:{function_name}:$LATEST"
     )
 
     # Test get function with version
@@ -656,7 +645,7 @@ def test_get_function():
     assert result["Configuration"]["Version"] == "1"
     assert (
         result["Configuration"]["FunctionArn"]
-        == f"arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:{function_name}:1"
+        == f"arn:aws:lambda:{region}:{account_id}:function:{function_name}:1"
     )
 
     # Test get function with version inside of name
@@ -664,9 +653,16 @@ def test_get_function():
     assert result["Configuration"]["Version"] == "1"
     assert (
         result["Configuration"]["FunctionArn"]
-        == f"arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:{function_name}:1"
+        == f"arn:aws:lambda:{region}:{account_id}:function:{function_name}:1"
     )
 
+    conn.delete_function(FunctionName=function_name)
+
+
+@mock_aws
+def test_get_unknown_function():
+    conn = boto3.client("lambda", _lambda_region)
+
     # Test get function when can't find function name
     with pytest.raises(conn.exceptions.ResourceNotFoundException):
         conn.get_function(FunctionName="junk", Qualifier="$LATEST")
@@ -717,9 +713,6 @@ def test_get_function_configuration(key):
     assert result["Runtime"] == PYTHON_VERSION
     assert result["Timeout"] == 3
     assert result["Version"] == "$LATEST"
-    assert "VpcConfig" in result
-    assert "Environment" in result
-    assert "Variables" in result["Environment"]
     assert result["Environment"]["Variables"] == {"test_variable": "test_value"}
 
     # Test get function with qualifier
@@ -1002,7 +995,7 @@ def test_list_create_list_get_delete_list():
     )
     expected_function_result = {
         "Code": {
-            "Location": f"s3://awslambda-{_lambda_region}-tasks.s3-{_lambda_region}.amazonaws.com/test.zip",
+            "Location": f"https://awslambda-{_lambda_region}-tasks.s3.{_lambda_region}.amazonaws.com/test.zip",
             "RepositoryType": "S3",
         },
         "Configuration": {
@@ -1019,7 +1012,6 @@ def test_list_create_list_get_delete_list():
             "Runtime": PYTHON_VERSION,
             "Timeout": 3,
             "Version": "$LATEST",
-            "VpcConfig": {"SecurityGroupIds": [], "SubnetIds": []},
             "State": "Active",
             "Layers": [],
             "LastUpdateStatus": "Successful",
@@ -1105,7 +1097,7 @@ def test_get_function_created_with_zipfile():
     assert len(response["Code"]) == 2
     assert response["Code"]["RepositoryType"] == "S3"
     assert response["Code"]["Location"].startswith(
-        f"s3://awslambda-{_lambda_region}-tasks.s3-{_lambda_region}.amazonaws.com"
+        f"https://awslambda-{_lambda_region}-tasks.s3.{_lambda_region}.amazonaws.com"
     )
     assert "Configuration" in response
     config = response["Configuration"]
@@ -1344,25 +1336,23 @@ def test_list_versions_by_function_for_nonexistent_function():
     assert len(versions["Versions"]) == 0
 
 
+@pytest.mark.aws_verified
 @pytest.mark.parametrize("key", ["FunctionName", "FunctionArn"])
-@mock_aws
-def test_update_configuration(key):
-    bucket_name = str(uuid4())
+@lambda_aws_verified
+@s3_aws_verified
+def test_update_configuration(key, bucket_name=None, iam_role_arn=None):
+    region = "us-east-1"
     function_name = str(uuid4())[0:6]
-    s3_conn = boto3.client("s3", _lambda_region)
-    s3_conn.create_bucket(
-        Bucket=bucket_name,
-        CreateBucketConfiguration={"LocationConstraint": _lambda_region},
-    )
+    s3_conn = boto3.client("s3", region)
 
     zip_content = get_test_zip_file2()
     s3_conn.put_object(Bucket=bucket_name, Key="test.zip", Body=zip_content)
-    conn = boto3.client("lambda", _lambda_region)
+    conn = boto3.client("lambda", region)
 
     fxn = conn.create_function(
         FunctionName=function_name,
         Runtime=PYTHON_VERSION,
-        Role=get_role_name(),
+        Role=iam_role_arn,
         Handler="lambda_function.lambda_handler",
         Code={"S3Bucket": bucket_name, "S3Key": "test.zip"},
         Description="test lambda function",
@@ -1373,6 +1363,9 @@ def test_update_configuration(key):
     )
     name_or_arn = fxn[key]
 
+    wait_for_func = conn.get_waiter("function_active")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
     assert fxn["Description"] == "test lambda function"
     assert fxn["Handler"] == "lambda_function.lambda_handler"
     assert fxn["MemorySize"] == 128
@@ -1383,31 +1376,37 @@ def test_update_configuration(key):
         FunctionName=name_or_arn,
         Description="updated test lambda function",
         Handler="lambda_function.new_lambda_handler",
-        Runtime="python3.6",
+        Runtime="python3.12",
         Timeout=7,
-        VpcConfig={"SecurityGroupIds": ["sg-123abc"], "SubnetIds": ["subnet-123abc"]},
         Environment={"Variables": {"test_environment": "test_value"}},
     )
 
+    wait_for_func = conn.get_waiter("function_updated")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
     assert updated_config["ResponseMetadata"]["HTTPStatusCode"] == 200
     assert updated_config["Description"] == "updated test lambda function"
     assert updated_config["Handler"] == "lambda_function.new_lambda_handler"
     assert updated_config["MemorySize"] == 128
-    assert updated_config["Runtime"] == "python3.6"
+    assert updated_config["Runtime"] == "python3.12"
     assert updated_config["Timeout"] == 7
     assert updated_config["Environment"]["Variables"] == {
         "test_environment": "test_value"
     }
-    assert updated_config["VpcConfig"] == {
-        "SecurityGroupIds": ["sg-123abc"],
-        "SubnetIds": ["subnet-123abc"],
-        "VpcId": "vpc-123abc",
-    }
 
+    # publish - results in a new version because the config is updated
+    assert conn.publish_version(FunctionName=name_or_arn)["Version"] == "2"
 
+    # publish again - no changes
+    assert conn.publish_version(FunctionName=name_or_arn)["Version"] == "2"
+
+    conn.delete_function(FunctionName=function_name)
+
+
+@pytest.mark.aws_verified
 @pytest.mark.parametrize("key", ["FunctionName", "FunctionArn"])
-@mock_aws
-def test_update_function_zip(key):
+@lambda_aws_verified
+def test_update_function_zip(key, iam_role_arn=None):
     conn = boto3.client("lambda", _lambda_region)
 
     zip_content_one = get_test_zip_file1()
@@ -1416,7 +1415,7 @@ def test_update_function_zip(key):
     fxn = conn.create_function(
         FunctionName=function_name,
         Runtime=PYTHON_VERSION,
-        Role=get_role_name(),
+        Role=iam_role_arn,
         Handler="lambda_function.lambda_handler",
         Code={"ZipFile": zip_content_one},
         Description="test lambda function",
@@ -1425,6 +1424,10 @@ def test_update_function_zip(key):
         Publish=True,
     )
     name_or_arn = fxn[key]
+
+    wait_for_func = conn.get_waiter("function_active")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
     first_sha = fxn["CodeSha256"]
 
     zip_content_two = get_test_zip_file2()
@@ -1434,49 +1437,61 @@ def test_update_function_zip(key):
     )
     assert update1["CodeSha256"] != first_sha
 
+    wait_for_func = conn.get_waiter("function_updated")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
     response = conn.get_function(FunctionName=function_name, Qualifier="2")
 
-    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
-    assert len(response["Code"]) == 2
     assert response["Code"]["RepositoryType"] == "S3"
     assert response["Code"]["Location"].startswith(
-        f"s3://awslambda-{_lambda_region}-tasks.s3-{_lambda_region}.amazonaws.com"
+        f"https://awslambda-{_lambda_region}-tasks.s3.{_lambda_region}.amazonaws.com"
     )
 
     config = response["Configuration"]
     assert config["CodeSize"] == len(zip_content_two)
-    assert config["Description"] == "test lambda function"
-    assert (
-        config["FunctionArn"]
-        == f"arn:aws:lambda:{_lambda_region}:{ACCOUNT_ID}:function:{function_name}:2"
-    )
+    assert config["FunctionArn"].endswith(f"{function_name}:2")
     assert config["FunctionName"] == function_name
     assert config["Version"] == "2"
-    assert config["LastUpdateStatus"] == "Successful"
     assert config["CodeSha256"] == update1["CodeSha256"]
 
     most_recent_config = conn.get_function(FunctionName=function_name)
     assert most_recent_config["Configuration"]["CodeSha256"] == update1["CodeSha256"]
 
-    # Publishing this again, with the same code, gives us the same version
+    # Update the same code, and manually publish
+    conn.update_function_code(FunctionName=name_or_arn, ZipFile=zip_content_two)
+    wait_for_func = conn.get_waiter("function_updated")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
+    resp = conn.publish_version(FunctionName=name_or_arn)
+    # The code is the same, but the version is still updated
+    assert resp["CodeSha256"] == update1["CodeSha256"]
+    assert resp["Version"] == "3"
+
+    # Calling publish again
+    resp = conn.publish_version(FunctionName=name_or_arn)
+    # Code is the same, as is the version - just because we didn't call 'update_function_code'
+    assert resp["CodeSha256"] == update1["CodeSha256"]
+    assert resp["Version"] == "3"
+
+    # Explicitly publishing this again, with the same code, gives us a new version
     same_update = conn.update_function_code(
         FunctionName=name_or_arn, ZipFile=zip_content_two, Publish=True
     )
-    assert (
-        same_update["FunctionArn"]
-        == most_recent_config["Configuration"]["FunctionArn"] + ":2"
-    )
-    assert same_update["Version"] == "2"
+    assert same_update["FunctionArn"].endswith(f":{function_name}:4")
+    assert same_update["Version"] == "4"
 
-    # 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
-    )
-    assert (
-        new_update["FunctionArn"]
-        == most_recent_config["Configuration"]["FunctionArn"] + ":3"
-    )
-    assert new_update["Version"] == "3"
+    wait_for_func = conn.get_waiter("function_updated")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
+    resp = conn.publish_version(FunctionName=name_or_arn)
+    # The code is the same, so is the version, as the previous update was already published
+    assert resp["CodeSha256"] == update1["CodeSha256"]
+    assert resp["Version"] == "4"
+
+    wait_for_func = conn.get_waiter("function_updated")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
+    conn.delete_function(FunctionName=function_name)
 
 
 @mock_aws
@@ -1522,7 +1537,7 @@ def test_update_function_s3():
     assert len(response["Code"]) == 2
     assert response["Code"]["RepositoryType"] == "S3"
     assert response["Code"]["Location"].startswith(
-        f"s3://awslambda-{_lambda_region}-tasks.s3-{_lambda_region}.amazonaws.com"
+        f"https://awslambda-{_lambda_region}-tasks.s3.{_lambda_region}.amazonaws.com"
     )
 
     config = response["Configuration"]