From ea806ec97a4078aa41dca1449072e1aae6f9f6f9 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 13 Jul 2026 13:45:21 +0200 Subject: [PATCH] Refactor event handling AppLauchEvent became a sealed interface with implemented Events RevealRunningApp-, OpenFile- and ValtCreateEvent. Signed-off-by: Armin Schrenk --- .../cryptomator/launcher/AppLaunchEvent.java | 36 +++---- .../cryptomator/launcher/DeeplinkAction.java | 38 -------- .../launcher/FileOpenRequestHandler.java | 4 +- .../launcher/IpcMessageHandler.java | 2 +- .../launcher/NoopRequestHandler.java | 2 +- .../cryptomator/launcher/OpenFileEvent.java | 13 +++ .../launcher/RevealRunningEvent.java | 8 ++ .../launcher/URIOpenRequestHandler.java | 29 +++++- ...eDeeplink.java => VaultCreationEvent.java} | 39 +++++--- .../ui/fxapp/AppLaunchEventHandler.java | 19 ++-- .../launcher/DeeplinkActionTest.java | 40 -------- .../launcher/FileOpenRequestHandlerTest.java | 5 +- .../launcher/VaultCreateDeeplinkTest.java | 69 -------------- .../launcher/VaultCreationEventTest.java | 94 +++++++++++++++++++ 14 files changed, 195 insertions(+), 203 deletions(-) delete mode 100644 src/main/java/org/cryptomator/launcher/DeeplinkAction.java create mode 100644 src/main/java/org/cryptomator/launcher/OpenFileEvent.java create mode 100644 src/main/java/org/cryptomator/launcher/RevealRunningEvent.java rename src/main/java/org/cryptomator/launcher/{VaultCreateDeeplink.java => VaultCreationEvent.java} (61%) delete mode 100644 src/test/java/org/cryptomator/launcher/DeeplinkActionTest.java delete mode 100644 src/test/java/org/cryptomator/launcher/VaultCreateDeeplinkTest.java create mode 100644 src/test/java/org/cryptomator/launcher/VaultCreationEventTest.java diff --git a/src/main/java/org/cryptomator/launcher/AppLaunchEvent.java b/src/main/java/org/cryptomator/launcher/AppLaunchEvent.java index 5c9ecde54..b7ee72f8f 100644 --- a/src/main/java/org/cryptomator/launcher/AppLaunchEvent.java +++ b/src/main/java/org/cryptomator/launcher/AppLaunchEvent.java @@ -1,28 +1,16 @@ package org.cryptomator.launcher; -import java.net.URI; -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; - -public record AppLaunchEvent(AppLaunchEvent.EventType type, Collection pathsToOpen, URI uri) { - - public enum EventType { - REVEAL_APP, - OPEN_FILE, - OPEN_URI - } - - static AppLaunchEvent revealApp() { - return new AppLaunchEvent(EventType.REVEAL_APP, List.of(), null); - } - - static AppLaunchEvent openFiles(Collection pathsToOpen) { - return new AppLaunchEvent(EventType.OPEN_FILE, pathsToOpen, null); - } - - static AppLaunchEvent openUri(URI uri) { - return new AppLaunchEvent(EventType.OPEN_URI, List.of(), uri); - } +/** + * An event triggering an action in the running application instance. + *

+ * Produced by the launch-argument handling (see {@link LaunchArgsParser} and the {@code *RequestHandler}s) and consumed + * by the UI's {@code AppLaunchEventHandler}. Each permitted subtype represents one supported action: + *

    + *
  • {@link RevealRunningEvent} - reveal the already-running app,
  • + *
  • {@link OpenFileEvent} - open one or more paths,
  • + *
  • {@link VaultCreationEvent} - create a vault from a deeplink.
  • + *
+ */ +public sealed interface AppLaunchEvent permits RevealRunningEvent, OpenFileEvent, VaultCreationEvent { } diff --git a/src/main/java/org/cryptomator/launcher/DeeplinkAction.java b/src/main/java/org/cryptomator/launcher/DeeplinkAction.java deleted file mode 100644 index c110ab393..000000000 --- a/src/main/java/org/cryptomator/launcher/DeeplinkAction.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.cryptomator.launcher; - -import java.net.URI; -import java.util.Arrays; -import java.util.Optional; - -/** - * Enumerates the supported deeplink actions and maps an incoming {@link URI} to one of them. - *

- * A deeplink action is identified by the URI's host and path, e.g. {@code cryptomator://vault/create} maps to - * {@link #VAULT_CREATE}. Unknown host/path combinations yield an empty {@link Optional} and should be treated as - * unsupported. New actions are added by extending this enum. - */ -public enum DeeplinkAction { - - VAULT_CREATE("vault", "/create"); - - private final String host; - private final String path; - - DeeplinkAction(String host, String path) { - this.host = host; - this.path = path; - } - - /** - * Matches the given URI against the known deeplink actions. - * - * @param uri the deeplink URI - * @return the matching action, or an empty optional if the host/path is not recognized - */ - public static Optional match(URI uri) { - return Arrays.stream(values()) // - .filter(action -> action.host.equalsIgnoreCase(uri.getHost()) && action.path.equals(uri.getPath())) // - .findFirst(); - } - -} diff --git a/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java b/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java index cf991d054..269dcfb9a 100644 --- a/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java +++ b/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java @@ -41,7 +41,7 @@ class FileOpenRequestHandler { private void openFiles(OpenFilesEvent evt) { Collection pathsToOpen = evt.getFiles().stream().map(File::toPath).toList(); - AppLaunchEvent launchEvent = AppLaunchEvent.openFiles(pathsToOpen); + AppLaunchEvent launchEvent = new OpenFileEvent(pathsToOpen); tryToEnqueueFileOpenRequest(launchEvent); } @@ -60,7 +60,7 @@ class FileOpenRequestHandler { } }).filter(Objects::nonNull).toList(); if (!pathsToOpen.isEmpty()) { - AppLaunchEvent launchEvent = AppLaunchEvent.openFiles(pathsToOpen); + AppLaunchEvent launchEvent = new OpenFileEvent(pathsToOpen); tryToEnqueueFileOpenRequest(launchEvent); } } diff --git a/src/main/java/org/cryptomator/launcher/IpcMessageHandler.java b/src/main/java/org/cryptomator/launcher/IpcMessageHandler.java index 290e88ada..f338fdafa 100644 --- a/src/main/java/org/cryptomator/launcher/IpcMessageHandler.java +++ b/src/main/java/org/cryptomator/launcher/IpcMessageHandler.java @@ -26,7 +26,7 @@ class IpcMessageHandler implements IpcMessageListener { @Override public void revealRunningApp() { - launchEventQueue.add(AppLaunchEvent.revealApp()); + launchEventQueue.add(new RevealRunningEvent()); } @Override diff --git a/src/main/java/org/cryptomator/launcher/NoopRequestHandler.java b/src/main/java/org/cryptomator/launcher/NoopRequestHandler.java index 176b20c0b..5c6876cd2 100644 --- a/src/main/java/org/cryptomator/launcher/NoopRequestHandler.java +++ b/src/main/java/org/cryptomator/launcher/NoopRequestHandler.java @@ -21,7 +21,7 @@ public class NoopRequestHandler { } public void revealApp() { - AppLaunchEvent launchEvent = AppLaunchEvent.revealApp(); + AppLaunchEvent launchEvent = new RevealRunningEvent(); if (!launchEventQueue.offer(launchEvent)) { LOG.warn("Could not enqueue application launch event {}.", launchEvent); } diff --git a/src/main/java/org/cryptomator/launcher/OpenFileEvent.java b/src/main/java/org/cryptomator/launcher/OpenFileEvent.java new file mode 100644 index 000000000..d5021d111 --- /dev/null +++ b/src/main/java/org/cryptomator/launcher/OpenFileEvent.java @@ -0,0 +1,13 @@ +package org.cryptomator.launcher; + +import java.nio.file.Path; +import java.util.Collection; + +/** + * Requests that the given paths (e.g. {@code .cryptomator} vault files) are opened. + * + * @param pathsToOpen the paths to open + */ +public record OpenFileEvent(Collection pathsToOpen) implements AppLaunchEvent { + +} diff --git a/src/main/java/org/cryptomator/launcher/RevealRunningEvent.java b/src/main/java/org/cryptomator/launcher/RevealRunningEvent.java new file mode 100644 index 000000000..13a33c39a --- /dev/null +++ b/src/main/java/org/cryptomator/launcher/RevealRunningEvent.java @@ -0,0 +1,8 @@ +package org.cryptomator.launcher; + +/** + * Requests that the already-running application instance reveals itself (brings its main window to the front). + */ +public record RevealRunningEvent() implements AppLaunchEvent { + +} diff --git a/src/main/java/org/cryptomator/launcher/URIOpenRequestHandler.java b/src/main/java/org/cryptomator/launcher/URIOpenRequestHandler.java index d71d1ff93..57eb41bae 100644 --- a/src/main/java/org/cryptomator/launcher/URIOpenRequestHandler.java +++ b/src/main/java/org/cryptomator/launcher/URIOpenRequestHandler.java @@ -7,13 +7,24 @@ import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import java.net.URI; +import java.util.List; +import java.util.Optional; import java.util.concurrent.BlockingQueue; +import java.util.function.Function; @Singleton public class URIOpenRequestHandler { private static final Logger LOG = LoggerFactory.getLogger(URIOpenRequestHandler.class); + /** + * The registered deeplink parsers, tried in order. Each returns a matching event, an empty optional if the URI is + * not its concern, or throws {@link IllegalArgumentException} if the URI is its concern but malformed. + */ + private static final List>> DEEPLINK_PARSERS = List.of( // + VaultCreationEvent::tryParse // + ); + private final BlockingQueue launchEventQueue; @Inject @@ -22,10 +33,26 @@ public class URIOpenRequestHandler { } public void handleLaunchArgs(URI uri) { - AppLaunchEvent launchEvent = AppLaunchEvent.openUri(uri); + AppLaunchEvent launchEvent = toLaunchEvent(uri); if (!launchEventQueue.offer(launchEvent)) { LOG.warn("Could not enqueue application launch event {}.", launchEvent); } } + private AppLaunchEvent toLaunchEvent(URI uri) { + try { + for (var parser : DEEPLINK_PARSERS) { + var event = parser.apply(uri); + if (event.isPresent()) { + return event.get(); + } + } + } catch (IllegalArgumentException e) { + LOG.warn("Received malformed deeplink {}: {}. Revealing running app instead.", uri, e.getMessage()); + return new RevealRunningEvent(); + } + LOG.warn("Received unsupported deeplink {}, revealing running app instead.", uri); + return new RevealRunningEvent(); + } + } diff --git a/src/main/java/org/cryptomator/launcher/VaultCreateDeeplink.java b/src/main/java/org/cryptomator/launcher/VaultCreationEvent.java similarity index 61% rename from src/main/java/org/cryptomator/launcher/VaultCreateDeeplink.java rename to src/main/java/org/cryptomator/launcher/VaultCreationEvent.java index c82987f82..107d3d0cd 100644 --- a/src/main/java/org/cryptomator/launcher/VaultCreateDeeplink.java +++ b/src/main/java/org/cryptomator/launcher/VaultCreationEvent.java @@ -1,17 +1,18 @@ package org.cryptomator.launcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.Optional; /** - * Parsed representation of a {@code cryptomator://vault/create?name=…&template=…} deeplink. + * Requests creation of a new vault from a {@code cryptomator://vault/create?name=…&template=…} deeplink. *

* The {@code template} is a Base64URL-encoded ZIP archive holding a ready-made (signed) vault. The {@code name} fixes * the vault's directory name and is restricted to a single, safe path segment so that resolving it against a @@ -20,19 +21,27 @@ import org.slf4j.LoggerFactory; * @param name the fixed vault name (a single, safe path segment) * @param template the Base64URL-decoded vault template */ -public record VaultCreateDeeplink(String name, byte[] template) { +public record VaultCreationEvent(String name, byte[] template) implements AppLaunchEvent { - private static final Logger LOG = LoggerFactory.getLogger(VaultCreateDeeplink.class); + private static final Logger LOG = LoggerFactory.getLogger(VaultCreationEvent.class); + private static final String SCHEME = "cryptomator"; + private static final String HOST = "vault"; + private static final String PATH = "/create"; /** - * Parses the query parameters of a {@code vault/create} deeplink. + * Attempts to interpret the given URI as a {@code cryptomator://vault/create?name=…&template=…} deeplink. * * @param uri the deeplink URI - * @return the parsed deeplink - * @throws IllegalArgumentException if a required parameter is missing, the template is not valid Base64URL, or the - * name is not a single safe path segment + * @return the parsed event, or an empty optional if the URI's scheme, host or path do not identify a + * vault-creation deeplink + * @throws IllegalArgumentException if the URI identifies a vault-creation deeplink, but a required parameter is + * missing, the template is not valid Base64URL, or the name is not a single safe + * path segment */ - public static VaultCreateDeeplink parse(URI uri) { + public static Optional tryParse(URI uri) { + if (!SCHEME.equalsIgnoreCase(uri.getScheme()) || !HOST.equalsIgnoreCase(uri.getHost()) || !PATH.equals(uri.getPath())) { + return Optional.empty(); + } var params = parseQuery(uri.getRawQuery()); var name = params.get("name"); var templateParam = params.get("template"); @@ -49,11 +58,11 @@ public record VaultCreateDeeplink(String name, byte[] template) { } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Query parameter 'template' is not valid Base64URL.", e); } - var leftoverParams = params.keySet().stream().filter(k -> ! (k.equals("template") || k.equals("name"))).toList(); - if(!leftoverParams.isEmpty()) { - LOG.debug("Ignoring unknow parameters {}", leftoverParams); + var leftoverParams = params.keySet().stream().filter(k -> !(k.equals("template") || k.equals("name"))).toList(); + if (!leftoverParams.isEmpty()) { + LOG.debug("Ignoring unknown parameters {}", leftoverParams); } - return new VaultCreateDeeplink(name, template); + return Optional.of(new VaultCreationEvent(name, template)); } private static void validateName(String name) { diff --git a/src/main/java/org/cryptomator/ui/fxapp/AppLaunchEventHandler.java b/src/main/java/org/cryptomator/ui/fxapp/AppLaunchEventHandler.java index 9a0cc1ed7..61f299cac 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/AppLaunchEventHandler.java +++ b/src/main/java/org/cryptomator/ui/fxapp/AppLaunchEventHandler.java @@ -4,6 +4,9 @@ import org.cryptomator.common.vaults.NotAVaultDirectoryException; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.launcher.AppLaunchEvent; +import org.cryptomator.launcher.OpenFileEvent; +import org.cryptomator.launcher.RevealRunningEvent; +import org.cryptomator.launcher.VaultCreationEvent; import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.dialogs.Dialogs; import org.slf4j.Logger; @@ -14,7 +17,6 @@ import javax.inject.Named; import javafx.application.Platform; import javafx.stage.Stage; import java.io.IOException; -import java.net.URI; import java.nio.file.Path; import java.util.Optional; import java.util.concurrent.BlockingQueue; @@ -64,17 +66,16 @@ class AppLaunchEventHandler { } private void handleLaunchEvent(AppLaunchEvent event) { - switch (event.type()) { - case REVEAL_APP -> appWindows.showMainWindow(); - case OPEN_FILE -> event.pathsToOpen().forEach(this::openPotentialVault); - case OPEN_URI -> handleUri(event.uri()); - default -> LOG.warn("Unsupported event type: {}", event.type()); + switch (event) { + case RevealRunningEvent _ -> appWindows.showMainWindow(); + case OpenFileEvent openFileEvent -> openFileEvent.pathsToOpen().forEach(this::openPotentialVault); + case VaultCreationEvent vaultCreationEvent -> handleVaultCreation(vaultCreationEvent); } } - private void handleUri(URI uri) { - // TODO: dispatch to a handler depending on the URI (e.g. host/path) once deeplink actions are defined - LOG.warn("Received deeplink {}, but handling of this scheme is not yet implemented.", uri); + private void handleVaultCreation(VaultCreationEvent event) { + // TODO (deeplink step 5): open the dedicated vault-creation dialog via appWindows + LOG.warn("Received vault-creation deeplink for '{}', but handling is not yet implemented.", event.name()); appWindows.showMainWindow(); } diff --git a/src/test/java/org/cryptomator/launcher/DeeplinkActionTest.java b/src/test/java/org/cryptomator/launcher/DeeplinkActionTest.java deleted file mode 100644 index 9968a5566..000000000 --- a/src/test/java/org/cryptomator/launcher/DeeplinkActionTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.cryptomator.launcher; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.net.URI; -import java.util.Optional; - -public class DeeplinkActionTest { - - @Test - @DisplayName("cryptomator://vault/create maps to VAULT_CREATE") - public void testVaultCreateMatches() { - var result = DeeplinkAction.match(URI.create("cryptomator://vault/create?name=foo&template=bar")); - - Assertions.assertEquals(Optional.of(DeeplinkAction.VAULT_CREATE), result); - } - - @Test - @DisplayName("host matching is case-insensitive") - public void testHostCaseInsensitive() { - var result = DeeplinkAction.match(URI.create("cryptomator://VAULT/create")); - - Assertions.assertEquals(Optional.of(DeeplinkAction.VAULT_CREATE), result); - } - - @Test - @DisplayName("an unknown host yields empty") - public void testUnknownHost() { - Assertions.assertEquals(Optional.empty(), DeeplinkAction.match(URI.create("cryptomator://foo/create"))); - } - - @Test - @DisplayName("an unknown path yields empty") - public void testUnknownPath() { - Assertions.assertEquals(Optional.empty(), DeeplinkAction.match(URI.create("cryptomator://vault/bar"))); - } - -} diff --git a/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java b/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java index afe0e072a..d35c492d2 100644 --- a/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java +++ b/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java @@ -40,8 +40,7 @@ public class FileOpenRequestHandlerTest { public void testOpenArgsWithCorrectPaths() { inTest.handleLaunchArgs(List.of("foo", "bar")); - AppLaunchEvent evt = queue.poll(); - Assertions.assertNotNull(evt); + OpenFileEvent evt = Assertions.assertInstanceOf(OpenFileEvent.class, queue.poll()); Collection paths = evt.pathsToOpen(); MatcherAssert.assertThat(paths, CoreMatchers.hasItems(Paths.get("foo"), Paths.get("bar"))); } @@ -60,7 +59,7 @@ public class FileOpenRequestHandlerTest { @Test @DisplayName("./cryptomator.exe foo (with full event queue)") public void testOpenArgsWithFullQueue() { - queue.add(AppLaunchEvent.openFiles(Collections.emptyList())); + queue.add(new OpenFileEvent(Collections.emptyList())); Assumptions.assumeTrue(queue.remainingCapacity() == 0); inTest.handleLaunchArgs(List.of("foo")); diff --git a/src/test/java/org/cryptomator/launcher/VaultCreateDeeplinkTest.java b/src/test/java/org/cryptomator/launcher/VaultCreateDeeplinkTest.java deleted file mode 100644 index 723f528ad..000000000 --- a/src/test/java/org/cryptomator/launcher/VaultCreateDeeplinkTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.cryptomator.launcher; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -public class VaultCreateDeeplinkTest { - - private static final byte[] TEMPLATE_BYTES = "a-ready-made-vault-zip".getBytes(StandardCharsets.UTF_8); - private static final String TEMPLATE_B64 = Base64.getUrlEncoder().withoutPadding().encodeToString(TEMPLATE_BYTES); - - @Test - @DisplayName("a valid vault/create deeplink is parsed") - public void testValid() { - var inTest = VaultCreateDeeplink.parse(URI.create("cryptomator://vault/create?name=MyVault&template=" + TEMPLATE_B64)); - - Assertions.assertEquals("MyVault", inTest.name()); - Assertions.assertArrayEquals(TEMPLATE_BYTES, inTest.template()); - } - - @Test - @DisplayName("a URL-encoded name is decoded") - public void testUrlEncodedName() { - var inTest = VaultCreateDeeplink.parse(URI.create("cryptomator://vault/create?name=My%20Vault&template=" + TEMPLATE_B64)); - - Assertions.assertEquals("My Vault", inTest.name()); - } - - @Test - @DisplayName("a missing name fails") - public void testMissingName() { - Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreateDeeplink.parse(URI.create("cryptomator://vault/create?template=" + TEMPLATE_B64))); - } - - @Test - @DisplayName("a blank name fails") - public void testBlankName() { - Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreateDeeplink.parse(URI.create("cryptomator://vault/create?name=&template=" + TEMPLATE_B64))); - } - - @Test - @DisplayName("a missing template fails") - public void testMissingTemplate() { - Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreateDeeplink.parse(URI.create("cryptomator://vault/create?name=MyVault"))); - } - - @Test - @DisplayName("an invalid Base64URL template fails") - public void testInvalidTemplate() { - Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreateDeeplink.parse(URI.create("cryptomator://vault/create?name=MyVault&template=@@@"))); - } - - @Test - @DisplayName("a name containing a path separator fails") - public void testNameWithSlash() { - Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreateDeeplink.parse(URI.create("cryptomator://vault/create?name=foo%2Fbar&template=" + TEMPLATE_B64))); - } - - @Test - @DisplayName("a name with parent-dir traversal fails") - public void testNameWithTraversal() { - Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreateDeeplink.parse(URI.create("cryptomator://vault/create?name=..&template=" + TEMPLATE_B64))); - } - -} diff --git a/src/test/java/org/cryptomator/launcher/VaultCreationEventTest.java b/src/test/java/org/cryptomator/launcher/VaultCreationEventTest.java new file mode 100644 index 000000000..b007f54f7 --- /dev/null +++ b/src/test/java/org/cryptomator/launcher/VaultCreationEventTest.java @@ -0,0 +1,94 @@ +package org.cryptomator.launcher; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Optional; + +public class VaultCreationEventTest { + + private static final byte[] TEMPLATE_BYTES = "a-ready-made-vault-zip".getBytes(StandardCharsets.UTF_8); + private static final String TEMPLATE_B64 = Base64.getUrlEncoder().withoutPadding().encodeToString(TEMPLATE_BYTES); + + @Test + @DisplayName("a valid vault/create deeplink is parsed") + public void testValid() { + var inTest = VaultCreationEvent.tryParse(URI.create("cryptomator://vault/create?name=MyVault&template=" + TEMPLATE_B64)).orElseThrow(); + + Assertions.assertEquals("MyVault", inTest.name()); + Assertions.assertArrayEquals(TEMPLATE_BYTES, inTest.template()); + } + + @Test + @DisplayName("a URL-encoded name is decoded") + public void testUrlEncodedName() { + var inTest = VaultCreationEvent.tryParse(URI.create("cryptomator://vault/create?name=My%20Vault&template=" + TEMPLATE_B64)).orElseThrow(); + + Assertions.assertEquals("My Vault", inTest.name()); + } + + @Test + @DisplayName("a non-cryptomator scheme yields empty") + public void testWrongScheme() { + Assertions.assertEquals(Optional.empty(), VaultCreationEvent.tryParse(URI.create("foobar://vault/create?name=MyVault&template=" + TEMPLATE_B64))); + } + + @Test + @DisplayName("an unknown host yields empty") + public void testWrongHost() { + Assertions.assertEquals(Optional.empty(), VaultCreationEvent.tryParse(URI.create("cryptomator://foo/create?name=MyVault&template=" + TEMPLATE_B64))); + } + + @Test + @DisplayName("an unknown path yields empty") + public void testWrongPath() { + Assertions.assertEquals(Optional.empty(), VaultCreationEvent.tryParse(URI.create("cryptomator://vault/bar?name=MyVault&template=" + TEMPLATE_B64))); + } + + @Test + @DisplayName("a matching host is recognized case-insensitively") + public void testHostCaseInsensitive() { + Assertions.assertTrue(VaultCreationEvent.tryParse(URI.create("cryptomator://VAULT/create?name=MyVault&template=" + TEMPLATE_B64)).isPresent()); + } + + @Test + @DisplayName("a missing name fails") + public void testMissingName() { + Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreationEvent.tryParse(URI.create("cryptomator://vault/create?template=" + TEMPLATE_B64))); + } + + @Test + @DisplayName("a blank name fails") + public void testBlankName() { + Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreationEvent.tryParse(URI.create("cryptomator://vault/create?name=&template=" + TEMPLATE_B64))); + } + + @Test + @DisplayName("a missing template fails") + public void testMissingTemplate() { + Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreationEvent.tryParse(URI.create("cryptomator://vault/create?name=MyVault"))); + } + + @Test + @DisplayName("an invalid Base64URL template fails") + public void testInvalidTemplate() { + Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreationEvent.tryParse(URI.create("cryptomator://vault/create?name=MyVault&template=@@@"))); + } + + @Test + @DisplayName("a name containing a path separator fails") + public void testNameWithSlash() { + Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreationEvent.tryParse(URI.create("cryptomator://vault/create?name=foo%2Fbar&template=" + TEMPLATE_B64))); + } + + @Test + @DisplayName("a name with parent-dir traversal fails") + public void testNameWithTraversal() { + Assertions.assertThrows(IllegalArgumentException.class, () -> VaultCreationEvent.tryParse(URI.create("cryptomator://vault/create?name=..&template=" + TEMPLATE_B64))); + } + +}