feat(cloudfront): serve POST, PUT, PATCH and DELETE viewer requests (#4229) #4

Merged
Zakaria merged 6 commits from feat/cloudfront-upstream-fixes into main 2026-10-24 18:38:32 +00:00
3 changed files with 90 additions and 0 deletions
Showing only changes of commit 13e91cbb5a - Show all commits
@@ -59,6 +59,7 @@ public class CloudFrontService {
Set.of("s3", "mediastore", "mediapackagev2", "lambda"); Set.of("s3", "mediastore", "mediapackagev2", "lambda");
private static final Set<String> OAC_SIGNING_BEHAVIORS = private static final Set<String> OAC_SIGNING_BEHAVIORS =
Set.of("always", "never", "no-override"); Set.of("always", "never", "no-override");
private static final Set<String> 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_ORIGIN_CUSTOM_HEADERS = 30;
private static final int MAX_CUSTOM_HEADER_NAME_LENGTH = 256; private static final int MAX_CUSTOM_HEADER_NAME_LENGTH = 256;
private static final int MAX_CUSTOM_HEADER_VALUE_LENGTH = 1_783; private static final int MAX_CUSTOM_HEADER_VALUE_LENGTH = 1_783;
@@ -1227,6 +1228,7 @@ public class CloudFrontService {
// ── CloudFront Functions ────────────────────────────────────────────────── // ── CloudFront Functions ──────────────────────────────────────────────────
public synchronized CloudFrontFunction createFunction(CloudFrontFunction fn) { public synchronized CloudFrontFunction createFunction(CloudFrontFunction fn) {
validateFunctionRuntime(fn.getRuntime());
fn.setStage("DEVELOPMENT"); fn.setStage("DEVELOPMENT");
fn.setStatus("UNPUBLISHED"); fn.setStatus("UNPUBLISHED");
fn.setEtag(UUID.randomUUID().toString()); fn.setEtag(UUID.randomUUID().toString());
@@ -1251,6 +1253,7 @@ public class CloudFrontService {
public synchronized CloudFrontFunction updateFunction(String name, String ifMatch, public synchronized CloudFrontFunction updateFunction(String name, String ifMatch,
CloudFrontFunction updated) { CloudFrontFunction updated) {
validateFunctionRuntime(updated.getRuntime());
CloudFrontFunction existing = describeFunction(name, null); CloudFrontFunction existing = describeFunction(name, null);
if (!existing.getEtag().equals(ifMatch)) { if (!existing.getEtag().equals(ifMatch)) {
throw new AwsException("InvalidIfMatchVersion", throw new AwsException("InvalidIfMatchVersion",
@@ -1323,6 +1326,13 @@ public class CloudFrontService {
return effectiveStage; 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) { private static AwsException invalidFunctionStage(String stage) {
return new AwsException("InvalidArgument", return new AwsException("InvalidArgument",
"The parameter Stage must be DEVELOPMENT or LIVE: " + stage, 400); "The parameter Stage must be DEVELOPMENT or LIVE: " + stage, 400);
@@ -109,6 +109,38 @@ class CloudFrontManagementProtocolTest {
equalTo("InvalidArgument"))); equalTo("InvalidArgument")));
} }
@Test
void createFunctionRejectsAnUnknownRuntime() {
String name = "runtime-test-" + UUID.randomUUID();
String createBody = """
<CreateFunctionRequest xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
<Name>%s</Name>
<FunctionConfig>
<Comment>routing function</Comment>
<Runtime>python3</Runtime>
</FunctionConfig>
<FunctionCode>function handler(event) { return event.request; }</FunctionCode>
</CreateFunctionRequest>
""".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 @Test
void describeAndPublishFunctionPreserveBothStages() { void describeAndPublishFunctionPreserveBothStages() {
String name = "describe-test-" + UUID.randomUUID(); String name = "describe-test-" + UUID.randomUUID();
@@ -164,6 +164,54 @@ class CloudFrontServiceTest {
assertNotEquals(development.getEtag(), live.getEtag()); 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 @Test
void listFunctionsWithoutStageReturnsDevelopmentAndLive() { void listFunctionsWithoutStageReturnsDevelopmentAndLive() {
CloudFrontService service = serviceWithDomainSuffix("cloudfront.net"); CloudFrontService service = serviceWithDomainSuffix("cloudfront.net");