From db4e29cc2d9f87f4c78beed5db9c68f5b0d946fb Mon Sep 17 00:00:00 2001 From: tcheeric Date: Wed, 31 Dec 2025 19:58:15 +0000 Subject: [PATCH 1/3] fix(test): add await for server connection registration in idempotent test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The connectIsIdempotentWhenConnected test was flaky because it checked mockRelay.getConnectionCount() immediately after client.connect() returned. Due to async nature, the server's onOpen callback may not have executed yet. Added mockRelay.awaitConnection() to wait for server-side connection registration before asserting connection count. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../admin/integration/AdminConnectionLifecycleTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java index c7ba252..3909575 100644 --- a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java +++ b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java @@ -86,6 +86,8 @@ void connectIsIdempotentWhenConnected() throws Exception { client.connect(); assertThat(client.isConnected()).isTrue(); + // Wait for server to register the connection + assertThat(mockRelay.awaitConnection(5, TimeUnit.SECONDS)).isTrue(); // Second connect should not throw client.connect(); From 39479f99a4ae704a1233f7e1ca9a1a76e10619aa Mon Sep 17 00:00:00 2001 From: tcheeric Date: Sun, 4 Jan 2026 20:25:07 +0000 Subject: [PATCH 2/3] fix: handle null passphrase in key management methods - Normalize null or blank passphrases to empty string for create, unlock, and rotate key operations to allow unencrypted keys. - Remove `requirePassphrase` validation and add `normalizePassphrase` method. - Fix MockWebServer lifecycle issues in `RelayConnectionChaosTest` by implementing proper WebSocket event handling. --- .../admin/key/DefaultKeyManager.java | 32 +++++++++------ .../admin/key/DefaultKeyManagerTest.java | 39 +++++++++++++++++++ .../chaos/RelayConnectionChaosTest.java | 27 ++++++++++++- 3 files changed, 85 insertions(+), 13 deletions(-) diff --git a/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManager.java b/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManager.java index 5c08217..45eb4e5 100644 --- a/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManager.java +++ b/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManager.java @@ -58,20 +58,22 @@ public DefaultKeyManager(NsecBunkerAdminClient adminClient, ObjectMapper objectM @Override public CompletableFuture createKey(String name, String nsec, String passphrase) { validateName(name); - requirePassphrase(passphrase); if (nsec == null || nsec.isBlank()) { throw new IllegalArgumentException("nsec must not be null or blank"); } - return sendForKey(METHOD_CREATE_NEW_KEY, List.of(name, passphrase, nsec), name); + // Empty passphrase is allowed - nsecbunkerd will store the key unencrypted + String normalizedPassphrase = normalizePassphrase(passphrase); + return sendForKey(METHOD_CREATE_NEW_KEY, List.of(name, normalizedPassphrase, nsec), name); } @Override public CompletableFuture createKey(String name, String passphrase) { validateName(name); - requirePassphrase(passphrase); - return sendForKey(METHOD_CREATE_NEW_KEY, List.of(name, passphrase), name); + // Empty passphrase is allowed - nsecbunkerd will store the key unencrypted + String normalizedPassphrase = normalizePassphrase(passphrase); + return sendForKey(METHOD_CREATE_NEW_KEY, List.of(name, normalizedPassphrase), name); } @Override @@ -83,9 +85,10 @@ public CompletableFuture> listKeys() { @Override public CompletableFuture unlockKey(String name, String passphrase) { validateName(name); - requirePassphrase(passphrase); - return sendForResult(METHOD_UNLOCK_KEY, List.of(name, passphrase), "unlock key " + name) + // Empty passphrase is allowed - for keys stored without encryption + String normalizedPassphrase = normalizePassphrase(passphrase); + return sendForResult(METHOD_UNLOCK_KEY, List.of(name, normalizedPassphrase), "unlock key " + name) .thenApply(ignored -> Boolean.TRUE); } @@ -108,9 +111,9 @@ public CompletableFuture getKeyDetails(String name) { public CompletableFuture rotateKey(String oldName, String newName, String passphrase) { validateName(oldName); validateName(newName); - requirePassphrase(passphrase); - return sendForKey(METHOD_ROTATE_KEY, List.of(oldName, newName, passphrase), newName); + String normalizedPassphrase = normalizePassphrase(passphrase); + return sendForKey(METHOD_ROTATE_KEY, List.of(oldName, newName, normalizedPassphrase), newName); } private CompletableFuture sendForResult(String method, List params, String description) { @@ -185,9 +188,14 @@ private void validateName(String name) { } } - private void requirePassphrase(String passphrase) { - if (passphrase == null || passphrase.isBlank()) { - throw new IllegalArgumentException("Passphrase must not be null or blank"); - } + /** + * Normalizes a passphrase to an empty string if null or blank. + * This allows creating/unlocking keys without encryption. + * + * @param passphrase the passphrase to normalize + * @return the passphrase or empty string if null/blank + */ + private String normalizePassphrase(String passphrase) { + return passphrase != null && !passphrase.isBlank() ? passphrase : ""; } } diff --git a/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManagerTest.java b/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManagerTest.java index 2cbf27b..6ecac68 100644 --- a/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManagerTest.java +++ b/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManagerTest.java @@ -213,6 +213,45 @@ void shouldRotateKey() throws Exception { assertThat(request.getParams()).containsExactlyElementsOf(List.of("cashu-old", "cashu-rotated", TEST_PASSPHRASE)); } + /** + * Ensures creating a key with null passphrase normalizes it to empty string. + */ + @Test + void shouldCreateKeyWithNullPassphraseNormalizedToEmpty() { + // Arrange + String npub = "npub1nopwd"; + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Nip46Request.class); + when(adminClient.sendRequest(requestCaptor.capture())) + .thenReturn(CompletableFuture.completedFuture(Nip46Response.success("1", npub))); + + // Act + BunkerKey result = keyManager.createKey("key-no-passphrase", null).join(); + + // Assert + assertThat(result.getName()).isEqualTo("key-no-passphrase"); + Nip46Request request = requestCaptor.getValue(); + assertThat(request.getParams()).containsExactlyElementsOf(List.of("key-no-passphrase", "")); + } + + /** + * Ensures unlocking a key with null passphrase normalizes it to empty string. + */ + @Test + void shouldUnlockKeyWithNullPassphraseNormalizedToEmpty() { + // Arrange + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Nip46Request.class); + when(adminClient.sendRequest(requestCaptor.capture())) + .thenReturn(CompletableFuture.completedFuture(Nip46Response.success("1", "ok"))); + + // Act + boolean result = keyManager.unlockKey("key-unencrypted", null).join(); + + // Assert + assertThat(result).isTrue(); + Nip46Request request = requestCaptor.getValue(); + assertThat(request.getParams()).containsExactlyElementsOf(List.of("key-unencrypted", "")); + } + /** * Ensures NIP-46 errors are surfaced as AdminException through the future. */ diff --git a/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java b/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java index c0d1c43..c7a746f 100644 --- a/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java +++ b/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java @@ -89,9 +89,34 @@ void shouldStopAfterMaxReconnectAttempts() throws Exception { } /** - * No-op WebSocket listener for MockWebServer upgrades. + * WebSocket listener for MockWebServer upgrades that properly handles lifecycle. + * + *

A completely empty listener causes NPE in OkHttp's RealWebSocket.loopReader + * because the reader loop expects proper lifecycle handling. This implementation + * handles onOpen, onMessage, and onClosing to prevent the MockWebServer crash. */ private static final class NoopWebSocketListener extends okhttp3.WebSocketListener { + + @Override + public void onOpen(okhttp3.WebSocket webSocket, okhttp3.Response response) { + // Connection opened - no action needed for test + } + + @Override + public void onMessage(okhttp3.WebSocket webSocket, String text) { + // Message received - no action needed for test + } + + @Override + public void onClosing(okhttp3.WebSocket webSocket, int code, String reason) { + // Server closing - echo back close to complete handshake + webSocket.close(code, reason); + } + + @Override + public void onFailure(okhttp3.WebSocket webSocket, Throwable t, okhttp3.Response response) { + // Connection failed - no action needed for test + } } /** From 3603822fc32e0d1227fb7a6eb93bb95bb96847d9 Mon Sep 17 00:00:00 2001 From: tcheeric Date: Sun, 4 Jan 2026 20:32:52 +0000 Subject: [PATCH 3/3] chore: update version to 0.1.1 in pom.xml for all modules --- nsecbunker-account/pom.xml | 2 +- nsecbunker-admin/pom.xml | 2 +- nsecbunker-client/pom.xml | 2 +- nsecbunker-connection/pom.xml | 2 +- nsecbunker-core/pom.xml | 2 +- nsecbunker-monitoring/pom.xml | 2 +- nsecbunker-protocol/pom.xml | 2 +- nsecbunker-spring-boot-starter/pom.xml | 2 +- nsecbunker-tests/nsecbunker-chaos/pom.xml | 2 +- nsecbunker-tests/nsecbunker-e2e/pom.xml | 2 +- nsecbunker-tests/nsecbunker-it/pom.xml | 2 +- nsecbunker-tests/nsecbunker-perf/pom.xml | 2 +- nsecbunker-tests/nsecbunker-security/pom.xml | 2 +- nsecbunker-tests/pom.xml | 2 +- pom.xml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/nsecbunker-account/pom.xml b/nsecbunker-account/pom.xml index 0ec21eb..3793d1f 100644 --- a/nsecbunker-account/pom.xml +++ b/nsecbunker-account/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-account diff --git a/nsecbunker-admin/pom.xml b/nsecbunker-admin/pom.xml index c40c51f..26ce241 100644 --- a/nsecbunker-admin/pom.xml +++ b/nsecbunker-admin/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-admin diff --git a/nsecbunker-client/pom.xml b/nsecbunker-client/pom.xml index bbc8833..2e98e8c 100644 --- a/nsecbunker-client/pom.xml +++ b/nsecbunker-client/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-client diff --git a/nsecbunker-connection/pom.xml b/nsecbunker-connection/pom.xml index 99f1516..4ea604e 100644 --- a/nsecbunker-connection/pom.xml +++ b/nsecbunker-connection/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-connection diff --git a/nsecbunker-core/pom.xml b/nsecbunker-core/pom.xml index 2484c19..c58de7b 100644 --- a/nsecbunker-core/pom.xml +++ b/nsecbunker-core/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-core diff --git a/nsecbunker-monitoring/pom.xml b/nsecbunker-monitoring/pom.xml index a7dd48b..587a720 100644 --- a/nsecbunker-monitoring/pom.xml +++ b/nsecbunker-monitoring/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-monitoring diff --git a/nsecbunker-protocol/pom.xml b/nsecbunker-protocol/pom.xml index ff65d98..5570120 100644 --- a/nsecbunker-protocol/pom.xml +++ b/nsecbunker-protocol/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-protocol diff --git a/nsecbunker-spring-boot-starter/pom.xml b/nsecbunker-spring-boot-starter/pom.xml index eb44dc5..6115842 100644 --- a/nsecbunker-spring-boot-starter/pom.xml +++ b/nsecbunker-spring-boot-starter/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-spring-boot-starter diff --git a/nsecbunker-tests/nsecbunker-chaos/pom.xml b/nsecbunker-tests/nsecbunker-chaos/pom.xml index f085853..4a62193 100644 --- a/nsecbunker-tests/nsecbunker-chaos/pom.xml +++ b/nsecbunker-tests/nsecbunker-chaos/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0 + 0.1.1 nsecbunker-chaos diff --git a/nsecbunker-tests/nsecbunker-e2e/pom.xml b/nsecbunker-tests/nsecbunker-e2e/pom.xml index 1b5071d..d922128 100644 --- a/nsecbunker-tests/nsecbunker-e2e/pom.xml +++ b/nsecbunker-tests/nsecbunker-e2e/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0 + 0.1.1 nsecbunker-e2e diff --git a/nsecbunker-tests/nsecbunker-it/pom.xml b/nsecbunker-tests/nsecbunker-it/pom.xml index 48bc51a..e826aa0 100644 --- a/nsecbunker-tests/nsecbunker-it/pom.xml +++ b/nsecbunker-tests/nsecbunker-it/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0 + 0.1.1 nsecbunker-it diff --git a/nsecbunker-tests/nsecbunker-perf/pom.xml b/nsecbunker-tests/nsecbunker-perf/pom.xml index a7863f6..d279be8 100644 --- a/nsecbunker-tests/nsecbunker-perf/pom.xml +++ b/nsecbunker-tests/nsecbunker-perf/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0 + 0.1.1 nsecbunker-perf diff --git a/nsecbunker-tests/nsecbunker-security/pom.xml b/nsecbunker-tests/nsecbunker-security/pom.xml index babd250..d8536bc 100644 --- a/nsecbunker-tests/nsecbunker-security/pom.xml +++ b/nsecbunker-tests/nsecbunker-security/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0 + 0.1.1 nsecbunker-security diff --git a/nsecbunker-tests/pom.xml b/nsecbunker-tests/pom.xml index 82b6f47..f4c2c17 100644 --- a/nsecbunker-tests/pom.xml +++ b/nsecbunker-tests/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-tests diff --git a/pom.xml b/pom.xml index a3c3c22..8fb1850 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 pom nsecBunker Java Library