mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2025-01-25 01:21:41 +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;
|
if (config.isFalse(ExportSettings.PLAYER_PAGES)) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
playerPageExporter.export(toDirectory, playerUUID, playerName);
|
playerPageExporter.export(toDirectory, playerUUID);
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException | NotFoundException e) {
|
} catch (IOException | NotFoundException e) {
|
||||||
throw new ExportException("Failed to export player: " + playerName + ", " + e.toString(), e);
|
throw new ExportException("Failed to export player: " + playerName + ", " + e.toString(), e);
|
||||||
|
@ -16,8 +16,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.djrapitops.plan.delivery.export;
|
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.rendering.html.Html;
|
||||||
import com.djrapitops.plan.delivery.web.resource.WebResource;
|
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 com.djrapitops.plan.storage.file.Resource;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
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));
|
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 {
|
private void exportReactRedirects(Path toDirectory) throws IOException {
|
||||||
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
||||||
|
|
||||||
Resource redirect = files.getResourceFromJar("web/export-redirect.html");
|
exportReactRedirects(toDirectory, files, config, getRedirections());
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,11 +91,10 @@ public class PlayerPageExporter extends FileExporter {
|
|||||||
*
|
*
|
||||||
* @param toDirectory Path to Export directory
|
* @param toDirectory Path to Export directory
|
||||||
* @param playerUUID UUID of the player
|
* @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 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
|
* @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();
|
Database.State dbState = dbSystem.getDatabase().getState();
|
||||||
if (dbState == Database.State.CLOSED || dbState == Database.State.CLOSING) return;
|
if (dbState == Database.State.CLOSED || dbState == Database.State.CLOSING) return;
|
||||||
if (Boolean.FALSE.equals(dbSystem.getDatabase().query(PlayerFetchQueries.isPlayerRegistered(playerUUID)))) {
|
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 {
|
private void exportReactRedirects(Path toDirectory, UUID playerUUID) throws IOException {
|
||||||
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
||||||
|
|
||||||
Resource redirectPage = files.getResourceFromJar("web/export-redirect.html");
|
exportReactRedirects(toDirectory, files, config, getRedirections(playerUUID));
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportJSON(ExportPaths exportPaths, Path toDirectory, UUID playerUUID) throws IOException {
|
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 {
|
private void exportReactRedirects(Path toDirectory) throws IOException {
|
||||||
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
||||||
|
|
||||||
Resource redirect = files.getResourceFromJar("web/export-redirect.html");
|
String[] redirections = {"players"};
|
||||||
exportReactRedirect(toDirectory, redirect, "players");
|
exportReactRedirects(toDirectory, files, config, redirections);
|
||||||
}
|
|
||||||
|
|
||||||
private void exportReactRedirect(Path toDirectory, Resource redirectHtml, String path) throws IOException {
|
|
||||||
export(toDirectory.resolve(path).resolve("index.html"), redirectHtml.asString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportJSON(Path toDirectory) throws IOException {
|
private void exportJSON(Path toDirectory) throws IOException {
|
||||||
|
@ -67,7 +67,7 @@ public class ReactExporter extends FileExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void exportReactFiles(Path toDirectory) throws IOException {
|
public void exportReactFiles(Path toDirectory) throws IOException {
|
||||||
exportAsset(toDirectory, "index.html");
|
exportIndexHtml(toDirectory);
|
||||||
exportAsset(toDirectory, "asset-manifest.json");
|
exportAsset(toDirectory, "asset-manifest.json");
|
||||||
exportAsset(toDirectory, "favicon.ico");
|
exportAsset(toDirectory, "favicon.ico");
|
||||||
exportAsset(toDirectory, "logo192.png");
|
exportAsset(toDirectory, "logo192.png");
|
||||||
@ -101,11 +101,16 @@ public class ReactExporter extends FileExporter {
|
|||||||
for (String path : paths) {
|
for (String path : paths) {
|
||||||
Path to = toDirectory.resolve(path);
|
Path to = toDirectory.resolve(path);
|
||||||
Resource resource = files.getResourceFromJar("web/" + 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(
|
String withReplacedConstants = StringUtils.replaceEach(
|
||||||
resource.asString(),
|
resource.asString(),
|
||||||
new String[]{"PLAN_BASE_ADDRESS", "PLAN_EXPORTED_VERSION"},
|
new String[]{"PLAN_BASE_ADDRESS", "PLAN_EXPORTED_VERSION", "n.p=\"/\""},
|
||||||
new String[]{config.get(WebserverSettings.EXTERNAL_LINK), "true"}
|
new String[]{config.get(WebserverSettings.EXTERNAL_LINK), "true", "n.p=\"" + getBasePath() + "/\""}
|
||||||
);
|
);
|
||||||
export(to, withReplacedConstants);
|
export(to, withReplacedConstants);
|
||||||
} else {
|
} 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 {
|
private void exportAsset(Path toDirectory, String asset) throws IOException {
|
||||||
export(toDirectory.resolve(asset), files.getResourceFromJar("web/" + asset));
|
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 {
|
private void exportReactRedirects(Path toDirectory, ServerUUID serverUUID) throws IOException {
|
||||||
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
if (config.isFalse(PluginSettings.FRONTEND_BETA)) return;
|
||||||
|
|
||||||
Resource redirectPage = files.getResourceFromJar("web/export-redirect.html");
|
exportReactRedirects(toDirectory, files, config, getRedirections(serverUUID));
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,7 +4,17 @@
|
|||||||
<meta content="AuroraLS3" name="author">
|
<meta content="AuroraLS3" name="author">
|
||||||
<meta content="noindex, nofollow" name="robots">
|
<meta content="noindex, nofollow" name="robots">
|
||||||
<title>Plan | Player Analytics</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>Please enable javascript.</noscript>
|
<noscript>Please enable javascript.</noscript>
|
||||||
|
@ -17,49 +17,34 @@
|
|||||||
package com.djrapitops.plan.delivery.export;
|
package com.djrapitops.plan.delivery.export;
|
||||||
|
|
||||||
import com.djrapitops.plan.PlanSystem;
|
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.identification.ServerUUID;
|
||||||
import com.djrapitops.plan.settings.config.PlanConfig;
|
import com.djrapitops.plan.settings.config.PlanConfig;
|
||||||
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
|
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
|
||||||
import com.djrapitops.plan.settings.config.paths.ExportSettings;
|
import com.djrapitops.plan.settings.config.paths.ExportSettings;
|
||||||
import com.djrapitops.plan.settings.config.paths.PluginSettings;
|
import com.djrapitops.plan.settings.config.paths.PluginSettings;
|
||||||
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
|
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.storage.file.PlanFiles;
|
||||||
import com.djrapitops.plan.utilities.java.Lists;
|
|
||||||
import extension.FullSystemExtension;
|
import extension.FullSystemExtension;
|
||||||
import extension.SeleniumExtension;
|
import extension.SeleniumExtension;
|
||||||
import org.junit.jupiter.api.*;
|
import org.junit.jupiter.api.*;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.openqa.selenium.NoSuchElementException;
|
import org.openqa.selenium.WebDriver;
|
||||||
import org.openqa.selenium.*;
|
|
||||||
import org.openqa.selenium.chrome.ChromeDriver;
|
import org.openqa.selenium.chrome.ChromeDriver;
|
||||||
import org.openqa.selenium.logging.LogEntry;
|
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.GenericContainer;
|
||||||
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
|
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
|
||||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||||
import org.testcontainers.shaded.org.apache.commons.io.FileUtils;
|
import org.testcontainers.shaded.org.apache.commons.io.FileUtils;
|
||||||
import org.testcontainers.shaded.org.awaitility.Awaitility;
|
|
||||||
import org.testcontainers.utility.DockerImageName;
|
import org.testcontainers.utility.DockerImageName;
|
||||||
import utilities.RandomData;
|
|
||||||
import utilities.TestConstants;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.util.Collection;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.util.List;
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static com.djrapitops.plan.delivery.export.ExportTestUtilities.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author AuroraLS3
|
* @author AuroraLS3
|
||||||
@ -98,23 +83,7 @@ class ExportRegressionTest {
|
|||||||
system.enable();
|
system.enable();
|
||||||
serverUUID = system.getServerInfo().getServerUUID();
|
serverUUID = system.getServerInfo().getServerUUID();
|
||||||
savePlayerData(system.getDatabaseSystem().getDatabase(), serverUUID);
|
savePlayerData(system.getDatabaseSystem().getDatabase(), serverUUID);
|
||||||
export(system.getExportSystem().getExporter(), system.getDatabaseSystem().getDatabase());
|
export(system.getExportSystem().getExporter(), system.getDatabaseSystem().getDatabase(), serverUUID);
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
@ -124,54 +93,21 @@ class ExportRegressionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void clearExportDirectory(WebDriver driver) {
|
void clearBrowserConsole(WebDriver driver) {
|
||||||
SeleniumExtension.newTab(driver);
|
SeleniumExtension.newTab(driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestFactory
|
@TestFactory
|
||||||
Collection<DynamicTest> exportedWebpageDoesNotHaveErrors(ChromeDriver driver) throws Exception {
|
Collection<DynamicTest> exportedWebpageDoesNotHaveErrors(ChromeDriver driver) {
|
||||||
List<String> endpointsToTest = Lists.builder(String.class)
|
List<String> endpointsToTest = getEndpointsToTest(serverUUID);
|
||||||
.add("/")
|
|
||||||
.addAll(ServerPageExporter.getRedirections(serverUUID))
|
|
||||||
.addAll(PlayerPageExporter.getRedirections(TestConstants.PLAYER_ONE_UUID))
|
|
||||||
.add("/players")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return endpointsToTest.stream().map(
|
return endpointsToTest.stream().map(
|
||||||
endpoint -> DynamicTest.dynamicTest("Exported page does not log errors to js console " + endpoint, () -> {
|
endpoint -> DynamicTest.dynamicTest("Exported page does not log errors to js console " + endpoint, () -> {
|
||||||
|
|
||||||
String address = "http://" + webserver.getHost() + ":" + webserver.getMappedPort(8080)
|
String address = "http://" + webserver.getHost() + ":" + webserver.getMappedPort(8080)
|
||||||
+ (endpoint.startsWith("/") ? endpoint : '/' + endpoint);
|
+ (endpoint.startsWith("/") ? endpoint : '/' + endpoint);
|
||||||
System.out.println("GET: " + address);
|
List<LogEntry> logs = getLogsAfterRequestToAddress(driver, 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());
|
|
||||||
|
|
||||||
assertNoLogs(logs);
|
assertNoLogs(logs);
|
||||||
})
|
})
|
||||||
).collect(Collectors.toList());
|
).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 {AuthenticationContextProvider} from "./hooks/authenticationHook";
|
||||||
import {NavigationContextProvider} from "./hooks/navigationHook";
|
import {NavigationContextProvider} from "./hooks/navigationHook";
|
||||||
import MainPageRedirect from "./components/navigation/MainPageRedirect";
|
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 PlayerPage = React.lazy(() => import("./views/layout/PlayerPage"));
|
||||||
const PlayerOverview = React.lazy(() => import("./views/player/PlayerOverview"));
|
const PlayerOverview = React.lazy(() => import("./views/player/PlayerOverview"));
|
||||||
@ -81,6 +81,18 @@ const Lazy = ({children}) => (
|
|||||||
</React.Suspense>
|
</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() {
|
function App() {
|
||||||
axios.defaults.withCredentials = true;
|
axios.defaults.withCredentials = true;
|
||||||
|
|
||||||
@ -88,7 +100,7 @@ function App() {
|
|||||||
<div className="App">
|
<div className="App">
|
||||||
<ContextProviders>
|
<ContextProviders>
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
<BrowserRouter>
|
<BrowserRouter basename={getBasename()}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="" element={<MainPageRedirect/>}/>
|
<Route path="" element={<MainPageRedirect/>}/>
|
||||||
<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 staticSite = javaReplaced.isStatic === 'true';
|
||||||
|
|
||||||
export const doSomeGetRequest = async (url, statusOptions) => {
|
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) => {
|
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) => {
|
export const doSomeRequest = async (url, statusOptions, axiosFunction) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user