From 4617a7bb255d38fe01ba5f5dab6593bb87906a20 Mon Sep 17 00:00:00 2001 From: Iliyan Velichkov Date: Mon, 8 Jun 2026 14:52:36 +0300 Subject: [PATCH] Make proxy integration tests hermetic (#178) AirflowPerspectiveIT and ProxyIT pointed the Airflow proxy at external hosts (httpbin.org / api.ipify.org), so they failed whenever those services were slow, rate-limited or down. This produced spurious red checks on unrelated PRs (e.g. the parent bump in #177, where httpbin.org returned a transient 503). Replace the external targets with a local JDK-based HTTP stub (LocalHttpStub, no new dependency) that serves a fixed response on an ephemeral port. AirflowPerspectiveIT now asserts the stubbed page renders in the proxied iframe; ProxyIT asserts the proxy rewrites a root-relative link in the stub response to stay under /services/airflow. Both tests are now fully hermetic and reach no external network host. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tests/AirflowPerspectiveIT.java | 21 +++++- .../integration/tests/LocalHttpStub.java | 72 +++++++++++++++++++ .../phoebe/integration/tests/ProxyIT.java | 22 ++++-- 3 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 integration-tests/src/test/java/com/codbex/phoebe/integration/tests/LocalHttpStub.java diff --git a/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/AirflowPerspectiveIT.java b/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/AirflowPerspectiveIT.java index 64e3d94..1a77184 100644 --- a/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/AirflowPerspectiveIT.java +++ b/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/AirflowPerspectiveIT.java @@ -12,19 +12,34 @@ import com.codbex.phoebe.cfg.AppConfig; import org.eclipse.dirigible.tests.framework.browser.HtmlElementType; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; class AirflowPerspectiveIT extends PhoebeIntegrationTest { + private static final String AIRFLOW_TITLE = "Apache Airflow"; + + // A local stub stands in for the Airflow instance so the test does not depend on an external + // host. The URL must be configured before the Spring context (and the proxy beans) start, hence + // the static initializer. + private static final LocalHttpStub AIRFLOW_STUB = + LocalHttpStub.startServingHtml("" + AIRFLOW_TITLE + "" // + + "

" + AIRFLOW_TITLE + "

"); + static { - AppConfig.AIRFLOW_URL.setValue("http://httpbin.org"); + AppConfig.AIRFLOW_URL.setValue(AIRFLOW_STUB.baseUrl()); + } + + @AfterAll + static void stopStub() { + AIRFLOW_STUB.stop(); } @Test void testPerspective() { - // expected to open https://httpbin.org + // the perspective iframes the proxied Airflow UI served by the local stub ide.openPath("/services/web/perspective-airflow/index.html"); - browser.assertElementExistsByTypeAndContainsText(HtmlElementType.HEADER2, "httpbin.org"); + browser.assertElementExistsByTypeAndContainsText(HtmlElementType.HEADER2, AIRFLOW_TITLE); } } diff --git a/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/LocalHttpStub.java b/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/LocalHttpStub.java new file mode 100644 index 0000000..baec6cd --- /dev/null +++ b/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/LocalHttpStub.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 codbex or an codbex affiliate company and contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: 2022 codbex or an codbex affiliate company and contributors + * SPDX-License-Identifier: EPL-2.0 + */ +package com.codbex.phoebe.integration.tests; + +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +/** + * A minimal, self-contained HTTP server backed by the JDK (no external dependency) used as the + * upstream "Airflow" target in proxy integration tests. It serves a fixed HTML body for every + * request on an OS-assigned ephemeral port, which makes the proxy tests hermetic instead of relying + * on flaky external hosts (e.g. httpbin.org). + */ +final class LocalHttpStub { + + private final HttpServer server; + private final String baseUrl; + + private LocalHttpStub(HttpServer server) { + this.server = server; + this.baseUrl = "http://localhost:" + server.getAddress() + .getPort(); + } + + /** + * Starts a stub server that replies to every request with the given HTML body. + * + * @param htmlBody the response body served as {@code text/html} + * @return the started stub; the caller is responsible for {@link #stop()} + */ + static LocalHttpStub startServingHtml(String htmlBody) { + try { + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + byte[] body = htmlBody.getBytes(StandardCharsets.UTF_8); + server.createContext("/", exchange -> { + exchange.getResponseHeaders() + .set("Content-Type", "text/html; charset=utf-8"); + exchange.sendResponseHeaders(200, body.length); + try (OutputStream responseBody = exchange.getResponseBody()) { + responseBody.write(body); + } + }); + server.start(); + return new LocalHttpStub(server); + } catch (IOException ex) { + throw new IllegalStateException("Failed to start local HTTP stub server", ex); + } + } + + /** + * @return the base URL (scheme, host and port) the stub is listening on + */ + String baseUrl() { + return baseUrl; + } + + void stop() { + server.stop(0); + } +} diff --git a/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/ProxyIT.java b/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/ProxyIT.java index de27e13..23f8eda 100644 --- a/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/ProxyIT.java +++ b/integration-tests/src/test/java/com/codbex/phoebe/integration/tests/ProxyIT.java @@ -12,30 +12,42 @@ import com.codbex.phoebe.cfg.AppConfig; import org.eclipse.dirigible.tests.framework.restassured.RestAssuredExecutor; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.containsString; class ProxyIT extends PhoebeIntegrationTest { private static final String AIRFLOW_PROXY_PATH = "/services/airflow"; + // The stub returns a root-relative link; the proxy is expected to rewrite it so it stays within + // the "/services/airflow" prefix when the UI is served behind the proxy. + private static final LocalHttpStub AIRFLOW_STUB = + LocalHttpStub.startServingHtml("home"); + static { - AppConfig.AIRFLOW_URL.setValue("https://api.ipify.org"); + AppConfig.AIRFLOW_URL.setValue(AIRFLOW_STUB.baseUrl()); } @Autowired private RestAssuredExecutor restAssuredExecutor; + @AfterAll + static void stopStub() { + AIRFLOW_STUB.stop(); + } + @Test void textProxyPath() { - // expected to open https://api.ipify.org?format=json + // the proxy forwards to the local stub and rewrites the root-relative link in the response + // body from "/airflow/home" to "/services/airflow/airflow/home" restAssuredExecutor.execute(() -> given().when() - .get(AIRFLOW_PROXY_PATH + "?format=json") + .get(AIRFLOW_PROXY_PATH + "/home") .then() .statusCode(200) - .body("ip", notNullValue())); + .body(containsString("href=\"" + AIRFLOW_PROXY_PATH + "/airflow/home\""))); } }