test: add NIP-46 test suite for desktop, quartz, and fix pre-existing chess test errors
Add comprehensive test coverage for NIP-46 bunker login across quartz and desktopApp: Quartz (4 files, 48 tests): - ResponseParserTest: all 7 response parsers (success/error/unexpected) - FromBunkerUriTest: URI parsing, validation, edge cases - ConvertExceptionsTest: SignerResult→Exception mapping - NostrConnectEventTest: canDecrypt, talkingWith, verifiedRecipientPubKey Desktop (6 files, 45 tests): - BunkerUriUtilsTest: validateBunkerUri + stripBunkerSecret - AccountManagerKeyLoginTest: nsec/npub/invalid login, save, generate - AccountManagerLogoutTest: logout, forceLogout, state transitions - AccountManagerLoadAccountTest: internal/bunker/missing-key scenarios - AccountManagerBunkerLoginTest: hasBunkerAccount, setConnectingRelays - AccountManagerHeartbeatTest: start/stop, no-crash with internal signer Production changes: - AccountManager: constructor private→internal, add homeDir param for test injection, extract stripBunkerSecret to internal top-level, constants internal - desktopApp/build.gradle.kts: add mockk test dependency Fix pre-existing chess test compilation errors: - ChessStateReconstructorTest: add missing jester subpackage imports - ChessGameEventTest: altText()→alt() + add nip31Alts import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+152
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Tests for NostrConnectEvent pure logic (canDecrypt, talkingWith, verifiedRecipientPubKey).
|
||||
*
|
||||
* Crypto-dependent tests (create + decrypt roundtrip) live in androidDeviceTest/Nip46Test.kt
|
||||
* because NIP-44 encryption requires lazysodium which is only available on Android/device tests.
|
||||
*/
|
||||
class NostrConnectEventTest {
|
||||
private val senderKey = NostrSignerInternal(KeyPair())
|
||||
private val recipientKey = NostrSignerInternal(KeyPair())
|
||||
private val thirdPartyKey = NostrSignerInternal(KeyPair())
|
||||
|
||||
/** Construct a NostrConnectEvent with known pubKey and p-tag, no real crypto needed */
|
||||
private fun buildEvent(
|
||||
authorPubKey: String,
|
||||
recipientPubKey: String,
|
||||
) = NostrConnectEvent(
|
||||
id = "a".repeat(64),
|
||||
pubKey = authorPubKey,
|
||||
createdAt = 1L,
|
||||
tags = arrayOf(arrayOf("p", recipientPubKey)),
|
||||
content = "encrypted-placeholder",
|
||||
sig = "b".repeat(128),
|
||||
)
|
||||
|
||||
// --- canDecrypt ---
|
||||
|
||||
@Test
|
||||
fun canDecryptAsSender() {
|
||||
val event = buildEvent(senderKey.pubKey, recipientKey.pubKey)
|
||||
assertTrue(event.canDecrypt(senderKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canDecryptAsRecipient() {
|
||||
val event = buildEvent(senderKey.pubKey, recipientKey.pubKey)
|
||||
assertTrue(event.canDecrypt(recipientKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canDecryptUnauthorizedReturnsFalse() {
|
||||
val event = buildEvent(senderKey.pubKey, recipientKey.pubKey)
|
||||
assertFalse(event.canDecrypt(thirdPartyKey))
|
||||
}
|
||||
|
||||
// --- talkingWith ---
|
||||
|
||||
@Test
|
||||
fun talkingWithAsSenderReturnsRecipient() {
|
||||
val event = buildEvent(senderKey.pubKey, recipientKey.pubKey)
|
||||
assertEquals(recipientKey.pubKey, event.talkingWith(senderKey.pubKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun talkingWithAsRecipientReturnsSender() {
|
||||
val event = buildEvent(senderKey.pubKey, recipientKey.pubKey)
|
||||
assertEquals(senderKey.pubKey, event.talkingWith(recipientKey.pubKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun talkingWithUnknownReturnsSender() {
|
||||
val event = buildEvent(senderKey.pubKey, recipientKey.pubKey)
|
||||
// When oneSideHex doesn't match pubKey, returns pubKey (sender)
|
||||
assertEquals(senderKey.pubKey, event.talkingWith(thirdPartyKey.pubKey))
|
||||
}
|
||||
|
||||
// --- verifiedRecipientPubKey ---
|
||||
|
||||
@Test
|
||||
fun verifiedRecipientPubKeyWithValidHex() {
|
||||
val event = buildEvent(senderKey.pubKey, recipientKey.pubKey)
|
||||
assertEquals(recipientKey.pubKey, event.verifiedRecipientPubKey())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifiedRecipientPubKeyWithInvalidHex() {
|
||||
val event =
|
||||
NostrConnectEvent(
|
||||
id = "a".repeat(64),
|
||||
pubKey = senderKey.pubKey,
|
||||
createdAt = 1L,
|
||||
tags = arrayOf(arrayOf("p", "not-hex!")),
|
||||
content = "encrypted",
|
||||
sig = "b".repeat(128),
|
||||
)
|
||||
assertNull(event.verifiedRecipientPubKey())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifiedRecipientPubKeyWithNoPTag() {
|
||||
val event =
|
||||
NostrConnectEvent(
|
||||
id = "a".repeat(64),
|
||||
pubKey = senderKey.pubKey,
|
||||
createdAt = 1L,
|
||||
tags = emptyArray(),
|
||||
content = "encrypted",
|
||||
sig = "b".repeat(128),
|
||||
)
|
||||
assertNull(event.verifiedRecipientPubKey())
|
||||
}
|
||||
|
||||
// --- Kind ---
|
||||
|
||||
@Test
|
||||
fun kindIs24133() {
|
||||
assertEquals(24133, NostrConnectEvent.KIND)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun eventHasCorrectKind() {
|
||||
val event = buildEvent(senderKey.pubKey, recipientKey.pubKey)
|
||||
assertEquals(24133, event.kind)
|
||||
}
|
||||
|
||||
// --- isContentEncoded ---
|
||||
|
||||
@Test
|
||||
fun isContentEncodedReturnsTrue() {
|
||||
val event = buildEvent(senderKey.pubKey, recipientKey.pubKey)
|
||||
assertTrue(event.isContentEncoded())
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ConvertExceptionsTest {
|
||||
private val remote =
|
||||
NostrSignerRemote.fromBunkerUri(
|
||||
"bunker://${"a".repeat(64)}?relay=wss://r.com",
|
||||
NostrSignerInternal(KeyPair()),
|
||||
EmptyNostrClient,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun successfulReturnsBug() {
|
||||
val result = SignerResult.RequestAddressed.Successful(PingResult("pong"))
|
||||
val ex = remote.convertExceptions("Test", result)
|
||||
assertIs<IllegalStateException>(ex)
|
||||
assertTrue(ex.message!!.contains("bug"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectedReturnsManuallyUnauthorized() {
|
||||
val result = SignerResult.RequestAddressed.Rejected<PingResult>()
|
||||
val ex = remote.convertExceptions("Test", result)
|
||||
assertIs<SignerExceptions.ManuallyUnauthorizedException>(ex)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun timedOutReturnsTimedOutException() {
|
||||
val result = SignerResult.RequestAddressed.TimedOut<PingResult>()
|
||||
val ex = remote.convertExceptions("Test", result)
|
||||
assertIs<SignerExceptions.TimedOutException>(ex)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun couldNotPerformReturnsCouldNotPerformException() {
|
||||
val result = SignerResult.RequestAddressed.ReceivedButCouldNotPerform<PingResult>("custom msg")
|
||||
val ex = remote.convertExceptions("Test", result)
|
||||
assertIs<SignerExceptions.CouldNotPerformException>(ex)
|
||||
assertTrue(ex.message!!.contains("custom msg"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun couldNotParseReturnsIllegalState() {
|
||||
val result = SignerResult.RequestAddressed.ReceivedButCouldNotParseEventFromResult<PingResult>("{bad}")
|
||||
val ex = remote.convertExceptions("Test", result)
|
||||
assertIs<IllegalStateException>(ex)
|
||||
assertTrue(ex.message!!.contains("{bad}"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun couldNotVerifyReturnsIllegalState() {
|
||||
val event =
|
||||
Event(
|
||||
id = "a".repeat(64),
|
||||
pubKey = "b".repeat(64),
|
||||
createdAt = 1L,
|
||||
kind = 1,
|
||||
tags = emptyArray(),
|
||||
content = "",
|
||||
sig = "c".repeat(128),
|
||||
)
|
||||
val result = SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent<PingResult>(event)
|
||||
val ex = remote.convertExceptions("Test", result)
|
||||
assertIs<IllegalStateException>(ex)
|
||||
assertTrue(ex.message!!.contains("verify"))
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class FromBunkerUriTest {
|
||||
private val signer = NostrSignerInternal(KeyPair())
|
||||
private val client = EmptyNostrClient
|
||||
private val validHex = "a".repeat(64)
|
||||
|
||||
@Test
|
||||
fun validSingleRelay() {
|
||||
val uri = "bunker://$validHex?relay=wss://relay.example.com"
|
||||
val remote = NostrSignerRemote.fromBunkerUri(uri, signer, client)
|
||||
assertEquals(1, remote.relays.size)
|
||||
assertEquals(validHex, remote.remotePubkey)
|
||||
assertNull(remote.secret)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validMultipleRelays() {
|
||||
val uri = "bunker://$validHex?relay=wss://a.com&relay=wss://b.com"
|
||||
val remote = NostrSignerRemote.fromBunkerUri(uri, signer, client)
|
||||
assertEquals(2, remote.relays.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validWithSecret() {
|
||||
val uri = "bunker://$validHex?relay=wss://r.com&secret=abc123"
|
||||
val remote = NostrSignerRemote.fromBunkerUri(uri, signer, client)
|
||||
assertEquals("abc123", remote.secret)
|
||||
assertEquals(1, remote.relays.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingSchemeThrows() {
|
||||
assertFailsWith<Exception> {
|
||||
NostrSignerRemote.fromBunkerUri("npub1abc", signer, client)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidHexPubkeyThrows() {
|
||||
assertFailsWith<Exception> {
|
||||
NostrSignerRemote.fromBunkerUri("bunker://notHex?relay=wss://r.com", signer, client)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingQueryParamsThrows() {
|
||||
assertFailsWith<Exception> {
|
||||
NostrSignerRemote.fromBunkerUri("bunker://$validHex", signer, client)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skipsMalformedParams() {
|
||||
val uri = "bunker://$validHex?relay=wss://r.com&badparam"
|
||||
val remote = NostrSignerRemote.fromBunkerUri(uri, signer, client)
|
||||
assertEquals(1, remote.relays.size)
|
||||
assertNull(remote.secret)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user