- Migrates Quartz from Android to CommonMain

- Fully converts OpenTimestamp Java codebase to Kotlin, migrating the sync and async HTTP call interfaces to OkHttp and coroutines
- Redesigns parsing of relay commands, messages and filters for performance in Jackson.
- Starts the use of KotlinX Serialization when speed is not a requirement
- Migrates all Jackson field annotations to Kotlin Serialization
- Migrates Regex use in Quarts to Kotlin's Regex class
- Migrates Base64 from Android to Kotlin
- Migrates UUID from Android/Java to Kotlin
- Migrates LRUCache usage from Android/Java to Kotlin collections
- Migrates all String to bytearray conversions to Kotlin methods
- Migrates all System.arraycopy calls to kotlin native ones.
- Separates parsing code from the data classes Companion objects
- Exposes Rfc3986 normalizations to each platform.
- Exposes URI parsing classes to each platform.
- Exposes URL Encoders to each platform.
- Exposes BigDecimal to each platform.
- Exposes the Url Detector to each platform.
- Exposes MacInstances to each platform
- Exposes Diggest instances to each platform.
- Exposes a BitSet to each platform.
- Exposes GZip to each platform.
- Exposes Secp256k1 to each platform.
- Exposes SecureRandom to each platform.
- Exposes Time in seconds to each platform.
- Exposes the LargeCache to each platform.
- Exposes AES CBC and AES GCM encryption/decryption to each platform
- Migrate test assertions to Kotlin Tests
- Exposes Address class to each platform because of the Parceleable
- Creates our own ByteArrayOutputStream.
- Removes Lock features inside the Bloomfilters because we don't need data consistency/
- Migrates UserMetadata parser from Jackson to Kotlin serialization
- Removes the need for Static methods in each tag.
- Adds an event template serializer
- Adds KotlinX Datetime to migrate some of the date-based logs
- Adds support for LibSodium in the JVM platform
- Creates a shared test build for iOS targets
- Fixes several usages of Reflection when serializing classes
- Fixes a bug on loading RelayDB for the HintBloom filter test
- Increases the Bloom filter space to better use hints in the app.
- Removes support for iOS in x86
- Creates a Jackson mapper just for NIP-55, which stays in the Android build only.
- Keeps the event store in the android build as well.
- Removes @Syncronized tags in favor of Mutexes.
- Improved sendAndWaitForResponse NostrClient method to properly account for returns from each relay.
- Removes the need for GlobalScope and async calls in the downloadFirstEvent method.
- Restructures the parser and serialization of the relay messages and commands for performance with Jackson
- Removes the dependency on Jackson's error classes across the codebase.
- Moves the hint to quote tag extension methods to their own packages.
- Speeds up the generation of Bech32 addresses
- Migrates NIP-6 and Blossom uploads to use Kotlin Serialization
This commit is contained in:
Vitor Pamplona
2025-09-22 19:44:51 -04:00
parent 7949fc66b2
commit 7a280a3cde
1195 changed files with 12671 additions and 5183 deletions
@@ -18,12 +18,13 @@
* 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.utils
package com.vitorpamplona.quartz
import com.vitorpamplona.quartz.nip01Core.crypto.DeterministicSigner
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
fun String.nsecToKeyPair() = KeyPair(this.bechToBytes())
fun String.nsecToSigner() = this.nsecToKeyPair().let { DeterministicSigner(it) }
actual class TestResourceLoader {
actual fun loadString(file: String): String =
this@TestResourceLoader
.javaClass.classLoader!!
.getResourceAsStream(file)
.bufferedReader()
.use { it.readText() }
}
@@ -1,114 +0,0 @@
/**
* 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.nip01Core.hints
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.hints.bloom.BloomFilterMurMur3
import com.vitorpamplona.quartz.utils.RandomInstance
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import org.junit.Test
class BloomFilterMurMur3Test {
val testEncoded = "100:10:AKiEIEQKALgRACEABA==:3"
val testInBinary = "00000000000101010010000100000100001000100101000000000000000111011000100000000000100001000000000000100000000000000000000000000000"
val key1 = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b".hexToByteArray()
val key2 = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c".hexToByteArray()
val key3 = "560c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c".hexToByteArray()
val keys =
mutableListOf<ByteArray>().apply {
for (seed in 0..1_000_000) {
add(RandomInstance.bytes(32))
}
}
@Test
fun testCreate() {
val bloomFilter = BloomFilterMurMur3(100, 10, commonSalt = 3)
bloomFilter.add(key1)
bloomFilter.add(key2)
assertEquals(testEncoded, bloomFilter.encode())
assertEquals(testInBinary, bloomFilter.printBits())
assertTrue(bloomFilter.mightContain(key1))
assertTrue(bloomFilter.mightContain(key2))
assertFalse(bloomFilter.mightContain(key3))
}
@Test
fun testDecoding() {
val bloomFilter = BloomFilterMurMur3.decode(testEncoded)
assertEquals(testEncoded, bloomFilter.encode())
assertEquals(testInBinary, bloomFilter.printBits())
assertTrue(bloomFilter.mightContain(key1))
assertTrue(bloomFilter.mightContain(key2))
assertFalse(bloomFilter.mightContain(key3))
}
@Test
fun runProb() {
val bloomFilter = BloomFilterMurMur3.decode(testEncoded)
var failureCounter = 0
repeat(1_000_000) {
if (bloomFilter.mightContain(RandomInstance.bytes(32))) {
failureCounter++
}
}
assertTrue(failureCounter <= 2)
}
@Test
fun runProb2() {
val bloomFilter = BloomFilterMurMur3(10_000_000, 4)
keys.forEach(bloomFilter::add)
var failureCounter = 0
repeat(1_000_000) {
if (bloomFilter.mightContain(RandomInstance.bytes(32))) {
failureCounter++
}
}
assertTrue("Failures $failureCounter ${failureCounter / 1_000_000.0}", failureCounter / 1_000_000.0f < 0.015)
}
@Test
fun runProb3() {
val bloomFilter = BloomFilterMurMur3(10_000_000, 4)
keys.forEach(bloomFilter::add)
var failureCounter = 0
repeat(1_000_000) {
if (bloomFilter.mightContain(RandomInstance.bytes(32))) {
failureCounter++
}
}
assertTrue("Failures $failureCounter ${failureCounter / 1_000_000.0}", failureCounter / 1_000_000.0f < 0.015)
}
}
@@ -18,18 +18,21 @@
* 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.lightning
package com.vitorpamplona.quartz.nip01Core.hints
import org.junit.Assert.assertEquals
import org.junit.Test
import com.vitorpamplona.quartz.utils.usedMemoryMb
import kotlin.test.Test
class Lud06Test {
val lnTips =
"LNURL1DP68GURN8GHJ7MRW9E6XJURN9UH8WETVDSKKKMN0WAHZ7MRWW4EXCUP0XPURXEFEX9SKGCT9V5ER2V33X4NRGP2NE42"
class HintIndexerMemoryTest {
@Test
fun testSizeInMemory() {
System.gc()
Thread.sleep(1000)
@Test()
fun parseLnUrlp() {
assertEquals("https://ln.tips/.well-known/lnurlp/0x3e91adaee25215f4", Lud06().toLnUrlp(lnTips))
assertEquals("0x3e91adaee25215f4@ln.tips", Lud06().toLud16(lnTips))
val startingMemory = Runtime.getRuntime().usedMemoryMb()
val result = HintIndexer()
val endingMemory = Runtime.getRuntime().usedMemoryMb()
println("Filter using ${endingMemory - startingMemory}MB")
}
}
@@ -1,163 +0,0 @@
/**
* 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.nip01Core.hints
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.usedMemoryMb
import junit.framework.TestCase.assertTrue
import org.junit.Test
class HintIndexerTest {
companion object {
val VALID_CHARS: List<Char> = ('0'..'9') + ('a'..'z') + ('A'..'Z')
private fun randomChars(size: Int): String = CharArray(size) { VALID_CHARS.random() }.concatToString()
val keys =
mutableListOf<HexKey>().apply {
for (seed in 0..1_000_000) {
add(RandomInstance.bytes(32).toHexKey())
}
}
val eventIds =
mutableListOf<HexKey>().apply {
for (seed in 0..1_000_000) {
add(RandomInstance.bytes(32).toHexKey())
}
}
val addresses =
keys.take(100_000).map {
Address.assemble(
RandomInstance.int(65_000),
it,
randomChars(10),
)
}
val relays =
this::class.java
.getResourceAsStream("relayDB.txt")
?.readAllBytes()
.toString()
.split("\n")
.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }
val indexer by lazy {
System.gc()
Thread.sleep(1000)
val startingMemory = Runtime.getRuntime().usedMemoryMb()
val result = HintIndexer()
val endingMemory = Runtime.getRuntime().usedMemoryMb()
println("Filter using ${endingMemory - startingMemory}MB")
// Simulates 5 outbox relays for each key
keys.forEach { key ->
(0..5).map {
result.addKey(key, relays.random())
}
}
// Simulates each event being in 8 + 0..10 relays.
eventIds.forEach { id ->
repeat(8 + RandomInstance.int(10)) {
result.addEvent(id, relays.random())
}
}
// Simulates each address being in 8 + 0..10 relays.
addresses.forEach { address ->
repeat(8 + RandomInstance.int(10)) {
result.addAddress(address, relays.random())
}
}
result
}
}
val testSize = 1000
val testProb = 0.02f
fun assert99PercentSucess(success: () -> Boolean) {
var failureCounter = 0
repeat(testSize) {
if (!success()) {
failureCounter++
}
}
assertTrue(
"Failure rate: $failureCounter of 1000 elements => ${(failureCounter / testSize.toFloat()) * 100}%",
failureCounter / testSize.toFloat() < testProb,
)
}
@Test
fun runProbExistingKeys() =
assert99PercentSucess {
indexer.hintsForKey(keys.random()).isNotEmpty()
}
@Test
fun runProbNewKeys() =
assert99PercentSucess {
indexer.hintsForKey(RandomInstance.bytes(32)).isEmpty()
}
@Test
fun runProbExistingEventIds() =
assert99PercentSucess {
indexer.hintsForEvent(eventIds.random()).isNotEmpty()
}
@Test
fun runProbNewEventIds() =
assert99PercentSucess {
indexer.hintsForEvent(RandomInstance.bytes(32)).isEmpty()
}
@Test
fun runProbExistingAddresses() =
assert99PercentSucess {
indexer.hintsForAddress(addresses.random()).isNotEmpty()
}
@Test
fun runProbNewAddresses() =
assert99PercentSucess {
val newAddress =
Address.assemble(
RandomInstance.int(65000),
RandomInstance.bytes(32).toHexKey(),
randomChars(10),
)
indexer.hintsForAddress(newAddress).isEmpty()
}
}
@@ -1,76 +0,0 @@
/**
* 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.nip01Core.hints
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.hints.bloom.MurmurHash3
import junit.framework.TestCase.assertEquals
import org.junit.Test
class MurMur3Test {
class Case(
val bytesHex: HexKey,
val seed: Int,
val result: Int,
)
val testCases =
listOf(
Case("9fd4e9a905ca9e1a3086fa4c0a1ed829dbf18c15ec05af95c76b78d3d2f5651b", 886838366, -525456393),
Case("e6c8f70f0d35a983bfebd00e5f29787c009c52971cfb4ac3a49b534b256b59cc", 1717487548, 1605080838),
Case("7f7113833feb31e877f193e2fc75a64e9c70252c3ae3c73373ff34430ae40ea6", 1275582690, 225480992),
Case("61770be6ec9df0f490743318e796e28ae34609732b61d365947871532d77d697", 514559346, 1424957638),
Case("375f46b4687ba3cd035db303fa294d943816e64ca6b3adcda2ae40e8ac9d91a0", 1898708424, 1730418066),
Case("c67044cd1d07a2aeb92b7bec973b6feb8abb9197840c59c101cacaa992489d49", 294602161, -1944496371),
Case("49db4bfcc4da62e38c4076843cdde1425570806f09f121f5e7f2507c5ee1db85", 910710684, 944243368),
Case("c5e98a30dead5ade4900b26eabae3435cfcdb64ff5e55c99641915a0c6ee73fc", 1107230285, 1550302684),
Case("b0ed2e7568e6b4e1d5e5bab46fde01149331b824e48a281798d7216dde8f5890", 1013875681, -1265544300),
Case("805f290e865bde094d77e82fb8b338d83347bc5449a4aed9fb08afb6a53a079b", 1674416787, -1821262025),
// special cases
Case("805f290e865bde094d77e82fb8b338d83347bc5449a4aed9fb08afb6a53a079b", Int.MAX_VALUE, -422576759),
Case("805f290e865bde094d77e82fb8b338d83347bc5449a4aed9fb08afb6a53a079b", Int.MAX_VALUE + 1, 851385048),
Case("805f290e865bde094d77e82fb8b338d83347bc5449a4aed9fb08afb6a53a079b", Int.MIN_VALUE, 851385048),
Case("805f290e865bde094d77e82fb8b338d83347bc5449a4aed9fb08afb6a53a079b", 0, 1615518380),
Case("fd", 1, 975430984),
Case("00", 1, 0),
Case("FF", 1, -797126820),
Case("3033", 1, 1435178296),
Case("0000", 1, -2047822809),
Case("FFFF", 1, 1459517456),
Case("3652a8", 1, 103723868),
Case("000000", 1, 821347078),
Case("FFFFFF", 1, -761438248),
Case("00000000", 1, 2028806445),
Case("FFFFFFFF", 1, 919009801),
)
@Test
fun testMurMur() {
val hasher = MurmurHash3()
testCases.forEach {
assertEquals(
it.result,
hasher.hash(it.bytesHex.hexToByteArray(), it.seed),
)
}
}
}
@@ -1,65 +0,0 @@
/**
* 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.nip01Core.jackson
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.verify
import org.junit.Test
class EventDeserializerTest {
@Test
fun testEventTemplateToJson() {
val templateJson = """{"created_at":1234,"kind":1,"tags":[],"content":"This is an unsigned event."}"""
val template = EventTemplate.fromJson(templateJson)
val json = template.toJson()
assert(json == templateJson)
}
@Test
fun testEventTemplate() {
val templateJson = """{"kind":"1","content":"This is an unsigned event.","created_at":1234,"tags":[]}"""
val template = EventTemplate.fromJson(templateJson)
assert(template.kind == 1)
assert(template.content == "This is an unsigned event.")
assert(template.tags.isEmpty())
}
@Test
fun testSignedEvent() {
val keyPair = KeyPair()
val signer = NostrSignerInternal(keyPair)
val templateJson = """{"kind":"1","content":"This is an unsigned event.","created_at":1234,"tags":[]}"""
val template = EventTemplate.fromJson(templateJson)
val signedEvent = signer.signerSync.sign(template)!!
assert(signedEvent.id.isNotEmpty())
assert(signedEvent.sig.isNotEmpty())
assert(signedEvent.pubKey.isNotEmpty())
assert(signedEvent.verify())
}
}
@@ -1,95 +0,0 @@
/**
* 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.nip01Core.jackson
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.Assert.assertEquals
import org.junit.Test
class InliningTagArrayPrettyPrinterTest {
val mapper =
jacksonObjectMapper().apply {
setDefaultPrettyPrinter(InliningTagArrayPrettyPrinter())
}
@Test
fun test() {
val writer = mapper.writerWithDefaultPrettyPrinter()
val data =
mapOf(
"tags" to arrayOf(intArrayOf(1, 2, 3), intArrayOf(4, 5, 6), intArrayOf(7, 8, 9)),
)
val expected =
"""
{
"tags": [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
}
""".trimIndent()
val json = writer.writeValueAsString(data)
assertEquals(expected, json)
val data2 =
mapOf(
"tags" to arrayOf(arrayOf(intArrayOf(1, 2), intArrayOf(3, 4)), arrayOf(intArrayOf(5, 6), intArrayOf(7, 8))),
)
val expected2 =
"""
{
"tags": [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]]
]
}
""".trimIndent()
val json2 = writer.writeValueAsString(data2)
assertEquals(expected2, json2)
}
@Test
fun testEvent() {
val nostrObject =
"""
{
"id": "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe",
"pubkey": "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
"created_at": 1740669816,
"kind": 0,
"tags": [
["alt", "User profile for Vitor"],
["name", "Vitor"]
],
"content": "{\"name\":\"Vitor\"}",
"sig": "977a6152199f17d103d8d56736ed1b7767054464cf9423d017c01c8cdd2344698f0a5e13da8dff98d01bb1f798837e3b6271e1fd1cac861bb90686f622ae6ef4"
}
""".trimIndent()
val tree = mapper.readTree(nostrObject)
val prettified = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(tree)
assertEquals(nostrObject, prettified)
}
}
@@ -1,198 +0,0 @@
/**
* 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.nip01Core.metadata
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.jackson.EventManualSerializer
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.utils.nsecToSigner
import org.junit.Assert.assertEquals
import org.junit.Test
class UpdateMetadataTest {
val signer = "nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToSigner()
/**
* For debug purposes only
*/
fun Event.toPrettyJson(): String {
val obj = EventManualSerializer.assemble(id, pubKey, createdAt, kind, tags, content, sig)
return JsonMapper.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj)
}
@Test
fun createNewMetadata() {
val test = signer.sign(MetadataEvent.createNew("Vitor", createdAt = 1740669816))
val expected =
"""
{
"id": "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe",
"pubkey": "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
"created_at": 1740669816,
"kind": 0,
"tags": [
["alt", "User profile for Vitor"],
["name", "Vitor"]
],
"content": "{\"name\":\"Vitor\"}",
"sig": "977a6152199f17d103d8d56736ed1b7767054464cf9423d017c01c8cdd2344698f0a5e13da8dff98d01bb1f798837e3b6271e1fd1cac861bb90686f622ae6ef4"
}
""".trimIndent()
assertEquals(expected, test.toPrettyJson())
}
@Test
fun updateMetadata() {
val test =
signer.sign(
MetadataEvent.createNew(
name = "Vitor",
displayName = "Vitor Pamplona",
about = "Nostr's Chief Android Officer - #Amethyst",
picture = "https://vitorpamplona.com/images/me_300.jpg",
banner = "https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360",
pronouns = "he/him",
website = "https://vitorpamplona.com",
nip05 = "_@vitorpamplona.com",
lnAddress = "vitor@vitorpamplona.com",
lnURL = "TEST",
github = "https://gist.github.com/vitorpamplona/cf19e2d1d7f8dac6348ad37b35ec8421",
createdAt = 1740669816,
),
)
val expected =
"""
{
"id": "2b2761d66db4a83d5fb7a98cafb8414e9b0c238ceb67d729bedacc1a092d516f",
"pubkey": "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
"created_at": 1740669816,
"kind": 0,
"tags": [
["alt", "User profile for Vitor"],
["name", "Vitor"],
["display_name", "Vitor Pamplona"],
["picture", "https://vitorpamplona.com/images/me_300.jpg"],
["banner", "https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360"],
["website", "https://vitorpamplona.com"],
["pronouns", "he/him"],
["about", "Nostr's Chief Android Officer - #Amethyst"],
["nip05", "_@vitorpamplona.com"],
["lud16", "vitor@vitorpamplona.com"],
["lud06", "TEST"],
["i", "github:vitorpamplona", "cf19e2d1d7f8dac6348ad37b35ec8421"]
],
"content": "{\"name\":\"Vitor\",\"display_name\":\"Vitor Pamplona\",\"picture\":\"https://vitorpamplona.com/images/me_300.jpg\",\"banner\":\"https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360\",\"website\":\"https://vitorpamplona.com\",\"pronouns\":\"he/him\",\"about\":\"Nostr's Chief Android Officer - #Amethyst\",\"nip05\":\"_@vitorpamplona.com\",\"lud16\":\"vitor@vitorpamplona.com\",\"lud06\":\"TEST\"}",
"sig": "0a8c78eb0c5e0ba46e4781cc445fb7b6d275b434cead9231bd19b4f95671e3ab872264e50d4456d6036a84cc81e517bfa24229571519aabefcbce431e0c7163e"
}
""".trimIndent()
assertEquals(expected, test.toPrettyJson())
val expected2 =
"""
{
"id": "2e1e57fae4e4baddac025ea0b49afc093f2aa27610a05e584184ed26b29d7590",
"pubkey": "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
"created_at": 1740669817,
"kind": 0,
"tags": [
["alt", "User profile for 2 Vitor"],
["name", "2 Vitor"],
["display_name", "2 Vitor Pamplona"],
["picture", "2 https://vitorpamplona.com/images/me_300.jpg"],
["banner", "2 https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360"],
["website", "2 https://vitorpamplona.com"],
["pronouns", "2 he/him"],
["about", "2 Nostr's Chief Android Officer - #Amethyst"],
["nip05", "2 _@vitorpamplona.com"],
["lud16", "2 vitor@vitorpamplona.com"],
["lud06", "2 TEST"],
["i", "github:vitorpamplona", "2cf19e2d1d7f8dac6348ad37b35ec8421"]
],
"content": "{\"name\":\"2 Vitor\",\"display_name\":\"2 Vitor Pamplona\",\"picture\":\"2 https://vitorpamplona.com/images/me_300.jpg\",\"banner\":\"2 https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360\",\"website\":\"2 https://vitorpamplona.com\",\"pronouns\":\"2 he/him\",\"about\":\"2 Nostr's Chief Android Officer - #Amethyst\",\"nip05\":\"2 _@vitorpamplona.com\",\"lud16\":\"2 vitor@vitorpamplona.com\",\"lud06\":\"2 TEST\"}",
"sig": "a25483bc0fcc79ccd337e3ff846351097109fc13f1cd1c9cc15f7f2ad46417aeb7c05c1524aeadbab93036fc0743c8c310a5b2b7b482e1808649b12a3ee29d49"
}
""".trimIndent()
val test2 =
signer.sign(
MetadataEvent.updateFromPast(
latest = test,
name = "2 Vitor",
displayName = "2 Vitor Pamplona",
about = "2 Nostr's Chief Android Officer - #Amethyst",
picture = "2 https://vitorpamplona.com/images/me_300.jpg",
banner = "2 https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360",
pronouns = "2 he/him",
website = "2 https://vitorpamplona.com",
nip05 = "2 _@vitorpamplona.com",
lnAddress = "2 vitor@vitorpamplona.com",
lnURL = "2 TEST",
github = "https://gist.github.com/vitorpamplona/2cf19e2d1d7f8dac6348ad37b35ec8421",
createdAt = 1740669817,
),
)
assertEquals(expected2, test2.toPrettyJson())
val expected3 =
"""
{
"id": "94879b9a27fecf1337ede32013006ec4dfd5a3286a1e819abddfa1c3c132f008",
"pubkey": "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
"created_at": 1740669817,
"kind": 0,
"tags": [
["alt", "User profile for 2 Vitor"],
["name", "2 Vitor"],
["picture", "2 https://vitorpamplona.com/images/me_300.jpg"],
["banner", "2 https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360"]
],
"content": "{\"name\":\"2 Vitor\",\"picture\":\"2 https://vitorpamplona.com/images/me_300.jpg\",\"banner\":\"2 https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360\"}",
"sig": "729ee02364b4d429b6a66400ec850e80424602e3981ff2ad8a14d61bdee04f76ec68ddc4a0f38b0527f0218f7fa67668012a5c0f3c8ef943517b504040caa07e"
}
""".trimIndent()
val test3 =
signer.sign(
MetadataEvent.updateFromPast(
latest = test2,
name = null,
displayName = "",
about = "",
picture = null,
banner = null,
pronouns = "",
website = "",
nip05 = "",
lnAddress = "",
lnURL = "",
github = "",
createdAt = 1740669817,
),
)
assertEquals(expected3, test3.toPrettyJson())
}
}
@@ -1,47 +0,0 @@
/**
* 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.nip01Core.relay
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import junit.framework.TestCase.assertEquals
import org.junit.Test
class RelayUrlFormatterTest {
@Test
fun format() {
assertEquals("wss://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("wss://nostr.mom")?.url)
assertEquals("wss://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("nostr.mom")?.url)
assertEquals("ws://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("ws://nostr.mom")?.url)
assertEquals("wss://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("wss://nostr.mom/")?.url)
assertEquals("wss://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("https://nostr.mom/")?.url)
assertEquals("ws://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("http://nostr.mom/")?.url)
assertEquals("wss://localhost:3030/", RelayUrlNormalizer.normalizeOrNull("wss://localhost:3030")?.url)
assertEquals("ws://localhost:3030/", RelayUrlNormalizer.normalizeOrNull("localhost:3030")?.url)
assertEquals("wss://a.onion/", RelayUrlNormalizer.normalizeOrNull("wss://a.onion")?.url)
assertEquals("ws://a.onion/", RelayUrlNormalizer.normalizeOrNull("a.onion")?.url)
assertEquals("wss://a.onion/", RelayUrlNormalizer.normalizeOrNull("wss://a.onion/")?.url)
assertEquals("ws://a.onion/", RelayUrlNormalizer.normalizeOrNull("a.onion/")?.url)
assertEquals("wss://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("wss://nostr.mom")?.url)
}
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import junit.framework.TestCase
import org.junit.Test
import kotlin.test.Test
class EventDbQueryAssemblerTest {
val builder = EventIndexesModule(FullTextSearchModule())
@@ -1,93 +0,0 @@
/**
* 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.nip05DnsIdentifiers
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.fail
import org.junit.Test
class Nip05Test {
companion object {
private val ALL_UPPER_CASE_USER_NAME = "ONETWO"
private val ALL_LOWER_CASE_USER_NAME = "onetwo"
}
var nip05Verifier = Nip05()
@Test
fun `test with matching case on user name`() =
runBlocking {
// Set-up
val userNameToTest = ALL_UPPER_CASE_USER_NAME
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
val nostrJson = "{\n \"names\": {\n \"$userNameToTest\": \"$expectedPubKey\" \n }\n}"
val nip05 = "$userNameToTest@domain.com"
nip05Verifier.parseHexKeyFor(nip05, nostrJson).fold(
onSuccess = { assertEquals(expectedPubKey, it) },
onFailure = { fail("Test failure") },
)
}
@Test
fun `test with NOT matching case on user name`() =
runBlocking {
// Set-up
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
val nostrJson = "{ \"names\": { \"$ALL_UPPER_CASE_USER_NAME\": \"$expectedPubKey\" }}"
val nip05 = "$ALL_LOWER_CASE_USER_NAME@domain.com"
nip05Verifier.parseHexKeyFor(nip05, nostrJson).fold(
onSuccess = { assertEquals(expectedPubKey, it) },
onFailure = { fail("Test failure") },
)
}
@Test
fun `execute assemble url with invalid value returns null`() {
// given
val nip05address = "this@that@that.com"
// when
val actualValue = nip05Verifier.assembleUrl(nip05address)
// then
assertNull(actualValue)
}
@Test
fun `execute assemble url with valid value returns nip05 url`() {
// given
val userName = "TheUser"
val domain = "domain.com"
val nip05address = "$userName@$domain"
val expectedValue = "https://$domain/.well-known/nostr.json?name=$userName"
// when
val actualValue = nip05Verifier.assembleUrl(nip05address)
// then
assertEquals(expectedValue, actualValue)
}
}
@@ -1,448 +0,0 @@
/**
* 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.nip19Bech32
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
class NIP19ParserTest {
@Test()
fun uri_to_route_null() {
val actual = Nip19Parser.uriToRoute(null)
Assert.assertEquals(null, actual)
}
@Test()
fun uri_to_route_unknown() {
val actual = Nip19Parser.uriToRoute("nostr:unknown")
Assert.assertEquals(null, actual)
}
@Test()
fun uri_to_route_npub() {
val actual =
Nip19Parser.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y")
Assert.assertTrue(actual?.entity is NPub)
Assert.assertEquals(
"bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365",
(actual?.entity as? NPub)?.hex,
)
}
@Test()
fun uri_to_route_note() {
val result =
Nip19Parser.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv")?.entity as? NNote
assertNotNull(result)
Assert.assertEquals(
"82c19ee9db7644829b4420f40b72ce2d32e5b7962744b261d0d53e770432dac3",
result?.hex,
)
}
@Test()
fun uri_to_route_nprofile() {
val actual = Nip19Parser.uriToRoute("nostr:nprofile")
Assert.assertEquals(null, actual)
}
@Test()
fun uri_to_route_incomplete_nevent() {
val actual = Nip19Parser.uriToRoute("nostr:nevent")
Assert.assertEquals(null, actual)
}
@Test()
fun uri_to_route_incomplete_nrelay() {
val actual = Nip19Parser.uriToRoute("nostr:nrelay")
Assert.assertEquals(null, actual)
}
@Test()
fun uri_to_route_incomplete_naddr() {
val actual = Nip19Parser.uriToRoute("nostr:naddr")
Assert.assertEquals(null, actual)
}
@Test()
fun uri_to_route_complete_nprofile_2() {
val actual = Nip19Parser.uriToRoute("nostr:nprofile1qqsyvrp9u6p0mfur9dfdru3d853tx9mdjuhkphxuxgfwmryja7zsvhqpzamhxue69uhhv6t5daezumn0wd68yvfwvdhk6tcpz9mhxue69uhkummnw3ezuamfdejj7qgwwaehxw309ahx7uewd3hkctcscpyug")
Assert.assertNotNull(actual)
Assert.assertTrue(actual?.entity is NProfile)
Assert.assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", (actual?.entity as? NProfile)?.hex)
Assert.assertEquals(NormalizedRelayUrl("wss://vitor.nostr1.com/"), (actual?.entity as? NProfile)?.relay?.first())
}
@Test()
fun uri_to_route_complete_nprofile() {
val actual = Nip19Parser.uriToRoute("nostr:nprofile1qy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7qgwwaehxw309ahx7uewd3hkctcpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uqzq9thu3vem5gvsc6f3l3uyz7c92h6lq56t9wws0zulzkrgc6nrvym5jfztf")
Assert.assertTrue(actual?.entity is NProfile)
Assert.assertEquals("1577e4599dd10c863498fe3c20bd82aafaf829a595ce83c5cf8ac3463531b09b", (actual?.entity as? NProfile)?.hex)
}
@Test()
fun uri_to_route_complete_nevent() {
val actual = Nip19Parser.uriToRoute("nostr:nevent1qy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7qgwwaehxw309ahx7uewd3hkctcpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uq36amnwvaz7tmjv4kxz7fwvd5xjcmpvahhqmr9vfejucm0d5hsz9mhwden5te0wfjkccte9ec8y6tdv9kzumn9wshsz8thwden5te0dehhxarj9ekh2arfdeuhwctvd3jhgtnrdakj7qg3waehxw309ucngvpwvcmh5tnfduhszythwden5te0dehhxarj9emkjmn99uq3jamnwvaz7tmhv4kxxmmdv5hxummnw3ezuamfdejj7qpqvsup5xk3e2quedxjvn2gjppc0lqny5dmnr2ypc9tftwmdxta0yjqrd6n50")
Assert.assertTrue(actual?.entity is NEvent)
Assert.assertEquals("64381a1ad1ca81ccb4d264d48904387fc13251bb98d440e0ab4addb6997d7924", (actual?.entity as? NEvent)?.hex)
}
@Test()
fun uri_to_route_complete_naddr() {
val actual = Nip19Parser.uriToRoute("nostr:naddr1qqyxzmt9w358jum5qyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypd7v3r24z33cydnk3fmlrd0exe5dlej3506zxs05q4puerp765mzqcyqqq8scsq6mk7u")
Assert.assertTrue(actual?.entity is NAddress)
Assert.assertEquals("30818:5be6446aa8a31c11b3b453bf8dafc9b346ff328d1fa11a0fa02a1e6461f6a9b1:amethyst", (actual?.entity as? NAddress)?.aTag())
}
@Test
fun nAddrParser() {
val result =
Nip19Parser.uriToRoute(
"nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus",
)
assertEquals(
"30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:",
(result?.entity as? NAddress)?.aTag(),
)
}
@Test
fun nAddrParser2() {
val result =
Nip19Parser.uriToRoute(
"nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8",
)
assertEquals(
"30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard",
(result?.entity as? NAddress)?.aTag(),
)
}
@Test
fun nAddrParse3() {
val result =
Nip19Parser.uriToRoute(
"naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38",
)
assertTrue(result?.entity is NAddress)
assertEquals(
"30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920",
(result?.entity as? NAddress)?.aTag(),
)
assertEquals(NormalizedRelayUrl("wss://relay.damus.io/"), (result?.entity as? NAddress)?.relay?.get(0))
}
@Test
fun nAddrATagParse3() {
val address =
ATag.parse(
"30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920",
"relay.damus.io",
)
assertEquals(30023, address?.kind)
assertEquals(
"d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193",
address?.pubKeyHex,
)
assertEquals("89de7920", address?.dTag)
assertEquals("wss://relay.damus.io/", address?.relay?.url)
assertEquals(
"naddr1qqyrswtyv5mnjv3sqy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7q3q68nqgewzkamnyh53x0epqrftkv2pdh9gzr6558v4vetzr3w7uxfsxpqqqp65wmpxfdu",
address?.toNAddr(),
)
}
@Test
fun nAddrFormatter() {
val address =
ATag(
30023,
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
"",
null,
)
assertEquals(
"naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus",
address.toNAddr(),
)
}
@Test
fun nAddrFormatter2() {
val address =
ATag(
30023,
"d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c",
"guide-wireguard",
null,
)
assertEquals(
"naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8",
address.toNAddr(),
)
}
@Test
fun nAddrFormatter3() {
val address =
ATag(
30023,
"d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193",
"89de7920",
RelayUrlNormalizer.normalizeOrNull("wss://relay.damus.io")!!,
)
assertEquals(
"naddr1qqyrswtyv5mnjv3sqy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7q3q68nqgewzkamnyh53x0epqrftkv2pdh9gzr6558v4vetzr3w7uxfsxpqqqp65wmpxfdu",
address.toNAddr(),
)
}
@Test
fun nAddrParserPablo() {
val result =
Nip19Parser
.uriToRoute(
"naddr1qq2hs7p30p6kcunxxamkgcnyd33xxve3veshyq3qyujphdcz69z6jafxpnldae3xtymdekfeatkt3r4qusr3w5krqspqxpqqqpaxjlg805f",
)?.entity as? NAddress
assertNotNull(result)
assertEquals(
"31337:27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402:xx1xulrf7wdbdlbc31far",
result?.aTag(),
)
assertEquals(true, result?.relay?.isEmpty())
assertEquals("27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402", result?.author)
assertEquals(31337, result?.kind)
}
@Test
fun nAddrParserGizmo() {
val result =
Nip19Parser
.uriToRoute(
"naddr1qpqrvvfnvccrzdryxgunzvtxvgukge34xfjnqdpcv9sk2desxgmrscesvserzd3h8ycrywphvg6nsvf58ycnqef3v5mnsvt98pjnqdfs8ypzq3huhccxt6h34eupz3jeynjgjgek8lel2f4adaea0svyk94a3njdqvzqqqr4gudhrkyk",
)?.entity as? NAddress
assertNotNull(result)
assertEquals(
"30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:613f014d2911fb9df52e048aae70268c0d216790287b5814910e1e781e8e0509",
result?.aTag(),
)
assertEquals(true, result?.relay?.isEmpty())
assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
assertEquals(30023, result?.kind)
}
@Test
fun nAddrParserGizmo2() {
val result =
Nip19Parser
.uriToRoute(
"naddr1qq9rzd3h8y6nqwf5xyuqygzxljlrqe027xh8sy2xtyjwfzfrxcll8afxh4hh847psjckhkxwf5psgqqqw4rsty50fx",
)?.entity as? NAddress
assertNotNull(result)
assertEquals(
"30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418",
result?.aTag(),
)
assertEquals(true, result?.relay?.isEmpty())
assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
assertEquals(30023, result?.kind)
}
@Test
fun nEventParserCompleteTest() {
val result =
Nip19Parser.uriToRoute("nostr:nevent1qqsdw6xpk28tjnrajz4xhy2jqg0md8ywxj6997rsutjzxs0207tedjspz4mhxue69uhhyetvv9ujumn0wd68ytnzvuhsygx2crjrydvqdksffurc0fdsfc566pxtrg78afw0v8kursecwdqg9vpsgqqqqqqsnknas6")?.entity as? NEvent
assertNotNull(result)
assertEquals("d768c1b28eb94c7d90aa6b9152021fb69c8e34b452f870e2e42341ea7f9796ca", result?.hex)
assertEquals("wss://relay.nostr.bg/", result?.relay?.firstOrNull()?.url)
assertEquals("cac0e43235806da094f0787a5b04e29ad04cb1a3c7ea5cf61edc1c338734082b", result?.author)
assertEquals(1, result?.kind)
}
@Test
fun nEventParserTest() {
val result =
Nip19Parser.uriToRoute("nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy")?.entity as? NEvent
assertNotNull(result)
assertEquals("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", result?.hex)
assertEquals(true, result?.relay?.isEmpty())
assertEquals(null, result?.author)
assertEquals(null, result?.kind)
}
@Test
fun nEventParser2Test() {
val result =
Nip19Parser.uriToRoute("nostr:nevent1qqsfvaa2w3nkw472lt2ezr6x5x347k8hht398vp7hrl6wrdjldry86sprfmhxue69uhhyetvv9ujuam9wd6x2unwvf6xxtnrdaks5myyah")?.entity as? NEvent
assertNotNull(result)
assertEquals("9677aa74676757cafad5910f46a1a35f58f7bae253b03eb8ffa70db2fb4643ea", result?.hex)
assertEquals("wss://relay.westernbtc.com/", result?.relay?.firstOrNull()?.url)
assertEquals(null, result?.author)
assertEquals(null, result?.kind)
}
@Test
fun nEventParser() {
val result =
Nip19Parser
.uriToRoute(
"nostr:nevent1qqstvrl6wftd8ht4g0vrp6m30tjs6pdxcvk977g769dcvlptkzu4ftqppamhxue69uhkummnw3ezumt0d5pzp78lz8r60568sd2a8dx3wnj6gume02gxaf92vx4fk67qv5kpagt6qvzqqqqqqygqr86c",
)?.entity as? NEvent
assertNotNull(result)
assertEquals("b60ffa7256d3dd7543d830eb717ae50d05a6c32c5f791ed15b867c2bb0b954ac", result?.hex)
assertEquals(
NormalizedRelayUrl("wss://nostr.mom/"),
result?.relay?.get(0),
)
assertEquals("f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a", result?.author)
assertEquals(1, result?.kind)
}
@Test
fun nEventParser2() {
val result =
Nip19Parser
.uriToRoute(
"nostr:nevent1qqsplpuwsgrrmq85rfup6w3w777rxmcmadu590emfx6z4msj2844euqpz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqye3a70w",
)?.entity as? NEvent
assertNotNull(result)
assertEquals("1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0", result?.hex)
assertEquals(NormalizedRelayUrl("wss://relay.damus.io/"), result?.relay?.get(0))
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author)
assertEquals(1, result?.kind)
}
@Test
fun nEventParser3() {
val result =
Nip19Parser
.uriToRoute(
"nostr:nevent1qqsg6gechd3dhzx38n4z8a2lylzgsmmgeamhmtzz72m9ummsnf0xjfsppemhxue69uhkummn9ekx7mp0qy2hwumn8ghj7mn0wd68ytn00p68ytnyv4mz7qg4waehxw309aex2mrp0yhxummnw3ezucn89uqjqamnwvaz7tmwdaehgu3wv45kuatwv3a8wctw0f5kwtnnwpskxef0qythwumn8ghj7un9d3shjtnwdaehgu3wvfskuep0qy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7dyp4wy",
)?.entity as? NEvent
assertNotNull(result)
assertEquals("8d2338bb62db88d13cea23f55f27c4886f68cf777dac42f2b65e6f709a5e6926", result?.hex)
assertEquals(
"wss://nos.lol/,wss://nostr.oxtr.dev/,wss://relay.nostr.bg/,wss://nostr.einundzwanzig.space/,wss://relay.nostr.band/,wss://relay.damus.io/",
result?.relay?.joinToString(",") { it.url },
)
}
@Test
fun nEventParserInvalidChecksum() {
val result =
Nip19Parser
.uriToRoute(
"nostr:nevent1qqsyxq8v0730nz38dupnjzp5jegkyz4gu2ptwcps4v32hjnrap0q0espz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqyn3t9gj",
)?.entity as? NEvent
assertNotNull(result)
assertEquals("4300ec7fa2f98a276f033908349651620aa8e282b76030ab22abca63e85e07e6", result?.hex)
assertEquals("wss://relay.damus.io/", result?.relay?.get(0)?.url)
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author)
assertEquals(1, result?.kind)
}
@Test
fun nEventFormatter() {
val nevent =
NEvent.create(
"f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc",
null,
null,
null,
)
assertEquals("nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy", nevent)
}
@Test
fun nEventFormatterWithExtraInfo() {
val nevent =
NEvent.create(
"f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc",
"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194",
40,
null,
)
assertEquals(
"nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhqzypl62m6ad932k83u6sjwwkxrqq4cve0hkrvdem5la83g34m4rtqegqcyqqqqq2qh26va4",
nevent,
)
}
@Test
fun nEventFormatterWithFullInfo() {
val nevent =
NEvent.create(
"1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
1,
RelayUrlNormalizer.normalizeOrNull("wss://relay.damus.io"),
)
assertEquals(
"nevent1qqsplpuwsgrrmq85rfup6w3w777rxmcmadu590emfx6z4msj2844euqpz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqqqqsqvc0ku",
nevent,
)
}
@Test
fun decodeBech32WithInvisibleCharacter() {
val bomChar = '\uFEFF'
val withBom = bomChar + "nsec1lfkarc7439n4l3uahr45ej8mrjc39dd879t0ps355550dj8j9uzs3rnw24"
assertEquals(
"nsec1lfkarc7439n4l3uahr45ej8mrjc39dd879t0ps355550dj8j9uzs3rnw24",
withBom.bechToBytes().toNsec(),
)
}
}
@@ -1,59 +0,0 @@
/**
* 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.nip19Bech32
import com.vitorpamplona.quartz.nip19Bech32.tlv.to32BitByteArray
import com.vitorpamplona.quartz.nip19Bech32.tlv.toInt32
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
class TlvIntegerTest {
fun to_int_32_length_smaller_than_4() {
Assert.assertNull(byteArrayOfInts(1, 2, 3).toInt32())
}
fun to_int_32_length_bigger_than_4() {
Assert.assertNull(byteArrayOfInts(1, 2, 3, 4, 5).toInt32())
}
@Test()
fun to_int_32_length_4() {
val actual = byteArrayOfInts(1, 2, 3, 4).toInt32()
assertEquals(16909060, actual)
}
@Test()
fun backAndForth() {
assertEquals(234, 234.to32BitByteArray().toInt32())
assertEquals(1, 1.to32BitByteArray().toInt32())
assertEquals(0, 0.to32BitByteArray().toInt32())
assertEquals(1000, 1000.to32BitByteArray().toInt32())
assertEquals(-234, (-234).to32BitByteArray().toInt32())
assertEquals(-1, (-1).to32BitByteArray().toInt32())
assertEquals(-0, (-0).to32BitByteArray().toInt32())
assertEquals(-1000, (-1000).to32BitByteArray().toInt32())
}
private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
}
@@ -1,168 +0,0 @@
/**
* 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.nip30CustomEmoji
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
import org.junit.Test
class Nip30Test {
@Test()
fun parseEmoji() {
val tags = mapOf(":soapbox:" to "http://soapbox")
val input = "Alex Gleason :soapbox:"
val result = CustomEmoji.assembleAnnotatedList(input, tags)
assertEquals(2, result!!.size)
assertEquals(
"Alex Gleason ",
(result[0] as CustomEmoji.TextType).text,
)
assertEquals(
"http://soapbox",
(result[1] as CustomEmoji.ImageUrlType).url,
)
}
@Test()
fun parseEmojiInverted() {
val tags = mapOf(":soapbox:" to "http://soapbox")
val input = ":soapbox:Alex Gleason"
val result = CustomEmoji.assembleAnnotatedList(input, tags)
assertEquals(2, result!!.size)
assertEquals(
"http://soapbox",
(result[0] as CustomEmoji.ImageUrlType).url,
)
assertEquals(
"Alex Gleason",
(result[1] as CustomEmoji.TextType).text,
)
}
@Test()
fun parseEmoji2() {
val tags =
mapOf(
":gleasonator:" to "http://gleasonator",
":ablobcatrainbow:" to "http://ablobcatrainbow",
":disputed:" to "http://disputed",
)
val input = "Hello :gleasonator: \uD83D\uDE02 :ablobcatrainbow: :disputed: yolo"
val result = CustomEmoji.assembleAnnotatedList(input, tags)
assertEquals(7, result!!.size)
assertEquals("Hello ", (result[0] as CustomEmoji.TextType).text)
assertEquals("http://gleasonator", (result[1] as CustomEmoji.ImageUrlType).url)
assertEquals(" 😂 ", (result[2] as CustomEmoji.TextType).text)
assertEquals("http://ablobcatrainbow", (result[3] as CustomEmoji.ImageUrlType).url)
assertEquals(" ", (result[4] as CustomEmoji.TextType).text)
assertEquals("http://disputed", (result[5] as CustomEmoji.ImageUrlType).url)
assertEquals(" yolo", (result[6] as CustomEmoji.TextType).text)
}
@Test()
fun parseEmoji3() {
val tags = emptyMap<String, String>()
val input = "hello vitor: how can I help:"
val result = CustomEmoji.assembleAnnotatedList(input, tags)
assertNull(result)
}
@Test()
fun parseEmoji4() {
val tags = mapOf(":vitor:" to "http://vitor")
val input = "hello :vitor: how :can I help:"
val result = CustomEmoji.assembleAnnotatedList(input, tags)
assertEquals(3, result!!.size)
assertEquals("hello ", (result[0] as CustomEmoji.TextType).text)
assertEquals("http://vitor", (result[1] as CustomEmoji.ImageUrlType).url)
assertEquals(" how :can I help:", (result[2] as CustomEmoji.TextType).text)
}
@Test()
fun parseJapanese() {
val tags = mapOf(":x30EDE:" to "http://x30EDE", ":\uD883\uDEDE:" to "http://\uD883\uDEDE")
val input = "\uD883\uDEDE\uD883\uDEDE麺の:x30EDE:。:\uD883\uDEDE:(Violation of NIP-30)"
val result = CustomEmoji.assembleAnnotatedList(input, tags)
assertEquals(3, result!!.size)
assertEquals("\uD883\uDEDE\uD883\uDEDE麺の", (result[0] as CustomEmoji.TextType).text)
assertEquals("http://x30EDE", (result[1] as CustomEmoji.ImageUrlType).url)
assertEquals("。:\uD883\uDEDE:(Violation of NIP-30)", (result[2] as CustomEmoji.TextType).text)
}
@Test()
fun parseJapanese2() {
val tags =
arrayOf(
arrayOf("t", "ioメシヨソイゲーム"),
arrayOf("emoji", "_ri", "https://media.misskeyusercontent.com/emoji/_ri.png"),
arrayOf("emoji", "petthex_japanesecake", "https://media.misskeyusercontent.com/emoji/petthex_japanesecake.gif"),
arrayOf("emoji", "ai_nomming", "https://media.misskeyusercontent.com/misskey/f6294900-f678-43cc-bc36-3ee5deeca4c2.gif"),
arrayOf("proxy", "https://misskey.io/notes/9q0x6gtdysir03qh", "activitypub"),
)
val input =
"\u200B:_ri:\u200B\u200B:_ri:\u200Bはベイクドモチョチョ\u200B:petthex_japanesecake:\u200Bを食べました\u200B:ai_nomming:\u200B\n" +
"#ioメシヨソイゲーム\n" +
"https://misskey.io/play/9g3qza4jow"
val result = CustomEmoji.assembleAnnotatedList(input, ImmutableListOfLists(tags))
assertEquals(9, result!!.size)
var i = 0
assertEquals("\u200B", (result[i++] as CustomEmoji.TextType).text)
assertEquals("https://media.misskeyusercontent.com/emoji/_ri.png", (result[i++] as CustomEmoji.ImageUrlType).url)
assertEquals("\u200B\u200B", (result[i++] as CustomEmoji.TextType).text)
assertEquals("https://media.misskeyusercontent.com/emoji/_ri.png", (result[i++] as CustomEmoji.ImageUrlType).url)
assertEquals("\u200Bはベイクドモチョチョ\u200B", (result[i++] as CustomEmoji.TextType).text)
assertEquals("https://media.misskeyusercontent.com/emoji/petthex_japanesecake.gif", (result[i++] as CustomEmoji.ImageUrlType).url)
assertEquals("\u200Bを食べました\u200B", (result[i++] as CustomEmoji.TextType).text)
assertEquals("https://media.misskeyusercontent.com/misskey/f6294900-f678-43cc-bc36-3ee5deeca4c2.gif", (result[i++] as CustomEmoji.ImageUrlType).url)
assertEquals("\u200B\n#ioメシヨソイゲーム\nhttps://misskey.io/play/9g3qza4jow", (result[i] as CustomEmoji.TextType).text)
}
}
@@ -1,103 +0,0 @@
/**
* 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.nip44Encryption.crypto
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import org.junit.Test
import kotlin.test.assertEquals
class HkdfText {
companion object {
val hkdf = Hkdf()
}
@Test
fun testExpand1() {
val result =
hkdf.expand(
"b0a6232d3e5444dbf2291f3290504989716f1dc78ac2da812230720fd76c8f06".hexToByteArray(),
"c9300041754c97f030ec9c35db607cb642db0f4305f178692954f3a883ea3209".hexToByteArray(),
76,
)
assertEquals("9077071b92dfbd725f5a1dd2dbbf349615f1d0ce22b436e2f505cc05984ef588", result.hmacKey.toHexKey())
assertEquals("3256a46b762158a6e08338daf2c5b3eefd2bed343cb22df8b6315567b9207cef", result.chachaKey.toHexKey())
assertEquals("670bb239ac07dbb801653aa5", result.chachaNonce.toHexKey())
val result2 =
hkdf.fastExpand(
"b0a6232d3e5444dbf2291f3290504989716f1dc78ac2da812230720fd76c8f06".hexToByteArray(),
"c9300041754c97f030ec9c35db607cb642db0f4305f178692954f3a883ea3209".hexToByteArray(),
)
assertEquals("9077071b92dfbd725f5a1dd2dbbf349615f1d0ce22b436e2f505cc05984ef588", result2.hmacKey.toHexKey())
assertEquals("3256a46b762158a6e08338daf2c5b3eefd2bed343cb22df8b6315567b9207cef", result2.chachaKey.toHexKey())
assertEquals("670bb239ac07dbb801653aa5", result2.chachaNonce.toHexKey())
}
@Test
fun testExpand2() {
val result =
hkdf.expand(
"bc5d4e032696ef107ef1c7b6fc5f00c6e7b31ae4f86ee486ce24aa0d84847d83".hexToByteArray(),
"830426c471c0870afb0208cabcaa0e4d66fe51af7163336b7b9ec1846c31e900".hexToByteArray(),
76,
)
assertEquals("26ccd9471681fc42459dbf7b14fc54d3f5276ddad482f7c9f81dc021ccbe5592", result.hmacKey.toHexKey())
assertEquals("a13a519db107cf9ecbca6ba79ab2428fcd624286025b7ee452f153ae770f07a8", result.chachaKey.toHexKey())
assertEquals("4c8218640c43de928f4c52e1", result.chachaNonce.toHexKey())
val result2 =
hkdf.fastExpand(
"bc5d4e032696ef107ef1c7b6fc5f00c6e7b31ae4f86ee486ce24aa0d84847d83".hexToByteArray(),
"830426c471c0870afb0208cabcaa0e4d66fe51af7163336b7b9ec1846c31e900".hexToByteArray(),
)
assertEquals("26ccd9471681fc42459dbf7b14fc54d3f5276ddad482f7c9f81dc021ccbe5592", result2.hmacKey.toHexKey())
assertEquals("a13a519db107cf9ecbca6ba79ab2428fcd624286025b7ee452f153ae770f07a8", result2.chachaKey.toHexKey())
assertEquals("4c8218640c43de928f4c52e1", result2.chachaNonce.toHexKey())
}
@Test
fun testExpand3() {
val result =
hkdf.expand(
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
76,
)
assertEquals("a30b80c908d01aa28eae9dcdec55ac104f9c889b989ee563919985a84f66360d", result.hmacKey.toHexKey())
assertEquals("3769af12ff4dbf44e516a22d1d0512e8bc42516d59e8bf401ea346a4d60dccf7", result.chachaKey.toHexKey())
assertEquals("77938d29bb13ea73f677ac27", result.chachaNonce.toHexKey())
val result2 =
hkdf.fastExpand(
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
)
assertEquals("a30b80c908d01aa28eae9dcdec55ac104f9c889b989ee563919985a84f66360d", result2.hmacKey.toHexKey())
assertEquals("3769af12ff4dbf44e516a22d1d0512e8bc42516d59e8bf401ea346a4d60dccf7", result2.chachaKey.toHexKey())
assertEquals("77938d29bb13ea73f677ac27", result2.chachaNonce.toHexKey())
}
}
@@ -1,35 +0,0 @@
/**
* 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.jackson.JsonMapper
import org.junit.Test
class BunkerRequestTest {
@Test
fun testBunkerRequestDeSerialization() {
val requestJson = """{"id":"123","method":"sign_event","params":["{\"created_at\":1234,\"kind\":1,\"tags\":[],\"content\":\"This is an unsigned event.\"}"]}"""
val bunkerRequest = JsonMapper.mapper.readValue(requestJson, BunkerRequest::class.java)
assert(bunkerRequest is BunkerRequestSign)
assert((bunkerRequest as BunkerRequestSign).event.kind == 1)
}
}
@@ -1,59 +0,0 @@
/**
* 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.nip51Lists
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import junit.framework.TestCase.assertTrue
import org.junit.Test
import java.util.Arrays
class TagArrayExt {
val tags =
arrayOf(
arrayOf("group", "test", "wss://nos.lol"),
arrayOf("group", "_", "wss://nos.lol"),
)
val expectedTags =
arrayOf(
arrayOf("group", "test", "wss://nos.lol"),
)
@Test
fun testRemove() {
assertTrue(
Arrays.deepEquals(expectedTags, tags.remove(arrayOf("group", "_", "wss://nos.lol"))),
)
}
@Test
fun testRemoveParsing() {
val removing = RoomId("_", RelayUrlNormalizer.normalize("wss://nos.lol"))
assertTrue(
Arrays.deepEquals(
expectedTags,
tags.removeParsing(RoomIdTag::parse, removing),
),
)
}
}
@@ -1,94 +0,0 @@
/**
* 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.nip65RelayList
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import org.junit.Test
class RelayListRecommendationProcessorTest {
fun norm(str: String) = RelayUrlNormalizer.normalizeOrNull(str)!!
val userList =
mutableMapOf(
"User1" to mutableSetOf(norm("wss://relay1.com"), norm("wss://relay2.com"), norm("wss://relay3.com")),
"User2" to mutableSetOf(norm("wss://relay4.com"), norm("wss://relay5.com"), norm("wss://relay6.com")),
"User3" to mutableSetOf(norm("wss://relay1.com"), norm("wss://relay4.com"), norm("wss://relay6.com")),
"User4" to mutableSetOf(norm("wss://relay2.com"), norm("wss://relay1.com"), norm("wss://relay4.com")),
)
@Test
fun testTranspose() {
assertEquals(
mapOf(
norm("wss://relay1.com") to listOf("User1", "User3", "User4"),
norm("wss://relay2.com") to listOf("User1", "User4"),
norm("wss://relay3.com") to listOf("User1"),
norm("wss://relay4.com") to listOf("User2", "User3", "User4"),
norm("wss://relay5.com") to listOf("User2"),
norm("wss://relay6.com") to listOf("User2", "User3"),
).toString(),
RelayListRecommendationProcessor.transpose(userList).toString(),
)
}
@Test
fun testProcessor() {
val recommendations = RelayListRecommendationProcessor.reliableRelaySetFor(userList).toList()
val rec1 = recommendations[0]
assertEquals("wss://relay1.com/", rec1.relay.url)
assertEquals(true, rec1.requiredToNotMissEvents)
assertTrue("User1" in rec1.users)
assertTrue("User2" !in rec1.users)
assertTrue("User3" in rec1.users)
assertTrue("User4" in rec1.users)
val rec2 = recommendations[1]
assertEquals("wss://relay4.com/", rec2.relay.url)
assertEquals(true, rec2.requiredToNotMissEvents)
assertTrue("User1" !in rec2.users)
assertTrue("User2" in rec2.users)
assertTrue("User3" !in rec2.users) // already included in relay 1
assertTrue("User4" !in rec2.users) // already included in relay 1
val rec3 = recommendations[2]
assertEquals("wss://relay2.com/", rec3.relay.url)
assertEquals(false, rec3.requiredToNotMissEvents)
assertTrue("User1" in rec3.users)
assertTrue("User2" !in rec3.users)
assertTrue("User3" !in rec3.users)
assertTrue("User4" !in rec3.users) // already included in relay 1 and 2
val rec4 = recommendations[3]
assertEquals("wss://relay5.com/", rec4.relay.url)
assertEquals(false, rec4.requiredToNotMissEvents)
assertTrue("User1" !in rec4.users)
assertTrue("User2" in rec4.users)
assertTrue("User3" !in rec4.users)
assertTrue("User4" !in rec4.users)
}
}
@@ -1,176 +0,0 @@
/**
* 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.nip96FileStorage
import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfoParser
import junit.framework.TestCase.assertEquals
import org.junit.Test
class Nip96Test {
val relativeUrlTest =
"""
{
"api_url": "/n96",
"download_url": "/",
"content_types": [
"image/*",
"video/*",
"audio/*"
],
"plans": {
"free": {
"name": "",
"is_nip98_required": true,
"max_byte_size": 5000000000
}
}
}
"""
val json =
"""
{
"api_url": "https://nostr.build/api/v2/nip96/upload",
"download_url": "https://media.nostr.build",
"supported_nips": [
94,
96,
98
],
"tos_url": "https://nostr.build/tos/",
"content_types": [
"image/*",
"video/*",
"audio/*"
],
"plans": {
"free": {
"name": "Free",
"is_nip98_required": true,
"url": "https://nostr.build",
"max_byte_size": 26214400,
"file_expiration": [
0,
0
],
"media_transformations": {
"image": [
"resizing",
"format_conversion",
"compression",
"metadata_stripping"
],
"video": [
"resizing",
"format_conversion",
"compression"
]
}
},
"professional": {
"name": "Professional",
"is_nip98_required": true,
"url": "https://nostr.build/signup/new/",
"max_byte_size": 10737418240,
"file_expiration": [
0,
0
],
"media_transformations": {
"image": [
"resizing",
"format_conversion",
"compression",
"metadata_stripping"
],
"video": [
"resizing",
"format_conversion",
"compression"
]
}
},
"creator": {
"name": "Creator",
"is_nip98_required": true,
"url": "https://nostr.build/signup/new/",
"max_byte_size": 26843545600,
"file_expiration": [
0,
0
],
"media_transformations": {
"image": [
"resizing",
"format_conversion",
"compression",
"metadata_stripping"
],
"video": [
"resizing",
"format_conversion",
"compression"
]
}
}
}
}
""".trimIndent()
@Test()
fun parseNostrBuild() {
val info = ServerInfoParser().parse("https://nostr.build", json)
assertEquals("https://nostr.build/api/v2/nip96/upload", info.apiUrl)
assertEquals("https://media.nostr.build", info.downloadUrl)
assertEquals(listOf(94, 96, 98), info.supportedNips)
assertEquals("https://nostr.build/tos/", info.tosUrl)
assertEquals(listOf("image/*", "video/*", "audio/*"), info.contentTypes)
assertEquals(listOf("creator", "free", "professional"), info.plans.keys.sorted())
assertEquals("Free", info.plans["free"]?.name)
assertEquals(true, info.plans["free"]?.isNip98Required)
assertEquals("https://nostr.build", info.plans["free"]?.url)
assertEquals(26214400L, info.plans["free"]?.maxByteSize)
assertEquals(listOf(0, 0), info.plans["free"]?.fileExpiration)
assertEquals(
listOf("image", "video"),
info.plans["free"]
?.mediaTransformations
?.keys
?.sorted(),
)
assertEquals(26843545600L, info.plans["creator"]?.maxByteSize)
assertEquals(10737418240L, info.plans["professional"]?.maxByteSize)
}
@Test()
fun parseRelativeUrls() {
val info = ServerInfoParser().parse("https://test.com", relativeUrlTest)
assertEquals("https://test.com/n96", info.apiUrl)
assertEquals("https://test.com/", info.downloadUrl)
assertEquals(null, info.tosUrl)
assertEquals(null, info.delegatedToUrl)
}
}
@@ -1,80 +0,0 @@
/**
* 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.utils
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.random.Random
class HexEncodingTest {
val testHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf"
@Test
fun testHexEncodeDecodeOurs() {
assertEquals(
testHex,
Hex.encode(
Hex.decode(testHex),
),
)
}
@Test
fun testIsHex() {
assertFalse("/0", Hex.isHex("/0"))
assertFalse("/.", Hex.isHex("/."))
assertFalse("::", Hex.isHex("::"))
assertFalse("!!", Hex.isHex("!!"))
assertFalse("@@", Hex.isHex("@@"))
assertFalse("GG", Hex.isHex("GG"))
assertFalse("FG", Hex.isHex("FG"))
assertFalse("`a", Hex.isHex("`a"))
assertFalse("gg", Hex.isHex("gg"))
assertFalse("fg", Hex.isHex("fg"))
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testRandomsIsHex() {
for (i in 0..10000) {
val bytes = Random.nextBytes(32)
val hex = bytes.toHexString(HexFormat.Default)
assertTrue(hex, Hex.isHex(hex))
val hexUpper = bytes.toHexString(HexFormat.UpperCase)
assertTrue(hexUpper, Hex.isHex(hexUpper))
}
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testRandomsUppercase() {
for (i in 0..1000) {
val bytes = Random.nextBytes(32)
val hex = bytes.toHexString(HexFormat.UpperCase)
assertEquals(
bytes.toList(),
Hex.decode(hex).toList(),
)
}
}
}
@@ -20,8 +20,10 @@
*/
package com.vitorpamplona.quartz.utils
import kotlin.math.round
fun Runtime.usedMemoryMb(): Long {
val totalMemoryMb = totalMemory() / (1024 * 1024)
val freeMemoryMb = freeMemory() / (1024 * 1024)
val totalMemoryMb = round(totalMemory() / (1024 * 1024.0)).toLong()
val freeMemoryMb = round(freeMemory() / (1024 * 1024.0)).toLong()
return totalMemoryMb - freeMemoryMb
}
@@ -1,62 +0,0 @@
/**
* 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.utils
import junit.framework.TestCase
import org.junit.Test
class StringUtilsTest {
private val test =
"""Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
""".intern()
val atTheMiddle = DualCase("Lorem Ipsum".lowercase(), "Lorem Ipsum".uppercase())
val atTheBeginning = DualCase("contrAry".lowercase(), "contrAry".uppercase())
val atTheEndCase = DualCase("h. rackham".lowercase(), "h. rackham".uppercase())
val lastCase =
listOf(
DualCase("my mom".lowercase(), "my mom".uppercase()),
DualCase("my dad".lowercase(), "my dad".uppercase()),
DualCase("h. rackham".lowercase(), "h. rackham".uppercase()),
)
@Test
fun middleCaseOurs() {
val list = listOf(atTheMiddle)
TestCase.assertTrue(test.containsAny(list))
}
@Test
fun atTheBeginningOurs() {
val list = listOf(atTheBeginning)
TestCase.assertTrue(test.containsAny(list))
}
@Test
fun atTheEndOurs() {
val list = listOf(atTheEndCase)
TestCase.assertTrue(test.containsAny(list))
}
}