Update command #539

This commit is contained in:
Rsl1122 2018-04-14 11:18:30 +03:00
parent 23a3fc441b
commit 4e0f1b38f1
17 changed files with 592 additions and 14 deletions

View File

@ -4,6 +4,7 @@ import com.djrapitops.plan.PlanBungee;
import com.djrapitops.plan.command.commands.*;
import com.djrapitops.plan.command.commands.manage.ManageConDebugCommand;
import com.djrapitops.plan.system.settings.Permissions;
import com.djrapitops.plan.system.settings.Settings;
import com.djrapitops.plan.system.settings.locale.Locale;
import com.djrapitops.plan.system.settings.locale.Msg;
import com.djrapitops.plugin.command.CommandNode;
@ -49,6 +50,7 @@ public class PlanBungeeCommand extends TreeCmdNode {
new BungeeSetupToggleCommand(),
new ReloadCommand(plugin),
new StatusCommand<>(plugin, Permissions.MANAGE.getPermission(), plugin.getColorScheme()),
(Settings.ALLOW_UPDATE.isTrue() ? new UpdateCommand() : null)
}
);
}

View File

@ -46,7 +46,8 @@ public class PlanCommand extends TreeCmdNode {
new ReloadCommand(plugin),
new ManageCommand(plugin, this),
new StatusCommand<>(plugin, Permissions.MANAGE.getPermission(), plugin.getColorScheme()),
(Settings.DEV_MODE.isTrue() ? new DevCommand() : null)
(Settings.DEV_MODE.isTrue() ? new DevCommand() : null),
(Settings.ALLOW_UPDATE.isTrue() ? new UpdateCommand() : null)
}
);
}

View File

@ -0,0 +1,210 @@
package com.djrapitops.plan.command.commands;
import com.djrapitops.plan.api.exceptions.connection.*;
import com.djrapitops.plan.api.exceptions.database.DBException;
import com.djrapitops.plan.command.commands.manage.ManageConDebugCommand;
import com.djrapitops.plan.system.database.databases.Database;
import com.djrapitops.plan.system.database.databases.operation.FetchOperations;
import com.djrapitops.plan.system.info.InfoSystem;
import com.djrapitops.plan.system.info.request.CheckConnectionRequest;
import com.djrapitops.plan.system.info.request.UpdateCancelRequest;
import com.djrapitops.plan.system.info.server.Server;
import com.djrapitops.plan.system.settings.Permissions;
import com.djrapitops.plan.system.update.VersionCheckSystem;
import com.djrapitops.plan.system.update.VersionInfo;
import com.djrapitops.plan.system.webserver.WebServerSystem;
import com.djrapitops.plugin.api.utility.log.Log;
import com.djrapitops.plugin.command.CommandNode;
import com.djrapitops.plugin.command.CommandType;
import com.djrapitops.plugin.command.ISender;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
* Command that updates all servers in the network
*
* @author Rsl1122
*/
public class UpdateCommand extends CommandNode {
public UpdateCommand() {
super("update", Permissions.MANAGE.getPermission(), CommandType.ALL);
setArguments("[-update]/[cancel]");
setShortHelp("Get change log link or update plugin.");
setInDepthHelp(
"/plan update",
" Used to update the plugin on the next shutdown\n",
" /plan update - get change log link",
" /plan update -update - Schedule update to happen on all network servers that are online next time they reboot.",
" /plan update cancel - Cancel scheduled update on servers that haven't rebooted yet."
);
}
@Override
public void onCommand(ISender sender, String commandLabel, String[] args) {
if (!VersionCheckSystem.isNewVersionAvailable()) {
sender.sendMessage("§aYou're running the latest version of Plan.");
return;
}
VersionInfo available = VersionCheckSystem.getInstance().getNewVersionAvailable();
String downloadUrl = available.getDownloadUrl();
if (!available.isTrusted()) {
sender.sendMessage("§cVersion download url did not start with " +
"https://github.com/Rsl1122/Plan-PlayerAnalytics/releases/ " +
"and might not be trusted. You can download this version manually here (Direct download):");
sender.sendLink(downloadUrl, downloadUrl);
return;
}
if (args.length == 0) {
sender.sendLink("Change Log v" + available.getVersion().toString() + ": ", "Click me", available.getChangeLogUrl());
return;
}
String firstArgument = args[0];
if ("-update".equals(firstArgument)) {
handleUpdate(sender, args);
} else if ("cancel".equals(firstArgument)) {
cancel(sender);
} else {
throw new IllegalArgumentException("Unknown argument, use '-update' or 'cancel'");
}
}
private void cancel(ISender sender) {
try {
cancel(sender, Database.getActive().fetch().getServers());
sender.sendMessage("§aUpdate has been cancelled.");
} catch (DBException e) {
sender.sendMessage("§cDatabase error occurred, cancel could not be performed.");
Log.toLog(this.getClass().getName(), e);
}
}
private void handleUpdate(ISender sender, String[] args) {
sender.sendMessage("§aYou can cancel the update on servers that haven't rebooted yet with /plan update cancel.");
sender.sendMessage("Checking that all servers are online..");
if (!checkNetworkStatus(sender)) {
sender.sendMessage("§cNot all servers were online or accessible, you can still update available servers using -force as a 2nd argument.");
if (args.length <= 1 || !"-force".equals(args[1])) {
return;
}
}
try {
List<Server> servers = Database.getActive().fetch().getServers();
update(sender, servers);
} catch (DBException e) {
Log.toLog(this.getClass().getName(), e);
}
}
private void update(ISender sender, List<Server> servers) {
for (Server server : servers) {
if (update(sender, server)) {
sender.sendMessage("§a" + server.getName() + " scheduled for update.");
} else {
sender.sendMessage("§cUpdate failed on a server, cancelling update on all servers..");
cancel(sender, servers);
sender.sendMessage("§cUpdate cancelled.");
break;
}
}
}
private void cancel(ISender sender, List<Server> servers) {
for (Server server : servers) {
cancel(sender, server);
}
}
private void cancel(ISender sender, Server server) {
try {
InfoSystem.getInstance().getConnectionSystem().sendInfoRequest(new UpdateCancelRequest(), server);
} catch (ForbiddenException | GatewayException | InternalErrorException e) {
sender.sendMessage("§cCancel failed on " + server.getName() + ": Odd Exception: " + e.getClass().getSimpleName());
} catch (UnauthorizedServerException e) {
sender.sendMessage("§cCancel failed on " + server.getName() + ": Unauthorized. " + server.getName() + " might be using different database.");
} catch (ConnectionFailException e) {
sender.sendMessage("§cCancel failed on " + server.getName() + ": " + e.getCause().getClass().getSimpleName() + " " + e.getCause().getMessage());
String address = server.getWebAddress();
boolean local = address.contains("localhost")
|| address.startsWith("https://:") // IP empty = Localhost
|| address.startsWith("http://:") // IP empty = Localhost
|| address.contains("127.0.0.1");
if (!local) {
sender.sendMessage("§cNon-local address, check that port is open");
}
} catch (NotFoundException e) {
/* Ignored, older version */
} catch (WebException e) {
sender.sendMessage("§cCancel failed on " + server.getName() + ": Odd Exception:" + e.getClass().getSimpleName());
}
}
private boolean update(ISender sender, Server server) {
try {
InfoSystem.getInstance().getConnectionSystem().sendInfoRequest(new CheckConnectionRequest(), server);
return true;
} catch (BadRequestException e) {
sender.sendMessage("§c" + server.getName() + " has Allow-Update set to false, aborting update.");
return false;
} catch (ForbiddenException | GatewayException | InternalErrorException e) {
sender.sendMessage("§c" + server.getName() + ": Odd Exception: " + e.getClass().getSimpleName());
return false;
} catch (UnauthorizedServerException e) {
sender.sendMessage("§cFail reason: Unauthorized. " + server.getName() + " might be using different database.");
return false;
} catch (ConnectionFailException e) {
sender.sendMessage("§cFail reason: " + e.getCause().getClass().getSimpleName() + " " + e.getCause().getMessage());
String address = server.getWebAddress();
boolean local = address.contains("localhost")
|| address.startsWith("https://:") // IP empty = Localhost
|| address.startsWith("http://:") // IP empty = Localhost
|| address.contains("127.0.0.1");
if (!local) {
sender.sendMessage("§cNon-local address, check that port is open");
}
return false;
} catch (NotFoundException e) {
sender.sendMessage("§e" + server.getName() + " is using older version and can not be scheduled for update. " +
"You can update it manually, update will proceed.");
return true;
} catch (WebException e) {
sender.sendMessage("§eOdd Exception: " + e.getClass().getSimpleName());
return false;
}
}
private boolean checkNetworkStatus(ISender sender) {
try {
FetchOperations fetch = Database.getActive().fetch();
Optional<Server> bungeeInformation = fetch.getBungeeInformation();
if (!bungeeInformation.isPresent()) {
return true;
}
Map<UUID, Server> bukkitServers = fetch.getBukkitServers();
String accessAddress = WebServerSystem.getInstance().getWebServer().getAccessAddress();
boolean success = true;
for (Server server : bukkitServers.values()) {
if (!ManageConDebugCommand.testServer(sender, accessAddress, server)) {
success = false;
}
}
Server bungee = bungeeInformation.get();
if (!ManageConDebugCommand.testServer(sender, accessAddress, bungee)) {
success = false;
}
return success;
} catch (DBException e) {
sender.sendMessage("§cDatabase error occurred, update has been cancelled.");
Log.toLog(this.getClass().getName(), e);
return false;
}
}
}

View File

@ -28,12 +28,9 @@ import java.util.UUID;
*/
public class ManageConDebugCommand extends CommandNode {
private final ColorScheme cs;
public ManageConDebugCommand() {
super("con", Permissions.MANAGE.getPermission(), CommandType.ALL);
setShortHelp("Debug Bukkit-Bungee Connections");
cs = PlanPlugin.getInstance().getColorScheme();
}
@Override
@ -68,18 +65,19 @@ public class ManageConDebugCommand extends CommandNode {
}
}
private void testServer(ISender sender, String accessAddress, Server server) {
public static boolean testServer(ISender sender, String accessAddress, Server server) {
String address = server.getWebAddress().toLowerCase();
boolean usingHttps = address.startsWith("https");
boolean local = address.contains("localhost")
|| address.startsWith("https://:")
|| address.startsWith("http://:")
|| address.startsWith("https://:") // IP empty = Localhost
|| address.startsWith("http://:") // IP empty = Localhost
|| address.contains("127.0.0.1");
try {
InfoSystem.getInstance().getConnectionSystem().sendInfoRequest(new CheckConnectionRequest(accessAddress), server);
sender.sendMessage(getMsgFor(address, usingHttps, local, true, true));
return true;
} catch (ForbiddenException | BadRequestException | InternalErrorException e) {
sender.sendMessage(getMsgFor(address, usingHttps, local, false, false));
@ -102,9 +100,11 @@ public class ManageConDebugCommand extends CommandNode {
sender.sendMessage(getMsgFor(address, usingHttps, local, false, false));
sender.sendMessage("§eOdd Exception: " + e.getClass().getSimpleName());
}
return false;
}
private String getMsgFor(String address, boolean usingHttps, boolean local, boolean successTo, boolean successFrom) {
private static String getMsgFor(String address, boolean usingHttps, boolean local, boolean successTo, boolean successFrom) {
ColorScheme cs = PlanPlugin.getInstance().getColorScheme();
String tCol = cs.getTertiaryColor();
String sCol = cs.getSecondaryColor();
return tCol + address + sCol + ": "

View File

@ -125,6 +125,9 @@ public abstract class ConnectionSystem implements SubSystem {
putRequest(requests, SaveDBSettingsRequest.createHandler());
putRequest(requests, SendDBSettingsRequest.createHandler());
putRequest(requests, CheckConnectionRequest.createHandler());
putRequest(requests, UpdateRequest.createHandler());
putRequest(requests, UpdateCancelRequest.createHandler());
return requests;
}

View File

@ -0,0 +1,37 @@
/*
* Licence is provided in the jar as license.yml also here:
* https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml
*/
package com.djrapitops.plan.system.info.request;
import com.djrapitops.plan.system.update.ShutdownUpdateHook;
import com.djrapitops.plan.system.webserver.response.DefaultResponses;
import com.djrapitops.plan.system.webserver.response.Response;
import java.util.Map;
/**
* InfoRequest used for Updating the plugin on a network.
*
* @author Rsl1122
*/
public class UpdateCancelRequest implements InfoRequest {
public UpdateCancelRequest() {
}
public static UpdateCancelRequest createHandler() {
return new UpdateCancelRequest();
}
@Override
public void runLocally() {
ShutdownUpdateHook.deActivate();
}
@Override
public Response handleRequest(Map<String, String> variables) {
ShutdownUpdateHook.deActivate();
return DefaultResponses.SUCCESS.get();
}
}

View File

@ -0,0 +1,43 @@
/*
* Licence is provided in the jar as license.yml also here:
* https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml
*/
package com.djrapitops.plan.system.info.request;
import com.djrapitops.plan.system.settings.Settings;
import com.djrapitops.plan.system.update.ShutdownUpdateHook;
import com.djrapitops.plan.system.webserver.response.DefaultResponses;
import com.djrapitops.plan.system.webserver.response.Response;
import com.djrapitops.plan.system.webserver.response.api.BadRequestResponse;
import java.util.Map;
/**
* InfoRequest used for Updating the plugin on a network.
*
* @author Rsl1122
*/
public class UpdateRequest implements InfoRequest {
public UpdateRequest() {
}
public static UpdateRequest createHandler() {
return new UpdateRequest();
}
@Override
public void runLocally() {
new ShutdownUpdateHook().register();
}
@Override
public Response handleRequest(Map<String, String> variables) {
if (Settings.ALLOW_UPDATE.isTrue()) {
new ShutdownUpdateHook().register();
return DefaultResponses.SUCCESS.get();
} else {
return new BadRequestResponse("Update not allowed on this server");
}
}
}

View File

@ -37,6 +37,8 @@ public enum Settings {
DISPLAY_PLAYER_IPS("Customization.Display.PlayerIPs"),
DISPLAY_GAPS_IN_GRAPH_DATA("Customization.Display.GapsInGraphData"),
DATA_GEOLOCATIONS("Data.Geolocations"),
ALLOW_UPDATE("Plugin.Allow-Update-Command"),
NOTIFY_ABOUT_DEV_RELEASES("Plugin.Notify-About-DEV-Releases"),
// Integer
WEBSERVER_PORT("WebServer.Port"),

View File

@ -0,0 +1,104 @@
package com.djrapitops.plan.system.update;
import com.djrapitops.plan.PlanPlugin;
import com.djrapitops.plugin.api.Check;
import com.djrapitops.plugin.api.utility.Version;
import com.djrapitops.plugin.api.utility.log.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
/**
* Shutdown hook that updates the plugin on server shutdown.
* <p>
* Does not perform update on force close.
*
* @author Rsl1122
*/
public class ShutdownUpdateHook extends Thread {
private static boolean activated = false;
private static boolean isActivated() {
return activated;
}
private static void activate(ShutdownUpdateHook hook) {
activated = true;
Runtime.getRuntime().addShutdownHook(hook);
}
public static void deActivate() {
activated = false;
Log.infoColor("§aUpdate has been cancelled.");
}
public void register() {
if (isActivated()) {
return;
}
Log.infoColor("§aUpdate has been scheduled, The new jar will be downloaded on server shutdown.");
activate(this);
}
@Override
public void run() {
if (!activated) {
return;
}
activated = false;
VersionInfo available = VersionCheckSystem.getInstance().getNewVersionAvailable();
if (!Version.isNewVersionAvailable(new Version(VersionCheckSystem.getCurrentVersion()), available.getVersion())) {
return;
}
File dataFolder = PlanPlugin.getInstance().getDataFolder();
File pluginsFolder = Check.isSpongeAvailable()
? dataFolder.getParentFile()
: new File(dataFolder.getParentFile().getParentFile(), "mods");
if (pluginsFolder == null || !pluginsFolder.isDirectory()) {
System.out.println("Could not get plugin folder for Plan.");
return;
}
File newFileLocation = new File(pluginsFolder, "Plan-" + available.getVersion() + ".jar");
try {
downloadNewJar(available, newFileLocation);
deleteOldJar(pluginsFolder, newFileLocation);
} catch (IOException e) {
Log.toLog(this.getClass().getName(), e);
}
}
private void deleteOldJar(File pluginsFolder, File newFileLocation) {
File[] files = pluginsFolder.listFiles();
if (files == null) {
System.out.println("Could not delete old jar.");
return;
}
for (File file : files) {
String fileName = file.getName();
boolean isPlanJar = (fileName.startsWith("Plan-")
&& fileName.endsWith(".jar"))
|| fileName.equals("Plan.jar");
boolean isNewJar = fileName.equals(newFileLocation.getName());
if (isPlanJar && !isNewJar) {
file.deleteOnExit();
}
}
}
private void downloadNewJar(VersionInfo available, File newFileLocation) throws IOException {
URL downloadFrom = new URL(available.getDownloadUrl());
ReadableByteChannel rbc = Channels.newChannel(downloadFrom.openStream());
FileOutputStream fos = new FileOutputStream(newFileLocation);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
}
}

View File

@ -6,6 +6,7 @@ package com.djrapitops.plan.system.update;
import com.djrapitops.plan.system.PlanSystem;
import com.djrapitops.plan.system.SubSystem;
import com.djrapitops.plan.system.settings.Settings;
import com.djrapitops.plugin.api.Priority;
import com.djrapitops.plugin.api.systems.NotificationCenter;
import com.djrapitops.plugin.api.utility.Version;
@ -13,6 +14,8 @@ import com.djrapitops.plugin.api.utility.log.Log;
import com.djrapitops.plugin.utilities.Verify;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
/**
* System for checking if new Version is available when the System initializes.
@ -22,7 +25,7 @@ import java.io.IOException;
public class VersionCheckSystem implements SubSystem {
private final String currentVersion;
private boolean newVersionAvailable = false;
private VersionInfo newVersionAvailable;
public VersionCheckSystem(String currentVersion) {
this.currentVersion = currentVersion;
@ -35,7 +38,7 @@ public class VersionCheckSystem implements SubSystem {
}
public static boolean isNewVersionAvailable() {
return getInstance().newVersionAvailable;
return getInstance().newVersionAvailable != null;
}
public static String getCurrentVersion() {
@ -44,14 +47,35 @@ public class VersionCheckSystem implements SubSystem {
@Override
public void enable() {
checkForNewVersion();
if (Settings.ALLOW_UPDATE.isTrue()) {
try {
List<VersionInfo> versions = VersionInfoLoader.load();
if (Settings.NOTIFY_ABOUT_DEV_RELEASES.isFalse()) {
versions = versions.stream().filter(VersionInfo::isRelease).collect(Collectors.toList());
}
VersionInfo newestVersion = versions.get(0);
if (Version.isNewVersionAvailable(new Version(currentVersion), newestVersion.getVersion())) {
String notification =
"New Release (" + newestVersion.getVersion().toString() + ") is available and can be updated " +
"to using update subcommand." + (newestVersion.isRelease() ? "" : " This is a DEV release.");
Log.info(notification);
NotificationCenter.addNotification(newestVersion.isRelease() ? Priority.HIGH : Priority.MEDIUM, notification);
} else {
Log.info("You're using the latest version.");
}
} catch (IOException e) {
Log.error("Version information could not be loaded from Github/versions.txt");
}
} else {
checkForNewVersion();
}
}
private void checkForNewVersion() {
String githubVersionUrl = "https://raw.githubusercontent.com/Rsl1122/Plan-PlayerAnalytics/master/Plan/src/main/resources/plugin.yml";
String spigotUrl = "https://www.spigotmc.org/resources/plan-player-analytics.32536/";
try {
newVersionAvailable = Version.checkVersion(currentVersion, githubVersionUrl);
boolean newVersionAvailable = Version.checkVersion(currentVersion, githubVersionUrl);
if (!newVersionAvailable) {
try {
newVersionAvailable = Version.checkVersion(currentVersion, spigotUrl);
@ -77,4 +101,8 @@ public class VersionCheckSystem implements SubSystem {
public void disable() {
/* Does not need to be closed */
}
public VersionInfo getNewVersionAvailable() {
return newVersionAvailable;
}
}

View File

@ -0,0 +1,63 @@
package com.djrapitops.plan.system.update;
import com.djrapitops.plugin.api.utility.Version;
import com.google.common.base.Objects;
/**
* Data class for reading version.txt in https://github.com/Rsl1122/Plan-PlayerAnalytics.
*
* @author Rsl1122
*/
public class VersionInfo implements Comparable<VersionInfo> {
private final boolean release;
private final Version version;
private final String downloadUrl;
private final String changeLogUrl;
public VersionInfo(boolean release, Version version, String downloadUrl, String changeLogUrl) {
this.release = release;
this.version = version;
this.downloadUrl = downloadUrl;
this.changeLogUrl = changeLogUrl;
}
public boolean isRelease() {
return release;
}
public Version getVersion() {
return version;
}
public String getDownloadUrl() {
return downloadUrl;
}
public String getChangeLogUrl() {
return changeLogUrl;
}
public boolean isTrusted() {
return downloadUrl.startsWith("https://github.com/Rsl1122/Plan-PlayerAnalytics/releases/download/");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VersionInfo that = (VersionInfo) o;
return release == that.release &&
Objects.equal(version, that.version);
}
@Override
public int hashCode() {
return Objects.hashCode(release, version);
}
@Override
public int compareTo(VersionInfo o) {
return -this.version.compareTo(o.version);
}
}

View File

@ -0,0 +1,57 @@
package com.djrapitops.plan.system.update;
import com.djrapitops.plugin.api.utility.Version;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
/**
* Utility for loading version information from github.
*
* @author Rsl1122
*/
public class VersionInfoLoader {
private static final String VERSION_TXT_URL =
"https://raw.githubusercontent.com/Rsl1122/Plan-PlayerAnalytics/master/versions.txt";
/**
* Loads version information from github.
*
* @return List of VersionInfo, newest version first.
* @throws IOException If site can not be accessed.
* @throws java.net.MalformedURLException If VERSION_TXT_URL is not valid.
*/
public static List<VersionInfo> load() throws IOException {
URL url = new URL(VERSION_TXT_URL);
List<VersionInfo> versionInfo = new ArrayList<>();
try (Scanner websiteScanner = new Scanner(url.openStream())) {
while (websiteScanner.hasNextLine()) {
String line = websiteScanner.nextLine();
if (!line.startsWith("REL") && !line.startsWith("DEV")) {
continue;
}
String[] parts = line.split("\\|");
if (parts.length < 4) {
continue;
}
boolean release = parts[0].equals("REL");
Version version = new Version(parts[1]);
String downloadUrl = parts[2];
String changeLogUrl = parts[3];
versionInfo.add(new VersionInfo(release, version, downloadUrl, changeLogUrl));
}
}
Collections.sort(versionInfo);
return versionInfo;
}
}

View File

@ -1,4 +1,4 @@
name: Plan
author: Rsl1122
main: com.djrapitops.plan.PlanBungee
version: 4.2.0
version: 4.2.0-b1

View File

@ -12,6 +12,8 @@ Network:
Plugin:
Debug: 'false'
Locale: default
Allow-Update-Command: true
Notify-About-DEV-Releases: false
# -----------------------------------------------------
# More information about SSL Certificate Settings:

View File

@ -17,6 +17,8 @@ Plugin:
Bungee-Override:
StandaloneMode: false
CopyBungeeConfig: true
Allow-Update-Command: true
Notify-About-DEV-Releases: false
# -----------------------------------------------------
# More information about SSL Certificate Settings:

View File

@ -1,7 +1,7 @@
name: Plan
author: Rsl1122
main: com.djrapitops.plan.Plan
version: 4.2.0
version: 4.2.0-b1
softdepend:
- EssentialsX
- Towny

View File

@ -0,0 +1,24 @@
package com.djrapitops.plan.system.update;
import com.djrapitops.plugin.api.utility.Version;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class VersionInfoLoaderTest {
@Test
public void versionLoaderTest() throws IOException {
List<VersionInfo> versions = VersionInfoLoader.load();
VersionInfo oldest = versions.get(versions.size() - 1);
assertEquals(new Version("4.1.7"), oldest.getVersion());
assertTrue(oldest.isRelease());
assertTrue(oldest.isTrusted());
}
}