"""#471 — FunctionConfiguration fields compatibility tests. Verifies that CreateFunction and UpdateFunctionConfiguration accept and round-trip Architectures, EphemeralStorage, TracingConfig, DeadLetterConfig, Environment, CodeSha256, and LastModified via the AWS SDK for Python (boto3). """ import re import pytest from botocore.exceptions import ClientError ROLE = "arn:aws:iam::000000000000:role/lambda-role" # ISO-8601 pattern AWS Lambda returns: 2024-01-15T10:30:00.000+0000 _LAST_MODIFIED_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{4}$") class TestFunctionConfigurationDefaults: """createFunction response includes all required fields with correct defaults.""" def test_create_function_has_code_sha256( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: resp = lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", Code={"ZipFile": minimal_lambda_zip}, ) assert resp.get("CodeSha256"), "CodeSha256 must be a non-empty string" finally: lambda_client.delete_function(FunctionName=fn) def test_create_function_has_iso8601_last_modified( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: resp = lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", Code={"ZipFile": minimal_lambda_zip}, ) last_modified = resp.get("LastModified", "") assert _LAST_MODIFIED_RE.match(last_modified), ( f"LastModified must match yyyy-MM-dd'T'HH:mm:ss.SSSZ, got: {last_modified!r}" ) finally: lambda_client.delete_function(FunctionName=fn) def test_get_function_configuration_default_architectures( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", Code={"ZipFile": minimal_lambda_zip}, ) resp = lambda_client.get_function_configuration(FunctionName=fn) assert resp.get("Architectures") == ["x86_64"], ( f"default Architectures must be ['x86_64'], got: {resp.get('Architectures')}" ) finally: lambda_client.delete_function(FunctionName=fn) def test_get_function_configuration_default_ephemeral_storage( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", Code={"ZipFile": minimal_lambda_zip}, ) resp = lambda_client.get_function_configuration(FunctionName=fn) ephemeral = resp.get("EphemeralStorage") assert ephemeral is not None, "EphemeralStorage must always be present" assert ephemeral.get("Size") == 512, ( f"default EphemeralStorage.Size must be 512, got: {ephemeral.get('Size')}" ) finally: lambda_client.delete_function(FunctionName=fn) def test_get_function_configuration_default_tracing_config( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", Code={"ZipFile": minimal_lambda_zip}, ) resp = lambda_client.get_function_configuration(FunctionName=fn) tracing = resp.get("TracingConfig") assert tracing is not None, "TracingConfig must always be present" assert tracing.get("Mode") == "PassThrough", ( f"default TracingConfig.Mode must be PassThrough, got: {tracing.get('Mode')}" ) finally: lambda_client.delete_function(FunctionName=fn) def test_get_function_configuration_omits_environment_when_unset( self, lambda_client, minimal_lambda_zip, unique_name ): """AWS omits Environment entirely for a function with no variables. Verified against lambda.us-east-1.amazonaws.com and lambda.ap-southeast-1.amazonaws.com: a function created without --environment comes back with no Environment key at all, the same way Layers and VpcConfig are omitted rather than returned empty. """ fn = f"pytest-cfg-{unique_name}" try: lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", Code={"ZipFile": minimal_lambda_zip}, ) resp = lambda_client.get_function_configuration(FunctionName=fn) assert "Environment" not in resp, ( "Environment must be omitted when no variables are set; an empty " "block gives Terraform a permanent `- environment {}` diff" ) finally: lambda_client.delete_function(FunctionName=fn) class TestCreateFunctionWithNewFields: """createFunction accepts and persists new fields.""" def test_create_function_with_arm64_architecture( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: resp = lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", Architectures=["arm64"], Code={"ZipFile": minimal_lambda_zip}, ) assert resp.get("Architectures") == ["arm64"], ( f"Architectures must be ['arm64'], got: {resp.get('Architectures')}" ) # Verify it persists cfg = lambda_client.get_function_configuration(FunctionName=fn) assert cfg.get("Architectures") == ["arm64"] finally: lambda_client.delete_function(FunctionName=fn) def test_create_function_with_ephemeral_storage( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: resp = lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", EphemeralStorage={"Size": 2048}, Code={"ZipFile": minimal_lambda_zip}, ) assert resp.get("EphemeralStorage", {}).get("Size") == 2048 finally: lambda_client.delete_function(FunctionName=fn) def test_create_function_with_tracing_config( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: resp = lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", TracingConfig={"Mode": "Active"}, Code={"ZipFile": minimal_lambda_zip}, ) assert resp.get("TracingConfig", {}).get("Mode") == "Active" finally: lambda_client.delete_function(FunctionName=fn) class TestUpdateFunctionConfiguration: """updateFunctionConfiguration round-trips new fields.""" def test_update_ephemeral_storage_and_tracing( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", Code={"ZipFile": minimal_lambda_zip}, ) resp = lambda_client.update_function_configuration( FunctionName=fn, Timeout=60, EphemeralStorage={"Size": 1024}, TracingConfig={"Mode": "Active"}, ) assert resp.get("Timeout") == 60 assert resp.get("EphemeralStorage", {}).get("Size") == 1024 assert resp.get("TracingConfig", {}).get("Mode") == "Active" # Verify persistence cfg = lambda_client.get_function_configuration(FunctionName=fn) assert cfg.get("EphemeralStorage", {}).get("Size") == 1024 assert cfg.get("TracingConfig", {}).get("Mode") == "Active" finally: lambda_client.delete_function(FunctionName=fn) def test_update_environment_variables_round_trip( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", Code={"ZipFile": minimal_lambda_zip}, ) resp = lambda_client.update_function_configuration( FunctionName=fn, Environment={"Variables": {"KEY_A": "value-a", "KEY_B": "value-b"}}, ) variables = resp.get("Environment", {}).get("Variables", {}) assert variables.get("KEY_A") == "value-a" assert variables.get("KEY_B") == "value-b" # Clear environment — AWS drops the block again. Measured live: # update_function_configuration(Environment={"Variables": {}}) returns # no Environment key, and the following get_function_configuration # agrees, so a cleared function is indistinguishable from one that # never had variables. cleared = lambda_client.update_function_configuration( FunctionName=fn, Environment={"Variables": {}}, ) assert "Environment" not in cleared, ( "Environment must be omitted once variables are cleared" ) finally: lambda_client.delete_function(FunctionName=fn) def test_stale_revision_id_returns_412( self, lambda_client, minimal_lambda_zip, unique_name ): fn = f"pytest-cfg-{unique_name}" try: lambda_client.create_function( FunctionName=fn, Runtime="nodejs20.x", Role=ROLE, Handler="index.handler", Code={"ZipFile": minimal_lambda_zip}, ) with pytest.raises(ClientError) as exc_info: lambda_client.update_function_configuration( FunctionName=fn, Timeout=10, RevisionId="00000000-0000-0000-0000-000000000000", ) assert exc_info.value.response["ResponseMetadata"]["HTTPStatusCode"] == 412, ( "Stale RevisionId must return 412 PreconditionFailedException" ) finally: lambda_client.delete_function(FunctionName=fn) class TestImageConfigWorkingDirectory: """ImageConfig.WorkingDirectory is persisted and returned correctly.""" IMAGE_URI = "000000000000.dkr.ecr.us-east-1.amazonaws.com/fake-repo:latest" def test_create_image_function_with_working_directory( self, lambda_client, unique_name ): fn = f"pytest-imgwd-{unique_name}" try: resp = lambda_client.create_function( FunctionName=fn, PackageType="Image", Role=ROLE, Code={"ImageUri": self.IMAGE_URI}, ImageConfig={"WorkingDirectory": "/app"}, ) wd = ( resp.get("ImageConfigResponse", {}) .get("ImageConfig", {}) .get("WorkingDirectory") ) assert wd == "/app", ( f"CreateFunction response must include ImageConfig.WorkingDirectory='/app', got: {wd!r}" ) finally: lambda_client.delete_function(FunctionName=fn) def test_get_function_configuration_persists_working_directory( self, lambda_client, unique_name ): fn = f"pytest-imgwd-get-{unique_name}" try: lambda_client.create_function( FunctionName=fn, PackageType="Image", Role=ROLE, Code={"ImageUri": self.IMAGE_URI}, ImageConfig={"WorkingDirectory": "/workspace"}, ) resp = lambda_client.get_function_configuration(FunctionName=fn) wd = ( resp.get("ImageConfigResponse", {}) .get("ImageConfig", {}) .get("WorkingDirectory") ) assert wd == "/workspace", ( f"GetFunctionConfiguration must persist WorkingDirectory='/workspace', got: {wd!r}" ) finally: lambda_client.delete_function(FunctionName=fn) def test_update_function_configuration_updates_working_directory( self, lambda_client, unique_name ): fn = f"pytest-imgwd-upd-{unique_name}" try: lambda_client.create_function( FunctionName=fn, PackageType="Image", Role=ROLE, Code={"ImageUri": self.IMAGE_URI}, ImageConfig={"WorkingDirectory": "/initial"}, ) resp = lambda_client.update_function_configuration( FunctionName=fn, ImageConfig={"WorkingDirectory": "/updated"}, ) wd = ( resp.get("ImageConfigResponse", {}) .get("ImageConfig", {}) .get("WorkingDirectory") ) assert wd == "/updated", ( f"UpdateFunctionConfiguration must update WorkingDirectory to '/updated', got: {wd!r}" ) finally: lambda_client.delete_function(FunctionName=fn)