From b8127b3f36db3ac8691d496dcc1e31e810276b24 Mon Sep 17 00:00:00 2001 From: Alyssa Date: Fri, 16 Aug 2024 21:30:17 +0100 Subject: [PATCH 1/6] replace update mechanism --- client/build.gradle.kts | 2 + .../src/main/java/bean/HyperlinkAdaptor.java | 50 --- .../src/main/java/bean/HyperlinkListener.java | 9 - .../online/util/DesktopEntropyClient.java | 9 +- .../java/online/util/ResponseHandler.java | 23 +- .../main/java/screen/AbstractAboutDialog.java | 4 +- client/src/main/java/util/DialogUtil.java | 11 + client/src/main/java/util/UpdateChecker.java | 110 ------- client/src/main/java/util/UpdateManager.kt | 152 +++++++++ client/src/main/java/util/UrlUtil.kt | 12 + .../src/main/kotlin/bean/HyperlinkAdaptor.kt | 36 +++ .../main/kotlin/bean/IHyperlinkListener.kt | 9 + client/src/main/kotlin/bean/LinkLabel.kt | 20 ++ client/src/test/kotlin/bean/LinkLabelTest.kt | 37 +++ core/build.gradle.kts | 1 + core/src/main/java/util/AbstractClient.java | 4 + core/src/main/java/util/Debug.java | 40 +-- core/src/main/java/util/DebugExtension.java | 8 - core/src/main/java/util/FileUtil.java | 296 ------------------ core/src/main/java/util/OnlineConstants.java | 3 + core/src/main/java/util/XmlConstants.java | 2 - .../src/main/java/server/DownloadHandler.java | 135 -------- .../main/java/server/DownloadListener.java | 21 -- .../src/main/java/server/EntropyServer.java | 60 ---- .../java/server/MessageHandlerRunnable.java | 12 +- .../src/main/java/server/ServerCommands.java | 1 - .../src/main/java/util/EntropyEmailUtil.java | 123 +------- .../main/java/util/ServerDebugExtension.java | 23 -- .../src/main/java/util/XmlBuilderServer.java | 5 +- 29 files changed, 306 insertions(+), 912 deletions(-) delete mode 100644 client/src/main/java/bean/HyperlinkAdaptor.java delete mode 100644 client/src/main/java/bean/HyperlinkListener.java delete mode 100644 client/src/main/java/util/UpdateChecker.java create mode 100644 client/src/main/java/util/UpdateManager.kt create mode 100644 client/src/main/java/util/UrlUtil.kt create mode 100644 client/src/main/kotlin/bean/HyperlinkAdaptor.kt create mode 100644 client/src/main/kotlin/bean/IHyperlinkListener.kt create mode 100644 client/src/main/kotlin/bean/LinkLabel.kt create mode 100644 client/src/test/kotlin/bean/LinkLabelTest.kt delete mode 100644 core/src/main/java/util/DebugExtension.java delete mode 100644 server/src/main/java/server/DownloadHandler.java delete mode 100644 server/src/main/java/server/DownloadListener.java delete mode 100644 server/src/main/java/util/ServerDebugExtension.java diff --git a/client/build.gradle.kts b/client/build.gradle.kts index 058b9f2..c4e737f 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -9,7 +9,9 @@ ktfmt { kotlinLangStyle() } dependencies { implementation("com.jgoodies:jgoodies-forms:1.6.0") implementation("com.miglayout:miglayout-swing:5.2") + implementation("com.konghq:unirest-java:3.14.2") implementation(project(":core")) + testImplementation(testFixtures(project(":core"))) } application { diff --git a/client/src/main/java/bean/HyperlinkAdaptor.java b/client/src/main/java/bean/HyperlinkAdaptor.java deleted file mode 100644 index f46d59a..0000000 --- a/client/src/main/java/bean/HyperlinkAdaptor.java +++ /dev/null @@ -1,50 +0,0 @@ -package bean; - -import java.awt.Component; -import java.awt.Cursor; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; - -import util.Debug; - -public class HyperlinkAdaptor extends MouseAdapter -{ - private HyperlinkListener listener = null; - private Component listenerWindow = null; - - public HyperlinkAdaptor(HyperlinkListener listener) - { - if (!(listener instanceof Component)) - { - Debug.stackTrace("Creating HyperlinkAdaptor with non-component: " + listener); - } - - this.listener = listener; - this.listenerWindow = (Component)listener; - } - - @Override - public void mouseClicked(MouseEvent arg0) - { - listener.linkClicked(arg0); - } - - @Override - public void mouseMoved(MouseEvent arg0) - { - if (listener.isOverHyperlink(arg0)) - { - listenerWindow.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - } - else - { - listenerWindow.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - } - } - - @Override - public void mouseExited(MouseEvent arg0) - { - listenerWindow.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - } -} diff --git a/client/src/main/java/bean/HyperlinkListener.java b/client/src/main/java/bean/HyperlinkListener.java deleted file mode 100644 index 747565e..0000000 --- a/client/src/main/java/bean/HyperlinkListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package bean; - -import java.awt.event.MouseEvent; - -public interface HyperlinkListener -{ - public void linkClicked(MouseEvent arg0); - public boolean isOverHyperlink(MouseEvent arg0); -} diff --git a/client/src/main/java/online/util/DesktopEntropyClient.java b/client/src/main/java/online/util/DesktopEntropyClient.java index 6a7d147..f5f0e35 100644 --- a/client/src/main/java/online/util/DesktopEntropyClient.java +++ b/client/src/main/java/online/util/DesktopEntropyClient.java @@ -75,13 +75,6 @@ public void sendAsyncInSingleThread(MessageSenderParams message) senderThread.start(); } - /*public void sendAsync(MessageSenderParams message) - { - MessageSender senderRunnable = new MessageSender(this, message); - Thread senderThread = new Thread(senderRunnable, "MessageSender"); - senderThread.start(); - }*/ - @Override public String sendSyncOnDevice(MessageSender runnable) { @@ -91,6 +84,6 @@ public String sendSyncOnDevice(MessageSender runnable) @Override public void checkForUpdates() { - UpdateChecker.checkForUpdates(FILE_NAME_ENTROPY_JAR, SERVER_PORT_NUMBER_DOWNLOAD); + UpdateManager.INSTANCE.checkForUpdates(OnlineConstants.ENTROPY_VERSION_NUMBER); } } diff --git a/client/src/main/java/online/util/ResponseHandler.java b/client/src/main/java/online/util/ResponseHandler.java index 36ef191..e6e606d 100644 --- a/client/src/main/java/online/util/ResponseHandler.java +++ b/client/src/main/java/online/util/ResponseHandler.java @@ -265,7 +265,7 @@ private static void handleConnectFailure(Element root) if (failureReason.contains("out of date")) { - promptForUpdate(root); + promptForUpdate(); } else { @@ -273,20 +273,15 @@ private static void handleConnectFailure(Element root) } } - private static void promptForUpdate(final Element rootElement) + private static void promptForUpdate() { - SwingUtilities.invokeLater(new Runnable() - { - @Override - public void run() - { - int answer = DialogUtil.showQuestion("Entropy needs to update in order to connect. \n\nUpdate now?", false); - if (answer == JOptionPane.YES_OPTION) - { - UpdateChecker.startUpdate(rootElement, OnlineConstants.FILE_NAME_ENTROPY_JAR, OnlineConstants.SERVER_PORT_NUMBER_DOWNLOAD); - } - } - }); + SwingUtilities.invokeLater(() -> { + int answer = DialogUtil.showQuestion("Entropy needs to update in order to connect. \n\nUpdate now?", false); + if (answer == JOptionPane.YES_OPTION) + { + UpdateManager.INSTANCE.checkForUpdates(OnlineConstants.ENTROPY_VERSION_NUMBER); + } + }); } private static void handleKickOff(Element root) diff --git a/client/src/main/java/screen/AbstractAboutDialog.java b/client/src/main/java/screen/AbstractAboutDialog.java index 9ee95f3..f33094a 100644 --- a/client/src/main/java/screen/AbstractAboutDialog.java +++ b/client/src/main/java/screen/AbstractAboutDialog.java @@ -14,10 +14,10 @@ import javax.swing.SwingConstants; import bean.HyperlinkAdaptor; -import bean.HyperlinkListener; +import bean.IHyperlinkListener; public abstract class AbstractAboutDialog extends JDialog - implements HyperlinkListener, + implements IHyperlinkListener, ActionListener { public AbstractAboutDialog() diff --git a/client/src/main/java/util/DialogUtil.java b/client/src/main/java/util/DialogUtil.java index 9639ebe..51220a8 100644 --- a/client/src/main/java/util/DialogUtil.java +++ b/client/src/main/java/util/DialogUtil.java @@ -5,6 +5,8 @@ import screen.LoadingDialog; +import java.awt.*; + import static utils.InjectedThings.logger; public class DialogUtil @@ -114,4 +116,13 @@ public static void dismissLoadingDialog() { loadingDialog.dismissDialog(); } + + public static void showCustomMessage(Object message) { + JOptionPane.showMessageDialog( + null, + message, + "Information", + JOptionPane.INFORMATION_MESSAGE + ); + } } diff --git a/client/src/main/java/util/UpdateChecker.java b/client/src/main/java/util/UpdateChecker.java deleted file mode 100644 index 291b49b..0000000 --- a/client/src/main/java/util/UpdateChecker.java +++ /dev/null @@ -1,110 +0,0 @@ -package util; - -import java.io.IOException; - -import javax.swing.JOptionPane; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -/** - * Class to check for updates and launch the EntropyUpdater via a batch file if they are available - */ -public class UpdateChecker implements XmlConstants -{ - public static void checkForUpdates(String filename, int portForDownload) - { - try - { - checkForUpdatesAndDoDownloadIfRequired(filename, portForDownload); - } - finally - { - DialogUtil.dismissLoadingDialog(); - } - } - - private static void checkForUpdatesAndDoDownloadIfRequired(String filename, int portForDownload) - { - //Show this here, checking the CRC can take time - DialogUtil.showLoadingDialog("Checking for updates..."); - String crc = FileUtil.getMd5Crc(filename); - if (crc == null) - { - DialogUtil.showError("Failed to check for updates (couldn't find " + filename + ")."); - return; - } - - Debug.append("Checking for updates - fileCrc is " + crc); - - //We have a CRC, so go to the Server and check against it's copy. - Document crcCheck = factoryCrcCheck(crc, filename); - String responseStr = AbstractClient.getInstance().sendSync(crcCheck, false); - if (responseStr == null) - { - DialogUtil.showError("Failed to check for updates (unable to connect)."); - return; - } - - DialogUtil.dismissLoadingDialog(); - - Document xmlResponse = XmlUtil.getDocumentFromXmlString(responseStr); - Element rootElement = xmlResponse.getDocumentElement(); - String responseName = rootElement.getNodeName(); - if (responseName.equals(RESPONSE_TAG_NO_UPDATES)) - { - //No need to show a message, should be pretty obvious - Debug.append("I am up to date"); - return; - } - - //An update is available - int answer = DialogUtil.showQuestion("An update is available. Would you like to download it now?", false); - if (answer == JOptionPane.NO_OPTION) - { - return; - } - - startUpdate(rootElement, filename, portForDownload); - } - - private static Document factoryCrcCheck(String fileCrc, String fileName) - { - Document document = XmlUtil.factoryNewDocument(); - Element rootElement = document.createElement(ROOT_TAG_CRC_CHECK); - rootElement.setAttribute("FileCrc", fileCrc); - rootElement.setAttribute("FileName", fileName); - - document.appendChild(rootElement); - return document; - } - - public static void startUpdate(Element rootElement, String filename, int portForUpdate) - { - int fileSize = XmlUtil.getAttributeInt(rootElement, "FileSize"); - String version = rootElement.getAttribute("VersionNumber"); - String args = fileSize + " " + version + " " + filename + " " + portForUpdate; - - try - { - if (AbstractClient.isAppleOs()) - { - Runtime.getRuntime().exec("update.command " + args); - } - else - { - Runtime.getRuntime().exec("cmd /c start update.bat " + args); - } - } - catch (IOException ioe) - { - Debug.stackTrace(ioe); - String manualCommand = "update.bat " + args; - - String msg = "Failed to launch update.bat - call the following manually to perform the update: \n\n" + manualCommand; - DialogUtil.showError(msg); - } - - System.exit(0); - } -} diff --git a/client/src/main/java/util/UpdateManager.kt b/client/src/main/java/util/UpdateManager.kt new file mode 100644 index 0000000..2acd364 --- /dev/null +++ b/client/src/main/java/util/UpdateManager.kt @@ -0,0 +1,152 @@ +package util + +import bean.LinkLabel +import java.awt.BorderLayout +import java.io.File +import javax.swing.JLabel +import javax.swing.JOptionPane +import javax.swing.JPanel +import kong.unirest.Unirest +import kong.unirest.json.JSONObject +import utils.InjectedThings.logger +import kotlin.system.exitProcess + +/** + * Automatically check for and download updates using the Github API + * + * https://developer.github.com/v3/repos/releases/#get-the-latest-release + */ +object UpdateManager { + fun checkForUpdates(currentVersion: String) { + // Show this here, checking the CRC can take time + logger.info("updateCheck", "Checking for updates - my version is $currentVersion") + + val jsonResponse = queryLatestReleaseJson(OnlineConstants.ENTROPY_REPOSITORY_URL) + jsonResponse ?: return + + val metadata = parseUpdateMetadata(jsonResponse) + if (metadata == null || !shouldUpdate(currentVersion, metadata)) { + return + } + + startUpdate(metadata.getArgs(), Runtime.getRuntime()) + } + + fun queryLatestReleaseJson(repositoryUrl: String): JSONObject? { + try { + DialogUtil.showLoadingDialog("Checking for updates...") + + val response = Unirest.get("$repositoryUrl/releases/latest").asJson() + if (response.status != 200) { + logger.error( + "updateError", + "Received non-success HTTP status: ${response.status} - ${response.statusText}", + "responseBody" to response.body, + ) + DialogUtil.showError("Failed to check for updates (unable to connect).") + return null + } + + return response.body.`object` + } catch (t: Throwable) { + logger.error("updateError", "Caught $t checking for updates", t) + DialogUtil.showError("Failed to check for updates (unable to connect).") + return null + } finally { + DialogUtil.dismissLoadingDialog() + } + } + + fun shouldUpdate(currentVersion: String, metadata: UpdateMetadata): Boolean { + val newVersion = metadata.version + if (newVersion == currentVersion) { + logger.info("updateResult", "Up to date") + return false + } + + // An update is available + logger.info("updateAvailable", "Newer release available - $newVersion") + + if (!AbstractClient.isWindowsOs()) { + showManualDownloadMessage(newVersion) + return false + } + + val answer = + DialogUtil.showQuestion( + "An update is available (${metadata.version}). Would you like to download it now?", + false + ) + return answer == JOptionPane.YES_OPTION + } + + private fun showManualDownloadMessage(newVersion: String) { + val fullUrl = "${OnlineConstants.ENTROPY_MANUAL_DOWNLOAD_URL}/tag/$newVersion" + val panel = JPanel() + panel.layout = BorderLayout(0, 0) + val lblOne = + JLabel("An update is available ($newVersion). You can download it manually from:") + val linkLabel = LinkLabel(fullUrl) { launchUrl(fullUrl) } + + panel.add(lblOne, BorderLayout.NORTH) + panel.add(linkLabel, BorderLayout.SOUTH) + + DialogUtil.showCustomMessage(panel) + } + + fun parseUpdateMetadata(responseJson: JSONObject): UpdateMetadata? { + return try { + val remoteVersion = responseJson.getString("tag_name") + val assets = responseJson.getJSONArray("assets") + val asset = assets.getJSONObject(0) + + val assetId = asset.getLong("id") + val fileName = asset.getString("name") + val size = asset.getLong("size") + UpdateMetadata(remoteVersion, assetId, fileName, size) + } catch (t: Throwable) { + logger.error( + "parseError", + "Error parsing update response", + t, + "responseBody" to responseJson + ) + null + } + } + + fun startUpdate(args: String, runtime: Runtime) { + prepareBatchFile() + + try { + runtime.exec("cmd /c start update.bat $args") + } catch (t: Throwable) { + logger.error("batchError", "Error running update.bat", t) + val manualCommand = "update.bat $args" + + val msg = + "Failed to launch update.bat - call the following manually to perform the update: \n\n$manualCommand" + DialogUtil.showError(msg) + return + } + + exitProcess(0) + } + + fun prepareBatchFile() { + val updateFile = File("update.bat") + + updateFile.delete() + val updateScript = javaClass.getResource("/update/update.bat").readText() + updateFile.writeText(updateScript) + } +} + +data class UpdateMetadata( + val version: String, + val assetId: Long, + val fileName: String, + val size: Long +) { + fun getArgs() = "$size $version $fileName $assetId" +} diff --git a/client/src/main/java/util/UrlUtil.kt b/client/src/main/java/util/UrlUtil.kt new file mode 100644 index 0000000..e8c0a2f --- /dev/null +++ b/client/src/main/java/util/UrlUtil.kt @@ -0,0 +1,12 @@ +package util + +import utils.InjectedThings.logger + +/** N.B. will likely only work on linux */ +fun launchUrl(url: String, runtime: Runtime = Runtime.getRuntime()) { + try { + runtime.exec("xdg-open $url") + } catch (e: Exception) { + logger.error("urlError", "Failed to launch $url", e) + } +} diff --git a/client/src/main/kotlin/bean/HyperlinkAdaptor.kt b/client/src/main/kotlin/bean/HyperlinkAdaptor.kt new file mode 100644 index 0000000..7510cd0 --- /dev/null +++ b/client/src/main/kotlin/bean/HyperlinkAdaptor.kt @@ -0,0 +1,36 @@ +package bean + +import java.awt.Component +import java.awt.Cursor +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent + +class HyperlinkAdaptor(private val listener: IHyperlinkListener) : MouseAdapter() { + private val listenerWindow = listener as Component + + override fun mouseClicked(arg0: MouseEvent) = listener.linkClicked(arg0) + + override fun mouseMoved(arg0: MouseEvent) { + if (listener.isOverHyperlink(arg0)) { + listenerWindow.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + } else { + listenerWindow.cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + } + } + + override fun mouseEntered(e: MouseEvent) { + if (listener.isOverHyperlink(e)) { + listenerWindow.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + } + } + + override fun mouseDragged(e: MouseEvent?) { + if (listenerWindow.cursor.type == Cursor.HAND_CURSOR) { + listenerWindow.cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + } + } + + override fun mouseExited(arg0: MouseEvent?) { + listenerWindow.cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + } +} diff --git a/client/src/main/kotlin/bean/IHyperlinkListener.kt b/client/src/main/kotlin/bean/IHyperlinkListener.kt new file mode 100644 index 0000000..58075c7 --- /dev/null +++ b/client/src/main/kotlin/bean/IHyperlinkListener.kt @@ -0,0 +1,9 @@ +package bean + +import java.awt.event.MouseEvent + +interface IHyperlinkListener { + fun linkClicked(arg0: MouseEvent) + + fun isOverHyperlink(arg0: MouseEvent): Boolean +} diff --git a/client/src/main/kotlin/bean/LinkLabel.kt b/client/src/main/kotlin/bean/LinkLabel.kt new file mode 100644 index 0000000..e20a46f --- /dev/null +++ b/client/src/main/kotlin/bean/LinkLabel.kt @@ -0,0 +1,20 @@ +package bean + +import java.awt.Color +import java.awt.event.MouseEvent +import javax.swing.JLabel + +class LinkLabel(text: String, private val linkClicked: () -> Unit) : + JLabel("$text"), IHyperlinkListener { + init { + foreground = Color.BLUE + + val adaptor = HyperlinkAdaptor(this) + addMouseListener(adaptor) + addMouseMotionListener(adaptor) + } + + override fun linkClicked(arg0: MouseEvent) = linkClicked() + + override fun isOverHyperlink(arg0: MouseEvent) = true +} diff --git a/client/src/test/kotlin/bean/LinkLabelTest.kt b/client/src/test/kotlin/bean/LinkLabelTest.kt new file mode 100644 index 0000000..cace779 --- /dev/null +++ b/client/src/test/kotlin/bean/LinkLabelTest.kt @@ -0,0 +1,37 @@ +package bean + +import com.github.alyssaburlton.swingtest.doClick +import com.github.alyssaburlton.swingtest.doHover +import com.github.alyssaburlton.swingtest.doHoverAway +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import io.mockk.verify +import java.awt.Color +import java.awt.Cursor +import org.junit.jupiter.api.Test +import helper.AbstractTest + +class LinkLabelTest : AbstractTest() { + @Test + fun `Should style text as URL`() { + val label = LinkLabel("https://foo.bar") {} + + label.text shouldBe "https://foo.bar" + label.foreground shouldBe Color.BLUE + } + + @Test + fun `Should update cursor on hover, and execute callback on click`() { + val callback = mockk<() -> Unit>(relaxed = true) + val label = LinkLabel("test", callback) + + label.doHover() + label.cursor shouldBe Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + + label.doHoverAway() + label.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + + label.doClick() + verify { callback() } + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index a766fbf..b73ff3b 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("Entropy.kotlin-common-conventions") id("com.ncorti.ktfmt.gradle") version "0.15.1" `java-library` + `java-test-fixtures` } ktfmt { kotlinLangStyle() } diff --git a/core/src/main/java/util/AbstractClient.java b/core/src/main/java/util/AbstractClient.java index 548e9a9..36b9417 100644 --- a/core/src/main/java/util/AbstractClient.java +++ b/core/src/main/java/util/AbstractClient.java @@ -76,6 +76,10 @@ public static boolean isAppleOs() { return operatingSystem.contains("mac") || operatingSystem.contains("darwin"); } + public static boolean isWindowsOs() + { + return operatingSystem.contains("windows"); + } public void checkForUpdatesIfRequired() { diff --git a/core/src/main/java/util/Debug.java b/core/src/main/java/util/Debug.java index 5297dc1..7bb4083 100644 --- a/core/src/main/java/util/Debug.java +++ b/core/src/main/java/util/Debug.java @@ -4,29 +4,16 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.text.SimpleDateFormat; -import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; public class Debug implements CoreRegistry { - private static final long ERROR_MESSAGE_DELAY_MILLIS = 10000; //10s - - private static long lastErrorMillis = -1; - private static DebugOutput output = null; - private static DebugExtension debugExtension = null; private static boolean logToSystemOut = false; - private static ThreadFactory loggerFactory = new ThreadFactory() - { - @Override - public Thread newThread(Runnable r) - { - return new Thread(r, "Logger"); - } - }; + private static ThreadFactory loggerFactory = r -> new Thread(r, "Logger"); private static ExecutorService logService = Executors.newFixedThreadPool(1, loggerFactory); @@ -117,27 +104,12 @@ public static void stackTrace(Throwable t) { stackTrace(t, ""); } - public static void stackTrace(Throwable t, String message) - { - stackTrace(t, message, false); - } public static void stackTraceNoError(Throwable t) { - stackTrace(t, "", true); + stackTrace(t, ""); } - public static void stackTrace(Throwable t, String message, boolean suppressError) + public static void stackTrace(Throwable t, String message) { - if (debugExtension != null - && !suppressError) - { - boolean showError = System.currentTimeMillis() - lastErrorMillis > ERROR_MESSAGE_DELAY_MILLIS; - debugExtension.exceptionCaught(showError); - if (showError) - { - lastErrorMillis = System.currentTimeMillis(); - } - } - String datetime = getCurrentTimeForLogging(); String trace = ""; @@ -178,11 +150,7 @@ public static void initialise(DebugOutput output) { Debug.output = output; } - - public static void setDebugExtension(DebugExtension debugExtension) - { - Debug.debugExtension = debugExtension; - } + public static void setLogToSystemOut(boolean logToSystemOut) { Debug.logToSystemOut = logToSystemOut; diff --git a/core/src/main/java/util/DebugExtension.java b/core/src/main/java/util/DebugExtension.java deleted file mode 100644 index a193629..0000000 --- a/core/src/main/java/util/DebugExtension.java +++ /dev/null @@ -1,8 +0,0 @@ -package util; - -public interface DebugExtension -{ - public void exceptionCaught(boolean showError); - public void unableToEmailLogs(); - public void sendEmail(String title, String message) throws Exception; -} diff --git a/core/src/main/java/util/FileUtil.java b/core/src/main/java/util/FileUtil.java index c66c49b..0f1bb79 100644 --- a/core/src/main/java/util/FileUtil.java +++ b/core/src/main/java/util/FileUtil.java @@ -24,42 +24,6 @@ public class FileUtil { - public static File createNewFile(String filePath, String contents) - { - File file = new File(filePath); - boolean success = false; - - try - { - success = file.createNewFile(); - } - catch (IOException ioe) - { - Debug.append("Caught " + ioe + " creating file " + filePath); - } - - if (!success) - { - return null; - } - - //We have created the empty file, now fill it - try (FileOutputStream fos = new FileOutputStream(filePath)) - { - byte[] bytes = contents.getBytes("UTF-8"); - fos.write(bytes); - } - catch (IOException ioe) - { - Debug.append("Caught " + ioe + " trying to insert bytes into file " + filePath); - deleteFileIfExists(filePath); - return null; - } - - Debug.append("Successfully created file " + file); - return file; - } - public static String getMd5Crc(String filePath) { String crc = null; @@ -97,61 +61,6 @@ public static long getFileSize(String filePath) return fileSize; } - public static boolean deleteFileIfExists(String filePath) - { - boolean success = false; - - try - { - Path path = Paths.get(filePath); - success = Files.deleteIfExists(path); - } - catch (Throwable t) - { - Debug.stackTrace(t, "Failed to delete file"); - } - - return success; - } - - public static String swapInFile(String oldFilePath, String newFilePath) - { - boolean success = true; - - File oldFile = new File(oldFilePath); - String oldFileName = oldFile.getName(); - File newFile = new File(newFilePath); - - File zzOldFile = new File(oldFile.getParent(), "zz" + oldFileName); - if (oldFile.exists() - && !oldFile.renameTo(zzOldFile)) - { - return "Failed to rename old out of the way."; - } - - success &= newFile.renameTo(new File(oldFile.getParent(), oldFileName)); - if (!success) - { - return "Failed to rename new file to " + oldFileName; - } - - if (zzOldFile.isFile()) - { - success &= deleteFileIfExists(zzOldFile.getPath()); - } - else - { - success &= deleteDirectoryIfExists(zzOldFile); - } - - if (!success) - { - return "Failed to delete zz'd old file: " + oldFile.getPath(); - } - - return null; - } - public static void saveTextToFile(String text, Path destinationPath) { Charset charset = Charset.forName("US-ASCII"); @@ -170,24 +79,6 @@ public static void saveTextToFile(String text, Path destinationPath) } } - public static String getFileContentsAsString(File file) - { - String ret = null; - - try - { - Path path = file.toPath(); - byte[] bytes = Files.readAllBytes(path); - ret = new String(bytes, "UTF-8"); - } - catch (Throwable t) - { - Debug.stackTrace(t); - } - - return ret; - } - public static String getBase64DecodedFileContentsAsString(File file) { try @@ -211,196 +102,9 @@ public static void encodeAndSaveToFile(Path destinationPath, String stringToWrit saveTextToFile(encodedStringToWrite, destinationPath); } - public static Dimension getImageDim(String path) - { - Dimension result = null; - String suffix = getFileSuffix(path); - Iterator iter = ImageIO.getImageReadersBySuffix(suffix); - if (iter.hasNext()) - { - ImageReader reader = iter.next(); - try (ImageInputStream stream = new FileImageInputStream(new File(path));) - { - reader.setInput(stream); - int width = reader.getWidth(reader.getMinIndex()); - int height = reader.getHeight(reader.getMinIndex()); - result = new Dimension(width, height); - } - catch (IOException e) - { - Debug.stackTrace(e); - } - finally - { - reader.dispose(); - } - } - else - { - Debug.stackTrace("No reader found for file extension: " + suffix + " (full path: " + path + ")"); - } - - return result; - } - - private static String getFileSuffix(String path) - { - if (path == null - || path.lastIndexOf('.') == -1) - { - return ""; - } - - int dotIndex = path.lastIndexOf('.'); - return path.substring(dotIndex + 1); - } - - /** - * Helper to create a file object for a URL, e.g. from a classpath resource. - * No longer used - */ - /*public static File getForURL(URL url) - { - try - { - URI uri = url.toURI(); - return new File(uri); - } - catch (Throwable t) - { - Debug.append("Failed to construct file for URL: " + url); - Debug.stackTrace(t); - return null; - } - }*/ - public static String stripFileExtension(String filename) { int ix = filename.indexOf('.'); return filename.substring(0, ix); } - - public static byte[] getByteArrayForResource(String resourcePath) - { - try (InputStream is = FileUtil.class.getResourceAsStream(resourcePath); - ByteArrayOutputStream baos = new ByteArrayOutputStream()) - { - byte[] b = new byte[4096]; - int n = 0; - while ((n = is.read(b)) != -1) - { - baos.write(b, 0, n); - } - - return baos.toByteArray(); - } - catch (IOException ioe) - { - Debug.stackTrace(ioe, "Failed to read classpath resource: " + resourcePath); - return null; - } - } - - /** - * Delete a whole directory, recursively clearing out the files/subfolders too. - */ - public static boolean deleteDirectoryIfExists(File dir) - { - if (!dir.exists() - || !dir.isDirectory()) - { - //Just don't do anything - return true; - } - - boolean success = true; - File[] files = dir.listFiles(); - for (int i=0; i 0) - { - outStream.write(buffer, 0, count); - } - } - catch (SocketException se) - { - Debug.append("Caught " + se + " for JAR download."); - } - catch (Throwable t) - { - Debug.stackTrace(t); - } - finally - { - server.incrementFunctionsHandled(); - server.incrementFunctionsHandledForMessage("DownloadRequest"); - - if (socket != null) - { - try {socket.close();} catch (Throwable t) {} - } - } - } -} diff --git a/server/src/main/java/server/DownloadListener.java b/server/src/main/java/server/DownloadListener.java deleted file mode 100644 index 6c3919b..0000000 --- a/server/src/main/java/server/DownloadListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package server; - -import java.net.Socket; - -public class DownloadListener extends ListenerRunnable -{ - String filename = ""; - - public DownloadListener(EntropyServer server, int port, String filename) - { - super(server, port); - - this.filename = filename; - } - - @Override - public void handleInboundMessage(EntropyServer server, Socket clientSocket) - { - DownloadHandler.handleDownload(server, clientSocket, filename); - } -} diff --git a/server/src/main/java/server/EntropyServer.java b/server/src/main/java/server/EntropyServer.java index d7127ff..00f2682 100644 --- a/server/src/main/java/server/EntropyServer.java +++ b/server/src/main/java/server/EntropyServer.java @@ -103,7 +103,6 @@ public final class EntropyServer extends JFrame //Other private String lastCommand = ""; - private SuperHashMap hmClientJarToVersion = new SuperHashMap<>(); private PrivateKey privateKey = null; @@ -179,7 +178,6 @@ public static void main(String args[]) //Initialise interfaces etc EncryptionUtil.setBase64Interface(new Base64Desktop()); - Debug.setDebugExtension(new ServerDebugExtension()); Debug.initialise(console); //Set other variables on Debug @@ -223,7 +221,6 @@ private void onStart() readInPrivateKey(); readUsedKeysFromFile(); - readClientVersion(); registerDefaultRooms(); Debug.append("Starting permanent threads"); @@ -367,14 +364,6 @@ private void startListenerThreads() Debug.stackTrace(t, "Unable to start listener thread on port " + i); } } - - startDownloadListener(FILE_NAME_ENTROPY_JAR, SERVER_PORT_NUMBER_DOWNLOAD); - } - private void startDownloadListener(String filename, int port) - { - DownloadListener listener = new DownloadListener(this, port, filename); - ServerThread downloadListener = new ServerThread(listener, "DownloadListener-" + port); - downloadListener.start(); } private void startFunctionThread() @@ -857,10 +846,6 @@ public Room getRoomForName(String name) { return hmRoomByName.get(name); } - public String getClientVersion(String jar) - { - return hmClientJarToVersion.get(jar); - } public void toggleOnline() { online = !online; @@ -1061,45 +1046,6 @@ private void readUsedKeysFromFile() } } - private void readClientVersion() - { - try - { - Charset charset = Charset.forName("US-ASCII"); - List jarsAndVersions = Files.readAllLines(FILE_PATH_CLIENT_VERSION, charset); - - for (int i=0; i jarAndVersion = StringUtil.getListFromDelims(jarAndVersionStr, ";"); - - String jar = jarAndVersion.get(0); - String version = jarAndVersion.get(1); - - hmClientJarToVersion.put(jar, version); - } - - logClientVersions(); - } - catch (Throwable t) - { - Debug.append("Caught " + t + " trying to read in client version"); - } - } - private void logClientVersions() - { - Iterator> it = hmClientJarToVersion.entrySet().iterator(); - for (; it.hasNext(); ) - { - Map.Entry jarAndVersion = it.next(); - - String jar = jarAndVersion.getKey(); - String version = jarAndVersion.getValue(); - - Debug.append(FileUtil.stripFileExtension(jar) + " Version: " + version); - } - } - public ConcurrentHashMap getBlacklist() { return blacklist; @@ -1415,11 +1361,6 @@ else if (command.startsWith(COMMAND_NOTIFY_USER)) else if (command.equals(COMMAND_SERVER_VERSION)) { Debug.append("Server version: " + OnlineConstants.SERVER_VERSION); - logClientVersions(); - } - else if (command.equals(COMMAND_SERVER_RESET_CLIENT_VERSION)) - { - readClientVersion(); } else { @@ -1480,7 +1421,6 @@ private void printCommands() Debug.appendWithoutDate(COMMAND_NOTIFICATION_LOGGING); Debug.appendWithoutDate(COMMAND_NOTIFY_USER + ""); Debug.appendWithoutDate(COMMAND_SERVER_VERSION); - Debug.appendWithoutDate(COMMAND_SERVER_RESET_CLIENT_VERSION); } private void dumpMessageStats() diff --git a/server/src/main/java/server/MessageHandlerRunnable.java b/server/src/main/java/server/MessageHandlerRunnable.java index 76659a2..397f6db 100644 --- a/server/src/main/java/server/MessageHandlerRunnable.java +++ b/server/src/main/java/server/MessageHandlerRunnable.java @@ -101,8 +101,7 @@ public void run() Debug.appendWithoutDate("Response: " + responseStr); } - if (symmetricKey != null - && !DownloadHandler.isDownloadMessage(name)) + if (symmetricKey != null) { responseStr = EncryptionUtil.encrypt(responseStr, symmetricKey); } @@ -225,15 +224,6 @@ private Document handleUnencryptedMessage(Document message) Element root = message.getDocumentElement(); String name = root.getNodeName(); - if (DownloadHandler.isDownloadMessage(name)) - { - return DownloadHandler.processMessage(message, server); - } - - if (name.equals(ROOT_TAG_CLIENT_MAIL)) - { - return EntropyEmailUtil.handleClientMail(server, root); - } if (!name.equals(ROOT_TAG_NEW_SYMMETRIC_KEY)) { diff --git a/server/src/main/java/server/ServerCommands.java b/server/src/main/java/server/ServerCommands.java index a427ddd..90601f6 100644 --- a/server/src/main/java/server/ServerCommands.java +++ b/server/src/main/java/server/ServerCommands.java @@ -34,5 +34,4 @@ public interface ServerCommands public static final String COMMAND_NOTIFICATION_LOGGING = "notification logging"; public static final String COMMAND_NOTIFY_USER = "notify "; public static final String COMMAND_SERVER_VERSION = "version"; - public static final String COMMAND_SERVER_RESET_CLIENT_VERSION = "reset version"; } diff --git a/server/src/main/java/util/EntropyEmailUtil.java b/server/src/main/java/util/EntropyEmailUtil.java index 33cf234..813cd38 100644 --- a/server/src/main/java/util/EntropyEmailUtil.java +++ b/server/src/main/java/util/EntropyEmailUtil.java @@ -1,38 +1,12 @@ package util; -import java.io.File; -import java.io.FileOutputStream; -import java.util.ArrayList; - -import javax.crypto.SecretKey; import javax.mail.MessagingException; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; - -import server.EntropyServer; - public class EntropyEmailUtil { - public static final String EMAIL_ADDRESS_ENTROPY = "entropyDebug@gmail.com"; - - private static final String ENTROPY_USERNAME = "entropyDebug"; private static final String NO_REPLY_USERNAME = "entropynoreply"; - private static final String ENTROPY_PASSWORD = "****"; private static final String NO_REPLY_PASSWORD = "****"; - - /** - * Send an email via the usual EntropyDebug account - */ - public static void sendEmail(String title, String message) throws MessagingException - { - sendEmail(title, message, null); - } - public static void sendEmail(String title, String message, ArrayList attachments) throws MessagingException - { - EmailUtil.sendEmail(title, message, EMAIL_ADDRESS_ENTROPY, ENTROPY_USERNAME, ENTROPY_PASSWORD, attachments); - } + /** * Send an email from EntropyNoReply, e.g. a password reset @@ -41,99 +15,4 @@ public static void sendEmailNoReply(String title, String message, String targetE { EmailUtil.sendEmail(title, message, targetEmail, NO_REPLY_USERNAME, NO_REPLY_PASSWORD, null); } - - /** - * Handle a client mail message - */ - public static Document handleClientMail(EntropyServer server, Element rootElement) - { - String encryptedSymetricKeyStr = rootElement.getAttribute("EncryptedKey"); - String symmetricKeyStr = EncryptionUtil.decrypt(encryptedSymetricKeyStr, server.getPrivateKey(), true); - SecretKey symmetricKey = EncryptionUtil.reconstructKeyFromString(symmetricKeyStr); - - if (symmetricKey == null) - { - //Want to know about this. I've seen a client passing up a blank tag for EncryptedKey, and I don't - //know how that's possible. - Debug.stackTrace("Failed to reconstruct symmetricKey for EncryptedKey attribute"); - Debug.append("EncryptedKeyStr: " + encryptedSymetricKeyStr); - Debug.append("SymmetricKeyStr: " + symmetricKeyStr); - } - - String subject = EncryptionUtil.decryptIfPossible(rootElement.getAttribute("Subject"), symmetricKey); - String body = EncryptionUtil.decryptIfPossible(rootElement.getAttribute("Body"), symmetricKey); - - ArrayList attachments = new ArrayList<>(); - NodeList attachmentElements = rootElement.getElementsByTagName("Attachment"); - for (int i=0; i Date: Sat, 17 Aug 2024 15:38:08 +0100 Subject: [PATCH 2/6] test-core project to fix access to shared testing stuff --- client/build.gradle.kts | 2 +- client/src/test/kotlin/bean/LinkLabelTest.kt | 2 +- core/build.gradle.kts | 2 +- core/src/test/kotlin/bean/FocusableWindowTest.kt | 2 +- .../kotlin/logging/LogDestinationSystemOutTest.kt | 6 +++--- core/src/test/kotlin/logging/LoggerTest.kt | 8 ++++---- .../logging/LoggerUncaughtExceptionHandlerTest.kt | 4 ++-- .../src/test/kotlin/logging/LoggingConsoleTest.kt | 6 +++--- core/src/test/kotlin/logging/LoggingUtilsTest.kt | 2 +- core/src/test/kotlin/utils/MathsUtilTest.kt | 2 +- server/build.gradle.kts | 1 + settings.gradle.kts | 2 +- test-core/build.gradle.kts | 15 +++++++++++++++ .../src/main/kotlin/testCore}/AbstractTest.kt | 2 +- .../kotlin/testCore}/BeforeAllTestsExtension.kt | 3 +-- .../main/kotlin/testCore}/FakeLogDestination.kt | 2 +- .../src/main/kotlin/testCore}/TestConstants.kt | 2 ++ .../src/main/kotlin/testCore}/TestUtils.kt | 3 +-- 18 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 test-core/build.gradle.kts rename {core/src/test/kotlin/helper => test-core/src/main/kotlin/testCore}/AbstractTest.kt (98%) rename {core/src/test/kotlin/helper => test-core/src/main/kotlin/testCore}/BeforeAllTestsExtension.kt (95%) rename {core/src/test/kotlin/helper => test-core/src/main/kotlin/testCore}/FakeLogDestination.kt (92%) rename {core/src/test/kotlin/helper => test-core/src/main/kotlin/testCore}/TestConstants.kt (92%) rename {core/src/test/kotlin/helper => test-core/src/main/kotlin/testCore}/TestUtils.kt (97%) diff --git a/client/build.gradle.kts b/client/build.gradle.kts index c4e737f..e4ab79b 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -11,7 +11,7 @@ dependencies { implementation("com.miglayout:miglayout-swing:5.2") implementation("com.konghq:unirest-java:3.14.2") implementation(project(":core")) - testImplementation(testFixtures(project(":core"))) + testImplementation(project(":test-core")) } application { diff --git a/client/src/test/kotlin/bean/LinkLabelTest.kt b/client/src/test/kotlin/bean/LinkLabelTest.kt index cace779..dddb799 100644 --- a/client/src/test/kotlin/bean/LinkLabelTest.kt +++ b/client/src/test/kotlin/bean/LinkLabelTest.kt @@ -1,5 +1,6 @@ package bean +import main.kotlin.testCore.AbstractTest import com.github.alyssaburlton.swingtest.doClick import com.github.alyssaburlton.swingtest.doHover import com.github.alyssaburlton.swingtest.doHoverAway @@ -9,7 +10,6 @@ import io.mockk.verify import java.awt.Color import java.awt.Cursor import org.junit.jupiter.api.Test -import helper.AbstractTest class LinkLabelTest : AbstractTest() { @Test diff --git a/core/build.gradle.kts b/core/build.gradle.kts index b73ff3b..b5a2465 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -2,7 +2,6 @@ plugins { id("Entropy.kotlin-common-conventions") id("com.ncorti.ktfmt.gradle") version "0.15.1" `java-library` - `java-test-fixtures` } ktfmt { kotlinLangStyle() } @@ -10,4 +9,5 @@ ktfmt { kotlinLangStyle() } dependencies { implementation("javax.mail:javax.mail-api:1.6.2") implementation("javax.activation:activation:1.1.1") + testImplementation(project(":test-core")) } diff --git a/core/src/test/kotlin/bean/FocusableWindowTest.kt b/core/src/test/kotlin/bean/FocusableWindowTest.kt index c780907..d7c0f99 100644 --- a/core/src/test/kotlin/bean/FocusableWindowTest.kt +++ b/core/src/test/kotlin/bean/FocusableWindowTest.kt @@ -1,6 +1,6 @@ package bean -import helper.AbstractTest +import main.kotlin.testCore.AbstractTest import io.kotest.matchers.shouldBe import io.mockk.mockk import logging.KEY_ACTIVE_WINDOW diff --git a/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt b/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt index cd12cc4..a7e0f67 100644 --- a/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt +++ b/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt @@ -1,8 +1,8 @@ package logging -import CURRENT_TIME_STRING -import helper.AbstractTest -import helper.makeLogRecord +import main.kotlin.testCore.CURRENT_TIME_STRING +import main.kotlin.testCore.AbstractTest +import main.kotlin.testCore.makeLogRecord import io.kotest.matchers.string.shouldContain import java.io.ByteArrayOutputStream import java.io.PrintStream diff --git a/core/src/test/kotlin/logging/LoggerTest.kt b/core/src/test/kotlin/logging/LoggerTest.kt index 0c0fe4a..4bfff06 100644 --- a/core/src/test/kotlin/logging/LoggerTest.kt +++ b/core/src/test/kotlin/logging/LoggerTest.kt @@ -1,9 +1,9 @@ package logging -import CURRENT_TIME -import helper.AbstractTest -import helper.FakeLogDestination -import helper.shouldContainKeyValues +import main.kotlin.testCore.CURRENT_TIME +import main.kotlin.testCore.AbstractTest +import main.kotlin.testCore.FakeLogDestination +import main.kotlin.testCore.shouldContainKeyValues import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe diff --git a/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt b/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt index 757becd..1bfb781 100644 --- a/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt +++ b/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt @@ -1,7 +1,7 @@ package logging -import helper.AbstractTest -import helper.shouldContainKeyValues +import main.kotlin.testCore.AbstractTest +import main.kotlin.testCore.shouldContainKeyValues import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import org.junit.jupiter.api.Test diff --git a/core/src/test/kotlin/logging/LoggingConsoleTest.kt b/core/src/test/kotlin/logging/LoggingConsoleTest.kt index 69aa59f..7588b5b 100644 --- a/core/src/test/kotlin/logging/LoggingConsoleTest.kt +++ b/core/src/test/kotlin/logging/LoggingConsoleTest.kt @@ -1,9 +1,9 @@ package logging import com.github.alyssaburlton.swingtest.flushEdt -import helper.AbstractTest -import helper.getAllChildComponentsForType -import helper.makeLogRecord +import main.kotlin.testCore.AbstractTest +import main.kotlin.testCore.getAllChildComponentsForType +import main.kotlin.testCore.makeLogRecord import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.comparables.shouldBeGreaterThan diff --git a/core/src/test/kotlin/logging/LoggingUtilsTest.kt b/core/src/test/kotlin/logging/LoggingUtilsTest.kt index a356ce9..ad713e4 100644 --- a/core/src/test/kotlin/logging/LoggingUtilsTest.kt +++ b/core/src/test/kotlin/logging/LoggingUtilsTest.kt @@ -1,6 +1,6 @@ package logging -import helper.AbstractTest +import main.kotlin.testCore.AbstractTest import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import org.junit.jupiter.api.Test diff --git a/core/src/test/kotlin/utils/MathsUtilTest.kt b/core/src/test/kotlin/utils/MathsUtilTest.kt index 8fef509..c4cab77 100644 --- a/core/src/test/kotlin/utils/MathsUtilTest.kt +++ b/core/src/test/kotlin/utils/MathsUtilTest.kt @@ -1,7 +1,7 @@ package utils import getPercentage -import helper.AbstractTest +import main.kotlin.testCore.AbstractTest import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test diff --git a/server/build.gradle.kts b/server/build.gradle.kts index aeebb8a..d1d5a01 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -9,6 +9,7 @@ ktfmt { kotlinLangStyle() } dependencies { implementation("javax.mail:javax.mail-api:1.6.2") implementation(project(":core")) + testImplementation(project(":test-core")) } application { diff --git a/settings.gradle.kts b/settings.gradle.kts index 22d75d3..e1b1cc0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,4 +8,4 @@ */ rootProject.name = "Entropy" -include("core", "client", "server") +include("core", "client", "server", "test-core") diff --git a/test-core/build.gradle.kts b/test-core/build.gradle.kts new file mode 100644 index 0000000..2cf9417 --- /dev/null +++ b/test-core/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("Entropy.kotlin-common-conventions") + id("com.ncorti.ktfmt.gradle") version "0.15.1" + `java-library` +} + +ktfmt { kotlinLangStyle() } + +dependencies { + implementation(project(":core")) + implementation("org.junit.jupiter:junit-jupiter:5.9.2") + implementation("io.mockk:mockk:1.13.4") + implementation("io.kotest:kotest-assertions-core:5.5.4") + implementation("com.github.alexburlton:swing-test:4.0.0") +} diff --git a/core/src/test/kotlin/helper/AbstractTest.kt b/test-core/src/main/kotlin/testCore/AbstractTest.kt similarity index 98% rename from core/src/test/kotlin/helper/AbstractTest.kt rename to test-core/src/main/kotlin/testCore/AbstractTest.kt index 51b755f..bf874bb 100644 --- a/core/src/test/kotlin/helper/AbstractTest.kt +++ b/test-core/src/main/kotlin/testCore/AbstractTest.kt @@ -1,4 +1,4 @@ -package helper +package main.kotlin.testCore import com.github.alyssaburlton.swingtest.purgeWindows import io.kotest.assertions.fail diff --git a/core/src/test/kotlin/helper/BeforeAllTestsExtension.kt b/test-core/src/main/kotlin/testCore/BeforeAllTestsExtension.kt similarity index 95% rename from core/src/test/kotlin/helper/BeforeAllTestsExtension.kt rename to test-core/src/main/kotlin/testCore/BeforeAllTestsExtension.kt index cc05091..33088e0 100644 --- a/core/src/test/kotlin/helper/BeforeAllTestsExtension.kt +++ b/test-core/src/main/kotlin/testCore/BeforeAllTestsExtension.kt @@ -1,6 +1,5 @@ -package helper +package main.kotlin.testCore -import CURRENT_TIME import java.time.Clock import java.time.ZoneId import logging.LoggerUncaughtExceptionHandler diff --git a/core/src/test/kotlin/helper/FakeLogDestination.kt b/test-core/src/main/kotlin/testCore/FakeLogDestination.kt similarity index 92% rename from core/src/test/kotlin/helper/FakeLogDestination.kt rename to test-core/src/main/kotlin/testCore/FakeLogDestination.kt index ab75c8c..7b5c0cf 100644 --- a/core/src/test/kotlin/helper/FakeLogDestination.kt +++ b/test-core/src/main/kotlin/testCore/FakeLogDestination.kt @@ -1,4 +1,4 @@ -package helper +package main.kotlin.testCore import logging.ILogDestination import logging.LogRecord diff --git a/core/src/test/kotlin/helper/TestConstants.kt b/test-core/src/main/kotlin/testCore/TestConstants.kt similarity index 92% rename from core/src/test/kotlin/helper/TestConstants.kt rename to test-core/src/main/kotlin/testCore/TestConstants.kt index a69de53..6db1008 100644 --- a/core/src/test/kotlin/helper/TestConstants.kt +++ b/test-core/src/main/kotlin/testCore/TestConstants.kt @@ -1,3 +1,5 @@ +package main.kotlin.testCore + import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter diff --git a/core/src/test/kotlin/helper/TestUtils.kt b/test-core/src/main/kotlin/testCore/TestUtils.kt similarity index 97% rename from core/src/test/kotlin/helper/TestUtils.kt rename to test-core/src/main/kotlin/testCore/TestUtils.kt index fca301b..d286ac9 100644 --- a/core/src/test/kotlin/helper/TestUtils.kt +++ b/test-core/src/main/kotlin/testCore/TestUtils.kt @@ -1,6 +1,5 @@ -package helper +package main.kotlin.testCore -import CURRENT_TIME import io.kotest.matchers.maps.shouldContainExactly import java.awt.Component import java.awt.Container From 48d56f6e479ae59b7b013c367ed8ba13c940bb89 Mon Sep 17 00:00:00 2001 From: Alyssa Date: Sat, 17 Aug 2024 16:14:29 +0100 Subject: [PATCH 3/6] Port more tests, fix formatting --- client/src/main/java/util/DialogUtil.java | 25 +- client/src/main/kotlin/util/DialogUtilNew.kt | 164 +++++++++++ .../{java => kotlin}/util/UpdateManager.kt | 16 +- .../src/main/{java => kotlin}/util/UrlUtil.kt | 0 client/src/main/resources/update/update.bat | 17 ++ .../test/kotlin/bean/HyperlinkAdaptorTest.kt | 94 +++++++ client/src/test/kotlin/bean/LinkLabelTest.kt | 2 +- .../src/test/kotlin/util/DialogUtilNewTest.kt | 106 +++++++ .../src/test/kotlin/util/UpdateManagerTest.kt | 262 ++++++++++++++++++ client/src/test/kotlin/util/UrlUtilTest.kt | 34 +++ .../test/kotlin/bean/FocusableWindowTest.kt | 2 +- .../logging/LogDestinationSystemOutTest.kt | 6 +- core/src/test/kotlin/logging/LoggerTest.kt | 8 +- .../LoggerUncaughtExceptionHandlerTest.kt | 4 +- .../test/kotlin/logging/LoggingConsoleTest.kt | 6 +- .../test/kotlin/logging/LoggingUtilsTest.kt | 2 +- core/src/test/kotlin/utils/MathsUtilTest.kt | 2 +- .../main/kotlin/testCore/ExitAssertions.kt | 50 ++++ .../src/main/kotlin/testCore/TestUtils.kt | 27 ++ 19 files changed, 779 insertions(+), 48 deletions(-) create mode 100644 client/src/main/kotlin/util/DialogUtilNew.kt rename client/src/main/{java => kotlin}/util/UpdateManager.kt (91%) rename client/src/main/{java => kotlin}/util/UrlUtil.kt (100%) create mode 100644 client/src/main/resources/update/update.bat create mode 100644 client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt create mode 100644 client/src/test/kotlin/util/DialogUtilNewTest.kt create mode 100644 client/src/test/kotlin/util/UpdateManagerTest.kt create mode 100644 client/src/test/kotlin/util/UrlUtilTest.kt create mode 100644 test-core/src/main/kotlin/testCore/ExitAssertions.kt diff --git a/client/src/main/java/util/DialogUtil.java b/client/src/main/java/util/DialogUtil.java index 51220a8..f00f3f6 100644 --- a/client/src/main/java/util/DialogUtil.java +++ b/client/src/main/java/util/DialogUtil.java @@ -3,15 +3,11 @@ import javax.swing.JOptionPane; import javax.swing.SwingUtilities; -import screen.LoadingDialog; - -import java.awt.*; - import static utils.InjectedThings.logger; +@Deprecated() public class DialogUtil { - private static LoadingDialog loadingDialog = new LoadingDialog(); private static boolean shownConnectionLost = false; public static void showInfo(String infoText) @@ -106,23 +102,4 @@ public static void showConnectionLost() shownConnectionLost = true; } } - - public static void showLoadingDialog(String text) - { - logger.info("loaderShown", text); - loadingDialog.showDialog(text); - } - public static void dismissLoadingDialog() - { - loadingDialog.dismissDialog(); - } - - public static void showCustomMessage(Object message) { - JOptionPane.showMessageDialog( - null, - message, - "Information", - JOptionPane.INFORMATION_MESSAGE - ); - } } diff --git a/client/src/main/kotlin/util/DialogUtilNew.kt b/client/src/main/kotlin/util/DialogUtilNew.kt new file mode 100644 index 0000000..929e30f --- /dev/null +++ b/client/src/main/kotlin/util/DialogUtilNew.kt @@ -0,0 +1,164 @@ +package util + +import java.awt.Component +import java.io.File +import javax.swing.JFileChooser +import javax.swing.JOptionPane +import javax.swing.SwingUtilities +import screen.LoadingDialog +import screen.ScreenCache +import utils.InjectedThings.logger + +object DialogUtilNew { + private var loadingDialog: LoadingDialog? = null + + fun showInfo(infoText: String, parent: Component = ScreenCache.getMainScreen()) { + logDialogShown("Info", "Information", infoText) + JOptionPane.showMessageDialog( + parent, + infoText, + "Information", + JOptionPane.INFORMATION_MESSAGE + ) + logDialogClosed("Info", null) + } + + fun showCustomMessage(message: Any, parent: Component = ScreenCache.getMainScreen()) { + logDialogShown("CustomInfo", "Information", "?") + JOptionPane.showMessageDialog( + parent, + message, + "Information", + JOptionPane.INFORMATION_MESSAGE + ) + logDialogClosed("CustomInfo", null) + } + + fun showError(errorText: String, parent: Component? = ScreenCache.getMainScreen()) { + dismissLoadingDialog() + + logDialogShown("Error", "Error", errorText) + JOptionPane.showMessageDialog(parent, errorText, "Error", JOptionPane.ERROR_MESSAGE) + logDialogClosed("Error", null) + } + + fun showErrorLater(errorText: String) { + SwingUtilities.invokeLater { showError(errorText) } + } + + @JvmOverloads + fun showQuestion( + message: String, + allowCancel: Boolean = false, + parent: Component = ScreenCache.getMainScreen() + ): Int { + logDialogShown("Question", "Question", message) + val option = + if (allowCancel) JOptionPane.YES_NO_CANCEL_OPTION else JOptionPane.YES_NO_OPTION + val selection = + JOptionPane.showConfirmDialog( + parent, + message, + "Question", + option, + JOptionPane.QUESTION_MESSAGE + ) + logDialogClosed("Question", selection) + return selection + } + + fun showLoadingDialog(text: String) { + logDialogShown("Loading", "", text) + loadingDialog = LoadingDialog() + loadingDialog?.showDialog(text) + } + + fun dismissLoadingDialog() { + val wasVisible = loadingDialog?.isVisible ?: false + loadingDialog?.dismissDialog() + if (wasVisible) { + logDialogClosed("Loading", null) + } + } + + fun showOption(title: String, message: String, options: List): String? { + logDialogShown("Option", title, message) + val typedArray = options.toTypedArray() + val selection = + JOptionPane.showOptionDialog( + null, + message, + title, + JOptionPane.DEFAULT_OPTION, + JOptionPane.QUESTION_MESSAGE, + null, + typedArray, + options.first() + ) + val selectionStr = if (selection > -1) typedArray[selection] else null + + logDialogClosed("Option", selectionStr) + return selectionStr + } + + fun showInput( + title: String, + message: String, + options: Array? = null, + defaultOption: K? = null + ): K? { + logDialogShown("Input", title, message) + val selection = + JOptionPane.showInputDialog( + null, + message, + title, + JOptionPane.PLAIN_MESSAGE, + null, + options, + defaultOption + ) as K? + + logDialogClosed("Input", selection) + return selection + } + + fun chooseDirectory(parent: Component?): File? { + logDialogShown("File selector", "", "") + val fc = JFileChooser() + fc.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY + val option = fc.showDialog(parent, "Select") + if (option != JFileChooser.APPROVE_OPTION) { + return null + } + + val file = fc.selectedFile + logDialogClosed("File selector", file?.absolutePath) + return file + } + + private fun logDialogShown(type: String, title: String, message: String) { + logger.info( + "dialogShown", + "$type dialog shown: $message", + "dialogType" to type, + "dialogTitle" to title, + "dialogMessage" to message + ) + } + + private fun logDialogClosed(type: String, selection: Any?) { + var message = "$type dialog closed" + selection?.let { message += " - selected ${translateOption(it)}" } + + logger.info("dialogClosed", message, "dialogType" to type, "dialogSelection" to selection) + } + + private fun translateOption(option: Any?) = + when (option) { + JOptionPane.YES_OPTION -> "Yes" + JOptionPane.NO_OPTION -> "No" + JOptionPane.CANCEL_OPTION -> "Cancel" + else -> option + } +} diff --git a/client/src/main/java/util/UpdateManager.kt b/client/src/main/kotlin/util/UpdateManager.kt similarity index 91% rename from client/src/main/java/util/UpdateManager.kt rename to client/src/main/kotlin/util/UpdateManager.kt index 2acd364..f2d7ce9 100644 --- a/client/src/main/java/util/UpdateManager.kt +++ b/client/src/main/kotlin/util/UpdateManager.kt @@ -8,8 +8,8 @@ import javax.swing.JOptionPane import javax.swing.JPanel import kong.unirest.Unirest import kong.unirest.json.JSONObject -import utils.InjectedThings.logger import kotlin.system.exitProcess +import utils.InjectedThings.logger /** * Automatically check for and download updates using the Github API @@ -34,7 +34,7 @@ object UpdateManager { fun queryLatestReleaseJson(repositoryUrl: String): JSONObject? { try { - DialogUtil.showLoadingDialog("Checking for updates...") + DialogUtilNew.showLoadingDialog("Checking for updates...") val response = Unirest.get("$repositoryUrl/releases/latest").asJson() if (response.status != 200) { @@ -43,17 +43,17 @@ object UpdateManager { "Received non-success HTTP status: ${response.status} - ${response.statusText}", "responseBody" to response.body, ) - DialogUtil.showError("Failed to check for updates (unable to connect).") + DialogUtilNew.showError("Failed to check for updates (unable to connect).") return null } return response.body.`object` } catch (t: Throwable) { logger.error("updateError", "Caught $t checking for updates", t) - DialogUtil.showError("Failed to check for updates (unable to connect).") + DialogUtilNew.showError("Failed to check for updates (unable to connect).") return null } finally { - DialogUtil.dismissLoadingDialog() + DialogUtilNew.dismissLoadingDialog() } } @@ -73,7 +73,7 @@ object UpdateManager { } val answer = - DialogUtil.showQuestion( + DialogUtilNew.showQuestion( "An update is available (${metadata.version}). Would you like to download it now?", false ) @@ -91,7 +91,7 @@ object UpdateManager { panel.add(lblOne, BorderLayout.NORTH) panel.add(linkLabel, BorderLayout.SOUTH) - DialogUtil.showCustomMessage(panel) + DialogUtilNew.showCustomMessage(panel) } fun parseUpdateMetadata(responseJson: JSONObject): UpdateMetadata? { @@ -126,7 +126,7 @@ object UpdateManager { val msg = "Failed to launch update.bat - call the following manually to perform the update: \n\n$manualCommand" - DialogUtil.showError(msg) + DialogUtilNew.showError(msg) return } diff --git a/client/src/main/java/util/UrlUtil.kt b/client/src/main/kotlin/util/UrlUtil.kt similarity index 100% rename from client/src/main/java/util/UrlUtil.kt rename to client/src/main/kotlin/util/UrlUtil.kt diff --git a/client/src/main/resources/update/update.bat b/client/src/main/resources/update/update.bat new file mode 100644 index 0000000..92f8a7b --- /dev/null +++ b/client/src/main/resources/update/update.bat @@ -0,0 +1,17 @@ +@echo off + +REM %1 = noOfBytes +REM %2 = version number +REM %3 = filename +REM %4 = assetId + +echo Performing download of %1 bytes (Version %2) + +curl -LJO -H "Accept: application/octet-stream" https://api.github.com/repos/alyssaburlton/Dartzee/releases/assets/%4 + +ren Dartzee.jar Dartzee_OLD.jar +ren %3 Dartzee.jar +del Dartzee_OLD.jar + +start javaw -Xms256m -Xmx512m -jar Dartzee.jar justUpdated trueLaunch +exit \ No newline at end of file diff --git a/client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt b/client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt new file mode 100644 index 0000000..e111a1c --- /dev/null +++ b/client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt @@ -0,0 +1,94 @@ +package bean + +import com.github.alyssaburlton.swingtest.makeMouseEvent +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import io.mockk.verifySequence +import java.awt.Cursor +import java.awt.event.MouseEvent +import javax.swing.JButton +import javax.swing.JPanel +import main.kotlin.testCore.AbstractTest +import org.junit.jupiter.api.Test + +private val mouseEventOverLink = makeMouseEvent(JButton()) +private val mouseEventNotOverLink = makeMouseEvent(JButton()) + +class HyperlinkAdaptorTest : AbstractTest() { + @Test + fun `Should not accept a non-component listener`() { + shouldThrow { HyperlinkAdaptor(NonComponentHyperlinkListener()) } + } + + @Test + fun `Should respond to mouse clicks`() { + val listener = mockk(relaxed = true) + + val adaptor = HyperlinkAdaptor(listener) + adaptor.mouseClicked(mouseEventOverLink) + adaptor.mouseClicked(mouseEventNotOverLink) + + verifySequence { + listener.linkClicked(mouseEventOverLink) + listener.linkClicked(mouseEventNotOverLink) + } + } + + @Test + fun `Should change the cursor on mouse movement`() { + val listener = TestHyperlinkListener() + val adaptor = HyperlinkAdaptor(listener) + + adaptor.mouseMoved(mouseEventNotOverLink) + listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + + adaptor.mouseMoved(mouseEventOverLink) + listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + + adaptor.mouseMoved(mouseEventNotOverLink) + listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + + adaptor.mouseEntered(mouseEventNotOverLink) + listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + + adaptor.mouseEntered(mouseEventOverLink) + listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + } + + @Test + fun `Should revert the cursor on mouseExit`() { + val listener = TestHyperlinkListener() + val adaptor = HyperlinkAdaptor(listener) + + adaptor.mouseMoved(mouseEventOverLink) + listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + + adaptor.mouseExited(null) + listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + } + + @Test + fun `Should revert the cursor on mouseDragged`() { + val listener = TestHyperlinkListener() + val adaptor = HyperlinkAdaptor(listener) + + adaptor.mouseMoved(mouseEventOverLink) + listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + + adaptor.mouseDragged(null) + listener.cursor shouldBe Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) + } +} + +private class TestHyperlinkListener : JPanel(), IHyperlinkListener { + override fun isOverHyperlink(arg0: MouseEvent) = arg0 === mouseEventOverLink + + override fun linkClicked(arg0: MouseEvent) {} +} + +private class NonComponentHyperlinkListener : IHyperlinkListener { + override fun isOverHyperlink(arg0: MouseEvent) = false + + override fun linkClicked(arg0: MouseEvent) {} +} diff --git a/client/src/test/kotlin/bean/LinkLabelTest.kt b/client/src/test/kotlin/bean/LinkLabelTest.kt index dddb799..f881a21 100644 --- a/client/src/test/kotlin/bean/LinkLabelTest.kt +++ b/client/src/test/kotlin/bean/LinkLabelTest.kt @@ -1,6 +1,5 @@ package bean -import main.kotlin.testCore.AbstractTest import com.github.alyssaburlton.swingtest.doClick import com.github.alyssaburlton.swingtest.doHover import com.github.alyssaburlton.swingtest.doHoverAway @@ -9,6 +8,7 @@ import io.mockk.mockk import io.mockk.verify import java.awt.Color import java.awt.Cursor +import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test class LinkLabelTest : AbstractTest() { diff --git a/client/src/test/kotlin/util/DialogUtilNewTest.kt b/client/src/test/kotlin/util/DialogUtilNewTest.kt new file mode 100644 index 0000000..7e6219d --- /dev/null +++ b/client/src/test/kotlin/util/DialogUtilNewTest.kt @@ -0,0 +1,106 @@ +package util + +import com.github.alyssaburlton.swingtest.clickCancel +import com.github.alyssaburlton.swingtest.clickNo +import com.github.alyssaburlton.swingtest.clickOk +import com.github.alyssaburlton.swingtest.clickYes +import com.github.alyssaburlton.swingtest.findWindow +import com.github.alyssaburlton.swingtest.flushEdt +import com.github.alyssaburlton.swingtest.purgeWindows +import io.kotest.matchers.shouldBe +import javax.swing.JDialog +import javax.swing.SwingUtilities +import logging.Severity +import main.kotlin.testCore.AbstractTest +import main.kotlin.testCore.getErrorDialog +import main.kotlin.testCore.getInfoDialog +import main.kotlin.testCore.getQuestionDialog +import main.kotlin.testCore.runAsync +import org.junit.jupiter.api.Test + +class DialogUtilNewTest : AbstractTest() { + @Test + fun `Should log for INFO dialogs`() { + runAsync { DialogUtilNew.showInfo("Something useful") } + + verifyLog("dialogShown", Severity.INFO).message shouldBe + "Info dialog shown: Something useful" + + getInfoDialog().clickOk() + flushEdt() + verifyLog("dialogClosed", Severity.INFO).message shouldBe "Info dialog closed" + } + + @Test + fun `Should log for ERROR dialogs`() { + runAsync { DialogUtilNew.showError("Something bad") } + + verifyLog("dialogShown", Severity.INFO).message shouldBe "Error dialog shown: Something bad" + getErrorDialog().clickOk() + flushEdt() + verifyLog("dialogClosed", Severity.INFO).message shouldBe "Error dialog closed" + } + + @Test + fun `Should show an ERROR dialog later`() { + SwingUtilities.invokeLater { Thread.sleep(500) } + DialogUtilNew.showErrorLater("Some error") + + findWindow() shouldBe null + + flushEdt() + getErrorDialog().clickOk() + flushEdt() + verifyLog("dialogClosed", Severity.INFO).message shouldBe "Error dialog closed" + } + + @Test + fun `Should log for QUESTION dialogs, with the correct selection`() { + runAsync { DialogUtilNew.showQuestion("Do you like cheese?") } + verifyLog("dialogShown", Severity.INFO).message shouldBe + "Question dialog shown: Do you like cheese?" + getQuestionDialog().clickYes() + flushEdt() + verifyLog("dialogClosed", Severity.INFO).message shouldBe + "Question dialog closed - selected Yes" + + clearLogs() + purgeWindows() + + runAsync { DialogUtilNew.showQuestion("Do you like mushrooms?") } + verifyLog("dialogShown", Severity.INFO).message shouldBe + "Question dialog shown: Do you like mushrooms?" + getQuestionDialog().clickNo() + flushEdt() + verifyLog("dialogClosed", Severity.INFO).message shouldBe + "Question dialog closed - selected No" + + clearLogs() + purgeWindows() + + runAsync { DialogUtilNew.showQuestion("Do you want to delete all data?", true) } + verifyLog("dialogShown", Severity.INFO).message shouldBe + "Question dialog shown: Do you want to delete all data?" + getQuestionDialog().clickCancel() + flushEdt() + verifyLog("dialogClosed", Severity.INFO).message shouldBe + "Question dialog closed - selected Cancel" + } + + @Test + fun `Should log when showing and dismissing loading dialog`() { + DialogUtilNew.showLoadingDialog("One moment...") + flushEdt() + verifyLog("dialogShown", Severity.INFO).message shouldBe + "Loading dialog shown: One moment..." + + DialogUtilNew.dismissLoadingDialog() + verifyLog("dialogClosed", Severity.INFO).message shouldBe "Loading dialog closed" + } + + @Test + fun `Should not log if loading dialog wasn't visible`() { + DialogUtilNew.dismissLoadingDialog() + verifyNoLogs("dialogClosed") + } +} diff --git a/client/src/test/kotlin/util/UpdateManagerTest.kt b/client/src/test/kotlin/util/UpdateManagerTest.kt new file mode 100644 index 0000000..d0d9de7 --- /dev/null +++ b/client/src/test/kotlin/util/UpdateManagerTest.kt @@ -0,0 +1,262 @@ +package util + +import bean.LinkLabel +import com.github.alyssaburlton.swingtest.clickNo +import com.github.alyssaburlton.swingtest.clickOk +import com.github.alyssaburlton.swingtest.clickYes +import com.github.alyssaburlton.swingtest.findWindow +import com.github.alyssaburlton.swingtest.flushEdt +import com.github.alyssaburlton.swingtest.getChild +import com.github.alyssaburlton.swingtest.shouldNotBeVisible +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldEndWith +import io.kotest.matchers.string.shouldStartWith +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.every +import io.mockk.mockk +import java.io.File +import java.io.IOException +import java.util.concurrent.atomic.AtomicBoolean +import javax.swing.SwingUtilities +import kong.unirest.Unirest +import kong.unirest.UnirestException +import kong.unirest.json.JSONException +import kong.unirest.json.JSONObject +import logging.Severity +import main.kotlin.testCore.AbstractTest +import main.kotlin.testCore.getDialogMessage +import main.kotlin.testCore.getErrorDialog +import main.kotlin.testCore.getInfoDialog +import main.kotlin.testCore.getQuestionDialog +import main.kotlin.testCore.runAsync +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import screen.LoadingDialog +import testCore.assertDoesNotExit +import testCore.assertExits + +class UpdateManagerTest : AbstractTest() { + @BeforeEach + fun beforeEach() { + Unirest.config().reset() + Unirest.config().connectTimeout(2000) + Unirest.config().socketTimeout(2000) + } + + /** Communication */ + @Test + @Tag("integration") + fun `Should log out an unexpected HTTP response, along with the full JSON payload`() { + val errorMessage = + queryLatestReleastJsonExpectingError("https://api.github.com/repos/alyssaburlton/foo") + errorMessage shouldBe "Failed to check for updates (unable to connect)." + + val log = verifyLog("updateError", Severity.ERROR) + log.message shouldBe "Received non-success HTTP status: 404 - Not Found" + log.keyValuePairs["responseBody"].toString() shouldContain """"message":"Not Found"""" + + findWindow()!!.shouldNotBeVisible() + } + + @Test + @Tag("integration") + fun `Should catch and log any exceptions communicating over HTTPS`() { + Unirest.config().connectTimeout(100) + Unirest.config().socketTimeout(100) + + val errorMessage = queryLatestReleastJsonExpectingError("https://ww.blargh.zcss.w") + errorMessage shouldBe "Failed to check for updates (unable to connect)." + + val errorLog = verifyLog("updateError", Severity.ERROR) + errorLog.errorObject.shouldBeInstanceOf() + + findWindow()!!.shouldNotBeVisible() + } + + private fun queryLatestReleastJsonExpectingError(repositoryUrl: String): String { + val result = runAsync { UpdateManager.queryLatestReleaseJson(repositoryUrl) } + + val error = getErrorDialog() + val errorText = error.getDialogMessage() + + error.clickOk() + flushEdt() + + result shouldBe null + return errorText + } + + @Test + @Tag("integration") + fun `Should retrieve a valid latest asset from the remote repo`() { + val responseJson = + UpdateManager.queryLatestReleaseJson(OnlineConstants.ENTROPY_REPOSITORY_URL)!! + + val version = responseJson.getString("tag_name") + version.shouldStartWith("v") + responseJson.getJSONArray("assets").length() shouldBe 1 + + val asset = responseJson.getJSONArray("assets").getJSONObject(0) + asset.getLong("id") shouldNotBe null + asset.getString("name") shouldStartWith "Dartzee" + asset.getString("name") shouldEndWith ".jar" + asset.getLong("size") shouldNotBe null + } + + /** Parsing */ + @Test + fun `Should parse correctly formed JSON`() { + val json = + """{ + "tag_name": "foo", + "assets": [ + { + "id": 123456, + "name": "Dartzee_v_foo.jar", + "size": 1 + } + ] + }""" + + val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json))!! + metadata.version shouldBe "foo" + metadata.assetId shouldBe 123456 + metadata.fileName shouldBe "Dartzee_v_foo.jar" + metadata.size shouldBe 1 + } + + @Test + fun `Should log an error if no tag_name is present`() { + val json = "{\"other_tag\":\"foo\"}" + val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json)) + metadata shouldBe null + + val log = verifyLog("parseError", Severity.ERROR) + log.errorObject.shouldBeInstanceOf() + log.keyValuePairs["responseBody"].toString() shouldBe json + } + + @Test + fun `Should log an error if no assets are found`() { + val json = """{"assets":[],"tag_name":"foo"}""" + val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json)) + metadata shouldBe null + + val log = verifyLog("parseError", Severity.ERROR) + log.errorObject.shouldBeInstanceOf() + log.keyValuePairs["responseBody"].toString() shouldBe json + } + + /** Should update? */ + @Test + fun `Should not proceed with the update if the versions match`() { + val metadata = + UpdateMetadata( + OnlineConstants.ENTROPY_VERSION_NUMBER, + 123456, + "EntropyLive_x_y.jar", + 100 + ) + + UpdateManager.shouldUpdate(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata) shouldBe false + val log = verifyLog("updateResult") + log.message shouldBe "Up to date" + } + + @Test + fun `Should show an info and not proceed to auto update if OS is not windows`() { + AbstractClient.operatingSystem = "foo" + + val metadata = UpdateMetadata("v100", 123456, "Dartzee_x_y.jar", 100) + shouldUpdateAsync(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata).get() shouldBe false + + val log = verifyLog("updateAvailable") + log.message shouldBe "Newer release available - v100" + + val info = getInfoDialog() + val linkLabel = info.getChild() + linkLabel.text shouldBe + "${OnlineConstants.ENTROPY_MANUAL_DOWNLOAD_URL}/tag/v100" + } + + @Test + fun `Should not proceed with the update if user selects 'No'`() { + AbstractClient.operatingSystem = "windows" + + val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100) + val result = shouldUpdateAsync("bar", metadata) + + val question = getQuestionDialog() + question.getDialogMessage() shouldBe + "An update is available (foo). Would you like to download it now?" + question.clickNo() + flushEdt() + + result.get() shouldBe false + } + + @Test + fun `Should proceed with the update if user selects 'Yes'`() { + AbstractClient.operatingSystem = "windows" + + val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100) + val result = shouldUpdateAsync("bar", metadata) + + val question = getQuestionDialog() + question.getDialogMessage() shouldBe + "An update is available (foo). Would you like to download it now?" + question.clickYes() + flushEdt() + + result.get() shouldBe true + } + + private fun shouldUpdateAsync(currentVersion: String, metadata: UpdateMetadata): AtomicBoolean { + val result = AtomicBoolean(false) + SwingUtilities.invokeLater { + result.set(UpdateManager.shouldUpdate(currentVersion, metadata)) + } + + flushEdt() + return result + } + + /** Prepare batch file */ + @Test + fun `Should overwrite existing batch file with the correct contents`() { + val updateFile = File("update.bat") + updateFile.writeText("blah") + + UpdateManager.prepareBatchFile() + + updateFile.readText() shouldBe javaClass.getResource("/update/update.bat").readText() + updateFile.delete() + } + + /** Run update */ + @Test + fun `Should log an error and not exit if batch file goes wrong`() { + val runtime = mockk() + val error = IOException("Argh") + every { runtime.exec(any()) } throws error + + runAsync { assertDoesNotExit { UpdateManager.startUpdate("foo", runtime) } } + + val errorDialog = getErrorDialog() + errorDialog.getDialogMessage() shouldBe + "Failed to launch update.bat - call the following manually to perform the update: \n\nupdate.bat foo" + + val log = verifyLog("batchError", Severity.ERROR) + log.errorObject shouldBe error + } + + @Test + fun `Should exit normally if batch file succeeds`() { + val runtime = mockk(relaxed = true) + + assertExits(0) { UpdateManager.startUpdate("foo", runtime) } + } +} diff --git a/client/src/test/kotlin/util/UrlUtilTest.kt b/client/src/test/kotlin/util/UrlUtilTest.kt new file mode 100644 index 0000000..e6625df --- /dev/null +++ b/client/src/test/kotlin/util/UrlUtilTest.kt @@ -0,0 +1,34 @@ +package util + +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import java.io.IOException +import logging.Severity +import main.kotlin.testCore.AbstractTest +import org.junit.jupiter.api.Test + +class UrlUtilTest : AbstractTest() { + @Test + fun `Should execute the expected command`() { + val runtime = mockk(relaxed = true) + launchUrl("foo.bar", runtime) + + verify { runtime.exec("xdg-open foo.bar") } + } + + @Test + fun `Should log an appropriate error if launching the URL fails`() { + val error = IOException("Oops") + + val runtime = mockk() + every { runtime.exec(any()) } throws error + + launchUrl("foo.bar", runtime) + + val log = verifyLog("urlError", Severity.ERROR) + log.message shouldBe "Failed to launch foo.bar" + log.errorObject shouldBe error + } +} diff --git a/core/src/test/kotlin/bean/FocusableWindowTest.kt b/core/src/test/kotlin/bean/FocusableWindowTest.kt index d7c0f99..bebfbbe 100644 --- a/core/src/test/kotlin/bean/FocusableWindowTest.kt +++ b/core/src/test/kotlin/bean/FocusableWindowTest.kt @@ -1,9 +1,9 @@ package bean -import main.kotlin.testCore.AbstractTest import io.kotest.matchers.shouldBe import io.mockk.mockk import logging.KEY_ACTIVE_WINDOW +import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test import utils.InjectedThings.logger diff --git a/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt b/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt index a7e0f67..92e4ddf 100644 --- a/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt +++ b/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt @@ -1,11 +1,11 @@ package logging -import main.kotlin.testCore.CURRENT_TIME_STRING -import main.kotlin.testCore.AbstractTest -import main.kotlin.testCore.makeLogRecord import io.kotest.matchers.string.shouldContain import java.io.ByteArrayOutputStream import java.io.PrintStream +import main.kotlin.testCore.AbstractTest +import main.kotlin.testCore.CURRENT_TIME_STRING +import main.kotlin.testCore.makeLogRecord import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test diff --git a/core/src/test/kotlin/logging/LoggerTest.kt b/core/src/test/kotlin/logging/LoggerTest.kt index 4bfff06..52c0abb 100644 --- a/core/src/test/kotlin/logging/LoggerTest.kt +++ b/core/src/test/kotlin/logging/LoggerTest.kt @@ -1,14 +1,14 @@ package logging -import main.kotlin.testCore.CURRENT_TIME -import main.kotlin.testCore.AbstractTest -import main.kotlin.testCore.FakeLogDestination -import main.kotlin.testCore.shouldContainKeyValues import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe import io.mockk.mockk import io.mockk.verify +import main.kotlin.testCore.AbstractTest +import main.kotlin.testCore.CURRENT_TIME +import main.kotlin.testCore.FakeLogDestination +import main.kotlin.testCore.shouldContainKeyValues import org.junit.jupiter.api.Test class LoggerTest : AbstractTest() { diff --git a/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt b/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt index 1bfb781..0d207e0 100644 --- a/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt +++ b/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt @@ -1,9 +1,9 @@ package logging -import main.kotlin.testCore.AbstractTest -import main.kotlin.testCore.shouldContainKeyValues import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain +import main.kotlin.testCore.AbstractTest +import main.kotlin.testCore.shouldContainKeyValues import org.junit.jupiter.api.Test class LoggerUncaughtExceptionHandlerTest : AbstractTest() { diff --git a/core/src/test/kotlin/logging/LoggingConsoleTest.kt b/core/src/test/kotlin/logging/LoggingConsoleTest.kt index 7588b5b..a612886 100644 --- a/core/src/test/kotlin/logging/LoggingConsoleTest.kt +++ b/core/src/test/kotlin/logging/LoggingConsoleTest.kt @@ -1,9 +1,6 @@ package logging import com.github.alyssaburlton.swingtest.flushEdt -import main.kotlin.testCore.AbstractTest -import main.kotlin.testCore.getAllChildComponentsForType -import main.kotlin.testCore.makeLogRecord import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.comparables.shouldBeGreaterThan @@ -12,6 +9,9 @@ import io.kotest.matchers.string.shouldContain import java.awt.Color import javax.swing.JLabel import javax.swing.text.StyleConstants +import main.kotlin.testCore.AbstractTest +import main.kotlin.testCore.getAllChildComponentsForType +import main.kotlin.testCore.makeLogRecord import org.junit.jupiter.api.Test class LoggingConsoleTest : AbstractTest() { diff --git a/core/src/test/kotlin/logging/LoggingUtilsTest.kt b/core/src/test/kotlin/logging/LoggingUtilsTest.kt index ad713e4..125e412 100644 --- a/core/src/test/kotlin/logging/LoggingUtilsTest.kt +++ b/core/src/test/kotlin/logging/LoggingUtilsTest.kt @@ -1,8 +1,8 @@ package logging -import main.kotlin.testCore.AbstractTest import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain +import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test class LoggingUtilsTest : AbstractTest() { diff --git a/core/src/test/kotlin/utils/MathsUtilTest.kt b/core/src/test/kotlin/utils/MathsUtilTest.kt index c4cab77..07eea9a 100644 --- a/core/src/test/kotlin/utils/MathsUtilTest.kt +++ b/core/src/test/kotlin/utils/MathsUtilTest.kt @@ -1,8 +1,8 @@ package utils import getPercentage -import main.kotlin.testCore.AbstractTest import io.kotest.matchers.shouldBe +import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test class MathsUtilTest : AbstractTest() { diff --git a/test-core/src/main/kotlin/testCore/ExitAssertions.kt b/test-core/src/main/kotlin/testCore/ExitAssertions.kt new file mode 100644 index 0000000..16a9690 --- /dev/null +++ b/test-core/src/main/kotlin/testCore/ExitAssertions.kt @@ -0,0 +1,50 @@ +package testCore + +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe +import java.security.Permission + +private class ExitException(val status: Int) : SecurityException("Nope") + +private class NoExitSecurityManager(val originalSecurityManager: SecurityManager?) : + SecurityManager() { + override fun checkPermission(perm: Permission?) { + originalSecurityManager?.checkPermission(perm) + } + + override fun checkPermission(perm: Permission?, context: Any?) { + originalSecurityManager?.checkPermission(perm, context) + } + + override fun checkExit(status: Int) { + super.checkExit(status) + throw ExitException(status) + } +} + +fun assertExits(expectedStatus: Int, fn: () -> Unit) { + val originalSecurityManager = System.getSecurityManager() + System.setSecurityManager(NoExitSecurityManager(originalSecurityManager)) + + try { + fn() + fail("Expected exitProcess($expectedStatus), but it wasn't called") + } catch (e: ExitException) { + e.status shouldBe expectedStatus + } finally { + System.setSecurityManager(originalSecurityManager) + } +} + +fun assertDoesNotExit(fn: () -> Unit) { + val originalSecurityManager = System.getSecurityManager() + System.setSecurityManager(NoExitSecurityManager(originalSecurityManager)) + + try { + fn() + } catch (e: ExitException) { + fail("Called exitProcess(${e.status})") + } finally { + System.setSecurityManager(originalSecurityManager) + } +} diff --git a/test-core/src/main/kotlin/testCore/TestUtils.kt b/test-core/src/main/kotlin/testCore/TestUtils.kt index d286ac9..e628a9c 100644 --- a/test-core/src/main/kotlin/testCore/TestUtils.kt +++ b/test-core/src/main/kotlin/testCore/TestUtils.kt @@ -1,9 +1,15 @@ package main.kotlin.testCore +import com.github.alyssaburlton.swingtest.findAll +import com.github.alyssaburlton.swingtest.findWindow +import com.github.alyssaburlton.swingtest.flushEdt import io.kotest.matchers.maps.shouldContainExactly import java.awt.Component import java.awt.Container import java.time.Instant +import javax.swing.JDialog +import javax.swing.JLabel +import javax.swing.SwingUtilities import logging.LogRecord import logging.Severity @@ -46,3 +52,24 @@ fun addComponents(ret: MutableList, components: Array, desired } } } + +fun getInfoDialog() = getOptionPaneDialog("Information") + +fun getQuestionDialog() = getOptionPaneDialog("Question") + +fun getErrorDialog() = getOptionPaneDialog("Error") + +private fun getOptionPaneDialog(title: String) = findWindow { it.title == title }!! + +fun JDialog.getDialogMessage(): String { + val messageLabels = findAll().filter { it.name == "OptionPane.label" } + return messageLabels.joinToString("\n\n") { it.text } +} + +fun runAsync(block: () -> T?): T? { + var result: T? = null + SwingUtilities.invokeLater { result = block() } + + flushEdt() + return result +} From c292aedd62c292a1a7636df32c80b8a8958c0233 Mon Sep 17 00:00:00 2001 From: Alyssa Date: Sat, 17 Aug 2024 16:18:27 +0100 Subject: [PATCH 4/6] fix integration test --- client/src/test/kotlin/util/UpdateManagerTest.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/client/src/test/kotlin/util/UpdateManagerTest.kt b/client/src/test/kotlin/util/UpdateManagerTest.kt index d0d9de7..333cfd4 100644 --- a/client/src/test/kotlin/util/UpdateManagerTest.kt +++ b/client/src/test/kotlin/util/UpdateManagerTest.kt @@ -8,6 +8,7 @@ import com.github.alyssaburlton.swingtest.findWindow import com.github.alyssaburlton.swingtest.flushEdt import com.github.alyssaburlton.swingtest.getChild import com.github.alyssaburlton.swingtest.shouldNotBeVisible +import io.kotest.matchers.ints.shouldBeGreaterThan import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain @@ -97,13 +98,7 @@ class UpdateManagerTest : AbstractTest() { val version = responseJson.getString("tag_name") version.shouldStartWith("v") - responseJson.getJSONArray("assets").length() shouldBe 1 - - val asset = responseJson.getJSONArray("assets").getJSONObject(0) - asset.getLong("id") shouldNotBe null - asset.getString("name") shouldStartWith "Dartzee" - asset.getString("name") shouldEndWith ".jar" - asset.getLong("size") shouldNotBe null + responseJson.getJSONArray("assets").length() shouldBeGreaterThan 0 } /** Parsing */ From 0f9c5f61c9125fd147c0daa489656f440073d176 Mon Sep 17 00:00:00 2001 From: Alyssa Date: Sat, 17 Aug 2024 16:21:43 +0100 Subject: [PATCH 5/6] fix formatting again --- .../src/test/kotlin/util/UpdateManagerTest.kt | 404 +++++++++--------- 1 file changed, 198 insertions(+), 206 deletions(-) diff --git a/client/src/test/kotlin/util/UpdateManagerTest.kt b/client/src/test/kotlin/util/UpdateManagerTest.kt index 333cfd4..0e1bcca 100644 --- a/client/src/test/kotlin/util/UpdateManagerTest.kt +++ b/client/src/test/kotlin/util/UpdateManagerTest.kt @@ -10,9 +10,7 @@ import com.github.alyssaburlton.swingtest.getChild import com.github.alyssaburlton.swingtest.shouldNotBeVisible import io.kotest.matchers.ints.shouldBeGreaterThan import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain -import io.kotest.matchers.string.shouldEndWith import io.kotest.matchers.string.shouldStartWith import io.kotest.matchers.types.shouldBeInstanceOf import io.mockk.every @@ -40,72 +38,73 @@ import testCore.assertDoesNotExit import testCore.assertExits class UpdateManagerTest : AbstractTest() { - @BeforeEach - fun beforeEach() { - Unirest.config().reset() - Unirest.config().connectTimeout(2000) - Unirest.config().socketTimeout(2000) - } - - /** Communication */ - @Test - @Tag("integration") - fun `Should log out an unexpected HTTP response, along with the full JSON payload`() { - val errorMessage = - queryLatestReleastJsonExpectingError("https://api.github.com/repos/alyssaburlton/foo") - errorMessage shouldBe "Failed to check for updates (unable to connect)." - - val log = verifyLog("updateError", Severity.ERROR) - log.message shouldBe "Received non-success HTTP status: 404 - Not Found" - log.keyValuePairs["responseBody"].toString() shouldContain """"message":"Not Found"""" - - findWindow()!!.shouldNotBeVisible() - } - - @Test - @Tag("integration") - fun `Should catch and log any exceptions communicating over HTTPS`() { - Unirest.config().connectTimeout(100) - Unirest.config().socketTimeout(100) - - val errorMessage = queryLatestReleastJsonExpectingError("https://ww.blargh.zcss.w") - errorMessage shouldBe "Failed to check for updates (unable to connect)." - - val errorLog = verifyLog("updateError", Severity.ERROR) - errorLog.errorObject.shouldBeInstanceOf() - - findWindow()!!.shouldNotBeVisible() - } - - private fun queryLatestReleastJsonExpectingError(repositoryUrl: String): String { - val result = runAsync { UpdateManager.queryLatestReleaseJson(repositoryUrl) } - - val error = getErrorDialog() - val errorText = error.getDialogMessage() - - error.clickOk() - flushEdt() - - result shouldBe null - return errorText - } - - @Test - @Tag("integration") - fun `Should retrieve a valid latest asset from the remote repo`() { - val responseJson = - UpdateManager.queryLatestReleaseJson(OnlineConstants.ENTROPY_REPOSITORY_URL)!! - - val version = responseJson.getString("tag_name") - version.shouldStartWith("v") - responseJson.getJSONArray("assets").length() shouldBeGreaterThan 0 - } - - /** Parsing */ - @Test - fun `Should parse correctly formed JSON`() { - val json = - """{ + @BeforeEach + fun beforeEach() { + Unirest.config().reset() + Unirest.config().connectTimeout(2000) + Unirest.config().socketTimeout(2000) + } + + /** Communication */ + @Test + @Tag("integration") + fun `Should log out an unexpected HTTP response, along with the full JSON payload`() { + val errorMessage = + queryLatestReleastJsonExpectingError("https://api.github.com/repos/alyssaburlton/foo") + errorMessage shouldBe "Failed to check for updates (unable to connect)." + + val log = verifyLog("updateError", Severity.ERROR) + log.message shouldBe "Received non-success HTTP status: 404 - Not Found" + log.keyValuePairs["responseBody"].toString() shouldContain """"message":"Not Found"""" + + findWindow()!!.shouldNotBeVisible() + } + + @Test + @Tag("integration") + fun `Should catch and log any exceptions communicating over HTTPS`() { + Unirest.config().connectTimeout(100) + Unirest.config().socketTimeout(100) + + val errorMessage = queryLatestReleastJsonExpectingError("https://ww.blargh.zcss.w") + errorMessage shouldBe "Failed to check for updates (unable to connect)." + + val errorLog = verifyLog("updateError", Severity.ERROR) + errorLog.errorObject.shouldBeInstanceOf() + + findWindow()!!.shouldNotBeVisible() + } + + private fun queryLatestReleastJsonExpectingError(repositoryUrl: String): String { + val result = runAsync { UpdateManager.queryLatestReleaseJson(repositoryUrl) } + + val error = getErrorDialog() + val errorText = error.getDialogMessage() + + error.clickOk() + flushEdt() + + result shouldBe null + + return errorText + } + + @Test + @Tag("integration") + fun `Should retrieve a valid latest asset from the remote repo`() { + val responseJson = + UpdateManager.queryLatestReleaseJson(OnlineConstants.ENTROPY_REPOSITORY_URL)!! + + val version = responseJson.getString("tag_name") + version.shouldStartWith("v") + responseJson.getJSONArray("assets").length() shouldBeGreaterThan 0 + } + + /** Parsing */ + @Test + fun `Should parse correctly formed JSON`() { + val json = + """{ "tag_name": "foo", "assets": [ { @@ -116,142 +115,135 @@ class UpdateManagerTest : AbstractTest() { ] }""" - val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json))!! - metadata.version shouldBe "foo" - metadata.assetId shouldBe 123456 - metadata.fileName shouldBe "Dartzee_v_foo.jar" - metadata.size shouldBe 1 - } - - @Test - fun `Should log an error if no tag_name is present`() { - val json = "{\"other_tag\":\"foo\"}" - val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json)) - metadata shouldBe null - - val log = verifyLog("parseError", Severity.ERROR) - log.errorObject.shouldBeInstanceOf() - log.keyValuePairs["responseBody"].toString() shouldBe json - } - - @Test - fun `Should log an error if no assets are found`() { - val json = """{"assets":[],"tag_name":"foo"}""" - val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json)) - metadata shouldBe null - - val log = verifyLog("parseError", Severity.ERROR) - log.errorObject.shouldBeInstanceOf() - log.keyValuePairs["responseBody"].toString() shouldBe json - } - - /** Should update? */ - @Test - fun `Should not proceed with the update if the versions match`() { - val metadata = - UpdateMetadata( - OnlineConstants.ENTROPY_VERSION_NUMBER, - 123456, - "EntropyLive_x_y.jar", - 100 - ) - - UpdateManager.shouldUpdate(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata) shouldBe false - val log = verifyLog("updateResult") - log.message shouldBe "Up to date" - } - - @Test - fun `Should show an info and not proceed to auto update if OS is not windows`() { - AbstractClient.operatingSystem = "foo" - - val metadata = UpdateMetadata("v100", 123456, "Dartzee_x_y.jar", 100) - shouldUpdateAsync(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata).get() shouldBe false - - val log = verifyLog("updateAvailable") - log.message shouldBe "Newer release available - v100" - - val info = getInfoDialog() - val linkLabel = info.getChild() - linkLabel.text shouldBe - "${OnlineConstants.ENTROPY_MANUAL_DOWNLOAD_URL}/tag/v100" - } - - @Test - fun `Should not proceed with the update if user selects 'No'`() { - AbstractClient.operatingSystem = "windows" - - val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100) - val result = shouldUpdateAsync("bar", metadata) - - val question = getQuestionDialog() - question.getDialogMessage() shouldBe - "An update is available (foo). Would you like to download it now?" - question.clickNo() - flushEdt() - - result.get() shouldBe false - } - - @Test - fun `Should proceed with the update if user selects 'Yes'`() { - AbstractClient.operatingSystem = "windows" - - val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100) - val result = shouldUpdateAsync("bar", metadata) - - val question = getQuestionDialog() - question.getDialogMessage() shouldBe - "An update is available (foo). Would you like to download it now?" - question.clickYes() - flushEdt() - - result.get() shouldBe true - } - - private fun shouldUpdateAsync(currentVersion: String, metadata: UpdateMetadata): AtomicBoolean { - val result = AtomicBoolean(false) - SwingUtilities.invokeLater { - result.set(UpdateManager.shouldUpdate(currentVersion, metadata)) - } - - flushEdt() - return result - } - - /** Prepare batch file */ - @Test - fun `Should overwrite existing batch file with the correct contents`() { - val updateFile = File("update.bat") - updateFile.writeText("blah") - - UpdateManager.prepareBatchFile() - - updateFile.readText() shouldBe javaClass.getResource("/update/update.bat").readText() - updateFile.delete() - } - - /** Run update */ - @Test - fun `Should log an error and not exit if batch file goes wrong`() { - val runtime = mockk() - val error = IOException("Argh") - every { runtime.exec(any()) } throws error - - runAsync { assertDoesNotExit { UpdateManager.startUpdate("foo", runtime) } } - - val errorDialog = getErrorDialog() - errorDialog.getDialogMessage() shouldBe - "Failed to launch update.bat - call the following manually to perform the update: \n\nupdate.bat foo" - - val log = verifyLog("batchError", Severity.ERROR) - log.errorObject shouldBe error - } - - @Test - fun `Should exit normally if batch file succeeds`() { - val runtime = mockk(relaxed = true) - - assertExits(0) { UpdateManager.startUpdate("foo", runtime) } - } + val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json))!! + metadata.version shouldBe "foo" + metadata.assetId shouldBe 123456 + metadata.fileName shouldBe "Dartzee_v_foo.jar" + metadata.size shouldBe 1 + } + + @Test + fun `Should log an error if no tag_name is present`() { + val json = "{\"other_tag\":\"foo\"}" + val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json)) + metadata shouldBe null + + val log = verifyLog("parseError", Severity.ERROR) + log.errorObject.shouldBeInstanceOf() + log.keyValuePairs["responseBody"].toString() shouldBe json + } + + @Test + fun `Should log an error if no assets are found`() { + val json = """{"assets":[],"tag_name":"foo"}""" + val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json)) + metadata shouldBe null + + val log = verifyLog("parseError", Severity.ERROR) + log.errorObject.shouldBeInstanceOf() + log.keyValuePairs["responseBody"].toString() shouldBe json + } + + /** Should update? */ + @Test + fun `Should not proceed with the update if the versions match`() { + val metadata = + UpdateMetadata(OnlineConstants.ENTROPY_VERSION_NUMBER, 123456, "EntropyLive_x_y.jar", 100) + + UpdateManager.shouldUpdate(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata) shouldBe false + val log = verifyLog("updateResult") + log.message shouldBe "Up to date" + } + + @Test + fun `Should show an info and not proceed to auto update if OS is not windows`() { + AbstractClient.operatingSystem = "foo" + + val metadata = UpdateMetadata("v100", 123456, "Dartzee_x_y.jar", 100) + shouldUpdateAsync(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata).get() shouldBe false + + val log = verifyLog("updateAvailable") + log.message shouldBe "Newer release available - v100" + + val info = getInfoDialog() + val linkLabel = info.getChild() + linkLabel.text shouldBe + "${OnlineConstants.ENTROPY_MANUAL_DOWNLOAD_URL}/tag/v100" + } + + @Test + fun `Should not proceed with the update if user selects 'No'`() { + AbstractClient.operatingSystem = "windows" + + val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100) + val result = shouldUpdateAsync("bar", metadata) + + val question = getQuestionDialog() + question.getDialogMessage() shouldBe + "An update is available (foo). Would you like to download it now?" + question.clickNo() + flushEdt() + + result.get() shouldBe false + } + + @Test + fun `Should proceed with the update if user selects 'Yes'`() { + AbstractClient.operatingSystem = "windows" + + val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100) + val result = shouldUpdateAsync("bar", metadata) + + val question = getQuestionDialog() + question.getDialogMessage() shouldBe + "An update is available (foo). Would you like to download it now?" + question.clickYes() + flushEdt() + + result.get() shouldBe true + } + + private fun shouldUpdateAsync(currentVersion: String, metadata: UpdateMetadata): AtomicBoolean { + val result = AtomicBoolean(false) + SwingUtilities.invokeLater { result.set(UpdateManager.shouldUpdate(currentVersion, metadata)) } + + flushEdt() + return result + } + + /** Prepare batch file */ + @Test + fun `Should overwrite existing batch file with the correct contents`() { + val updateFile = File("update.bat") + updateFile.writeText("blah") + + UpdateManager.prepareBatchFile() + + updateFile.readText() shouldBe javaClass.getResource("/update/update.bat").readText() + updateFile.delete() + } + + /** Run update */ + @Test + fun `Should log an error and not exit if batch file goes wrong`() { + val runtime = mockk() + val error = IOException("Argh") + every { runtime.exec(any()) } throws error + + runAsync { assertDoesNotExit { UpdateManager.startUpdate("foo", runtime) } } + + val errorDialog = getErrorDialog() + errorDialog.getDialogMessage() shouldBe + "Failed to launch update.bat - call the following manually to perform the update: \n\nupdate.bat foo" + + val log = verifyLog("batchError", Severity.ERROR) + log.errorObject shouldBe error + } + + @Test + fun `Should exit normally if batch file succeeds`() { + val runtime = mockk(relaxed = true) + + assertExits(0) { UpdateManager.startUpdate("foo", runtime) } + } } From 59e740800ae3b4fefc8f9e7f603bde9dcbb75de1 Mon Sep 17 00:00:00 2001 From: Alyssa Date: Sat, 17 Aug 2024 16:22:09 +0100 Subject: [PATCH 6/6] fix formatting again again --- .../src/test/kotlin/util/UpdateManagerTest.kt | 403 +++++++++--------- 1 file changed, 205 insertions(+), 198 deletions(-) diff --git a/client/src/test/kotlin/util/UpdateManagerTest.kt b/client/src/test/kotlin/util/UpdateManagerTest.kt index 0e1bcca..3456f33 100644 --- a/client/src/test/kotlin/util/UpdateManagerTest.kt +++ b/client/src/test/kotlin/util/UpdateManagerTest.kt @@ -38,73 +38,73 @@ import testCore.assertDoesNotExit import testCore.assertExits class UpdateManagerTest : AbstractTest() { - @BeforeEach - fun beforeEach() { - Unirest.config().reset() - Unirest.config().connectTimeout(2000) - Unirest.config().socketTimeout(2000) - } - - /** Communication */ - @Test - @Tag("integration") - fun `Should log out an unexpected HTTP response, along with the full JSON payload`() { - val errorMessage = - queryLatestReleastJsonExpectingError("https://api.github.com/repos/alyssaburlton/foo") - errorMessage shouldBe "Failed to check for updates (unable to connect)." - - val log = verifyLog("updateError", Severity.ERROR) - log.message shouldBe "Received non-success HTTP status: 404 - Not Found" - log.keyValuePairs["responseBody"].toString() shouldContain """"message":"Not Found"""" - - findWindow()!!.shouldNotBeVisible() - } - - @Test - @Tag("integration") - fun `Should catch and log any exceptions communicating over HTTPS`() { - Unirest.config().connectTimeout(100) - Unirest.config().socketTimeout(100) - - val errorMessage = queryLatestReleastJsonExpectingError("https://ww.blargh.zcss.w") - errorMessage shouldBe "Failed to check for updates (unable to connect)." - - val errorLog = verifyLog("updateError", Severity.ERROR) - errorLog.errorObject.shouldBeInstanceOf() - - findWindow()!!.shouldNotBeVisible() - } - - private fun queryLatestReleastJsonExpectingError(repositoryUrl: String): String { - val result = runAsync { UpdateManager.queryLatestReleaseJson(repositoryUrl) } - - val error = getErrorDialog() - val errorText = error.getDialogMessage() - - error.clickOk() - flushEdt() - - result shouldBe null - - return errorText - } - - @Test - @Tag("integration") - fun `Should retrieve a valid latest asset from the remote repo`() { - val responseJson = - UpdateManager.queryLatestReleaseJson(OnlineConstants.ENTROPY_REPOSITORY_URL)!! - - val version = responseJson.getString("tag_name") - version.shouldStartWith("v") - responseJson.getJSONArray("assets").length() shouldBeGreaterThan 0 - } - - /** Parsing */ - @Test - fun `Should parse correctly formed JSON`() { - val json = - """{ + @BeforeEach + fun beforeEach() { + Unirest.config().reset() + Unirest.config().connectTimeout(2000) + Unirest.config().socketTimeout(2000) + } + + /** Communication */ + @Test + @Tag("integration") + fun `Should log out an unexpected HTTP response, along with the full JSON payload`() { + val errorMessage = + queryLatestReleastJsonExpectingError("https://api.github.com/repos/alyssaburlton/foo") + errorMessage shouldBe "Failed to check for updates (unable to connect)." + + val log = verifyLog("updateError", Severity.ERROR) + log.message shouldBe "Received non-success HTTP status: 404 - Not Found" + log.keyValuePairs["responseBody"].toString() shouldContain """"message":"Not Found"""" + + findWindow()!!.shouldNotBeVisible() + } + + @Test + @Tag("integration") + fun `Should catch and log any exceptions communicating over HTTPS`() { + Unirest.config().connectTimeout(100) + Unirest.config().socketTimeout(100) + + val errorMessage = queryLatestReleastJsonExpectingError("https://ww.blargh.zcss.w") + errorMessage shouldBe "Failed to check for updates (unable to connect)." + + val errorLog = verifyLog("updateError", Severity.ERROR) + errorLog.errorObject.shouldBeInstanceOf() + + findWindow()!!.shouldNotBeVisible() + } + + private fun queryLatestReleastJsonExpectingError(repositoryUrl: String): String { + val result = runAsync { UpdateManager.queryLatestReleaseJson(repositoryUrl) } + + val error = getErrorDialog() + val errorText = error.getDialogMessage() + + error.clickOk() + flushEdt() + + result shouldBe null + + return errorText + } + + @Test + @Tag("integration") + fun `Should retrieve a valid latest asset from the remote repo`() { + val responseJson = + UpdateManager.queryLatestReleaseJson(OnlineConstants.ENTROPY_REPOSITORY_URL)!! + + val version = responseJson.getString("tag_name") + version.shouldStartWith("v") + responseJson.getJSONArray("assets").length() shouldBeGreaterThan 0 + } + + /** Parsing */ + @Test + fun `Should parse correctly formed JSON`() { + val json = + """{ "tag_name": "foo", "assets": [ { @@ -115,135 +115,142 @@ class UpdateManagerTest : AbstractTest() { ] }""" - val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json))!! - metadata.version shouldBe "foo" - metadata.assetId shouldBe 123456 - metadata.fileName shouldBe "Dartzee_v_foo.jar" - metadata.size shouldBe 1 - } - - @Test - fun `Should log an error if no tag_name is present`() { - val json = "{\"other_tag\":\"foo\"}" - val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json)) - metadata shouldBe null - - val log = verifyLog("parseError", Severity.ERROR) - log.errorObject.shouldBeInstanceOf() - log.keyValuePairs["responseBody"].toString() shouldBe json - } - - @Test - fun `Should log an error if no assets are found`() { - val json = """{"assets":[],"tag_name":"foo"}""" - val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json)) - metadata shouldBe null - - val log = verifyLog("parseError", Severity.ERROR) - log.errorObject.shouldBeInstanceOf() - log.keyValuePairs["responseBody"].toString() shouldBe json - } - - /** Should update? */ - @Test - fun `Should not proceed with the update if the versions match`() { - val metadata = - UpdateMetadata(OnlineConstants.ENTROPY_VERSION_NUMBER, 123456, "EntropyLive_x_y.jar", 100) - - UpdateManager.shouldUpdate(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata) shouldBe false - val log = verifyLog("updateResult") - log.message shouldBe "Up to date" - } - - @Test - fun `Should show an info and not proceed to auto update if OS is not windows`() { - AbstractClient.operatingSystem = "foo" - - val metadata = UpdateMetadata("v100", 123456, "Dartzee_x_y.jar", 100) - shouldUpdateAsync(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata).get() shouldBe false - - val log = verifyLog("updateAvailable") - log.message shouldBe "Newer release available - v100" - - val info = getInfoDialog() - val linkLabel = info.getChild() - linkLabel.text shouldBe - "${OnlineConstants.ENTROPY_MANUAL_DOWNLOAD_URL}/tag/v100" - } - - @Test - fun `Should not proceed with the update if user selects 'No'`() { - AbstractClient.operatingSystem = "windows" - - val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100) - val result = shouldUpdateAsync("bar", metadata) - - val question = getQuestionDialog() - question.getDialogMessage() shouldBe - "An update is available (foo). Would you like to download it now?" - question.clickNo() - flushEdt() - - result.get() shouldBe false - } - - @Test - fun `Should proceed with the update if user selects 'Yes'`() { - AbstractClient.operatingSystem = "windows" - - val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100) - val result = shouldUpdateAsync("bar", metadata) - - val question = getQuestionDialog() - question.getDialogMessage() shouldBe - "An update is available (foo). Would you like to download it now?" - question.clickYes() - flushEdt() - - result.get() shouldBe true - } - - private fun shouldUpdateAsync(currentVersion: String, metadata: UpdateMetadata): AtomicBoolean { - val result = AtomicBoolean(false) - SwingUtilities.invokeLater { result.set(UpdateManager.shouldUpdate(currentVersion, metadata)) } - - flushEdt() - return result - } - - /** Prepare batch file */ - @Test - fun `Should overwrite existing batch file with the correct contents`() { - val updateFile = File("update.bat") - updateFile.writeText("blah") - - UpdateManager.prepareBatchFile() - - updateFile.readText() shouldBe javaClass.getResource("/update/update.bat").readText() - updateFile.delete() - } - - /** Run update */ - @Test - fun `Should log an error and not exit if batch file goes wrong`() { - val runtime = mockk() - val error = IOException("Argh") - every { runtime.exec(any()) } throws error - - runAsync { assertDoesNotExit { UpdateManager.startUpdate("foo", runtime) } } - - val errorDialog = getErrorDialog() - errorDialog.getDialogMessage() shouldBe - "Failed to launch update.bat - call the following manually to perform the update: \n\nupdate.bat foo" - - val log = verifyLog("batchError", Severity.ERROR) - log.errorObject shouldBe error - } - - @Test - fun `Should exit normally if batch file succeeds`() { - val runtime = mockk(relaxed = true) - - assertExits(0) { UpdateManager.startUpdate("foo", runtime) } - } + val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json))!! + metadata.version shouldBe "foo" + metadata.assetId shouldBe 123456 + metadata.fileName shouldBe "Dartzee_v_foo.jar" + metadata.size shouldBe 1 + } + + @Test + fun `Should log an error if no tag_name is present`() { + val json = "{\"other_tag\":\"foo\"}" + val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json)) + metadata shouldBe null + + val log = verifyLog("parseError", Severity.ERROR) + log.errorObject.shouldBeInstanceOf() + log.keyValuePairs["responseBody"].toString() shouldBe json + } + + @Test + fun `Should log an error if no assets are found`() { + val json = """{"assets":[],"tag_name":"foo"}""" + val metadata = UpdateManager.parseUpdateMetadata(JSONObject(json)) + metadata shouldBe null + + val log = verifyLog("parseError", Severity.ERROR) + log.errorObject.shouldBeInstanceOf() + log.keyValuePairs["responseBody"].toString() shouldBe json + } + + /** Should update? */ + @Test + fun `Should not proceed with the update if the versions match`() { + val metadata = + UpdateMetadata( + OnlineConstants.ENTROPY_VERSION_NUMBER, + 123456, + "EntropyLive_x_y.jar", + 100 + ) + + UpdateManager.shouldUpdate(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata) shouldBe false + val log = verifyLog("updateResult") + log.message shouldBe "Up to date" + } + + @Test + fun `Should show an info and not proceed to auto update if OS is not windows`() { + AbstractClient.operatingSystem = "foo" + + val metadata = UpdateMetadata("v100", 123456, "Dartzee_x_y.jar", 100) + shouldUpdateAsync(OnlineConstants.ENTROPY_VERSION_NUMBER, metadata).get() shouldBe false + + val log = verifyLog("updateAvailable") + log.message shouldBe "Newer release available - v100" + + val info = getInfoDialog() + val linkLabel = info.getChild() + linkLabel.text shouldBe + "${OnlineConstants.ENTROPY_MANUAL_DOWNLOAD_URL}/tag/v100" + } + + @Test + fun `Should not proceed with the update if user selects 'No'`() { + AbstractClient.operatingSystem = "windows" + + val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100) + val result = shouldUpdateAsync("bar", metadata) + + val question = getQuestionDialog() + question.getDialogMessage() shouldBe + "An update is available (foo). Would you like to download it now?" + question.clickNo() + flushEdt() + + result.get() shouldBe false + } + + @Test + fun `Should proceed with the update if user selects 'Yes'`() { + AbstractClient.operatingSystem = "windows" + + val metadata = UpdateMetadata("foo", 123456, "Dartzee_x_y.jar", 100) + val result = shouldUpdateAsync("bar", metadata) + + val question = getQuestionDialog() + question.getDialogMessage() shouldBe + "An update is available (foo). Would you like to download it now?" + question.clickYes() + flushEdt() + + result.get() shouldBe true + } + + private fun shouldUpdateAsync(currentVersion: String, metadata: UpdateMetadata): AtomicBoolean { + val result = AtomicBoolean(false) + SwingUtilities.invokeLater { + result.set(UpdateManager.shouldUpdate(currentVersion, metadata)) + } + + flushEdt() + return result + } + + /** Prepare batch file */ + @Test + fun `Should overwrite existing batch file with the correct contents`() { + val updateFile = File("update.bat") + updateFile.writeText("blah") + + UpdateManager.prepareBatchFile() + + updateFile.readText() shouldBe javaClass.getResource("/update/update.bat").readText() + updateFile.delete() + } + + /** Run update */ + @Test + fun `Should log an error and not exit if batch file goes wrong`() { + val runtime = mockk() + val error = IOException("Argh") + every { runtime.exec(any()) } throws error + + runAsync { assertDoesNotExit { UpdateManager.startUpdate("foo", runtime) } } + + val errorDialog = getErrorDialog() + errorDialog.getDialogMessage() shouldBe + "Failed to launch update.bat - call the following manually to perform the update: \n\nupdate.bat foo" + + val log = verifyLog("batchError", Severity.ERROR) + log.errorObject shouldBe error + } + + @Test + fun `Should exit normally if batch file succeeds`() { + val runtime = mockk(relaxed = true) + + assertExits(0) { UpdateManager.startUpdate("foo", runtime) } + } }