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); + } + } }