Feature/ext validation/request validation #15
@@ -33,5 +33,10 @@
|
|||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>flash-testing</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
+53
-94
@@ -1,122 +1,81 @@
|
|||||||
package dev.relism.flash.ext.webbundler;
|
package dev.relism.flash.ext.webbundler;
|
||||||
|
|
||||||
import dev.relism.flash.extension.FlashApp;
|
import dev.relism.flash.testing.FlashTest;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
import java.net.ServerSocket;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.io.UncheckedIOException;
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
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 {
|
class WebBundlerExtensionIntegrationTest {
|
||||||
|
|
||||||
@TempDir
|
@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
|
app.install(new WebBundlerExtension(WebBundlerConfig.builder()
|
||||||
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()
|
|
||||||
.runtimeMode(RuntimeMode.PROD)
|
.runtimeMode(RuntimeMode.PROD)
|
||||||
.operationMode(OperationMode.MANAGED)
|
.operationMode(OperationMode.MANAGED)
|
||||||
.webRoot(webRoot)
|
.webRoot(webRoot)
|
||||||
.assetsFromFilesystem(Path.of("dist"))
|
.assetsFromFilesystem(Path.of("dist"))
|
||||||
.basePath("/app")
|
.basePath("/app")
|
||||||
.build();
|
.build()));
|
||||||
|
|
||||||
app = FlashApp.create(port);
|
|
||||||
app.install(new WebBundlerExtension(config));
|
|
||||||
app.get("/api/ping", (req, res) -> "pong");
|
app.get("/api/ping", (req, res) -> "pong");
|
||||||
app.start();
|
});
|
||||||
|
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
// PROD is deterministic in a test JVM (Flash.DEV depends on env/system-property detection
|
||||||
HttpResponse<String> backend = client.send(
|
// that can't be forced per-test); STATIC's actual guarantee — that it never orchestrates,
|
||||||
HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/api/ping")).GET().build(),
|
// in DEV or PROD — is enforced structurally by the same requiresOrchestration() gate in
|
||||||
HttpResponse.BodyHandlers.ofString()
|
// both WebBundlerExtension.provide() and .routes(), not by this test.
|
||||||
);
|
@RegisterExtension
|
||||||
assertEquals(200, backend.statusCode());
|
static FlashTest staticFrontend = FlashTest.of(app -> {
|
||||||
assertEquals("pong", backend.body());
|
Path webRoot = write(tempDir.resolve("public"),
|
||||||
|
"index.html", "<html>static</html>",
|
||||||
|
"style.css", "body{color:red}");
|
||||||
|
|
||||||
HttpResponse<String> asset = client.send(
|
app.install(new WebBundlerExtension(WebBundlerConfig.builder()
|
||||||
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()
|
|
||||||
.runtimeMode(RuntimeMode.PROD)
|
.runtimeMode(RuntimeMode.PROD)
|
||||||
.frontendType(FrontendType.STATIC)
|
.frontendType(FrontendType.STATIC)
|
||||||
.webRoot(webRoot)
|
.webRoot(webRoot)
|
||||||
.build();
|
.build()));
|
||||||
|
|
||||||
app = FlashApp.create(port);
|
|
||||||
app.install(new WebBundlerExtension(config));
|
|
||||||
app.get("/api/ping", (req, res) -> "pong");
|
app.get("/api/ping", (req, res) -> "pong");
|
||||||
app.start();
|
});
|
||||||
|
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
@Test
|
||||||
HttpResponse<String> backend = client.send(
|
void prodMode_servesAssetsAndFallback_withoutBreakingBackendRoutes() {
|
||||||
HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/api/ping")).GET().build(),
|
managed.get("/api/ping").expectStatus(200).expectBody("pong");
|
||||||
HttpResponse.BodyHandlers.ofString()
|
managed.get("/app/app.js").expectStatus(200).expectBodyContains("console.log");
|
||||||
);
|
managed.get("/app/some/client/route").expectStatus(200).expectBodyContains("spa");
|
||||||
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user