diff --git a/src/main/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontService.java b/src/main/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontService.java index 74828ee..1d1176d 100644 --- a/src/main/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontService.java +++ b/src/main/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontService.java @@ -59,6 +59,7 @@ public class CloudFrontService { Set.of("s3", "mediastore", "mediapackagev2", "lambda"); private static final Set OAC_SIGNING_BEHAVIORS = Set.of("always", "never", "no-override"); + private static final Set FUNCTION_RUNTIMES = Set.of("cloudfront-js-1.0", "cloudfront-js-2.0"); private static final int MAX_ORIGIN_CUSTOM_HEADERS = 30; private static final int MAX_CUSTOM_HEADER_NAME_LENGTH = 256; private static final int MAX_CUSTOM_HEADER_VALUE_LENGTH = 1_783; @@ -1227,6 +1228,7 @@ public class CloudFrontService { // ── CloudFront Functions ────────────────────────────────────────────────── public synchronized CloudFrontFunction createFunction(CloudFrontFunction fn) { + validateFunctionRuntime(fn.getRuntime()); fn.setStage("DEVELOPMENT"); fn.setStatus("UNPUBLISHED"); fn.setEtag(UUID.randomUUID().toString()); @@ -1251,6 +1253,7 @@ public class CloudFrontService { public synchronized CloudFrontFunction updateFunction(String name, String ifMatch, CloudFrontFunction updated) { + validateFunctionRuntime(updated.getRuntime()); CloudFrontFunction existing = describeFunction(name, null); if (!existing.getEtag().equals(ifMatch)) { throw new AwsException("InvalidIfMatchVersion", @@ -1323,6 +1326,13 @@ public class CloudFrontService { return effectiveStage; } + private static void validateFunctionRuntime(String runtime) { + if (runtime != null && !FUNCTION_RUNTIMES.contains(runtime)) { + throw new AwsException("InvalidArgument", + "The parameter Runtime must be cloudfront-js-1.0 or cloudfront-js-2.0.", 400); + } + } + private static AwsException invalidFunctionStage(String stage) { return new AwsException("InvalidArgument", "The parameter Stage must be DEVELOPMENT or LIVE: " + stage, 400); diff --git a/src/test/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontManagementProtocolTest.java b/src/test/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontManagementProtocolTest.java index 0331f40..425589e 100644 --- a/src/test/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontManagementProtocolTest.java +++ b/src/test/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontManagementProtocolTest.java @@ -109,6 +109,38 @@ class CloudFrontManagementProtocolTest { equalTo("InvalidArgument"))); } + @Test + void createFunctionRejectsAnUnknownRuntime() { + String name = "runtime-test-" + UUID.randomUUID(); + String createBody = """ + + %s + + routing function + python3 + + function handler(event) { return event.request; } + + """.formatted(name); + + given() + .contentType("application/xml") + .body(createBody) + .when() + .post(API + "function") + .then() + .statusCode(400) + .body(hasXPath( + "//*[local-name()='Code']/text()", + equalTo("InvalidArgument"))); + + given() + .when() + .get(API + "function/" + name + "/describe") + .then() + .statusCode(404); + } + @Test void describeAndPublishFunctionPreserveBothStages() { String name = "describe-test-" + UUID.randomUUID(); diff --git a/src/test/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontServiceTest.java b/src/test/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontServiceTest.java index 45b52e6..9b57733 100644 --- a/src/test/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontServiceTest.java +++ b/src/test/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontServiceTest.java @@ -164,6 +164,54 @@ class CloudFrontServiceTest { assertNotEquals(development.getEtag(), live.getEtag()); } + @Test + void createFunction_unknownRuntime_isRejected() { + CloudFrontService service = serviceWithDomainSuffix("cloudfront.net"); + CloudFrontFunction function = new CloudFrontFunction(); + function.setName("routing"); + function.setRuntime("python3"); + + AwsException error = assertThrows(AwsException.class, () -> service.createFunction(function)); + + assertEquals("InvalidArgument", error.getErrorCode()); + assertEquals(400, error.getHttpStatus()); + assertThrows(AwsException.class, () -> service.describeFunction("routing", null)); + } + + @Test + void createFunction_supportedRuntimes_areAccepted() { + CloudFrontService service = serviceWithDomainSuffix("cloudfront.net"); + for (String runtime : List.of("cloudfront-js-1.0", "cloudfront-js-2.0")) { + CloudFrontFunction function = new CloudFrontFunction(); + function.setName("routing-" + runtime); + function.setRuntime(runtime); + + assertEquals(runtime, service.createFunction(function).getRuntime()); + } + } + + @Test + void updateFunction_unknownRuntime_isRejectedAndKeepsTheFunction() { + CloudFrontService service = serviceWithDomainSuffix("cloudfront.net"); + CloudFrontFunction function = new CloudFrontFunction(); + function.setName("routing"); + function.setFunctionCode("first"); + function.setRuntime("cloudfront-js-2.0"); + String etag = service.createFunction(function).getEtag(); + CloudFrontFunction update = new CloudFrontFunction(); + update.setFunctionCode("second"); + update.setRuntime("python3"); + + AwsException error = assertThrows(AwsException.class, + () -> service.updateFunction("routing", etag, update)); + + assertEquals("InvalidArgument", error.getErrorCode()); + CloudFrontFunction kept = service.describeFunction("routing", null); + assertEquals("first", kept.getFunctionCode()); + assertEquals("cloudfront-js-2.0", kept.getRuntime()); + assertEquals(etag, kept.getEtag()); + } + @Test void listFunctionsWithoutStageReturnsDevelopmentAndLive() { CloudFrontService service = serviceWithDomainSuffix("cloudfront.net");