backend services patching

This commit is contained in:
Zakaria
2026-09-30 17:46:10 -04:00
parent cede01969e
commit 23d24288cd
49 changed files with 1940 additions and 106 deletions
@@ -4,8 +4,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.github.hectorvent.floci.core.common.AwsException;
import io.github.hectorvent.floci.core.common.RegionResolver;
import io.github.hectorvent.floci.services.apigateway.model.ApiGatewayResource;
import io.github.hectorvent.floci.services.apigateway.model.Integration;
import io.github.hectorvent.floci.services.apigateway.model.MethodConfig;
import io.github.hectorvent.floci.services.apigatewayv2.ApiGatewayV2Service;
import io.github.hectorvent.floci.services.apigatewayv2.model.Api;
import io.github.hectorvent.floci.services.lambda.LambdaService;
import io.github.hectorvent.floci.services.lambda.model.InvocationType;
import io.github.hectorvent.floci.services.lambda.model.InvokeResult;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedHashMap;
@@ -22,6 +27,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -255,6 +262,85 @@ class ApiGatewayExecuteControllerTest {
assertEquals("application/problem+json", response.getMediaType().toString());
}
@Test
void buildProxyResponseRejectsBareLambdaPayload() {
ApiGatewayExecuteController controller = controller(new ObjectMapper());
Response response = controller.buildProxyResponse(proxyPayload("\"Hello from Lambda\""), false);
assertEquals(502, response.getStatus());
}
@Test
void testInvokeProxyLogsSentEventAndDoesNotRequireStage() throws Exception {
ApiGatewayService apiGatewayService = mock(ApiGatewayService.class);
LambdaService lambdaService = mock(LambdaService.class);
RegionResolver regionResolver = mock(RegionResolver.class);
when(regionResolver.getAccountId()).thenReturn("000000000000");
ApiGatewayExecuteController controller = new ApiGatewayExecuteController(apiGatewayService, null, lambdaService,
regionResolver, new ObjectMapper(), null, null, null, null, null,
new ApiGatewayExecuteRouteContext(), null, null, null);
ApiGatewayResource resource = new ApiGatewayResource();
resource.setId("resource-id");
resource.setPath("/orders");
MethodConfig method = new MethodConfig();
Integration integration = new Integration();
integration.setType("AWS_PROXY");
integration.setUri("arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/"
+ "arn:aws:lambda:us-east-1:000000000000:function:orders/invocations");
method.setMethodIntegration(integration);
when(apiGatewayService.getResource("us-east-1", "api-id", "resource-id")).thenReturn(resource);
when(apiGatewayService.getMethod("us-east-1", "api-id", "resource-id", "POST")).thenReturn(method);
when(lambdaService.invoke(eq("us-east-1"), eq("orders"), any(),
eq(InvocationType.RequestResponse)))
.thenReturn(proxyPayload("{\"statusCode\":201,\"body\":\"created\",\"headers\":{\"X-One\":\"one\"},"
+ "\"multiValueHeaders\":{\"Set-Cookie\":[\"a=1\",\"b=2\"]}}"));
ApiGatewayExecuteController.TestInvokeResult result = controller.testInvoke("us-east-1", "api-id",
"resource-id", "POST", new ApiGatewayExecuteController.TestInvokeRequest("/orders?q=one&q=two",
"request-body", Map.of("X-Input", "input"), Map.of("X-Multi", List.of("one", "two")),
Map.of("environment", "test")));
assertEquals(201, result.response().getStatus());
assertTrue(result.log().contains("Endpoint request URI: " + integration.getUri()));
assertTrue(result.log().contains("\"body\":\"request-body\""));
assertTrue(result.log().contains("\"environment\":\"test\""));
assertEquals(List.of("a=1", "b=2"), result.response().getStringHeaders().get("Set-Cookie"));
}
@Test
void testInvokeProxyRejectsMalformedLambdaResponse() {
ApiGatewayService apiGatewayService = mock(ApiGatewayService.class);
LambdaService lambdaService = mock(LambdaService.class);
RegionResolver regionResolver = mock(RegionResolver.class);
when(regionResolver.getAccountId()).thenReturn("000000000000");
ApiGatewayExecuteController controller = new ApiGatewayExecuteController(apiGatewayService, null, lambdaService,
regionResolver, new ObjectMapper(), null, null, null, null, null,
new ApiGatewayExecuteRouteContext(), null, null, null);
ApiGatewayResource resource = new ApiGatewayResource();
resource.setId("resource-id");
resource.setPath("/");
MethodConfig method = new MethodConfig();
Integration integration = new Integration();
integration.setType("AWS_PROXY");
integration.setUri("arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/"
+ "arn:aws:lambda:us-east-1:000000000000:function:orders/invocations");
method.setMethodIntegration(integration);
when(apiGatewayService.getResource("us-east-1", "api-id", "resource-id")).thenReturn(resource);
when(apiGatewayService.getMethod("us-east-1", "api-id", "resource-id", "GET")).thenReturn(method);
when(lambdaService.invoke(eq("us-east-1"), eq("orders"), any(),
eq(InvocationType.RequestResponse)))
.thenReturn(proxyPayload("[]"));
ApiGatewayExecuteController.TestInvokeResult result = controller.testInvoke("us-east-1", "api-id",
"resource-id", "GET", new ApiGatewayExecuteController.TestInvokeRequest(null, null, Map.of(), Map.of(), Map.of()));
assertEquals(500, result.response().getStatus());
assertEquals("Execution failed due to configuration error: Malformed Lambda proxy response",
result.response().getEntity());
assertTrue(result.log().contains("Execution failed due to configuration error: Malformed Lambda proxy response"));
}
@Test
void buildProxyResponseMatchesExactCaseContentType() {
// The pre-existing exact-case path must keep working — this is a regression guard,
@@ -117,6 +117,17 @@ class ApiGatewayIntegrationTest {
.body("authorizationType", equalTo("NONE"));
}
@Test @Order(9)
void getResourcesEmbedsMethods() {
given()
.queryParam("embed", "methods")
.when().get("/restapis/" + apiId + "/resources")
.then()
.statusCode(200)
.body("item.find { it.id == '" + resourceId + "' }.resourceMethods.GET.httpMethod", equalTo("GET"))
.body("item.find { it.id == '" + rootId + "' }.resourceMethods", nullValue());
}
@Test @Order(9)
void putMethodResponse() {
given()
@@ -0,0 +1,54 @@
package io.github.hectorvent.floci.services.apigateway;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
@QuarkusTest
class ApiGatewayTestInvokeMethodIntegrationTest {
@Test
void invokesMockMethodWithoutDeploymentOrStage() {
String apiId = given()
.contentType(ContentType.JSON)
.body("{\"name\":\"test-invoke-method\"}")
.post("/restapis")
.then().statusCode(201)
.extract().path("id");
String resourceId = given()
.get("/restapis/" + apiId + "/resources")
.then().statusCode(200)
.extract().path("item[0].id");
given()
.contentType(ContentType.JSON)
.body("{\"authorizationType\":\"NONE\"}")
.put("/restapis/" + apiId + "/resources/" + resourceId + "/methods/POST")
.then().statusCode(201);
given()
.contentType(ContentType.JSON)
.body("{\"type\":\"MOCK\",\"httpMethod\":\"POST\"}")
.put("/restapis/" + apiId + "/resources/" + resourceId + "/methods/POST/integration")
.then().statusCode(201);
given()
.contentType(ContentType.JSON)
.body("""
{
"pathWithQueryString": "/?q=one&q=two",
"body": "request-body",
"headers": {"X-Input": "input"},
"multiValueHeaders": {"X-Multi": ["one", "two"]},
"stageVariables": {"environment": "test"}
}
""")
.post("/restapis/" + apiId + "/resources/" + resourceId + "/methods/POST")
.then().statusCode(200)
.body("status", equalTo(200))
.body("log", containsString("Endpoint request URI: null"));
}
}
@@ -5,19 +5,44 @@ import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
@QuarkusTest
class ApiGatewayUpdateIntegrationResponseIntegrationTest {
@Test
void testUpdateIntegrationResponse() {
void updateIntegrationResponsePatchesAllCorsFields() {
String apiId = given().contentType("application/json").body("{\"name\":\"ir-update\"}").when().post("/restapis").then().statusCode(201).extract().path("id");
String rootId = given().when().get("/restapis/" + apiId + "/resources").then().statusCode(200).extract().path("item[0].id");
String resourceId = given().contentType("application/json").body("{\"pathPart\":\"items\"}").when().post("/restapis/" + apiId + "/resources/" + rootId).then().statusCode(201).extract().path("id");
given().contentType("application/json").body("{\"authorizationType\":\"NONE\"}").when().put("/restapis/" + apiId + "/resources/" + resourceId + "/methods/GET").then().statusCode(201);
given().contentType("application/json").body("{\"type\":\"MOCK\"}").when().put("/restapis/" + apiId + "/resources/" + resourceId + "/methods/GET/integration").then().statusCode(201);
given().contentType("application/json").body("{\"selectionPattern\":\"before\",\"responseTemplates\":{\"application/json\":\"before\"}}").when().put("/restapis/" + apiId + "/resources/" + resourceId + "/methods/GET/integration/responses/200").then().statusCode(201);
given().contentType("application/json").body("{\"patchOperations\":[{\"op\":\"replace\",\"path\":\"/selectionPattern\",\"value\":\"after\"}]}").when().patch("/restapis/" + apiId + "/resources/" + resourceId + "/methods/GET/integration/responses/200").then().statusCode(200).body("statusCode", equalTo("200")).body("selectionPattern", equalTo("after"));
given().when().get("/restapis/" + apiId + "/resources/" + resourceId + "/methods/GET/integration/responses/200").then().statusCode(200).body("selectionPattern", equalTo("after"));
given().contentType("application/json").body("""
{"selectionPattern":"before","contentHandling":"CONVERT_TO_BINARY",
"responseParameters":{"method.response.header.Access-Control-Allow-Origin":"'https://before.example'"},
"responseTemplates":{"application/json":"before"}}
""").when().put("/restapis/" + apiId + "/resources/" + resourceId + "/methods/GET/integration/responses/200").then().statusCode(201);
given().contentType("application/json").body("""
{"patchOperations":[
{"op":"replace","path":"/selectionPattern","value":"after"},
{"op":"replace","path":"/contentHandling","value":"CONVERT_TO_TEXT"},
{"op":"replace","path":"/responseParameters/method.response.header.Access-Control-Allow-Origin","value":"'*'"},
{"op":"add","path":"/responseParameters/method.response.header.Access-Control-Allow-Headers","value":"'Content-Type'"},
{"op":"remove","path":"/responseParameters/method.response.header.Access-Control-Allow-Headers"},
{"op":"add","path":"/responseTemplates/application~1xml","value":"after"},
{"op":"remove","path":"/responseTemplates/application~1json"}]}
""").when().patch("/restapis/" + apiId + "/resources/" + resourceId + "/methods/GET/integration/responses/200")
.then().statusCode(200).body("statusCode", equalTo("200"))
.body("selectionPattern", equalTo("after"))
.body("contentHandling", equalTo("CONVERT_TO_TEXT"))
.body("responseParameters.'method.response.header.Access-Control-Allow-Origin'", equalTo("'*'"))
.body("responseParameters.'method.response.header.Access-Control-Allow-Headers'", nullValue())
.body("responseTemplates.'application/json'", nullValue())
.body("responseTemplates.'application/xml'", equalTo("after"));
given().when().get("/restapis/" + apiId + "/resources/" + resourceId + "/methods/GET/integration/responses/200")
.then().statusCode(200).body("selectionPattern", equalTo("after"))
.body("contentHandling", equalTo("CONVERT_TO_TEXT"))
.body("responseParameters.'method.response.header.Access-Control-Allow-Origin'", equalTo("'*'"))
.body("responseTemplates.'application/xml'", equalTo("after"));
}
}
@@ -0,0 +1,50 @@
package io.github.hectorvent.floci.services.apigateway;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
@QuarkusTest
class ApiGatewayUpdateMethodResponseIntegrationTest {
@Test
void updateMethodResponsePatchesCorsConfigurationAndRoundTripsIt() {
String apiId = given().contentType("application/json").body("{\"name\":\"method-response-update\"}")
.when().post("/restapis").then().statusCode(201).extract().path("id");
String rootId = given().when().get("/restapis/" + apiId + "/resources")
.then().statusCode(200).extract().path("item[0].id");
String resourceId = given().contentType("application/json").body("{\"pathPart\":\"items\"}")
.when().post("/restapis/" + apiId + "/resources/" + rootId).then().statusCode(201).extract().path("id");
String methodPath = "/restapis/" + apiId + "/resources/" + resourceId + "/methods/OPTIONS";
given().contentType("application/json").body("{\"authorizationType\":\"NONE\"}")
.when().put(methodPath).then().statusCode(201);
given().contentType("application/json").body("""
{"responseParameters":{"method.response.header.Access-Control-Allow-Origin":false},
"responseModels":{"application/json":"Empty"}}
""").when().put(methodPath + "/responses/200").then().statusCode(201)
.body("responseParameters.'method.response.header.Access-Control-Allow-Origin'", equalTo(false))
.body("responseModels.'application/json'", equalTo("Empty"));
given().contentType("application/json").body("""
{"patchOperations":[
{"op":"replace","path":"/responseParameters/method.response.header.Access-Control-Allow-Origin","value":"true"},
{"op":"add","path":"/responseParameters/method.response.header.Access-Control-Allow-Headers","value":"true"},
{"op":"remove","path":"/responseParameters/method.response.header.Access-Control-Allow-Headers"},
{"op":"replace","path":"/responseModels/application~1json","value":"CorsModel"}]}
""").when().patch(methodPath + "/responses/200").then().statusCode(200)
.body("responseParameters.'method.response.header.Access-Control-Allow-Origin'", equalTo(true))
.body("responseParameters.'method.response.header.Access-Control-Allow-Headers'", nullValue())
.body("responseModels.'application/json'", equalTo("CorsModel"));
given().when().get(methodPath + "/responses/200").then().statusCode(200)
.body("responseParameters.'method.response.header.Access-Control-Allow-Origin'", equalTo(true))
.body("responseModels.'application/json'", equalTo("CorsModel"));
given().when().get(methodPath).then().statusCode(200)
.body("methodResponses.'200'.responseParameters.'method.response.header.Access-Control-Allow-Origin'", equalTo(true))
.body("methodResponses.'200'.responseModels.'application/json'", equalTo("CorsModel"));
}
}
@@ -92,6 +92,21 @@ class DynamoDbCfnProvisionerTest {
provisioner.resourceTypes());
}
@Test
void tablePassesProvisionedThroughputToDynamoDb() throws Exception {
when(dynamoDb.createTable(anyString(), anyList(), anyList(), any(), any(), anyList(), anyList(), anyString()))
.thenReturn(table(false));
StackResource r = resource("AWS::DynamoDB::Table", "Orders");
provisioner.provision(r, props("""
{"TableName":"orders","AttributeDefinitions":[{"AttributeName":"id","AttributeType":"S"}],
"KeySchema":[{"AttributeName":"id","KeyType":"HASH"}],
"ProvisionedThroughput":{"ReadCapacityUnits":20,"WriteCapacityUnits":5}}
"""), ctx());
verify(dynamoDb).createTable(eq("orders"), anyList(), anyList(), eq(20L), eq(5L), anyList(), anyList(), eq("us-east-1"));
}
/** The registry schema declares only Arn and StreamArn read-only on a plain table; TableId belongs to the global table. */
@Test
void tablePublishesOnlyArnWithoutAStream() throws Exception {
@@ -863,7 +863,7 @@ class Ec2ContainerManagerTest {
String script = Ec2ContainerManager.metadataProxyInstallCommand()[2];
assertTrue(script.contains("command -v yum"), script);
assertTrue(script.contains("yum install -y iproute socat curl ca-certificates"), script);
assertTrue(script.contains("yum install -y iproute iptables socat curl ca-certificates"), script);
}
@Test
@@ -878,7 +878,7 @@ class Ec2ContainerManagerTest {
// instance was left with no link-local IMDS endpoint.
String script = Ec2ContainerManager.metadataProxyInstallCommand()[2];
assertTrue(script.contains("dnf install -y --allowerasing iproute socat curl ca-certificates"),
assertTrue(script.contains("dnf install -y --allowerasing iproute iptables socat curl ca-certificates"),
script);
}
@@ -888,7 +888,8 @@ class Ec2ContainerManagerTest {
assertEquals("sh", command[0]);
assertTrue(command[2].contains("169.254.169.254/32"));
assertTrue(command[2].contains("TCP-LISTEN:80,bind=169.254.169.254"));
assertTrue(command[2].contains("TCP-LISTEN:1338,bind=127.0.0.1"));
assertTrue(command[2].contains("--dport 80 -j DNAT --to-destination 127.0.0.1:1338"));
assertTrue(command[2].contains("TCP:floci:9169"));
assertTrue(command[2].contains("http://169.254.169.254/latest/meta-data/instance-id"));
}
@@ -1798,6 +1799,23 @@ class Ec2ContainerManagerTest {
awaitUntil(() -> "terminated".equals(instance.getState().getName()), Duration.ofSeconds(2));
}
@Test
void terminateInvokesPersistenceCallbackAfterRecordingFinalState() throws Exception {
LaunchHarness harness = launchHarness();
stubDockerPing(harness.dockerClient(), false);
Instance instance = instance("i-persist-termination");
CountDownLatch persisted = new CountDownLatch(1);
harness.manager().launch(instance, "ubuntu:24.04", null, "us-west-2");
harness.manager().terminate(instance, () -> {
assertEquals("terminated", instance.getState().getName());
assertTrue(instance.getTerminatedAt() > 0);
persisted.countDown();
});
assertTrue(persisted.await(2, TimeUnit.SECONDS));
}
/** Stubs the daemon reachability probe every launch makes before touching Docker. */
private static void stubDockerPing(DockerClient dockerClient, boolean reachable) {
PingCmd ping = mock(PingCmd.class);
@@ -2,7 +2,6 @@ package io.github.hectorvent.floci.services.ec2;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
@@ -12,7 +11,6 @@ import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.github.hectorvent.floci.core.common.AwsException;
import jakarta.inject.Inject;
@QuarkusTest
@@ -40,7 +38,8 @@ class Ec2ImageCatalogTest {
"ami-ubuntu2404-cloud-arm64",
"ami-debian12",
"ami-alpine",
"ami-0abcdef1234567893"), imageIds);
"ami-fedora42",
"ami-opensuse-leap156"), imageIds);
assertTrue(imageCatalog.findByIdOrAlias("ami-amazonlinux2").isPresent());
assertTrue(imageCatalog.findByIdOrAlias("ami-amazonlinux2023").isPresent());
@@ -64,11 +63,11 @@ class Ec2ImageCatalogTest {
@Test
void resolverExposesCloudImageGuestRuntimeMetadata() {
ResolvedAmiImage image = amiImageResolver.resolveImage("ami-ubuntu2404-cloud");
ResolvedAmiImage image = amiImageResolver.resolveImage("ami-amazonlinux2023");
assertEquals("floci/ami-ubuntu:24.04-arm64", image.dockerImage());
assertEquals("floci/ami-amazonlinux:2023", image.dockerImage());
assertEquals(ResolvedAmiImage.SYSTEMD_RUNTIME, image.guestRuntime());
assertTrue(image.cloudInit());
assertFalse(image.cloudInit());
assertTrue(image.systemd());
}
@@ -79,11 +78,9 @@ class Ec2ImageCatalogTest {
}
@Test
void resolverRejectsWindowsImagesForContainerExecution() {
AwsException error = assertThrows(AwsException.class,
() -> amiImageResolver.resolveImage("ami-0abcdef1234567893"));
assertEquals("UnsupportedOperation", error.getErrorCode());
void resolverUsesFedoraAndOpenSuseImages() {
assertEquals("registry.fedoraproject.org/fedora:42", amiImageResolver.resolve("ami-fedora42"));
assertEquals("registry.opensuse.org/opensuse/leap:15.6", amiImageResolver.resolve("ami-opensuse-leap156"));
}
@Test
@@ -19,11 +19,11 @@ class Ec2MetadataProxyTest {
assertEquals("-c", command[1]);
String script = command[2];
assertTrue(script.contains("command -v ip >/dev/null 2>&1 && command -v socat >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then exit 0; fi"));
assertTrue(script.contains("apt-get install -y --no-install-recommends iproute2 socat curl ca-certificates"));
assertTrue(script.contains("dnf install -y --allowerasing iproute socat curl ca-certificates"));
assertTrue(script.contains("yum install -y iproute socat curl ca-certificates"));
assertTrue(script.contains("apk add --no-cache iproute2 socat curl ca-certificates"));
assertTrue(script.contains("command -v ip >/dev/null 2>&1 && command -v iptables >/dev/null 2>&1 && command -v socat >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then exit 0; fi"));
assertTrue(script.contains("apt-get install -y --no-install-recommends iproute2 iptables socat curl ca-certificates"));
assertTrue(script.contains("dnf install -y --allowerasing iproute iptables socat curl ca-certificates"));
assertTrue(script.contains("yum install -y iproute iptables socat curl ca-certificates"));
assertTrue(script.contains("apk add --no-cache iproute2 iptables socat curl ca-certificates"));
}
@Test
@@ -39,7 +39,8 @@ class Ec2MetadataProxyTest {
// Idempotent when pid file exists and process is alive
assertTrue(script.contains("if [ -f /tmp/floci-imds-proxy.pid ] && kill -0 \"$(cat /tmp/floci-imds-proxy.pid)\" 2>/dev/null; then\n exit 0\nfi"));
// Targets configured Floci host and IMDS port
assertTrue(script.contains("nohup socat TCP-LISTEN:80,bind=169.254.169.254,fork,reuseaddr TCP:10.0.0.1:9169 >/tmp/floci-imds-proxy.log 2>&1 &"));
assertTrue(script.contains("iptables -t nat -A OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j DNAT --to-destination 127.0.0.1:1338"));
assertTrue(script.contains("nohup socat TCP-LISTEN:1338,bind=127.0.0.1,fork,reuseaddr TCP:10.0.0.1:9169 >/tmp/floci-imds-proxy.log 2>&1 &"));
// Verifies via link-local curl
assertTrue(script.contains("curl -fsS --max-time 1 http://169.254.169.254/latest/meta-data/instance-id >/dev/null && exit 0"));
}
@@ -390,6 +390,31 @@ class Ec2ServiceTest {
verifyNoInteractions(containerManager);
}
@Test
void prunesTerminatedInstancesAfterOneHourAndRetainsRecentOnes() {
Ec2Service service = new Ec2Service(mockConfig(true), mock(Ec2ContainerManager.class),
mock(Ec2PortForwardManager.class), mock(AmiImageResolver.class), mock(Ec2ImageCatalog.class),
new Ec2InstanceTypeCatalog(), new InMemoryStorageFactory());
Reservation expiredReservation = service.runInstances("us-east-1", "ami-1234567890abcdef0", "t3.micro",
1, 1, null, List.of(), null, null, List.of(), null, null);
Reservation recentReservation = service.runInstances("us-east-1", "ami-1234567890abcdef0", "t3.micro",
1, 1, null, List.of(), null, null, List.of(), null, null);
String expiredId = expiredReservation.getInstances().getFirst().getInstanceId();
String recentId = recentReservation.getInstances().getFirst().getInstanceId();
service.terminateInstances("us-east-1", List.of(expiredId, recentId));
expiredReservation.getInstances().getFirst().setTerminatedAt(System.currentTimeMillis() - 3_600_001L);
service.pruneTerminatedInstances();
List<String> remainingIds = service.describeInstances("us-east-1", List.of(), Map.of()).stream()
.flatMap(reservation -> reservation.getInstances().stream())
.map(Instance::getInstanceId)
.toList();
assertFalse(remainingIds.contains(expiredId));
assertTrue(remainingIds.contains(recentId));
}
@Test
void describeNetworkInterfacesGroupIdFilterSkipsNullGroupEntry() {
Ec2Service service = new Ec2Service(mockConfig(true), mock(Ec2ContainerManager.class),
@@ -0,0 +1,46 @@
package io.github.hectorvent.floci.services.ecs;
import io.github.hectorvent.floci.testing.RestAssuredJsonUtils;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.response.ValidatableResponse;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
@QuarkusTest
class EcsEc2BridgeIntegrationTest {
private static final String TARGET = "AmazonEC2ContainerServiceV20141113.";
private static final String CONTENT_TYPE = "application/x-amz-json-1.1";
@BeforeAll
static void configure() {
RestAssuredJsonUtils.configureAwsContentTypes();
}
@Test
void runTaskAndCreateServiceRejectFargateAndNonBridgeTasks() {
call("RegisterTaskDefinition", "{\"family\":\"ec2-bridge\",\"networkMode\":\"bridge\","
+ "\"containerDefinitions\":[{\"name\":\"app\",\"image\":\"nginx\"}]}", 200);
call("RunTask", "{\"taskDefinition\":\"ec2-bridge\",\"launchType\":\"FARGATE\"}", 400)
.body("message", containsString("EC2 launch type"));
call("RunTask", "{\"taskDefinition\":\"ec2-bridge\",\"capacityProviderStrategy\":["
+ "{\"capacityProvider\":\"FARGATE\",\"weight\":1}]}", 400)
.body("message", containsString("FARGATE capacity providers"));
call("CreateService", "{\"serviceName\":\"ec2-bridge\",\"taskDefinition\":\"ec2-bridge\","
+ "\"desiredCount\":0,\"launchType\":\"FARGATE\"}", 400)
.body("message", containsString("EC2 launch type"));
call("RegisterTaskDefinition", "{\"family\":\"not-bridge\",\"networkMode\":\"awsvpc\","
+ "\"containerDefinitions\":[{\"name\":\"app\",\"image\":\"nginx\"}]}", 200);
call("RunTask", "{\"taskDefinition\":\"not-bridge\",\"launchType\":\"EC2\"}", 400)
.body("message", containsString("bridge network mode"));
}
private static ValidatableResponse call(String action, String body, int status) {
return given().contentType(CONTENT_TYPE).header("X-Amz-Target", TARGET + action).body(body)
.when().post("/").then().statusCode(status);
}
}
@@ -1,10 +1,15 @@
package io.github.hectorvent.floci.services.ecs;
import io.github.hectorvent.floci.testing.RestAssuredJsonUtils;
import io.github.hectorvent.floci.services.elbv2.ElbV2Service;
import io.github.hectorvent.floci.services.elbv2.model.TargetGroup;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
@@ -22,6 +27,9 @@ class EcsLoadBalancersIntegrationTest {
private static final String TARGET = "AmazonEC2ContainerServiceV20141113.";
private static final String CT = "application/x-amz-json-1.1";
@Inject
ElbV2Service elbV2Service;
@BeforeAll
static void configure() {
RestAssuredJsonUtils.configureAwsContentTypes();
@@ -41,10 +49,17 @@ class EcsLoadBalancersIntegrationTest {
+ "\"memory\":128,\"portMappings\":[{\"containerPort\":8080}]}]}");
}
private String createTargetGroup(String name) {
TargetGroup targetGroup = elbV2Service.createTargetGroup("us-east-1", name, "HTTP", "HTTP1",
8080, "vpc-default", "ip", null, null, false, null, null, null, null, null, null,
null, Map.of());
return targetGroup.getTargetGroupArn();
}
@Test
void createServiceParsesAndEchoesLoadBalancers() {
seedClusterAndTaskDef("lb-cluster-1", "lb-td-1");
String tgArn = "arn:aws:elasticloadbalancing:us-east-1:000000000000:targetgroup/lbtg1/abc123";
String tgArn = createTargetGroup("lbtg1");
given().contentType(CT).header("X-Amz-Target", TARGET + "CreateService")
.body("{\"cluster\":\"lb-cluster-1\",\"serviceName\":\"lb-svc-1\","
@@ -63,7 +78,7 @@ class EcsLoadBalancersIntegrationTest {
@Test
void describeServicesEchoesLoadBalancers() {
seedClusterAndTaskDef("lb-cluster-2", "lb-td-2");
String tgArn = "arn:aws:elasticloadbalancing:us-east-1:000000000000:targetgroup/lbtg2/def456";
String tgArn = createTargetGroup("lbtg2");
call("CreateService", "{\"cluster\":\"lb-cluster-2\",\"serviceName\":\"lb-svc-2\","
+ "\"taskDefinition\":\"lb-td-2\",\"desiredCount\":1,"
+ "\"loadBalancers\":[{\"targetGroupArn\":\"" + tgArn + "\","
@@ -116,11 +131,27 @@ class EcsLoadBalancersIntegrationTest {
.body("__type", org.hamcrest.Matchers.containsString("InvalidParameterException"));
}
@Test
void createServiceRejectsTargetGroupBindingToAnUndeclaredContainerPort() {
seedClusterAndTaskDef("lb-cluster-port", "lb-td-port");
String tgArn = createTargetGroup("lbtg-port");
given().contentType(CT).header("X-Amz-Target", TARGET + "CreateService")
.body("{\"cluster\":\"lb-cluster-port\",\"serviceName\":\"lb-svc-port\","
+ "\"taskDefinition\":\"lb-td-port\",\"desiredCount\":1,"
+ "\"loadBalancers\":[{\"targetGroupArn\":\"" + tgArn + "\","
+ "\"containerName\":\"web\",\"containerPort\":9999}]}")
.when().post("/")
.then().statusCode(400)
.body("__type", org.hamcrest.Matchers.containsString("InvalidParameterException"))
.body("message", org.hamcrest.Matchers.containsString("does not expose port 9999"));
}
@Test
void serviceWithLoadBalancersIsStableInMockMode() {
// Mock mode launches no real containers; the registrar must no-op cleanly.
seedClusterAndTaskDef("lb-cluster-4", "lb-td-4");
String tgArn = "arn:aws:elasticloadbalancing:us-east-1:000000000000:targetgroup/lbtg4/ghi789";
String tgArn = createTargetGroup("lbtg4");
call("CreateService", "{\"cluster\":\"lb-cluster-4\",\"serviceName\":\"lb-svc-4\","
+ "\"taskDefinition\":\"lb-td-4\",\"desiredCount\":1,"
+ "\"loadBalancers\":[{\"targetGroupArn\":\"" + tgArn + "\","
@@ -10,6 +10,7 @@ import io.github.hectorvent.floci.core.storage.StorageFactory;
import io.github.hectorvent.floci.services.ecs.container.EcsContainerManager;
import io.github.hectorvent.floci.services.ecs.container.EcsTaskHandle;
import io.github.hectorvent.floci.services.ecs.model.ContainerDefinition;
import io.github.hectorvent.floci.services.ecs.model.EcsLoadBalancer;
import io.github.hectorvent.floci.services.ecs.model.LaunchType;
import org.junit.jupiter.api.Test;
@@ -62,7 +63,7 @@ class EcsServiceTeardownTest {
cd.setImage("nginx:alpine");
service.registerTaskDefinition("teardown-fam", List.of(cd), null, null, null,
null, null, List.of(), REGION);
service.runTask(null, "teardown-fam", 1, LaunchType.FARGATE, null, null,
service.runTask(null, "teardown-fam", 1, LaunchType.EC2, null, null,
List.of(), null, REGION);
service.stopManagedContainers();
@@ -138,7 +139,7 @@ class EcsServiceTeardownTest {
cd.setImage("nginx:alpine");
service.registerTaskDefinition("retry-fam", List.of(cd), null, null, null,
null, null, List.of(), REGION);
String taskArn = service.runTask(null, "retry-fam", 1, LaunchType.FARGATE, null, null,
String taskArn = service.runTask(null, "retry-fam", 1, LaunchType.EC2, null, null,
List.of(), null, REGION).getFirst().getTaskArn();
service.stopTask(null, taskArn, null, REGION);
@@ -151,6 +152,41 @@ class EcsServiceTeardownTest {
verify(containerManager, times(2)).stopTaskAndCollectExitCodes(handle);
}
@Test
void reconcilerDeregistersLoadBalancerTargetsWhenDockerWasKilledExternally() {
EmulatorConfig config = mock(EmulatorConfig.class, RETURNS_DEEP_STUBS);
when(config.services().ecs().mock()).thenReturn(false);
when(config.effectiveBaseUrl()).thenReturn("http://localhost:4566");
EcsContainerManager containerManager = mock(EcsContainerManager.class);
EcsLoadBalancerRegistrar registrar = mock(EcsLoadBalancerRegistrar.class);
EcsTaskHandle handle = new EcsTaskHandle("task-arn", Map.of("app", "docker-id"), Map.of());
when(containerManager.startTask(any(), any(), any(), anyString())).thenReturn(handle);
when(containerManager.getExitCodeIfStopped("docker-id")).thenReturn(137);
EcsService service = new EcsService(
new RegionResolver(REGION, "000000000000"), containerManager, config, registrar,
new SingleUseStorageFactory(), null);
service.initializeStorage();
ContainerDefinition container = new ContainerDefinition();
container.setName("app");
container.setImage("nginx:alpine");
service.createCluster("reconcile-lb", REGION);
service.registerTaskDefinition("reconcile-lb", List.of(container), null, null, null,
null, null, List.of(), REGION);
EcsLoadBalancer loadBalancer = new EcsLoadBalancer();
loadBalancer.setTargetGroupArn("arn:aws:elasticloadbalancing:us-east-1:000000000000:targetgroup/app/id");
loadBalancer.setContainerName("app");
loadBalancer.setContainerPort(8080);
service.createService("reconcile-lb", "app", "reconcile-lb", 1, LaunchType.EC2,
List.of(loadBalancer), null, REGION);
service.reconcileServices();
service.reconcile();
verify(registrar).deregisterTask(any(), any(), anyString());
verify(containerManager).cleanupStoppedTask(handle);
}
private static final class SingleUseStorageFactory extends StorageFactory {
private final Map<String, StorageBackend<String, ?>> stores = new HashMap<>();
@@ -2,6 +2,7 @@ package io.github.hectorvent.floci.services.elbv2;
import io.github.hectorvent.floci.config.EmulatorConfig;
import io.github.hectorvent.floci.services.ec2.Ec2Service;
import io.github.hectorvent.floci.services.ec2.ContainerNetworkReachability;
import io.github.hectorvent.floci.services.elbv2.model.TargetDescription;
import io.github.hectorvent.floci.services.elbv2.model.TargetGroup;
import io.vertx.core.Vertx;
@@ -75,6 +76,41 @@ class ElbV2HealthCheckerTest {
}
}
@Test
void probeUsesConfiguredHealthCheckPortAndPath() throws Exception {
AtomicInteger requests = new AtomicInteger();
HttpServer server = vertx.createHttpServer()
.requestHandler(request -> {
if ("/ready".equals(request.path())) {
requests.incrementAndGet();
request.response().setStatusCode(204).end();
} else {
request.response().setStatusCode(404).end();
}
})
.listen(0, "127.0.0.1")
.toCompletionStage()
.toCompletableFuture()
.get(2, TimeUnit.SECONDS);
try {
ElbV2HealthChecker checker = healthChecker();
TargetGroup targetGroup = targetGroup(1, "204", 1, 1);
targetGroup.setHealthCheckPort(String.valueOf(server.actualPort()));
targetGroup.setHealthCheckPath("/ready");
TargetDescription target = target("127.0.0.1", 1);
checker.addTargets(targetGroup.getTargetGroupArn(), List.of(target), targetGroup);
await().atMost(Duration.ofSeconds(2)).untilAsserted(() -> {
assertEquals(1, requests.get());
assertEquals("healthy", checker.getHealth(targetGroup.getTargetGroupArn(),
target.getId(), server.actualPort()).state());
});
} finally {
close(server);
}
}
@Test
void timedOutProbeTransitionsTargetToUnhealthyWithAwsReason() throws Exception {
HttpServer server = startHangingServer();
@@ -174,7 +210,8 @@ class ElbV2HealthCheckerTest {
private ElbV2HealthChecker healthChecker() {
EmulatorConfig config = mock(EmulatorConfig.class, RETURNS_DEEP_STUBS);
when(config.services().elbv2().mock()).thenReturn(false);
return new ElbV2HealthChecker(vertx, config, mock(Ec2Service.class));
return new ElbV2HealthChecker(vertx, config, mock(Ec2Service.class),
mock(ContainerNetworkReachability.class));
}
private HttpServer startServer(int statusCode) throws Exception {
@@ -0,0 +1,13 @@
package io.github.hectorvent.floci.services.elbv2;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ElbV2ListenerForwardManagerTest {
@Test
void forwardContainerNameUsesListenerPort() {
assertEquals("floci-elbv2-fwd-8080", ElbV2ListenerForwardManager.forwardContainerName(8080));
}
}
@@ -107,6 +107,34 @@ class ElbV2ServiceTest {
assertTrue(newTargetGroup.getLoadBalancerArns().contains(lbArn));
}
@Test
void applicationListenerRejectsUnsupportedDataPlaneProtocolsAndTargetGroups() {
String lbArn = service.createLoadBalancer(
REGION, "http-only-lb", "internal", "application", "ipv4",
ALB_SUBNETS, List.of(), Map.of()).getLoadBalancerArn();
AwsException listenerError = assertThrows(AwsException.class, () -> service.createListener(
REGION, lbArn, "HTTPS", 8443, null, List.of(), List.of(), List.of(), Map.of()));
assertEquals("UnsupportedProtocol", listenerError.getErrorCode());
String tcpTargetGroupArn = service.createTargetGroup(
REGION, "tcp-targets", "TCP", null, 8080, "vpc-a", "ip",
"TCP", "traffic-port", true, null, null, null, null, null, null,
"ipv4", Map.of()).getTargetGroupArn();
AwsException targetGroupError = assertThrows(AwsException.class, () -> service.createListener(
REGION, lbArn, "HTTP", 8080, null, List.of(),
List.of(forwardAction(tcpTargetGroupArn)), List.of(), Map.of()));
assertEquals("UnsupportedProtocol", targetGroupError.getErrorCode());
}
@Test
void defaultLoadBalancerDnsNameUsesHostResolvableLocalhostDomain() {
String dnsName = service.createLoadBalancer(
REGION, "host-resolvable-lb", "internal", "application", "ipv4",
ALB_SUBNETS, List.of(), Map.of()).getDnsName();
assertTrue(dnsName.endsWith(".elb.localhost"));
}
@Test
void modifyListenerPortRestartsListener() {
String lbArn = service.createLoadBalancer(
@@ -1,6 +1,7 @@
package io.github.hectorvent.floci.services.elbv2;
import io.github.hectorvent.floci.services.ec2.Ec2Service;
import io.github.hectorvent.floci.services.ec2.ContainerNetworkReachability;
import io.github.hectorvent.floci.services.ec2.model.Instance;
import io.github.hectorvent.floci.services.elbv2.model.TargetDescription;
import io.github.hectorvent.floci.services.elbv2.model.TargetGroup;
@@ -44,6 +45,52 @@ class ElbV2TargetResolverTest {
assertEquals("10.0.0.12", ElbV2TargetResolver.resolveHost(ec2Service, targetGroup, target));
}
@Test
void instanceTargetUsesBridgeEndpointWhenContainerNetworkIsRoutable() {
Ec2Service ec2Service = mock(Ec2Service.class);
ContainerNetworkReachability reachability = mock(ContainerNetworkReachability.class);
Instance instance = instance("172.18.0.42");
when(ec2Service.findInstanceById("i-1234567890abcdef0")).thenReturn(instance);
when(reachability.isContainerIpRoutable("172.18.0.42")).thenReturn(true);
ElbV2TargetResolver.TargetEndpoint endpoint = ElbV2TargetResolver.resolveEndpoint(
ec2Service, reachability, instanceTargetGroup(), instanceTarget(), 8080);
assertEquals("172.18.0.42", endpoint.host());
assertEquals(8080, endpoint.port());
}
@Test
void unroutableInstanceBridgeUsesPublishedPortFromDockerHost() {
Ec2Service ec2Service = mock(Ec2Service.class);
ContainerNetworkReachability reachability = mock(ContainerNetworkReachability.class);
Instance instance = instance("172.18.0.42");
instance.getPublishedPorts().put(8080, 30000);
when(ec2Service.findInstanceById("i-1234567890abcdef0")).thenReturn(instance);
when(reachability.isContainerIpRoutable("172.18.0.42")).thenReturn(false);
ElbV2TargetResolver.TargetEndpoint endpoint = ElbV2TargetResolver.resolveEndpoint(
ec2Service, reachability, instanceTargetGroup(), instanceTarget(), 8080);
assertEquals(ElbV2TargetResolver.DOCKER_HOST, endpoint.host());
assertEquals(30000, endpoint.port());
}
@Test
void unroutableInstanceBridgeWithoutPublishedPortKeepsBridgeEndpoint() {
Ec2Service ec2Service = mock(Ec2Service.class);
ContainerNetworkReachability reachability = mock(ContainerNetworkReachability.class);
Instance instance = instance("172.18.0.42");
when(ec2Service.findInstanceById("i-1234567890abcdef0")).thenReturn(instance);
when(reachability.isContainerIpRoutable("172.18.0.42")).thenReturn(false);
ElbV2TargetResolver.TargetEndpoint endpoint = ElbV2TargetResolver.resolveEndpoint(
ec2Service, reachability, instanceTargetGroup(), instanceTarget(), 8080);
assertEquals("172.18.0.42", endpoint.host());
assertEquals(8080, endpoint.port());
}
@Test
void checkedAddressReturnsAnIpLiteralForLoopbackAndPrivateTargets() throws IOException {
assertEquals("127.0.0.1", ElbV2TargetResolver.resolveCheckedAddress("127.0.0.1"));
@@ -57,4 +104,23 @@ class ElbV2TargetResolverTest {
assertTrue(ex.getMessage().contains("link-local or metadata address"), ex.getMessage());
}
}
private static Instance instance(String bridgeIp) {
Instance instance = new Instance();
instance.setInstanceId("i-1234567890abcdef0");
instance.setContainerBridgeIp(bridgeIp);
return instance;
}
private static TargetGroup instanceTargetGroup() {
TargetGroup targetGroup = new TargetGroup();
targetGroup.setTargetType("instance");
return targetGroup;
}
private static TargetDescription instanceTarget() {
TargetDescription target = new TargetDescription();
target.setId("i-1234567890abcdef0");
return target;
}
}
@@ -686,6 +686,19 @@ class S3ServiceTest {
assertInstanceOf(S3Service.WebsiteResolution.ServeObject.class, resolution).key());
}
@Test
void resolveWebsiteRedirectReturnsConfiguredTargetWithoutObjectLookup() {
s3Service.createBucket("site", "us-east-1");
s3Service.putBucketWebsite("site", new WebsiteConfiguration(null, null, "redirect.example", "https"));
S3Service.WebsiteResolution resolution = s3Service.resolveWebsiteRedirect("site");
S3Service.WebsiteResolution.RedirectAllRequestsTo redirect =
assertInstanceOf(S3Service.WebsiteResolution.RedirectAllRequestsTo.class, resolution);
assertEquals("redirect.example", redirect.hostName());
assertEquals("https", redirect.protocol());
}
@Test
void resolveWebsiteRequestRedirectsFolderWithoutTrailingSlash() {
websiteBucket("index.html", null);
@@ -1243,4 +1256,3 @@ class S3ServiceTest {
assertDoesNotThrow(() -> s3Service.authorizeSignedDeleteObject("ASIAFAKEKEY00000001", "sessiontoken", "signed-del-bucket", "some/key"));
}
}
@@ -411,6 +411,124 @@ class S3WebsiteIntegrationTest {
@Test
@Order(24)
void redirectAllConfigurationRoundTripAndValidation() {
String redirect = """
<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<RedirectAllRequestsTo>
<HostName>redirect.example</HostName>
<Protocol>https</Protocol>
</RedirectAllRequestsTo>
</WebsiteConfiguration>
""";
given()
.queryParam("website", "")
.contentType("application/xml")
.body(redirect)
.when()
.put("/" + BUCKET)
.then()
.statusCode(200);
given()
.queryParam("website", "")
.when()
.get("/" + BUCKET)
.then()
.statusCode(200)
.body(containsString("<RedirectAllRequestsTo><HostName>redirect.example</HostName><Protocol>https</Protocol></RedirectAllRequestsTo>"))
.body(not(containsString("<IndexDocument>")));
String conflicting = """
<WebsiteConfiguration>
<IndexDocument><Suffix>index.html</Suffix></IndexDocument>
<RedirectAllRequestsTo><HostName>redirect.example</HostName></RedirectAllRequestsTo>
</WebsiteConfiguration>
""";
given()
.queryParam("website", "")
.contentType("application/xml")
.body(conflicting)
.when()
.put("/" + BUCKET)
.then()
.statusCode(400)
.body(containsString("InvalidRequest"));
String missingHost = "<WebsiteConfiguration><RedirectAllRequestsTo/></WebsiteConfiguration>";
given()
.queryParam("website", "")
.contentType("application/xml")
.body(missingHost)
.when()
.put("/" + BUCKET)
.then()
.statusCode(400)
.body(containsString("MalformedXML"));
String invalidProtocol = """
<WebsiteConfiguration>
<RedirectAllRequestsTo><HostName>redirect.example</HostName><Protocol>FTP</Protocol></RedirectAllRequestsTo>
</WebsiteConfiguration>
""";
given()
.queryParam("website", "")
.contentType("application/xml")
.body(invalidProtocol)
.when()
.put("/" + BUCKET)
.then()
.statusCode(400)
.body(containsString("MalformedXML"));
}
@Test
@Order(25)
void redirectAllWebsiteRequestsPreserveRawPathAndQueryForGetAndHead() {
String redirect = """
<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<RedirectAllRequestsTo><HostName>redirect.example</HostName></RedirectAllRequestsTo>
</WebsiteConfiguration>
""";
given()
.queryParam("website", "")
.contentType("application/xml")
.body(redirect)
.when()
.put("/" + BUCKET)
.then()
.statusCode(200);
given()
.header("Host", websiteHost())
.redirects().follow(false)
.when()
.get("/?code=abc&state=xyz")
.then()
.statusCode(301)
.header("Location", equalTo("http://redirect.example/?code=abc&state=xyz"));
given()
.header("Host", websiteHost())
.redirects().follow(false)
.when()
.get("/nested/path?code=abc&state=xyz")
.then()
.statusCode(301)
.header("Location", equalTo("http://redirect.example/nested/path?code=abc&state=xyz"));
given()
.header("Host", websiteHost())
.redirects().follow(false)
.when()
.head("/nested/path?code=abc&state=xyz")
.then()
.statusCode(301)
.header("Location", equalTo("http://redirect.example/nested/path?code=abc&state=xyz"))
.body(emptyString());
}
@Test
@Order(26)
void deleteWebsiteConfiguration() {
given()
.queryParam("website", "")
@@ -428,7 +546,7 @@ class S3WebsiteIntegrationTest {
}
@Test
@Order(25)
@Order(27)
void cleanup() {
given().delete("/" + BUCKET + "/index.html");
given().delete("/" + BUCKET + "/error.html");
+1
View File
@@ -326,6 +326,7 @@ floci:
elbv2:
enabled: true
mock: true
listener-forwarder-image: alpine/socat:1.8.1.3
codebuild:
enabled: true
# docker-network: floci-network