Feature/ext validation/request validation #15

Merged
Relism merged 10 commits from feature/ext-validation/request-validation into master 2026-09-09 14:21:17 +00:00
2 changed files with 58 additions and 94 deletions
Showing only changes of commit 7785712efe - Show all commits
@@ -33,5 +33,10 @@
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>flash-testing</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -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", "<html>spa</html>",
"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"), "<html>spa</html>");
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<String> 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<String> 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<String> 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"), "<html>static</html>");
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()
@RegisterExtension
static FlashTest staticFrontend = FlashTest.of(app -> {
Path webRoot = write(tempDir.resolve("public"),
"index.html", "<html>static</html>",
"style.css", "body{color:red}");
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<String> 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<String> 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);
}
}
}