mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2025-01-24 17:11:43 +01:00
Implemented support for subdirectory addresses
Export of React version of frontend now supports exporting to a subdirectory So now you can access exported site at /plan/... if it is hosted there. This might impact reverse proxy setups positively, but that has not yet been tested. The hypothetical positive impact is the inclusion of subdirectory in the React-router configuration, since now it can handle the reverse-proxy subdirectory in URL.
This commit is contained in:
parent
ac2fa2ecce
commit
5082f80030
@ -126,7 +126,7 @@ public class Exporter extends FileExporter {
|
||||
if (config.isFalse(ExportSettings.PLAYER_PAGES)) return false;
|
||||
|
||||
try {
|
||||
playerPageExporter.export(toDirectory, playerUUID, playerName);
|
||||
playerPageExporter.export(toDirectory, playerUUID);
|
||||
return true;
|
||||
} catch (IOException | NotFoundException e) {
|
||||
throw new ExportException("Failed to export player: " + playerName + ", " + e.toString(), e);
|
||||
|
@ -16,8 +16,12 @@
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.export;
|
||||
|
||||
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
|
||||
import com.djrapitops.plan.delivery.rendering.html.Html;
|
||||
import com.djrapitops.plan.delivery.web.resource.WebResource;
|
||||
import com.djrapitops.plan.settings.config.PlanConfig;
|
||||
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
|
||||
import com.djrapitops.plan.storage.file.PlanFiles;
|
||||
import com.djrapitops.plan.storage.file.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@ -99,4 +103,19 @@ abstract class FileExporter {
|
||||
);
|
||||
}
|
||||
|
||||
void exportReactRedirects(Path toDirectory, PlanFiles files, PlanConfig config, String[] redirections) throws IOException {
|
||||
String redirectPageHtml = files.getResourceFromJar("web/export-redirect.html").asString();
|
||||
PlaceholderReplacer placeholderReplacer = new PlaceholderReplacer();
|
||||
placeholderReplacer.put("PLAN_ADDRESS", config.get(WebserverSettings.EXTERNAL_LINK));
|
||||
redirectPageHtml = placeholderReplacer.apply(redirectPageHtml);
|
||||
|
||||
for (String redirection : redirections) {
|
||||
exportReactRedirect(toDirectory, redirectPageHtml, redirection);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportReactRedirect(Path toDirectory, String redirectHtml, String path) throws IOException {
|
||||
export(toDirectory.resolve(path).resolve("index.html"), redirectHtml);
|
||||
}
|
||||
|
||||
}
|
@ -118,23 +118,24 @@ public class NetworkPageExporter extends FileExporter {
|
||||
export(to, exportPaths.resolveExportPaths(html));
|
||||
}
|
||||
|
||||
public static String[] getRedirections() {
|
||||
return new String[]{
|
||||
"network",
|
||||
"network/overview",
|
||||
"network/serversOverview",
|
||||
"network/sessions",
|
||||
"network/playerbase",
|
||||
"network/join-addresses",
|
||||
"network/players",
|
||||
"network/geolocations",
|
||||
"network/plugins-overview",
|
||||
};
|
||||
}
|
||||
|
||||
private void exportReactRedirects(Path toDirectory) throws IOException {
|
||||
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
||||
|
||||
Resource redirect = files.getResourceFromJar("web/export-redirect.html");
|
||||
exportReactRedirect(toDirectory, redirect, "network");
|
||||
exportReactRedirect(toDirectory, redirect, "network/overview");
|
||||
exportReactRedirect(toDirectory, redirect, "network/serversOverview");
|
||||
exportReactRedirect(toDirectory, redirect, "network/sessions");
|
||||
exportReactRedirect(toDirectory, redirect, "network/playerbase");
|
||||
exportReactRedirect(toDirectory, redirect, "network/join-addresses");
|
||||
exportReactRedirect(toDirectory, redirect, "network/players");
|
||||
exportReactRedirect(toDirectory, redirect, "network/geolocations");
|
||||
exportReactRedirect(toDirectory, redirect, "network/plugins-overview");
|
||||
}
|
||||
|
||||
private void exportReactRedirect(Path toDirectory, Resource redirectHtml, String path) throws IOException {
|
||||
export(toDirectory.resolve(path).resolve("index.html"), redirectHtml.asString());
|
||||
exportReactRedirects(toDirectory, files, config, getRedirections());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,11 +91,10 @@ public class PlayerPageExporter extends FileExporter {
|
||||
*
|
||||
* @param toDirectory Path to Export directory
|
||||
* @param playerUUID UUID of the player
|
||||
* @param playerName Name of the player
|
||||
* @throws IOException If a template can not be read from jar/disk or the result written
|
||||
* @throws NotFoundException If a file or resource that is being exported can not be found
|
||||
*/
|
||||
public void export(Path toDirectory, UUID playerUUID, String playerName) throws IOException {
|
||||
public void export(Path toDirectory, UUID playerUUID) throws IOException {
|
||||
Database.State dbState = dbSystem.getDatabase().getState();
|
||||
if (dbState == Database.State.CLOSED || dbState == Database.State.CLOSING) return;
|
||||
if (Boolean.FALSE.equals(dbSystem.getDatabase().query(PlayerFetchQueries.isPlayerRegistered(playerUUID)))) {
|
||||
@ -130,14 +129,7 @@ public class PlayerPageExporter extends FileExporter {
|
||||
private void exportReactRedirects(Path toDirectory, UUID playerUUID) throws IOException {
|
||||
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
||||
|
||||
Resource redirectPage = files.getResourceFromJar("web/export-redirect.html");
|
||||
for (String redirection : getRedirections(playerUUID)) {
|
||||
exportReactRedirect(toDirectory, redirectPage, redirection);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportReactRedirect(Path toDirectory, Resource redirectHtml, String path) throws IOException {
|
||||
export(toDirectory.resolve(path).resolve("index.html"), redirectHtml.asString());
|
||||
exportReactRedirects(toDirectory, files, config, getRedirections(playerUUID));
|
||||
}
|
||||
|
||||
private void exportJSON(ExportPaths exportPaths, Path toDirectory, UUID playerUUID) throws IOException {
|
||||
|
@ -118,12 +118,8 @@ public class PlayersPageExporter extends FileExporter {
|
||||
private void exportReactRedirects(Path toDirectory) throws IOException {
|
||||
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
||||
|
||||
Resource redirect = files.getResourceFromJar("web/export-redirect.html");
|
||||
exportReactRedirect(toDirectory, redirect, "players");
|
||||
}
|
||||
|
||||
private void exportReactRedirect(Path toDirectory, Resource redirectHtml, String path) throws IOException {
|
||||
export(toDirectory.resolve(path).resolve("index.html"), redirectHtml.asString());
|
||||
String[] redirections = {"players"};
|
||||
exportReactRedirects(toDirectory, files, config, redirections);
|
||||
}
|
||||
|
||||
private void exportJSON(Path toDirectory) throws IOException {
|
||||
|
@ -67,7 +67,7 @@ public class ReactExporter extends FileExporter {
|
||||
}
|
||||
|
||||
public void exportReactFiles(Path toDirectory) throws IOException {
|
||||
exportAsset(toDirectory, "index.html");
|
||||
exportIndexHtml(toDirectory);
|
||||
exportAsset(toDirectory, "asset-manifest.json");
|
||||
exportAsset(toDirectory, "favicon.ico");
|
||||
exportAsset(toDirectory, "logo192.png");
|
||||
@ -101,11 +101,16 @@ public class ReactExporter extends FileExporter {
|
||||
for (String path : paths) {
|
||||
Path to = toDirectory.resolve(path);
|
||||
Resource resource = files.getResourceFromJar("web/" + path);
|
||||
if (path.endsWith(".js")) {
|
||||
// Make static asset loading work with subdirectory addresses
|
||||
if (path.endsWith(".css") || "asset-manifest.json".equals(path)) {
|
||||
String contents = resource.asString();
|
||||
String withReplacedStatic = StringUtils.replace(contents, "/static", getBasePath() + "/static");
|
||||
export(to, withReplacedStatic);
|
||||
} else if (path.endsWith(".js")) {
|
||||
String withReplacedConstants = StringUtils.replaceEach(
|
||||
resource.asString(),
|
||||
new String[]{"PLAN_BASE_ADDRESS", "PLAN_EXPORTED_VERSION"},
|
||||
new String[]{config.get(WebserverSettings.EXTERNAL_LINK), "true"}
|
||||
new String[]{"PLAN_BASE_ADDRESS", "PLAN_EXPORTED_VERSION", "n.p=\"/\""},
|
||||
new String[]{config.get(WebserverSettings.EXTERNAL_LINK), "true", "n.p=\"" + getBasePath() + "/\""}
|
||||
);
|
||||
export(to, withReplacedConstants);
|
||||
} else {
|
||||
@ -131,6 +136,26 @@ public class ReactExporter extends FileExporter {
|
||||
}
|
||||
}
|
||||
|
||||
private void exportIndexHtml(Path toDirectory) throws IOException {
|
||||
String contents = files.getResourceFromJar("web/index.html")
|
||||
.asString();
|
||||
String basePath = getBasePath();
|
||||
contents = StringUtils.replace(contents, "/static", basePath + "/static");
|
||||
|
||||
export(toDirectory.resolve("index.html"), contents);
|
||||
}
|
||||
|
||||
private String getBasePath() {
|
||||
String basePath = config.get(WebserverSettings.EXTERNAL_LINK)
|
||||
.replace("http://", "")
|
||||
.replace("https://", "");
|
||||
if (StringUtils.contains(basePath, '/')) {
|
||||
return basePath.substring(StringUtils.indexOf(basePath, '/'));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private void exportAsset(Path toDirectory, String asset) throws IOException {
|
||||
export(toDirectory.resolve(asset), files.getResourceFromJar("web/" + asset));
|
||||
}
|
||||
|
@ -152,14 +152,7 @@ public class ServerPageExporter extends FileExporter {
|
||||
private void exportReactRedirects(Path toDirectory, ServerUUID serverUUID) throws IOException {
|
||||
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
||||
|
||||
Resource redirectPage = files.getResourceFromJar("web/export-redirect.html");
|
||||
for (String redirection : getRedirections(serverUUID)) {
|
||||
exportReactRedirect(toDirectory, redirectPage, redirection);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportReactRedirect(Path toDirectory, Resource redirectHtml, String path) throws IOException {
|
||||
export(toDirectory.resolve(path).resolve("index.html"), redirectHtml.asString());
|
||||
exportReactRedirects(toDirectory, files, config, getRedirections(serverUUID));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,17 @@
|
||||
<meta content="AuroraLS3" name="author">
|
||||
<meta content="noindex, nofollow" name="robots">
|
||||
<title>Plan | Player Analytics</title>
|
||||
<script>window.location.href = `/?redirect=${encodeURIComponent(window.location.pathname + window.location.hash + window.location.search)}`</script>
|
||||
<script>
|
||||
const address = `${PLAN_ADDRESS}`;
|
||||
const currentAddress = window.location.pathname + window.location.hash + window.location.search;
|
||||
let basePath = address.replace("http://", "")
|
||||
.replace("https://", "");
|
||||
if (basePath.includes('/')) {
|
||||
basePath = basePath.substring(basePath.indexOf('/') + 1);
|
||||
}
|
||||
const redirectTo = currentAddress.replace(basePath, '');
|
||||
window.location.href = address + `/?redirect=${encodeURIComponent(redirectTo)}`;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>Please enable javascript.</noscript>
|
||||
|
@ -17,49 +17,34 @@
|
||||
package com.djrapitops.plan.delivery.export;
|
||||
|
||||
import com.djrapitops.plan.PlanSystem;
|
||||
import com.djrapitops.plan.gathering.domain.DataMap;
|
||||
import com.djrapitops.plan.gathering.domain.FinishedSession;
|
||||
import com.djrapitops.plan.identification.ServerUUID;
|
||||
import com.djrapitops.plan.settings.config.PlanConfig;
|
||||
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
|
||||
import com.djrapitops.plan.settings.config.paths.ExportSettings;
|
||||
import com.djrapitops.plan.settings.config.paths.PluginSettings;
|
||||
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
|
||||
import com.djrapitops.plan.storage.database.Database;
|
||||
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.PlayerRegisterTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.StoreSessionTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.StoreWorldNameTransaction;
|
||||
import com.djrapitops.plan.storage.file.PlanFiles;
|
||||
import com.djrapitops.plan.utilities.java.Lists;
|
||||
import extension.FullSystemExtension;
|
||||
import extension.SeleniumExtension;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.*;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.logging.LogEntry;
|
||||
import org.openqa.selenium.logging.LogType;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.shaded.org.apache.commons.io.FileUtils;
|
||||
import org.testcontainers.shaded.org.awaitility.Awaitility;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
import utilities.RandomData;
|
||||
import utilities.TestConstants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static com.djrapitops.plan.delivery.export.ExportTestUtilities.*;
|
||||
|
||||
/**
|
||||
* @author AuroraLS3
|
||||
@ -98,23 +83,7 @@ class ExportRegressionTest {
|
||||
system.enable();
|
||||
serverUUID = system.getServerInfo().getServerUUID();
|
||||
savePlayerData(system.getDatabaseSystem().getDatabase(), serverUUID);
|
||||
export(system.getExportSystem().getExporter(), system.getDatabaseSystem().getDatabase());
|
||||
}
|
||||
|
||||
private static void export(Exporter exporter, Database database) throws Exception {
|
||||
exporter.exportReact();
|
||||
assertTrue(exporter.exportServerPage(database.query(ServerQueries.fetchServerMatchingIdentifier(serverUUID.toString()))
|
||||
.orElseThrow(AssertionError::new)));
|
||||
assertTrue(exporter.exportPlayersPage());
|
||||
assertTrue(exporter.exportPlayerPage(TestConstants.PLAYER_ONE_UUID, TestConstants.PLAYER_ONE_NAME));
|
||||
}
|
||||
|
||||
private static void savePlayerData(Database database, ServerUUID serverUUID) {
|
||||
UUID uuid = TestConstants.PLAYER_ONE_UUID;
|
||||
database.executeTransaction(new PlayerRegisterTransaction(uuid, RandomData::randomTime, TestConstants.PLAYER_ONE_NAME));
|
||||
FinishedSession session = new FinishedSession(uuid, serverUUID, 1000L, 11000L, 500L, new DataMap());
|
||||
database.executeTransaction(new StoreWorldNameTransaction(serverUUID, "world"));
|
||||
database.executeTransaction(new StoreSessionTransaction(session));
|
||||
export(system.getExportSystem().getExporter(), system.getDatabaseSystem().getDatabase(), serverUUID);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
@ -124,54 +93,21 @@ class ExportRegressionTest {
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearExportDirectory(WebDriver driver) {
|
||||
void clearBrowserConsole(WebDriver driver) {
|
||||
SeleniumExtension.newTab(driver);
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Collection<DynamicTest> exportedWebpageDoesNotHaveErrors(ChromeDriver driver) throws Exception {
|
||||
List<String> endpointsToTest = Lists.builder(String.class)
|
||||
.add("/")
|
||||
.addAll(ServerPageExporter.getRedirections(serverUUID))
|
||||
.addAll(PlayerPageExporter.getRedirections(TestConstants.PLAYER_ONE_UUID))
|
||||
.add("/players")
|
||||
.build();
|
||||
Collection<DynamicTest> exportedWebpageDoesNotHaveErrors(ChromeDriver driver) {
|
||||
List<String> endpointsToTest = getEndpointsToTest(serverUUID);
|
||||
|
||||
return endpointsToTest.stream().map(
|
||||
endpoint -> DynamicTest.dynamicTest("Exported page does not log errors to js console " + endpoint, () -> {
|
||||
|
||||
String address = "http://" + webserver.getHost() + ":" + webserver.getMappedPort(8080)
|
||||
+ (endpoint.startsWith("/") ? endpoint : '/' + endpoint);
|
||||
System.out.println("GET: " + address);
|
||||
driver.get(address);
|
||||
|
||||
new WebDriverWait(driver, Duration.of(10, ChronoUnit.SECONDS)).until(
|
||||
webDriver -> ((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete"));
|
||||
Awaitility.await()
|
||||
.atMost(Duration.of(10, ChronoUnit.SECONDS))
|
||||
.until(() -> getElement(driver).map(WebElement::isDisplayed).orElse(false));
|
||||
|
||||
List<LogEntry> logs = new ArrayList<>();
|
||||
logs.addAll(driver.manage().logs().get(LogType.CLIENT).getAll());
|
||||
logs.addAll(driver.manage().logs().get(LogType.BROWSER).getAll());
|
||||
|
||||
List<LogEntry> logs = getLogsAfterRequestToAddress(driver, address);
|
||||
assertNoLogs(logs);
|
||||
})
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Optional<WebElement> getElement(ChromeDriver driver) {
|
||||
try {
|
||||
return Optional.of(driver.findElement(By.className("load-in")));
|
||||
} catch (NoSuchElementException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void assertNoLogs(List<LogEntry> logs) {
|
||||
List<String> loggedLines = logs.stream()
|
||||
.map(log -> "\n" + log.getLevel().getName() + " " + log.getMessage())
|
||||
.toList();
|
||||
assertTrue(loggedLines.isEmpty(), () -> "Browser console included " + loggedLines.size() + " logs: " + loggedLines);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.export;
|
||||
|
||||
import com.djrapitops.plan.PlanSystem;
|
||||
import com.djrapitops.plan.identification.ServerUUID;
|
||||
import com.djrapitops.plan.settings.config.PlanConfig;
|
||||
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
|
||||
import com.djrapitops.plan.settings.config.paths.ExportSettings;
|
||||
import com.djrapitops.plan.settings.config.paths.PluginSettings;
|
||||
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
|
||||
import com.djrapitops.plan.storage.file.PlanFiles;
|
||||
import extension.FullSystemExtension;
|
||||
import extension.SeleniumExtension;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.logging.LogEntry;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.shaded.org.apache.commons.io.FileUtils;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.djrapitops.plan.delivery.export.ExportTestUtilities.*;
|
||||
|
||||
/**
|
||||
* Tests exported website when exported to /plan/...
|
||||
*
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
@ExtendWith(SeleniumExtension.class)
|
||||
@ExtendWith(FullSystemExtension.class)
|
||||
class ExportSubdirRegressionTest {
|
||||
|
||||
public static GenericContainer<?> webserver;
|
||||
private static Path exportDir;
|
||||
private static ServerUUID serverUUID;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp(PlanFiles files, PlanConfig config, PlanSystem system) throws Exception {
|
||||
exportDir = files.getDataDirectory().resolve("export");
|
||||
Files.createDirectories(exportDir.resolve("plan"));
|
||||
System.out.println("Export to " + exportDir.resolve("plan").toFile().getAbsolutePath());
|
||||
|
||||
webserver = new GenericContainer<>(DockerImageName.parse("halverneus/static-file-server:latest"))
|
||||
.withExposedPorts(8080)
|
||||
.withFileSystemBind(exportDir.toFile().getAbsolutePath(), "/web")
|
||||
.waitingFor(new HttpWaitStrategy());
|
||||
webserver.start();
|
||||
|
||||
config.set(PluginSettings.FRONTEND_BETA, true);
|
||||
config.set(WebserverSettings.DISABLED, true);
|
||||
config.set(WebserverSettings.EXTERNAL_LINK, "http://" + webserver.getHost() + ":" + webserver.getMappedPort(8080) + "/plan");
|
||||
// Avoid accidentally DDoS:ing head image service during tests.
|
||||
config.set(DisplaySettings.PLAYER_HEAD_IMG_URL, "data:image/png;base64,AA==");
|
||||
// Using .resolve("plan") here to export to /web/plan
|
||||
config.set(ExportSettings.HTML_EXPORT_PATH, exportDir.resolve("plan").toFile().getAbsolutePath());
|
||||
config.set(ExportSettings.SERVER_PAGE, true);
|
||||
config.set(ExportSettings.PLAYERS_PAGE, true);
|
||||
config.set(ExportSettings.PLAYER_PAGES, true);
|
||||
|
||||
system.enable();
|
||||
serverUUID = system.getServerInfo().getServerUUID();
|
||||
savePlayerData(system.getDatabaseSystem().getDatabase(), serverUUID);
|
||||
export(system.getExportSystem().getExporter(), system.getDatabaseSystem().getDatabase(), serverUUID);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown(PlanSystem system) throws IOException {
|
||||
system.disable();
|
||||
FileUtils.cleanDirectory(exportDir.toFile());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearBrowserConsole(WebDriver driver) {
|
||||
SeleniumExtension.newTab(driver);
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Collection<DynamicTest> exportedWebpageDoesNotHaveErrors(ChromeDriver driver) {
|
||||
List<String> endpointsToTest = getEndpointsToTest(serverUUID);
|
||||
|
||||
return endpointsToTest.stream().map(
|
||||
endpoint -> DynamicTest.dynamicTest("Exported page does not log errors to js console " + endpoint, () -> {
|
||||
String address = "http://" + webserver.getHost() + ":" + webserver.getMappedPort(8080) + "/plan"
|
||||
+ (endpoint.startsWith("/") ? endpoint : '/' + endpoint);
|
||||
List<LogEntry> logs = getLogsAfterRequestToAddress(driver, address);
|
||||
assertNoLogsExceptFaviconError(logs);
|
||||
})
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.export;
|
||||
|
||||
import com.djrapitops.plan.gathering.domain.DataMap;
|
||||
import com.djrapitops.plan.gathering.domain.FinishedSession;
|
||||
import com.djrapitops.plan.identification.ServerUUID;
|
||||
import com.djrapitops.plan.storage.database.Database;
|
||||
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.PlayerRegisterTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.StoreSessionTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.StoreWorldNameTransaction;
|
||||
import com.djrapitops.plan.utilities.java.Lists;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.logging.LogEntry;
|
||||
import org.openqa.selenium.logging.LogType;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
import org.testcontainers.shaded.org.awaitility.Awaitility;
|
||||
import utilities.RandomData;
|
||||
import utilities.TestConstants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
class ExportTestUtilities {
|
||||
|
||||
private ExportTestUtilities() {
|
||||
/* Static utility method class */
|
||||
}
|
||||
|
||||
static void assertNoLogs(List<LogEntry> logs) {
|
||||
List<String> loggedLines = logs.stream()
|
||||
.map(log -> "\n" + log.getLevel().getName() + " " + log.getMessage())
|
||||
.toList();
|
||||
assertTrue(loggedLines.isEmpty(), () -> "Browser console included " + loggedLines.size() + " logs: " + loggedLines);
|
||||
}
|
||||
|
||||
static void assertNoLogsExceptFaviconError(List<LogEntry> logs) {
|
||||
List<String> loggedLines = logs.stream()
|
||||
.map(log -> "\n" + log.getLevel().getName() + " " + log.getMessage())
|
||||
.filter(log -> !log.contains("favicon.ico") && !log.contains("manifest.json"))
|
||||
.toList();
|
||||
assertTrue(loggedLines.isEmpty(), () -> "Browser console included " + loggedLines.size() + " logs: " + loggedLines);
|
||||
}
|
||||
|
||||
static Optional<WebElement> getElement(ChromeDriver driver) {
|
||||
try {
|
||||
return Optional.of(driver.findElement(By.className("load-in")));
|
||||
} catch (NoSuchElementException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
static List<LogEntry> getLogsAfterRequestToAddress(ChromeDriver driver, String address) {
|
||||
System.out.println("GET: " + address);
|
||||
driver.get(address);
|
||||
|
||||
new WebDriverWait(driver, Duration.of(10, ChronoUnit.SECONDS)).until(
|
||||
webDriver -> ((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete"));
|
||||
Awaitility.await()
|
||||
.atMost(Duration.of(10, ChronoUnit.SECONDS))
|
||||
.until(() -> getElement(driver).map(WebElement::isDisplayed).orElse(false));
|
||||
|
||||
List<LogEntry> logs = new ArrayList<>();
|
||||
logs.addAll(driver.manage().logs().get(LogType.CLIENT).getAll());
|
||||
logs.addAll(driver.manage().logs().get(LogType.BROWSER).getAll());
|
||||
return logs;
|
||||
}
|
||||
|
||||
static void export(Exporter exporter, Database database, ServerUUID serverUUID) throws Exception {
|
||||
exporter.exportReact();
|
||||
assertTrue(exporter.exportServerPage(database.query(ServerQueries.fetchServerMatchingIdentifier(serverUUID.toString()))
|
||||
.orElseThrow(AssertionError::new)));
|
||||
assertTrue(exporter.exportPlayersPage());
|
||||
assertTrue(exporter.exportPlayerPage(TestConstants.PLAYER_ONE_UUID, TestConstants.PLAYER_ONE_NAME));
|
||||
}
|
||||
|
||||
static void savePlayerData(Database database, ServerUUID serverUUID) {
|
||||
UUID uuid = TestConstants.PLAYER_ONE_UUID;
|
||||
database.executeTransaction(new PlayerRegisterTransaction(uuid, RandomData::randomTime, TestConstants.PLAYER_ONE_NAME));
|
||||
FinishedSession session = new FinishedSession(uuid, serverUUID, 1000L, 11000L, 500L, new DataMap());
|
||||
database.executeTransaction(new StoreWorldNameTransaction(serverUUID, "world"));
|
||||
database.executeTransaction(new StoreSessionTransaction(session));
|
||||
}
|
||||
|
||||
static List<String> getEndpointsToTest(ServerUUID serverUUID) {
|
||||
return Lists.builder(String.class)
|
||||
.add("/")
|
||||
.addAll(ServerPageExporter.getRedirections(serverUUID))
|
||||
.addAll(PlayerPageExporter.getRedirections(TestConstants.PLAYER_ONE_UUID))
|
||||
.add("/players")
|
||||
.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused") // Test debugging method
|
||||
static void logExportDirectoryContents(Path directory) throws IOException {
|
||||
System.out.println("Contents of " + directory);
|
||||
try (Stream<Path> walk = Files.walk(directory)) {
|
||||
walk.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -12,7 +12,7 @@ import {MetadataContextProvider} from "./hooks/metadataHook";
|
||||
import {AuthenticationContextProvider} from "./hooks/authenticationHook";
|
||||
import {NavigationContextProvider} from "./hooks/navigationHook";
|
||||
import MainPageRedirect from "./components/navigation/MainPageRedirect";
|
||||
import {staticSite} from "./service/backendConfiguration";
|
||||
import {baseAddress, staticSite} from "./service/backendConfiguration";
|
||||
|
||||
const PlayerPage = React.lazy(() => import("./views/layout/PlayerPage"));
|
||||
const PlayerOverview = React.lazy(() => import("./views/player/PlayerOverview"));
|
||||
@ -81,6 +81,18 @@ const Lazy = ({children}) => (
|
||||
</React.Suspense>
|
||||
)
|
||||
|
||||
const getBasename = () => {
|
||||
if (staticSite && baseAddress) {
|
||||
const addressWithoutProtocol = baseAddress
|
||||
.replace("http://", "")
|
||||
.replace("https://", "");
|
||||
const startOfPath = addressWithoutProtocol.indexOf("/");
|
||||
return startOfPath >= 0 ? addressWithoutProtocol.substring(startOfPath) : "";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
@ -88,7 +100,7 @@ function App() {
|
||||
<div className="App">
|
||||
<ContextProviders>
|
||||
<div id="wrapper">
|
||||
<BrowserRouter>
|
||||
<BrowserRouter basename={getBasename()}>
|
||||
<Routes>
|
||||
<Route path="" element={<MainPageRedirect/>}/>
|
||||
<Route path="/" element={<MainPageRedirect/>}/>
|
||||
|
@ -15,11 +15,11 @@ export const baseAddress = javaReplaced.address.startsWith('PLAN_') || !isCurren
|
||||
export const staticSite = javaReplaced.isStatic === 'true';
|
||||
|
||||
export const doSomeGetRequest = async (url, statusOptions) => {
|
||||
return doSomeRequest(url, statusOptions, async () => axios.get(url));
|
||||
return doSomeRequest(url, statusOptions, async () => axios.get(baseAddress + url));
|
||||
}
|
||||
|
||||
export const doSomePostRequest = async (url, statusOptions, body) => {
|
||||
return doSomeRequest(url, statusOptions, async () => axios.post(url, body));
|
||||
return doSomeRequest(url, statusOptions, async () => axios.post(baseAddress + url, body));
|
||||
}
|
||||
|
||||
export const doSomeRequest = async (url, statusOptions, axiosFunction) => {
|
||||
|
Loading…
Reference in New Issue
Block a user