diff --git a/build.gradle b/build.gradle index 200995c..b01ff76 100644 --- a/build.gradle +++ b/build.gradle @@ -21,15 +21,26 @@ repositories { } dependencies { - implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' - implementation group: 'io.javalin', name: 'javalin', version: '3.13.11' - implementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.32' - implementation group: 'commons-io', name: 'commons-io', version: '2.8.0' + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'io.javalin:javalin:5.6.3' + implementation 'org.slf4j:slf4j-simple:2.0.9' + implementation 'commons-io:commons-io:2.15.1' + implementation 'org.jetbrains:annotations:24.1.0' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + testImplementation "org.junit.jupiter:junit-jupiter-params:5.9.2" + testImplementation 'io.javalin:javalin-testtools:5.6.3' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } java { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = 21 + targetCompatibility = 21 +} + +test { + useJUnitPlatform() } jar { @@ -43,9 +54,7 @@ jar { tasks.withType(JavaCompile).configureEach { it.options.encoding = "UTF-8" - if (JavaVersion.current().isJava9Compatible()) { - it.options.release = 8 - } + it.options.release = 21 } spotless { diff --git a/src/main/java/net/fabricmc/meta/FabricMeta.java b/src/main/java/net/fabricmc/meta/FabricMeta.java index e439a69..e80714f 100644 --- a/src/main/java/net/fabricmc/meta/FabricMeta.java +++ b/src/main/java/net/fabricmc/meta/FabricMeta.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import com.google.gson.stream.JsonReader; +import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -92,6 +93,16 @@ private static void update() { } } + @VisibleForTesting + public static void setupForTesting() { + if (configInitialized) { + return; + } + + configInitialized = true; + update(); + } + private static void updateHeartbeat() { if (heartbeatUrl == null) return; diff --git a/src/main/java/net/fabricmc/meta/web/EndpointsV1.java b/src/main/java/net/fabricmc/meta/web/EndpointsV1.java index c5e20cf..8bb14e3 100644 --- a/src/main/java/net/fabricmc/meta/web/EndpointsV1.java +++ b/src/main/java/net/fabricmc/meta/web/EndpointsV1.java @@ -35,14 +35,14 @@ public static void setup() { WebServer.jsonGet("/v1/versions", () -> FabricMeta.database); WebServer.jsonGet("/v1/versions/game", () -> FabricMeta.database.game); - WebServer.jsonGet("/v1/versions/game/:game_version", context -> filter(context, FabricMeta.database.game)); + WebServer.jsonGet("/v1/versions/game/{game_version}", context -> filter(context, FabricMeta.database.game)); WebServer.jsonGet("/v1/versions/mappings", () -> FabricMeta.database.mappings); - WebServer.jsonGet("/v1/versions/mappings/:game_version", context -> filter(context, FabricMeta.database.mappings)); + WebServer.jsonGet("/v1/versions/mappings/{game_version}", context -> filter(context, FabricMeta.database.mappings)); WebServer.jsonGet("/v1/versions/loader", () -> FabricMeta.database.getLoader()); - WebServer.jsonGet("/v1/versions/loader/:game_version", EndpointsV1::getLoaderInfoAll); - WebServer.jsonGet("/v1/versions/loader/:game_version/:loader_version", EndpointsV1::getLoaderInfo); + WebServer.jsonGet("/v1/versions/loader/{game_version}", EndpointsV1::getLoaderInfoAll); + WebServer.jsonGet("/v1/versions/loader/{game_version}/{loader_version}", EndpointsV1::getLoaderInfo); } private static > List filter(Context context, List versionList) { diff --git a/src/main/java/net/fabricmc/meta/web/EndpointsV2.java b/src/main/java/net/fabricmc/meta/web/EndpointsV2.java index 52e8f63..cb488c3 100644 --- a/src/main/java/net/fabricmc/meta/web/EndpointsV2.java +++ b/src/main/java/net/fabricmc/meta/web/EndpointsV2.java @@ -26,8 +26,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import io.javalin.core.util.Header; import io.javalin.http.Context; +import io.javalin.http.Header; import net.fabricmc.meta.FabricMeta; import net.fabricmc.meta.web.models.BaseVersion; @@ -46,14 +46,14 @@ public static void setup() { WebServer.jsonGet("/v2/versions/game/intermediary", () -> compatibleGameVersions(FabricMeta.database.intermediary, BaseVersion::getVersion, v -> new BaseVersion(v.getVersion(), v.isStable()))); WebServer.jsonGet("/v2/versions/yarn", context -> withLimitSkip(context, FabricMeta.database.mappings)); - WebServer.jsonGet("/v2/versions/yarn/:game_version", context -> withLimitSkip(context, filter(context, FabricMeta.database.mappings))); + WebServer.jsonGet("/v2/versions/yarn/{game_version}", context -> withLimitSkip(context, filter(context, FabricMeta.database.mappings))); WebServer.jsonGet("/v2/versions/intermediary", () -> FabricMeta.database.intermediary); - WebServer.jsonGet("/v2/versions/intermediary/:game_version", context -> filter(context, FabricMeta.database.intermediary)); + WebServer.jsonGet("/v2/versions/intermediary/{game_version}", context -> filter(context, FabricMeta.database.intermediary)); WebServer.jsonGet("/v2/versions/loader", context -> withLimitSkip(context, FabricMeta.database.getLoader())); - WebServer.jsonGet("/v2/versions/loader/:game_version", context -> withLimitSkip(context, EndpointsV2.getLoaderInfoAll(context))); - WebServer.jsonGet("/v2/versions/loader/:game_version/:loader_version", EndpointsV2::getLoaderInfo); + WebServer.jsonGet("/v2/versions/loader/{game_version}", context -> withLimitSkip(context, EndpointsV2.getLoaderInfoAll(context))); + WebServer.jsonGet("/v2/versions/loader/{game_version}/{loader_version}", EndpointsV2::getLoaderInfo); WebServer.jsonGet("/v2/versions/installer", context -> withLimitSkip(context, FabricMeta.database.installer)); @@ -66,8 +66,8 @@ private static List withLimitSkip(Context context, List list) { return Collections.emptyList(); } - int limit = context.queryParam("limit", Integer.class, "0").check(i -> i >= 0).get(); - int skip = context.queryParam("skip", Integer.class, "0").check(i -> i >= 0).get(); + int limit = context.queryParamAsClass("limit", Integer.class).check(i -> i >= 0, "limit must be larger than one").getOrDefault(0); + int skip = context.queryParamAsClass("skip", Integer.class).check(i -> i >= 0, "skip must be larger than one").getOrDefault(0); Stream listStream = list.stream().skip(skip); @@ -157,7 +157,7 @@ private static List compatibleGameVersions( } public static void fileDownload(String path, String ext, Function fileNameFunction, Function> streamSupplier) { - WebServer.javalin.get("/v2/versions/loader/:game_version/:loader_version/" + path + "/" + ext, ctx -> { + WebServer.javalin.get("/v2/versions/loader/{game_version}/{loader_version}/" + path + "/" + ext, ctx -> { Object obj = getLoaderInfo(ctx); if (obj instanceof String) { @@ -165,8 +165,6 @@ public static void fileDownload(String path, String ext, Function streamFuture = streamSupplier.apply(versionInfo); - if (ext.equals("zip")) { //Set the filename to download ctx.header(Header.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", fileNameFunction.apply(versionInfo))); @@ -179,7 +177,7 @@ public static void fileDownload(String path, String ext, Function streamSupplier.apply(versionInfo).thenApply(ctx::result)); } else { ctx.result("An internal error occurred"); } diff --git a/src/main/java/net/fabricmc/meta/web/ServerBootstrap.java b/src/main/java/net/fabricmc/meta/web/ServerBootstrap.java index 00f213b..2f58149 100644 --- a/src/main/java/net/fabricmc/meta/web/ServerBootstrap.java +++ b/src/main/java/net/fabricmc/meta/web/ServerBootstrap.java @@ -33,10 +33,10 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; -import io.javalin.core.util.Header; import io.javalin.http.BadRequestResponse; import io.javalin.http.Context; import io.javalin.http.Handler; +import io.javalin.http.Header; import io.javalin.http.InternalServerErrorResponse; import org.apache.commons.io.FileUtils; @@ -50,7 +50,7 @@ public class ServerBootstrap { public static void setup() { // http://localhost:5555/v2/versions/loader/1.17.1/0.12.0/0.8.0/server/jar - WebServer.javalin.get("/v2/versions/loader/:game_version/:loader_version/:installer_version/server/jar", boostrapHandler()); + WebServer.javalin.get("/v2/versions/loader/{game_version}/{loader_version}/{installer_version}/server/jar", boostrapHandler()); } private static Handler boostrapHandler() { @@ -74,7 +74,7 @@ private static Handler boostrapHandler() { ctx.header(Header.CACHE_CONTROL, cacheControl); ctx.contentType("application/java-archive"); - ctx.result(getResultStream(installerVersion, gameVersion, loaderVersion)); + ctx.future(() -> getResultStream(installerVersion, gameVersion, loaderVersion).thenApply(ctx::result)); }; } diff --git a/src/main/java/net/fabricmc/meta/web/WebServer.java b/src/main/java/net/fabricmc/meta/web/WebServer.java index 97c9657..aa514b5 100644 --- a/src/main/java/net/fabricmc/meta/web/WebServer.java +++ b/src/main/java/net/fabricmc/meta/web/WebServer.java @@ -22,23 +22,34 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.javalin.Javalin; -import io.javalin.core.util.Header; -import io.javalin.core.util.RouteOverviewPlugin; import io.javalin.http.Context; +import io.javalin.http.Header; +import io.javalin.plugin.bundled.CorsPluginConfig; public class WebServer { public static Javalin javalin; public static Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - public static void start() { + public static Javalin create() { + if (javalin != null) { + javalin.stop(); + } + javalin = Javalin.create(config -> { - config.registerPlugin(new RouteOverviewPlugin("/")); + config.plugins.enableRouteOverview("/"); config.showJavalinBanner = false; - config.enableCorsForAllOrigins(); - }).start(5555); + config.plugins.enableCors(cors -> cors.add(CorsPluginConfig::anyHost)); + }); EndpointsV1.setup(); EndpointsV2.setup(); + + return javalin; + } + + public static void start() { + assert javalin == null; + create().start(5555); } public static void jsonGet(String route, Supplier supplier) { diff --git a/src/test/java/net/fabricmc/meta/test/integration/ComparisonTests.java b/src/test/java/net/fabricmc/meta/test/integration/ComparisonTests.java new file mode 100644 index 0000000..f8b21d3 --- /dev/null +++ b/src/test/java/net/fabricmc/meta/test/integration/ComparisonTests.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.meta.test.integration; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.stream.Stream; + +import io.javalin.testtools.HttpClient; +import io.javalin.testtools.JavalinTest; +import okhttp3.Response; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import net.fabricmc.meta.FabricMeta; +import net.fabricmc.meta.web.WebServer; + +// Tests that the local response matches the remote response of the version in prod +public class ComparisonTests { + private static final String REMOTE_FABRIC_META_URL = "https://meta.fabricmc.net"; + + @BeforeAll + static void beforeAll() { + FabricMeta.setupForTesting(); + } + + public static Stream provideEndpoints() { + return Stream.of( + // V1 + "/v1/versions", + "/v1/versions/game", + "/v1/versions/game/1.14.4", + "/v1/versions/mappings", + "/v1/versions/mappings/1.16.5", + "/v1/versions/loader", + "/v1/versions/loader/1.20.4", + "/v1/versions/loader/1.20.4/0.15.2", + + // V2 + "/v2/versions", + "/v2/versions/game", + "/v2/versions/game/yarn", + "/v2/versions/game/intermediary", + "/v2/versions/yarn", + "/v2/versions/yarn/1.20.4", + "/v2/versions/intermediary", + "/v2/versions/intermediary/1.20.4", + "/v2/versions/loader", + "/v2/versions/loader?limit=5", + "/v2/versions/loader?limit=5&skip=5", + // Disabled as this forces all the load metadata to be downloaded, timing out the test. + //"/v2/versions/loader/1.20.4", + "/v2/versions/loader/1.20.4/0.15.2", + "/v2/versions/installer" + // Disabled as this includes the release time, and is not stable + //"/v2/versions/loader/1.20.4/0.15.2/profile/json" + ).map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("provideEndpoints") + void compareEndpoint(String endpoint) { + JavalinTest.test(WebServer.create(), (server, client) -> { + compareEndpoint(endpoint, client); + }); + } + + private static void compareEndpoint(String endpoint, HttpClient client) throws Exception { + Response response = client.get(endpoint); + assertEquals(200, response.code()); + String localResponse = response.body().string(); + + String remoteResponse = getRemoteEndpoint(endpoint); + assertEquals(remoteResponse, localResponse); + } + + private static String getRemoteEndpoint(String endpoint) throws Exception { + try (var httpClient = java.net.http.HttpClient.newHttpClient()) { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(REMOTE_FABRIC_META_URL + endpoint)) + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + return response.body(); + } + } +} diff --git a/src/test/java/net/fabricmc/meta/test/unit/EndpointsV2Tests.java b/src/test/java/net/fabricmc/meta/test/unit/EndpointsV2Tests.java new file mode 100644 index 0000000..915000d --- /dev/null +++ b/src/test/java/net/fabricmc/meta/test/unit/EndpointsV2Tests.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.meta.test.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.javalin.testtools.JavalinTest; +import okhttp3.Response; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import net.fabricmc.meta.FabricMeta; +import net.fabricmc.meta.web.WebServer; + +public class EndpointsV2Tests { + @BeforeAll + static void beforeAll() { + // TODO provide a way to pass in dummy data for constant test results + FabricMeta.setupForTesting(); + } + + @Test + void versions() { + JavalinTest.test(WebServer.create(), (server, client) -> { + Response response = client.get("/v2/versions"); + assertEquals(200, response.code()); + String body = response.body().string(); + }); + } +} diff --git a/src/test/java/net/fabricmc/meta/test/unit/ServerBootstrapTests.java b/src/test/java/net/fabricmc/meta/test/unit/ServerBootstrapTests.java new file mode 100644 index 0000000..91bb015 --- /dev/null +++ b/src/test/java/net/fabricmc/meta/test/unit/ServerBootstrapTests.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.meta.test.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import io.javalin.testtools.JavalinTest; +import okhttp3.Response; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import net.fabricmc.meta.FabricMeta; +import net.fabricmc.meta.web.WebServer; + +public class ServerBootstrapTests { + @TempDir + static Path tempDir; + + @BeforeAll + static void beforeAll() { + // TODO provide a way to pass in dummy data for constant test results + FabricMeta.setupForTesting(); + } + + @Test + void serverJar() { + JavalinTest.test(WebServer.create(), (server, client) -> { + Response response = client.get("/v2/versions/loader/stable/stable/stable/server/jar"); + assertEquals(200, response.code()); + Path jarFile = tempDir.resolve("server.jar"); + Files.copy(response.body().byteStream(), jarFile); + assertTrue(Files.size(jarFile) > 0); + }); + } +}