diff --git a/flash-extensions/flash-ext-web-bundler/pom.xml b/flash-extensions/flash-ext-web-bundler/pom.xml
index 6fb24c2..45ffc5a 100644
--- a/flash-extensions/flash-ext-web-bundler/pom.xml
+++ b/flash-extensions/flash-ext-web-bundler/pom.xml
@@ -33,5 +33,10 @@
org.junit.jupiter
junit-jupiter
+
+ dev.relism
+ flash-testing
+ test
+
diff --git a/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/WebBundlerExtensionIntegrationTest.java b/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/WebBundlerExtensionIntegrationTest.java
index df7c443..6060fa4 100644
--- a/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/WebBundlerExtensionIntegrationTest.java
+++ b/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/WebBundlerExtensionIntegrationTest.java
@@ -1,122 +1,81 @@
package dev.relism.flash.ext.webbundler;
-import dev.relism.flash.extension.FlashApp;
-import org.junit.jupiter.api.AfterEach;
+import dev.relism.flash.testing.FlashTest;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
-import java.net.ServerSocket;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.io.IOException;
+import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
+/**
+ * Two frontend layouts served by a real server. Each is laid out on disk inside the
+ * application's own configure(), which the harness runs lazily at first access — by then
+ * {@link TempDir} is populated, and a server whose test never runs is never booted.
+ */
class WebBundlerExtensionIntegrationTest {
+
@TempDir
- Path tempDir;
+ static Path tempDir;
- private FlashApp app;
+ @RegisterExtension
+ static FlashTest managed = FlashTest.of(app -> {
+ Path webRoot = write(tempDir.resolve("web").resolve("dist"),
+ "index.html", "spa",
+ "app.js", "console.log('ok');").getParent();
- @AfterEach
- void tearDown() {
- if (app != null) app.stop();
- }
-
- @Test
- void prodMode_servesAssetsAndFallback_withoutBreakingBackendRoutes() throws Exception {
- Path webRoot = tempDir.resolve("web");
- Path dist = webRoot.resolve("dist");
- Files.createDirectories(dist);
- Files.writeString(dist.resolve("index.html"), "spa");
- Files.writeString(dist.resolve("app.js"), "console.log('ok');");
-
- int port;
- try (ServerSocket s = new ServerSocket(0)) {
- port = s.getLocalPort();
- }
-
- WebBundlerConfig config = WebBundlerConfig.builder()
+ app.install(new WebBundlerExtension(WebBundlerConfig.builder()
.runtimeMode(RuntimeMode.PROD)
.operationMode(OperationMode.MANAGED)
.webRoot(webRoot)
.assetsFromFilesystem(Path.of("dist"))
.basePath("/app")
- .build();
-
- app = FlashApp.create(port);
- app.install(new WebBundlerExtension(config));
+ .build()));
app.get("/api/ping", (req, res) -> "pong");
- app.start();
+ });
- HttpClient client = HttpClient.newHttpClient();
- HttpResponse backend = client.send(
- HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/api/ping")).GET().build(),
- HttpResponse.BodyHandlers.ofString()
- );
- assertEquals(200, backend.statusCode());
- assertEquals("pong", backend.body());
+ // PROD is deterministic in a test JVM (Flash.DEV depends on env/system-property detection
+ // that can't be forced per-test); STATIC's actual guarantee — that it never orchestrates,
+ // in DEV or PROD — is enforced structurally by the same requiresOrchestration() gate in
+ // both WebBundlerExtension.provide() and .routes(), not by this test.
+ @RegisterExtension
+ static FlashTest staticFrontend = FlashTest.of(app -> {
+ Path webRoot = write(tempDir.resolve("public"),
+ "index.html", "static",
+ "style.css", "body{color:red}");
- HttpResponse asset = client.send(
- HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/app/app.js")).GET().build(),
- HttpResponse.BodyHandlers.ofString()
- );
- assertEquals(200, asset.statusCode());
- assertTrue(asset.body().contains("console.log"));
-
- HttpResponse fallback = client.send(
- HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/app/some/client/route")).GET().build(),
- HttpResponse.BodyHandlers.ofString()
- );
- assertEquals(200, fallback.statusCode());
- assertTrue(fallback.body().contains("spa"));
- }
-
- @Test
- void staticFrontend_servesAssetsWithoutOrchestration() throws Exception {
- Path webRoot = tempDir.resolve("public");
- Files.createDirectories(webRoot);
- Files.writeString(webRoot.resolve("index.html"), "static");
- Files.writeString(webRoot.resolve("style.css"), "body{color:red}");
-
- int port;
- try (ServerSocket s = new ServerSocket(0)) {
- port = s.getLocalPort();
- }
-
- // PROD is deterministic in a test JVM (Flash.DEV depends on env/system-property detection
- // that can't be forced per-test); STATIC's actual guarantee — that it never orchestrates,
- // in DEV or PROD — is enforced structurally by the same requiresOrchestration() gate in
- // both WebBundlerExtension.provide() and .routes(), not by this test.
- WebBundlerConfig config = WebBundlerConfig.builder()
+ app.install(new WebBundlerExtension(WebBundlerConfig.builder()
.runtimeMode(RuntimeMode.PROD)
.frontendType(FrontendType.STATIC)
.webRoot(webRoot)
- .build();
-
- app = FlashApp.create(port);
- app.install(new WebBundlerExtension(config));
+ .build()));
app.get("/api/ping", (req, res) -> "pong");
- app.start();
+ });
- HttpClient client = HttpClient.newHttpClient();
- HttpResponse backend = client.send(
- HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/api/ping")).GET().build(),
- HttpResponse.BodyHandlers.ofString()
- );
- assertEquals(200, backend.statusCode());
- assertEquals("pong", backend.body());
-
- HttpResponse asset = client.send(
- HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/style.css")).GET().build(),
- HttpResponse.BodyHandlers.ofString()
- );
- assertEquals(200, asset.statusCode());
- assertTrue(asset.body().contains("color:red"));
+ @Test
+ void prodMode_servesAssetsAndFallback_withoutBreakingBackendRoutes() {
+ managed.get("/api/ping").expectStatus(200).expectBody("pong");
+ managed.get("/app/app.js").expectStatus(200).expectBodyContains("console.log");
+ managed.get("/app/some/client/route").expectStatus(200).expectBodyContains("spa");
}
+ @Test
+ void staticFrontend_servesAssetsWithoutOrchestration() {
+ staticFrontend.get("/api/ping").expectStatus(200).expectBody("pong");
+ staticFrontend.get("/style.css").expectStatus(200).expectBodyContains("color:red");
+ }
+
+ /** Creates {@code directory} and writes the given name/content pairs into it. */
+ private static Path write(Path directory, String... nameThenContent) {
+ try {
+ Files.createDirectories(directory);
+ for (int i = 0; i < nameThenContent.length; i += 2)
+ Files.writeString(directory.resolve(nameThenContent[i]), nameThenContent[i + 1]);
+ return directory;
+ } catch (IOException failure) {
+ throw new UncheckedIOException(failure);
+ }
+ }
}