refactor(geode): test fixtures + collectUntilEose helper, move to testFixtures sourceSet
The in-process relay was usable but every consumer test paid a 10–15
line tax for scope/client setup, manual cleanup inside the test body
(leaks on assertion failure), hardcoded "ws://127.0.0.1:7770/" magic
strings, and an inline collectUntilEose pattern reinvented per file.
Net: ~250 LOC removed, every test gets correct @After cleanup, and
the synthetic event builders no longer ship in geode's production jar.
- Move geode.fixtures (synthetic event builders) and the new
geode.testing package from src/main to a new src/testFixtures
source set via the java-test-fixtures plugin. Production geode jar
no longer includes them; consumers wire them with
testImplementation(testFixtures(project(":geode"))).
- Add geode.testing.RelayClientTest open class — owns hub, scope,
client, defaultRelay, defaultRelayUrl with @After-driven cleanup.
- Add geode.testing.collectUntilEose / collectUntilEoseMulti
NostrClient extensions with a shared subId counter and timeout knob.
Replaces hand-rolled subscriber loops in 4+ files.
- Add RelayHub.DEFAULT_URL constant so tests stop typing
"ws://127.0.0.1:7770/" inline.
- Migrate the 8 quartz NostrClient*Test files + geode's
Nip01ComplianceTest to extend RelayClientTest and (where REQ/EOSE
is the pattern) call collectUntilEose. Delete the redundant
BaseNostrClientTest wrapper.
This commit is contained in:
@@ -4,6 +4,7 @@ plugins {
|
|||||||
alias(libs.plugins.jetbrainsKotlinJvm)
|
alias(libs.plugins.jetbrainsKotlinJvm)
|
||||||
alias(libs.plugins.serialization)
|
alias(libs.plugins.serialization)
|
||||||
application
|
application
|
||||||
|
`java-test-fixtures`
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -25,6 +26,15 @@ sourceSets {
|
|||||||
test {
|
test {
|
||||||
kotlin.srcDir("src/test/kotlin")
|
kotlin.srcDir("src/test/kotlin")
|
||||||
}
|
}
|
||||||
|
// The `java-test-fixtures` plugin auto-creates a `testFixtures`
|
||||||
|
// source set; we just point it at our Kotlin layout so the
|
||||||
|
// `geode.fixtures` (synthetic events) and `geode.testing`
|
||||||
|
// (RelayClientTest, collectUntilEose) packages don't ship in
|
||||||
|
// production jars but are still usable by every consumer's test
|
||||||
|
// source via `testImplementation(testFixtures(project(":geode")))`.
|
||||||
|
named("testFixtures") {
|
||||||
|
kotlin.srcDir("src/testFixtures/kotlin")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test>().configureEach {
|
tasks.withType<Test>().configureEach {
|
||||||
@@ -63,6 +73,13 @@ dependencies {
|
|||||||
// port their configs nearly verbatim.
|
// port their configs nearly verbatim.
|
||||||
implementation(libs.fourkoma)
|
implementation(libs.fourkoma)
|
||||||
|
|
||||||
|
// testFixtures: code in src/testFixtures/kotlin (RelayClientTest +
|
||||||
|
// synthetic event builders). Not shipped in the production jar but
|
||||||
|
// exposed to consumers via testImplementation(testFixtures(...)).
|
||||||
|
testFixturesApi(project(":quartz"))
|
||||||
|
testFixturesApi(libs.junit)
|
||||||
|
testFixturesImplementation(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
testImplementation(libs.kotlin.test)
|
testImplementation(libs.kotlin.test)
|
||||||
testImplementation(libs.kotlinx.coroutines.test)
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
testImplementation(libs.secp256k1.kmp.jni.jvm)
|
testImplementation(libs.secp256k1.kmp.jni.jvm)
|
||||||
|
|||||||
@@ -86,4 +86,16 @@ class RelayHub(
|
|||||||
relays.values.forEach { runCatching { it.close() } }
|
relays.values.forEach { runCatching { it.close() } }
|
||||||
relays.clear()
|
relays.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Default URL for tests that only need one relay. The URL itself
|
||||||
|
* has no semantic meaning — it's just a stable key into the hub
|
||||||
|
* — but it normalises through [RelayUrlNormalizer] (loopback) so
|
||||||
|
* the production [com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer]
|
||||||
|
* accepts it. Prefer this over typing `"ws://127.0.0.1:7770/"`
|
||||||
|
* everywhere.
|
||||||
|
*/
|
||||||
|
val DEFAULT_URL: NormalizedRelayUrl = RelayUrlNormalizer.normalize("ws://127.0.0.1:7770/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,26 +21,22 @@
|
|||||||
package com.vitorpamplona.geode
|
package com.vitorpamplona.geode
|
||||||
|
|
||||||
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
||||||
|
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||||
|
import com.vitorpamplona.geode.testing.collectUntilEose
|
||||||
|
import com.vitorpamplona.geode.testing.collectUntilEoseMulti
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import kotlin.test.AfterTest
|
|
||||||
import kotlin.test.BeforeTest
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNull
|
import kotlin.test.assertNull
|
||||||
@@ -62,29 +58,9 @@ import kotlin.test.assertTrue
|
|||||||
* spec-compliant relay (nostr-rs-relay, strfry, khatru, …) — only the
|
* spec-compliant relay (nostr-rs-relay, strfry, khatru, …) — only the
|
||||||
* `socketBuilder` and the relay URL would change.
|
* `socketBuilder` and the relay URL would change.
|
||||||
*/
|
*/
|
||||||
class Nip01ComplianceTest {
|
class Nip01ComplianceTest : RelayClientTest() {
|
||||||
private lateinit var hub: RelayHub
|
|
||||||
private lateinit var scope: CoroutineScope
|
|
||||||
private lateinit var client: NostrClient
|
|
||||||
|
|
||||||
private val relayUrl: NormalizedRelayUrl = RelayUrlNormalizer.normalize("ws://127.0.0.1:7770/")
|
|
||||||
|
|
||||||
@BeforeTest
|
|
||||||
fun setup() {
|
|
||||||
hub = RelayHub()
|
|
||||||
scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
client = NostrClient(hub, scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterTest
|
|
||||||
fun teardown() {
|
|
||||||
client.disconnect()
|
|
||||||
scope.cancel()
|
|
||||||
hub.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun preload(vararg events: Event) {
|
private suspend fun preload(vararg events: Event) {
|
||||||
hub.getOrCreate(relayUrl).preload(*events)
|
defaultRelay.preload(*events)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fakeEvent(
|
private fun fakeEvent(
|
||||||
@@ -108,7 +84,7 @@ class Nip01ComplianceTest {
|
|||||||
fakeEvent(3, kind = 1),
|
fakeEvent(3, kind = 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
val (events, eose) = collectUntilEose(Filter(kinds = listOf(1)))
|
val (events, eose) = client.collectUntilEose(defaultRelayUrl, Filter(kinds = listOf(1)))
|
||||||
|
|
||||||
assertEquals(2, events.size)
|
assertEquals(2, events.size)
|
||||||
assertEquals(setOf(SyntheticEvents.hexId(1), SyntheticEvents.hexId(3)), events.map { it.id }.toSet())
|
assertEquals(setOf(SyntheticEvents.hexId(1), SyntheticEvents.hexId(3)), events.map { it.id }.toSet())
|
||||||
@@ -126,7 +102,7 @@ class Nip01ComplianceTest {
|
|||||||
fakeEvent(4, kind = 1, createdAt = 400),
|
fakeEvent(4, kind = 1, createdAt = 400),
|
||||||
)
|
)
|
||||||
|
|
||||||
val (events, _) = collectUntilEose(Filter(kinds = listOf(1), limit = 2))
|
val (events, _) = client.collectUntilEose(defaultRelayUrl, Filter(kinds = listOf(1), limit = 2))
|
||||||
|
|
||||||
assertEquals(2, events.size)
|
assertEquals(2, events.size)
|
||||||
assertEquals(400L, events[0].createdAt)
|
assertEquals(400L, events[0].createdAt)
|
||||||
@@ -145,7 +121,7 @@ class Nip01ComplianceTest {
|
|||||||
fakeEvent(3, kind = 1, pubKey = alice),
|
fakeEvent(3, kind = 1, pubKey = alice),
|
||||||
)
|
)
|
||||||
|
|
||||||
val (events, _) = collectUntilEose(Filter(authors = listOf(alice)))
|
val (events, _) = client.collectUntilEose(defaultRelayUrl, Filter(authors = listOf(alice)))
|
||||||
|
|
||||||
assertEquals(2, events.size)
|
assertEquals(2, events.size)
|
||||||
assertTrue(events.all { it.pubKey == alice })
|
assertTrue(events.all { it.pubKey == alice })
|
||||||
@@ -157,7 +133,7 @@ class Nip01ComplianceTest {
|
|||||||
runBlocking {
|
runBlocking {
|
||||||
preload(fakeEvent(1), fakeEvent(2), fakeEvent(3))
|
preload(fakeEvent(1), fakeEvent(2), fakeEvent(3))
|
||||||
|
|
||||||
val (events, _) = collectUntilEose(Filter(ids = listOf(SyntheticEvents.hexId(2))))
|
val (events, _) = client.collectUntilEose(defaultRelayUrl, Filter(ids = listOf(SyntheticEvents.hexId(2))))
|
||||||
|
|
||||||
assertEquals(1, events.size)
|
assertEquals(1, events.size)
|
||||||
assertEquals(SyntheticEvents.hexId(2), events[0].id)
|
assertEquals(SyntheticEvents.hexId(2), events[0].id)
|
||||||
@@ -174,7 +150,7 @@ class Nip01ComplianceTest {
|
|||||||
fakeEvent(4, createdAt = 400),
|
fakeEvent(4, createdAt = 400),
|
||||||
)
|
)
|
||||||
|
|
||||||
val (events, _) = collectUntilEose(Filter(since = 150L, until = 350L))
|
val (events, _) = client.collectUntilEose(defaultRelayUrl, Filter(since = 150L, until = 350L))
|
||||||
|
|
||||||
assertEquals(setOf(200L, 300L), events.map { it.createdAt }.toSet())
|
assertEquals(setOf(200L, 300L), events.map { it.createdAt }.toSet())
|
||||||
}
|
}
|
||||||
@@ -190,7 +166,7 @@ class Nip01ComplianceTest {
|
|||||||
fakeEvent(3, tags = arrayOf(arrayOf("e", target), arrayOf("p", SyntheticEvents.hexId(8)))),
|
fakeEvent(3, tags = arrayOf(arrayOf("e", target), arrayOf("p", SyntheticEvents.hexId(8)))),
|
||||||
)
|
)
|
||||||
|
|
||||||
val (events, _) = collectUntilEose(Filter(tags = mapOf("e" to listOf(target))))
|
val (events, _) = client.collectUntilEose(defaultRelayUrl, Filter(tags = mapOf("e" to listOf(target))))
|
||||||
|
|
||||||
assertEquals(setOf(SyntheticEvents.hexId(1), SyntheticEvents.hexId(3)), events.map { it.id }.toSet())
|
assertEquals(setOf(SyntheticEvents.hexId(1), SyntheticEvents.hexId(3)), events.map { it.id }.toSet())
|
||||||
}
|
}
|
||||||
@@ -206,7 +182,7 @@ class Nip01ComplianceTest {
|
|||||||
fakeEvent(3, tags = arrayOf(arrayOf("p", targetPubkey), arrayOf("e", SyntheticEvents.hexId(99)))),
|
fakeEvent(3, tags = arrayOf(arrayOf("p", targetPubkey), arrayOf("e", SyntheticEvents.hexId(99)))),
|
||||||
)
|
)
|
||||||
|
|
||||||
val (events, _) = collectUntilEose(Filter(tags = mapOf("p" to listOf(targetPubkey))))
|
val (events, _) = client.collectUntilEose(defaultRelayUrl, Filter(tags = mapOf("p" to listOf(targetPubkey))))
|
||||||
|
|
||||||
assertEquals(setOf(SyntheticEvents.hexId(1), SyntheticEvents.hexId(3)), events.map { it.id }.toSet())
|
assertEquals(setOf(SyntheticEvents.hexId(1), SyntheticEvents.hexId(3)), events.map { it.id }.toSet())
|
||||||
}
|
}
|
||||||
@@ -221,7 +197,7 @@ class Nip01ComplianceTest {
|
|||||||
fakeEvent(3, tags = arrayOf(arrayOf("t", "nostr"), arrayOf("t", "kotlin"))),
|
fakeEvent(3, tags = arrayOf(arrayOf("t", "nostr"), arrayOf("t", "kotlin"))),
|
||||||
)
|
)
|
||||||
|
|
||||||
val (events, _) = collectUntilEose(Filter(tags = mapOf("t" to listOf("nostr"))))
|
val (events, _) = client.collectUntilEose(defaultRelayUrl, Filter(tags = mapOf("t" to listOf("nostr"))))
|
||||||
|
|
||||||
assertEquals(setOf(SyntheticEvents.hexId(1), SyntheticEvents.hexId(3)), events.map { it.id }.toSet())
|
assertEquals(setOf(SyntheticEvents.hexId(1), SyntheticEvents.hexId(3)), events.map { it.id }.toSet())
|
||||||
}
|
}
|
||||||
@@ -241,7 +217,7 @@ class Nip01ComplianceTest {
|
|||||||
fakeEvent(3, tags = arrayOf(arrayOf("e", SyntheticEvents.hexId(999)))),
|
fakeEvent(3, tags = arrayOf(arrayOf("e", SyntheticEvents.hexId(999)))),
|
||||||
)
|
)
|
||||||
|
|
||||||
val (events, _) = collectUntilEose(Filter(tags = mapOf("e" to listOf(a, b))))
|
val (events, _) = client.collectUntilEose(defaultRelayUrl, Filter(tags = mapOf("e" to listOf(a, b))))
|
||||||
|
|
||||||
assertEquals(setOf(SyntheticEvents.hexId(1), SyntheticEvents.hexId(2)), events.map { it.id }.toSet())
|
assertEquals(setOf(SyntheticEvents.hexId(1), SyntheticEvents.hexId(2)), events.map { it.id }.toSet())
|
||||||
}
|
}
|
||||||
@@ -262,7 +238,8 @@ class Nip01ComplianceTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val (events, _) =
|
val (events, _) =
|
||||||
collectUntilEoseMulti(
|
client.collectUntilEoseMulti(
|
||||||
|
defaultRelayUrl,
|
||||||
listOf(
|
listOf(
|
||||||
Filter(kinds = listOf(1)),
|
Filter(kinds = listOf(1)),
|
||||||
Filter(kinds = listOf(7)),
|
Filter(kinds = listOf(7)),
|
||||||
@@ -294,7 +271,7 @@ class Nip01ComplianceTest {
|
|||||||
|
|
||||||
client.subscribe(
|
client.subscribe(
|
||||||
"sub-A",
|
"sub-A",
|
||||||
mapOf(relayUrl to listOf(Filter(kinds = listOf(1)))),
|
mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(1)))),
|
||||||
object : SubscriptionListener {
|
object : SubscriptionListener {
|
||||||
override fun onEvent(
|
override fun onEvent(
|
||||||
event: Event,
|
event: Event,
|
||||||
@@ -315,7 +292,7 @@ class Nip01ComplianceTest {
|
|||||||
)
|
)
|
||||||
client.subscribe(
|
client.subscribe(
|
||||||
"sub-B",
|
"sub-B",
|
||||||
mapOf(relayUrl to listOf(Filter(kinds = listOf(4)))),
|
mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(4)))),
|
||||||
object : SubscriptionListener {
|
object : SubscriptionListener {
|
||||||
override fun onEvent(
|
override fun onEvent(
|
||||||
event: Event,
|
event: Event,
|
||||||
@@ -362,7 +339,7 @@ class Nip01ComplianceTest {
|
|||||||
fakeEvent(2, kind = 0, pubKey = pubkey, createdAt = 200, content = "new"),
|
fakeEvent(2, kind = 0, pubKey = pubkey, createdAt = 200, content = "new"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val (events, _) = collectUntilEose(Filter(kinds = listOf(0), authors = listOf(pubkey)))
|
val (events, _) = client.collectUntilEose(defaultRelayUrl, Filter(kinds = listOf(0), authors = listOf(pubkey)))
|
||||||
|
|
||||||
assertEquals(1, events.size)
|
assertEquals(1, events.size)
|
||||||
assertEquals("new", events[0].content)
|
assertEquals("new", events[0].content)
|
||||||
@@ -383,7 +360,11 @@ class Nip01ComplianceTest {
|
|||||||
val v3 = signer.sign(LongTextNoteEvent.build("list-b", "title", dTag = "list-b", createdAt = 100))
|
val v3 = signer.sign(LongTextNoteEvent.build("list-b", "title", dTag = "list-b", createdAt = 100))
|
||||||
preload(v1, v2, v3)
|
preload(v1, v2, v3)
|
||||||
|
|
||||||
val (events, _) = collectUntilEose(Filter(kinds = listOf(LongTextNoteEvent.KIND), authors = listOf(signer.pubKey)))
|
val (events, _) =
|
||||||
|
client.collectUntilEose(
|
||||||
|
defaultRelayUrl,
|
||||||
|
Filter(kinds = listOf(LongTextNoteEvent.KIND), authors = listOf(signer.pubKey)),
|
||||||
|
)
|
||||||
|
|
||||||
assertEquals(2, events.size)
|
assertEquals(2, events.size)
|
||||||
assertEquals(setOf("new", "list-b"), events.map { it.content }.toSet())
|
assertEquals(setOf("new", "list-b"), events.map { it.content }.toSet())
|
||||||
@@ -399,7 +380,7 @@ class Nip01ComplianceTest {
|
|||||||
val gotEose = Channel<Unit>(UNLIMITED)
|
val gotEose = Channel<Unit>(UNLIMITED)
|
||||||
client.subscribe(
|
client.subscribe(
|
||||||
"live-1",
|
"live-1",
|
||||||
mapOf(relayUrl to listOf(Filter(kinds = listOf(1)))),
|
mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(1)))),
|
||||||
object : SubscriptionListener {
|
object : SubscriptionListener {
|
||||||
override fun onEvent(
|
override fun onEvent(
|
||||||
event: Event,
|
event: Event,
|
||||||
@@ -423,7 +404,7 @@ class Nip01ComplianceTest {
|
|||||||
|
|
||||||
// Inject an event through the wire path (not preload — that bypasses
|
// Inject an event through the wire path (not preload — that bypasses
|
||||||
// the live broadcast that subscriptions feed off of).
|
// the live broadcast that subscriptions feed off of).
|
||||||
hub.getOrCreate(relayUrl).publish(fakeEvent(99, kind = 1, content = "live"))
|
defaultRelay.publish(fakeEvent(99, kind = 1, content = "live"))
|
||||||
|
|
||||||
val received = withTimeout(5000) { ch.receive() }
|
val received = withTimeout(5000) { ch.receive() }
|
||||||
assertEquals("live", received.content)
|
assertEquals("live", received.content)
|
||||||
@@ -438,7 +419,7 @@ class Nip01ComplianceTest {
|
|||||||
val gotEose = Channel<Unit>(UNLIMITED)
|
val gotEose = Channel<Unit>(UNLIMITED)
|
||||||
client.subscribe(
|
client.subscribe(
|
||||||
"live-2",
|
"live-2",
|
||||||
mapOf(relayUrl to listOf(Filter(kinds = listOf(1)))),
|
mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(1)))),
|
||||||
object : SubscriptionListener {
|
object : SubscriptionListener {
|
||||||
override fun onEvent(
|
override fun onEvent(
|
||||||
event: Event,
|
event: Event,
|
||||||
@@ -459,7 +440,7 @@ class Nip01ComplianceTest {
|
|||||||
)
|
)
|
||||||
withTimeout(5000) { gotEose.receive() }
|
withTimeout(5000) { gotEose.receive() }
|
||||||
|
|
||||||
hub.getOrCreate(relayUrl).publish(fakeEvent(98, kind = 4, content = "off-topic"))
|
defaultRelay.publish(fakeEvent(98, kind = 4, content = "off-topic"))
|
||||||
|
|
||||||
val seen = withTimeoutOrNull(500) { ch.receive() }
|
val seen = withTimeoutOrNull(500) { ch.receive() }
|
||||||
assertNull(seen, "kind 4 should not match a kind-1 subscription")
|
assertNull(seen, "kind 4 should not match a kind-1 subscription")
|
||||||
@@ -479,7 +460,7 @@ class Nip01ComplianceTest {
|
|||||||
val gotEose = Channel<Unit>(UNLIMITED)
|
val gotEose = Channel<Unit>(UNLIMITED)
|
||||||
client.subscribe(
|
client.subscribe(
|
||||||
"eph-1",
|
"eph-1",
|
||||||
mapOf(relayUrl to listOf(Filter(kinds = listOf(20_001)))),
|
mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(20_001)))),
|
||||||
object : SubscriptionListener {
|
object : SubscriptionListener {
|
||||||
override fun onEvent(
|
override fun onEvent(
|
||||||
event: Event,
|
event: Event,
|
||||||
@@ -500,7 +481,7 @@ class Nip01ComplianceTest {
|
|||||||
)
|
)
|
||||||
withTimeout(5000) { gotEose.receive() }
|
withTimeout(5000) { gotEose.receive() }
|
||||||
|
|
||||||
hub.getOrCreate(relayUrl).publish(fakeEvent(70, kind = 20_001, content = "ephemeral-payload"))
|
defaultRelay.publish(fakeEvent(70, kind = 20_001, content = "ephemeral-payload"))
|
||||||
|
|
||||||
val received = withTimeout(5000) { ch.receive() }
|
val received = withTimeout(5000) { ch.receive() }
|
||||||
assertEquals("ephemeral-payload", received.content)
|
assertEquals("ephemeral-payload", received.content)
|
||||||
@@ -516,10 +497,10 @@ class Nip01ComplianceTest {
|
|||||||
fun ephemeralEventIsNotStoredAndDoesNotShowOnFollowupReq() =
|
fun ephemeralEventIsNotStoredAndDoesNotShowOnFollowupReq() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
// Publish ephemeral first — no live subscriber listening.
|
// Publish ephemeral first — no live subscriber listening.
|
||||||
hub.getOrCreate(relayUrl).publish(fakeEvent(71, kind = 20_002, content = "vanish"))
|
defaultRelay.publish(fakeEvent(71, kind = 20_002, content = "vanish"))
|
||||||
|
|
||||||
// Late subscriber: should see EOSE with no events.
|
// Late subscriber: should see EOSE with no events.
|
||||||
val (events, eose) = collectUntilEose(Filter(kinds = listOf(20_002)))
|
val (events, eose) = client.collectUntilEose(defaultRelayUrl, Filter(kinds = listOf(20_002)))
|
||||||
assertTrue(eose, "EOSE must fire for an ephemeral kind even if zero events match")
|
assertTrue(eose, "EOSE must fire for an ephemeral kind even if zero events match")
|
||||||
assertEquals(0, events.size, "Ephemeral events must not be persisted")
|
assertEquals(0, events.size, "Ephemeral events must not be persisted")
|
||||||
}
|
}
|
||||||
@@ -570,93 +551,4 @@ class Nip01ComplianceTest {
|
|||||||
assertEquals("from-a", received[relayA])
|
assertEquals("from-a", received[relayA])
|
||||||
assertEquals("from-b", received[relayB])
|
assertEquals("from-b", received[relayB])
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Helpers ------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Subscribes synchronously and returns the events received before EOSE. */
|
|
||||||
private suspend fun collectUntilEose(filter: Filter): Pair<List<Event>, Boolean> {
|
|
||||||
val ch = Channel<Either>(UNLIMITED)
|
|
||||||
val subId = "sub-${System.nanoTime()}"
|
|
||||||
client.subscribe(
|
|
||||||
subId,
|
|
||||||
mapOf(relayUrl to listOf(filter)),
|
|
||||||
object : SubscriptionListener {
|
|
||||||
override fun onEvent(
|
|
||||||
event: Event,
|
|
||||||
isLive: Boolean,
|
|
||||||
relay: NormalizedRelayUrl,
|
|
||||||
forFilters: List<Filter>?,
|
|
||||||
) {
|
|
||||||
ch.trySend(Either.Ev(event))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEose(
|
|
||||||
relay: NormalizedRelayUrl,
|
|
||||||
forFilters: List<Filter>?,
|
|
||||||
) {
|
|
||||||
ch.trySend(Either.Eose)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
val events = mutableListOf<Event>()
|
|
||||||
var eose = false
|
|
||||||
withTimeout(5000) {
|
|
||||||
while (!eose) {
|
|
||||||
when (val msg = ch.receive()) {
|
|
||||||
is Either.Ev -> events += msg.event
|
|
||||||
Either.Eose -> eose = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client.unsubscribe(subId)
|
|
||||||
return events to eose
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Variant of [collectUntilEose] that subscribes with multiple filters. */
|
|
||||||
private suspend fun collectUntilEoseMulti(filters: List<Filter>): Pair<List<Event>, Boolean> {
|
|
||||||
val ch = Channel<Either>(UNLIMITED)
|
|
||||||
val subId = "sub-${System.nanoTime()}"
|
|
||||||
client.subscribe(
|
|
||||||
subId,
|
|
||||||
mapOf(relayUrl to filters),
|
|
||||||
object : SubscriptionListener {
|
|
||||||
override fun onEvent(
|
|
||||||
event: Event,
|
|
||||||
isLive: Boolean,
|
|
||||||
relay: NormalizedRelayUrl,
|
|
||||||
forFilters: List<Filter>?,
|
|
||||||
) {
|
|
||||||
ch.trySend(Either.Ev(event))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEose(
|
|
||||||
relay: NormalizedRelayUrl,
|
|
||||||
forFilters: List<Filter>?,
|
|
||||||
) {
|
|
||||||
ch.trySend(Either.Eose)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
val events = mutableListOf<Event>()
|
|
||||||
var eose = false
|
|
||||||
withTimeout(5000) {
|
|
||||||
while (!eose) {
|
|
||||||
when (val msg = ch.receive()) {
|
|
||||||
is Either.Ev -> events += msg.event
|
|
||||||
Either.Eose -> eose = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client.unsubscribe(subId)
|
|
||||||
return events to eose
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed interface Either {
|
|
||||||
data class Ev(
|
|
||||||
val event: Event,
|
|
||||||
) : Either
|
|
||||||
|
|
||||||
object Eose : Either
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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.geode.testing
|
||||||
|
|
||||||
|
import com.vitorpamplona.geode.Relay
|
||||||
|
import com.vitorpamplona.geode.RelayHub
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import org.junit.After
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for tests that drive a real [NostrClient] against an
|
||||||
|
* in-process [RelayHub]. Owns the lifecycle of the four pieces every
|
||||||
|
* such test needs:
|
||||||
|
*
|
||||||
|
* - [hub] — the registry of in-process relays (also serves as
|
||||||
|
* `WebsocketBuilder` for [NostrClient]).
|
||||||
|
* - [scope] — application coroutine scope for the client.
|
||||||
|
* - [client] — a [NostrClient] wired to [hub] and [scope].
|
||||||
|
* - [defaultRelay] / [defaultRelayUrl] — convenience handles for the
|
||||||
|
* single-relay case (the most common in tests).
|
||||||
|
*
|
||||||
|
* Cleanup happens in [tearDownRelayClientTest], registered with
|
||||||
|
* JUnit's [@After][After], so an assertion failure does NOT leak the
|
||||||
|
* scope, the SQLite event store, or the WebSocket bridge — a recurring
|
||||||
|
* problem with the previous "clean up at the end of the test body"
|
||||||
|
* pattern.
|
||||||
|
*
|
||||||
|
* Subclasses that need their own setup/teardown should add their own
|
||||||
|
* `@Before` / `@After` methods; JUnit runs all of them.
|
||||||
|
*
|
||||||
|
* Multi-relay tests use [hub] directly:
|
||||||
|
* ```
|
||||||
|
* val relayA = RelayUrlNormalizer.normalize("ws://relay-a/")
|
||||||
|
* hub.getOrCreate(relayA).preload(eventA)
|
||||||
|
* hub.getOrCreate(relayB).preload(eventB)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
open class RelayClientTest {
|
||||||
|
val hub: RelayHub = RelayHub()
|
||||||
|
val scope: CoroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
|
val client: NostrClient = NostrClient(hub, scope)
|
||||||
|
|
||||||
|
/** Stable URL for the single-relay case — see [RelayHub.DEFAULT_URL]. */
|
||||||
|
val defaultRelayUrl: NormalizedRelayUrl get() = RelayHub.DEFAULT_URL
|
||||||
|
|
||||||
|
/** Lazy handle to the relay at [defaultRelayUrl]. Auto-created on first read. */
|
||||||
|
val defaultRelay: Relay get() = hub.getOrCreate(defaultRelayUrl)
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDownRelayClientTest() {
|
||||||
|
client.disconnect()
|
||||||
|
scope.cancel()
|
||||||
|
hub.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* 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.geode.testing
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
|
||||||
|
/** Tagged result of [collectUntilEose]: stored events plus whether EOSE actually arrived. */
|
||||||
|
data class CollectResult(
|
||||||
|
val events: List<Event>,
|
||||||
|
val eoseReceived: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe with [filter] on a single [relay], drain the
|
||||||
|
* historical-replay phase, and return when EOSE arrives. The
|
||||||
|
* subscription is closed before this returns. The pattern that 80% of
|
||||||
|
* REQ-style tests need.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* val (events, eose) = client.collectUntilEose(defaultRelayUrl, Filter(kinds = listOf(1)))
|
||||||
|
* assertEquals(20, events.size)
|
||||||
|
* assertTrue(eose)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param timeoutMillis time to wait for EOSE before failing the test.
|
||||||
|
* Default 5 s — generous for in-process; tighten if needed.
|
||||||
|
*/
|
||||||
|
suspend fun NostrClient.collectUntilEose(
|
||||||
|
relay: NormalizedRelayUrl,
|
||||||
|
filter: Filter,
|
||||||
|
timeoutMillis: Long = 5_000,
|
||||||
|
): CollectResult = collectUntilEoseMulti(relay, listOf(filter), timeoutMillis)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-filter variant. NIP-01 allows a REQ to carry several filters
|
||||||
|
* that the relay OR's together. EOSE fires once after the union of all
|
||||||
|
* filters has been replayed.
|
||||||
|
*/
|
||||||
|
suspend fun NostrClient.collectUntilEoseMulti(
|
||||||
|
relay: NormalizedRelayUrl,
|
||||||
|
filters: List<Filter>,
|
||||||
|
timeoutMillis: Long = 5_000,
|
||||||
|
): CollectResult {
|
||||||
|
val ch = Channel<Signal>(UNLIMITED)
|
||||||
|
val subId = "test-sub-${nextSubId()}"
|
||||||
|
subscribe(
|
||||||
|
subId,
|
||||||
|
mapOf(relay to filters),
|
||||||
|
object : SubscriptionListener {
|
||||||
|
override fun onEvent(
|
||||||
|
event: Event,
|
||||||
|
isLive: Boolean,
|
||||||
|
relay: NormalizedRelayUrl,
|
||||||
|
forFilters: List<Filter>?,
|
||||||
|
) {
|
||||||
|
ch.trySend(Signal.Ev(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEose(
|
||||||
|
relay: NormalizedRelayUrl,
|
||||||
|
forFilters: List<Filter>?,
|
||||||
|
) {
|
||||||
|
ch.trySend(Signal.Eose)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val events = mutableListOf<Event>()
|
||||||
|
var eose = false
|
||||||
|
try {
|
||||||
|
withTimeout(timeoutMillis) {
|
||||||
|
while (!eose) {
|
||||||
|
when (val msg = ch.receive()) {
|
||||||
|
is Signal.Ev -> events += msg.event
|
||||||
|
Signal.Eose -> eose = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
unsubscribe(subId)
|
||||||
|
}
|
||||||
|
return CollectResult(events, eose)
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface Signal {
|
||||||
|
data class Ev(
|
||||||
|
val event: Event,
|
||||||
|
) : Signal
|
||||||
|
|
||||||
|
object Eose : Signal
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Monotonic counter for unique sub-ids inside a JVM. */
|
||||||
|
private var subIdSeq: Int = 0
|
||||||
|
|
||||||
|
private fun nextSubId(): Int = ++subIdSeq
|
||||||
+13
-2
@@ -183,8 +183,10 @@ kotlin {
|
|||||||
|
|
||||||
// In-process Nostr relay (geode) so JVM/Android host
|
// In-process Nostr relay (geode) so JVM/Android host
|
||||||
// tests don't need network access or a Rust toolchain.
|
// tests don't need network access or a Rust toolchain.
|
||||||
// The `geode.fixtures` package carries the test-only
|
// testFixtures (RelayClientTest base, fixtures,
|
||||||
// event generators and corpus loader.
|
// collectUntilEose) are wired below at the top-level
|
||||||
|
// `dependencies` block — the KMP source-set DSL
|
||||||
|
// doesn't expose the `testFixtures(...)` consumer.
|
||||||
implementation(project(":geode"))
|
implementation(project(":geode"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,6 +349,15 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testFixtures(...) consumer lives outside the KMP source-set DSL —
|
||||||
|
// the KMP source-set `dependencies { }` block uses
|
||||||
|
// `KotlinDependencyHandler`, which does not expose the
|
||||||
|
// `testFixtures(...)` projection. The standard Gradle dependency
|
||||||
|
// configuration name (`jvmAndroidTestImplementation`) does work here.
|
||||||
|
dependencies {
|
||||||
|
"jvmAndroidTestImplementation"(testFixtures(project(":geode")))
|
||||||
|
}
|
||||||
|
|
||||||
mavenPublishing {
|
mavenPublishing {
|
||||||
// sources publishing is always enabled by the Kotlin Multiplatform plugin
|
// sources publishing is always enabled by the Kotlin Multiplatform plugin
|
||||||
configure(
|
configure(
|
||||||
|
|||||||
-40
@@ -1,40 +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.geode.RelayHub
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base for tests that drive a real `NostrClient` against an in-process Nostr
|
|
||||||
* relay. Each subclass instance gets its own [RelayHub] so tests can
|
|
||||||
* preload events and assert deterministic counts without hitting the
|
|
||||||
* network or relying on production relays.
|
|
||||||
*
|
|
||||||
* To replace with the previous behaviour (real OkHttp WebSocket against
|
|
||||||
* `wss://nos.lol`), instantiate `BasicOkHttpWebSocket.Builder` directly in
|
|
||||||
* the specific test that needs it.
|
|
||||||
*/
|
|
||||||
open class BaseNostrClientTest {
|
|
||||||
val relayHub: RelayHub = RelayHub()
|
|
||||||
|
|
||||||
/** Plug into `NostrClient(socketBuilder, scope)`. */
|
|
||||||
val socketBuilder get() = relayHub
|
|
||||||
}
|
|
||||||
+5
-21
@@ -20,25 +20,20 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.nip01Core.relay
|
package com.vitorpamplona.quartz.nip01Core.relay
|
||||||
|
|
||||||
|
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchFirst
|
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchFirst
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NostrClientFirstEventTest : BaseNostrClientTest() {
|
class NostrClientFirstEventTest : RelayClientTest() {
|
||||||
@Test
|
@Test
|
||||||
fun testDownloadFirstEvent() =
|
fun testDownloadFirstEvent() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
|
val pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
|
||||||
val relayUrl = "ws://127.0.0.1:7770/"
|
|
||||||
|
|
||||||
val seed =
|
val seed =
|
||||||
Event(
|
Event(
|
||||||
@@ -50,25 +45,14 @@ class NostrClientFirstEventTest : BaseNostrClientTest() {
|
|||||||
content = """{"name":"vitor"}""",
|
content = """{"name":"vitor"}""",
|
||||||
sig = "b".repeat(128),
|
sig = "b".repeat(128),
|
||||||
)
|
)
|
||||||
relayHub.getOrCreate(relayUrl).preload(seed)
|
defaultRelay.preload(seed)
|
||||||
|
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val event =
|
val event =
|
||||||
client.fetchFirst(
|
client.fetchFirst(
|
||||||
relay = relayUrl,
|
relay = defaultRelayUrl,
|
||||||
filter =
|
filter = Filter(kinds = listOf(MetadataEvent.KIND), authors = listOf(pubKey)),
|
||||||
Filter(
|
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
|
||||||
authors = listOf(pubKey),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
assertEquals(MetadataEvent.KIND, event?.kind)
|
assertEquals(MetadataEvent.KIND, event?.kind)
|
||||||
assertEquals(pubKey, event?.pubKey)
|
assertEquals(pubKey, event?.pubKey)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-31
@@ -19,18 +19,14 @@
|
|||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.nip01Core.relay
|
package com.vitorpamplona.quartz.nip01Core.relay
|
||||||
|
|
||||||
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
||||||
|
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -38,17 +34,11 @@ import kotlinx.coroutines.withTimeoutOrNull
|
|||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NostrClientManualSubTest : BaseNostrClientTest() {
|
class NostrClientManualSubTest : RelayClientTest() {
|
||||||
@Test
|
@Test
|
||||||
fun testEoseAfter100Events() =
|
fun testEoseAfter100Events() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val relayUrl = RelayUrlNormalizer.normalize("ws://127.0.0.1:7770/")
|
defaultRelay.preload(SyntheticEvents.batch(150, kind = MetadataEvent.KIND))
|
||||||
relayHub
|
|
||||||
.getOrCreate(relayUrl)
|
|
||||||
.preload(SyntheticEvents.batch(150, kind = MetadataEvent.KIND))
|
|
||||||
|
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val resultChannel = Channel<String>(UNLIMITED)
|
val resultChannel = Channel<String>(UNLIMITED)
|
||||||
val events = mutableListOf<String>()
|
val events = mutableListOf<String>()
|
||||||
@@ -73,18 +63,11 @@ class NostrClientManualSubTest : BaseNostrClientTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val filters =
|
client.subscribe(
|
||||||
mapOf(
|
mySubId,
|
||||||
relayUrl to
|
mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(MetadataEvent.KIND), limit = 100))),
|
||||||
listOf(
|
listener,
|
||||||
Filter(
|
)
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
|
||||||
limit = 100,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
client.subscribe(mySubId, filters, listener)
|
|
||||||
|
|
||||||
withTimeoutOrNull(10000) {
|
withTimeoutOrNull(10000) {
|
||||||
while (events.size < 101) {
|
while (events.size < 101) {
|
||||||
@@ -94,12 +77,7 @@ class NostrClientManualSubTest : BaseNostrClientTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resultChannel.close()
|
resultChannel.close()
|
||||||
|
|
||||||
client.unsubscribe(mySubId)
|
client.unsubscribe(mySubId)
|
||||||
client.disconnect()
|
|
||||||
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
assertEquals(101, events.size)
|
assertEquals(101, events.size)
|
||||||
assertEquals(true, events.take(100).all { it.length == 64 })
|
assertEquals(true, events.take(100).all { it.length == 64 })
|
||||||
|
|||||||
+6
-33
@@ -21,19 +21,15 @@
|
|||||||
package com.vitorpamplona.quartz.nip01Core.relay
|
package com.vitorpamplona.quartz.nip01Core.relay
|
||||||
|
|
||||||
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.count
|
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.count
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NostrClientQueryCountTest : BaseNostrClientTest() {
|
class NostrClientQueryCountTest : RelayClientTest() {
|
||||||
private val relayA = "ws://127.0.0.1:7771/".normalizeRelayUrl()
|
private val relayA = "ws://127.0.0.1:7771/".normalizeRelayUrl()
|
||||||
private val relayB = "ws://127.0.0.1:7772/".normalizeRelayUrl()
|
private val relayB = "ws://127.0.0.1:7772/".normalizeRelayUrl()
|
||||||
|
|
||||||
@@ -44,16 +40,16 @@ class NostrClientQueryCountTest : BaseNostrClientTest() {
|
|||||||
// 5 metadata + 3 outbox relay events on A, 2 metadata + 7 outbox on B.
|
// 5 metadata + 3 outbox relay events on A, 2 metadata + 7 outbox on B.
|
||||||
// Each event needs a distinct (kind, pubkey, dTag) to avoid replaceable-event collisions.
|
// Each event needs a distinct (kind, pubkey, dTag) to avoid replaceable-event collisions.
|
||||||
fun pk(seed: Int) = SyntheticEvents.hexId(seed)
|
fun pk(seed: Int) = SyntheticEvents.hexId(seed)
|
||||||
relayHub.getOrCreate(relayA).preload(
|
hub.getOrCreate(relayA).preload(
|
||||||
(1..5).map { SyntheticEvents.fakeEvent(idSeed = it, kind = 0, pubKey = pk(it)) },
|
(1..5).map { SyntheticEvents.fakeEvent(idSeed = it, kind = 0, pubKey = pk(it)) },
|
||||||
)
|
)
|
||||||
relayHub.getOrCreate(relayA).preload(
|
hub.getOrCreate(relayA).preload(
|
||||||
(1..3).map { SyntheticEvents.fakeEvent(idSeed = 1000 + it, kind = 10002, pubKey = pk(1000 + it)) },
|
(1..3).map { SyntheticEvents.fakeEvent(idSeed = 1000 + it, kind = 10002, pubKey = pk(1000 + it)) },
|
||||||
)
|
)
|
||||||
relayHub.getOrCreate(relayB).preload(
|
hub.getOrCreate(relayB).preload(
|
||||||
(1..2).map { SyntheticEvents.fakeEvent(idSeed = 2000 + it, kind = 0, pubKey = pk(2000 + it)) },
|
(1..2).map { SyntheticEvents.fakeEvent(idSeed = 2000 + it, kind = 0, pubKey = pk(2000 + it)) },
|
||||||
)
|
)
|
||||||
relayHub.getOrCreate(relayB).preload(
|
hub.getOrCreate(relayB).preload(
|
||||||
(1..7).map { SyntheticEvents.fakeEvent(idSeed = 3000 + it, kind = 10002, pubKey = pk(3000 + it)) },
|
(1..7).map { SyntheticEvents.fakeEvent(idSeed = 3000 + it, kind = 10002, pubKey = pk(3000 + it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -62,41 +58,22 @@ class NostrClientQueryCountTest : BaseNostrClientTest() {
|
|||||||
fun testQueryCountSuspend() =
|
fun testQueryCountSuspend() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
seed()
|
seed()
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val result = client.count(relayA, metadata)
|
val result = client.count(relayA, metadata)
|
||||||
|
|
||||||
assertEquals(5, result?.count)
|
assertEquals(5, result?.count)
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testQueryCountSuspendAllEvents() =
|
fun testQueryCountSuspendAllEvents() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
seed()
|
seed()
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val result = client.count(relayA, Filter())
|
val result = client.count(relayA, Filter())
|
||||||
|
|
||||||
assertEquals(8, result?.count)
|
assertEquals(8, result?.count)
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testQueryCountSuspendMultipleRelays() =
|
fun testQueryCountSuspendMultipleRelays() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
seed()
|
seed()
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
client.count(
|
client.count(
|
||||||
mapOf(
|
mapOf(
|
||||||
@@ -107,9 +84,5 @@ class NostrClientQueryCountTest : BaseNostrClientTest() {
|
|||||||
|
|
||||||
assertEquals(8, results[relayA]?.count)
|
assertEquals(8, results[relayA]?.count)
|
||||||
assertEquals(9, results[relayB]?.count)
|
assertEquals(9, results[relayB]?.count)
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-54
@@ -19,22 +19,18 @@
|
|||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.nip01Core.relay
|
package com.vitorpamplona.quartz.nip01Core.relay
|
||||||
|
|
||||||
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
||||||
|
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayConnectionListener
|
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayConnectionListener
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage
|
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
|
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
|
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
@@ -44,12 +40,12 @@ import kotlinx.coroutines.withTimeoutOrNull
|
|||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NostrClientRepeatSubTest : BaseNostrClientTest() {
|
class NostrClientRepeatSubTest : RelayClientTest() {
|
||||||
@Test
|
@Test
|
||||||
fun testRepeatSubEvents() =
|
fun testRepeatSubEvents() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
// Each replaceable kind needs unique pubkeys.
|
// Each replaceable kind needs unique pubkeys.
|
||||||
relayHub.getOrCreate("ws://127.0.0.1:7770/").preload(
|
defaultRelay.preload(
|
||||||
(1..150).map {
|
(1..150).map {
|
||||||
SyntheticEvents.fakeEvent(
|
SyntheticEvents.fakeEvent(
|
||||||
idSeed = it,
|
idSeed = it,
|
||||||
@@ -58,7 +54,7 @@ class NostrClientRepeatSubTest : BaseNostrClientTest() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
relayHub.getOrCreate("ws://127.0.0.1:7770/").preload(
|
defaultRelay.preload(
|
||||||
(1..50).map {
|
(1..50).map {
|
||||||
SyntheticEvents.fakeEvent(
|
SyntheticEvents.fakeEvent(
|
||||||
idSeed = 100_000 + it,
|
idSeed = 100_000 + it,
|
||||||
@@ -68,9 +64,6 @@ class NostrClientRepeatSubTest : BaseNostrClientTest() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val resultChannel = Channel<String>(UNLIMITED)
|
val resultChannel = Channel<String>(UNLIMITED)
|
||||||
val events = mutableListOf<String>()
|
val events = mutableListOf<String>()
|
||||||
val mySubId = "test-sub-id-2"
|
val mySubId = "test-sub-id-2"
|
||||||
@@ -101,38 +94,11 @@ class NostrClientRepeatSubTest : BaseNostrClientTest() {
|
|||||||
|
|
||||||
client.addConnectionListener(listener)
|
client.addConnectionListener(listener)
|
||||||
|
|
||||||
val filters =
|
val filters = mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(MetadataEvent.KIND), limit = 100)))
|
||||||
mapOf(
|
|
||||||
RelayUrlNormalizer.normalize("ws://127.0.0.1:7770/") to
|
|
||||||
listOf(
|
|
||||||
Filter(
|
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
|
||||||
limit = 100,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
val filtersShouldIgnore =
|
val filtersShouldIgnore =
|
||||||
mapOf(
|
mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(AdvertisedRelayListEvent.KIND), limit = 500)))
|
||||||
RelayUrlNormalizer.normalize("ws://127.0.0.1:7770/") to
|
|
||||||
listOf(
|
|
||||||
Filter(
|
|
||||||
kinds = listOf(AdvertisedRelayListEvent.KIND),
|
|
||||||
limit = 500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
val filtersShouldSendAfterEOSE =
|
val filtersShouldSendAfterEOSE =
|
||||||
mapOf(
|
mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(AdvertisedRelayListEvent.KIND), limit = 10)))
|
||||||
RelayUrlNormalizer.normalize("ws://127.0.0.1:7770/") to
|
|
||||||
listOf(
|
|
||||||
Filter(
|
|
||||||
kinds = listOf(AdvertisedRelayListEvent.KIND),
|
|
||||||
limit = 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
launch {
|
launch {
|
||||||
@@ -161,30 +127,18 @@ class NostrClientRepeatSubTest : BaseNostrClientTest() {
|
|||||||
|
|
||||||
client.unsubscribe(mySubId)
|
client.unsubscribe(mySubId)
|
||||||
client.removeConnectionListener(listener)
|
client.removeConnectionListener(listener)
|
||||||
client.disconnect()
|
|
||||||
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
// The relay may return up to limit events before EOSE; some relays return
|
// The relay may return up to limit events before EOSE; some relays return
|
||||||
// one extra past the requested limit, so don't assert on the exact count.
|
// one extra past the requested limit, so don't assert on the exact count.
|
||||||
// First sub: <= 100 metadata events, then EOSE.
|
|
||||||
// Second sub: <= 10 advertised relay list events, then EOSE.
|
|
||||||
val firstEose = events.indexOf("EOSE")
|
val firstEose = events.indexOf("EOSE")
|
||||||
val lastEose = events.lastIndexOf("EOSE")
|
val lastEose = events.lastIndexOf("EOSE")
|
||||||
|
|
||||||
// both EOSEs must be present and distinct
|
|
||||||
assertEquals(true, firstEose >= 0)
|
assertEquals(true, firstEose >= 0)
|
||||||
assertEquals(true, lastEose > firstEose)
|
assertEquals(true, lastEose > firstEose)
|
||||||
// last entry is the second EOSE (loop stops on it)
|
|
||||||
assertEquals(events.size - 1, lastEose)
|
assertEquals(events.size - 1, lastEose)
|
||||||
// first sub stays within its limit (allow +1 for relay quirks)
|
|
||||||
assertEquals(true, firstEose in 1..101)
|
assertEquals(true, firstEose in 1..101)
|
||||||
// second sub stays within its limit (allow +1 for relay quirks)
|
|
||||||
assertEquals(true, (lastEose - firstEose - 1) in 1..11)
|
assertEquals(true, (lastEose - firstEose - 1) in 1..11)
|
||||||
// everything before the first EOSE is an event id
|
|
||||||
assertEquals(true, events.take(firstEose).all { it.length == 64 })
|
assertEquals(true, events.take(firstEose).all { it.length == 64 })
|
||||||
// everything between the two EOSEs is an event id
|
|
||||||
assertEquals(true, events.subList(firstEose + 1, lastEose).all { it.length == 64 })
|
assertEquals(true, events.subList(firstEose + 1, lastEose).all { it.length == 64 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-42
@@ -21,22 +21,17 @@
|
|||||||
package com.vitorpamplona.quartz.nip01Core.relay
|
package com.vitorpamplona.quartz.nip01Core.relay
|
||||||
|
|
||||||
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
||||||
|
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAllPages
|
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAllPages
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NostrClientReqBypassingRelayLimitsTest : BaseNostrClientTest() {
|
class NostrClientReqBypassingRelayLimitsTest : RelayClientTest() {
|
||||||
@Test
|
@Test
|
||||||
fun testDownloadFromRelayReturnsMetadataEvents() =
|
fun testDownloadFromRelayReturnsMetadataEvents() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
@@ -50,32 +45,18 @@ class NostrClientReqBypassingRelayLimitsTest : BaseNostrClientTest() {
|
|||||||
pubKey = SyntheticEvents.hexId(it),
|
pubKey = SyntheticEvents.hexId(it),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
relayHub.getOrCreate("ws://127.0.0.1:7770/").preload(corpus)
|
defaultRelay.preload(corpus)
|
||||||
|
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val events = mutableListOf<Event>()
|
val events = mutableListOf<Event>()
|
||||||
|
|
||||||
val totalFound =
|
val totalFound =
|
||||||
client.fetchAllPages(
|
client.fetchAllPages(
|
||||||
relay = "ws://127.0.0.1:7770/",
|
relay = defaultRelayUrl,
|
||||||
filters =
|
filters = listOf(Filter(kinds = listOf(MetadataEvent.KIND), limit = 1000)),
|
||||||
listOf(
|
|
||||||
Filter(
|
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
|
||||||
limit = 1000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) { event ->
|
) { event ->
|
||||||
events.add(event)
|
events.add(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
delay(500)
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
assertEquals(1000, totalFound)
|
assertEquals(1000, totalFound)
|
||||||
assertEquals(1000, events.size)
|
assertEquals(1000, events.size)
|
||||||
events.forEach { event ->
|
events.forEach { event ->
|
||||||
@@ -102,27 +83,18 @@ class NostrClientReqBypassingRelayLimitsTest : BaseNostrClientTest() {
|
|||||||
pubKey = SyntheticEvents.hexId(100_000 + it),
|
pubKey = SyntheticEvents.hexId(100_000 + it),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
relayHub.getOrCreate("ws://127.0.0.1:7770/").preload(metadata + contacts)
|
defaultRelay.preload(metadata + contacts)
|
||||||
|
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val metadataEvents = mutableListOf<Event>()
|
val metadataEvents = mutableListOf<Event>()
|
||||||
val contactListEvents = mutableListOf<Event>()
|
val contactListEvents = mutableListOf<Event>()
|
||||||
|
|
||||||
val totalFound =
|
val totalFound =
|
||||||
client.fetchAllPages(
|
client.fetchAllPages(
|
||||||
relay = "ws://127.0.0.1:7770/",
|
relay = defaultRelayUrl,
|
||||||
filters =
|
filters =
|
||||||
listOf(
|
listOf(
|
||||||
Filter(
|
Filter(kinds = listOf(MetadataEvent.KIND), limit = 1000),
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
Filter(kinds = listOf(ContactListEvent.KIND), limit = 1500),
|
||||||
limit = 1000,
|
|
||||||
),
|
|
||||||
Filter(
|
|
||||||
kinds = listOf(ContactListEvent.KIND),
|
|
||||||
limit = 1500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
) { event ->
|
) { event ->
|
||||||
if (event.kind == MetadataEvent.KIND) {
|
if (event.kind == MetadataEvent.KIND) {
|
||||||
@@ -133,11 +105,6 @@ class NostrClientReqBypassingRelayLimitsTest : BaseNostrClientTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
delay(500)
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
assertEquals(2500, totalFound)
|
assertEquals(2500, totalFound)
|
||||||
assertEquals(1000, metadataEvents.size)
|
assertEquals(1000, metadataEvents.size)
|
||||||
assertEquals(1500, contactListEvents.size)
|
assertEquals(1500, contactListEvents.size)
|
||||||
|
|||||||
+5
-25
@@ -19,49 +19,29 @@
|
|||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.nip01Core.relay
|
package com.vitorpamplona.quartz.nip01Core.relay
|
||||||
|
|
||||||
|
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.publishAndConfirm
|
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.publishAndConfirm
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NostrClientSendAndWaitTest : BaseNostrClientTest() {
|
class NostrClientSendAndWaitTest : RelayClientTest() {
|
||||||
@Test
|
@Test
|
||||||
fun testSendAndWaitForResponse() =
|
fun testSendAndWaitForResponse() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val randomSigner = NostrSignerInternal(KeyPair())
|
val randomSigner = NostrSignerInternal(KeyPair())
|
||||||
|
|
||||||
val event = randomSigner.sign(TextNoteEvent.build("Hello World"))
|
val event = randomSigner.sign(TextNoteEvent.build("Hello World"))
|
||||||
|
|
||||||
val relayA = "ws://127.0.0.1:7771/".normalizeRelayUrl()
|
val relayA = "ws://127.0.0.1:7771/".normalizeRelayUrl()
|
||||||
val relayB = "ws://127.0.0.1:7772/".normalizeRelayUrl()
|
val relayB = "ws://127.0.0.1:7772/".normalizeRelayUrl()
|
||||||
|
|
||||||
val resultA =
|
val resultA = client.publishAndConfirm(event = event, relayList = setOf(relayA))
|
||||||
client.publishAndConfirm(
|
val resultB = client.publishAndConfirm(event = event, relayList = setOf(relayB))
|
||||||
event = event,
|
|
||||||
relayList = setOf(relayA),
|
|
||||||
)
|
|
||||||
|
|
||||||
val resultB =
|
|
||||||
client.publishAndConfirm(
|
|
||||||
event = event,
|
|
||||||
relayList = setOf(relayB),
|
|
||||||
)
|
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
assertEquals(true, resultA)
|
assertEquals(true, resultA)
|
||||||
assertEquals(true, resultB)
|
assertEquals(true, resultB)
|
||||||
|
|||||||
+11
-39
@@ -19,19 +19,16 @@
|
|||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.nip01Core.relay
|
package com.vitorpamplona.quartz.nip01Core.relay
|
||||||
|
|
||||||
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
||||||
|
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.subscribeAsFlow
|
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.subscribeAsFlow
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
@@ -39,7 +36,7 @@ import kotlinx.coroutines.test.runTest
|
|||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() {
|
class NostrClientSubscriptionAsFlowTest : RelayClientTest() {
|
||||||
fun List<Event>.printDates(): String {
|
fun List<Event>.printDates(): String {
|
||||||
val starting = this[0].createdAt
|
val starting = this[0].createdAt
|
||||||
return joinToString { (it.createdAt - starting).toString() }
|
return joinToString { (it.createdAt - starting).toString() }
|
||||||
@@ -49,20 +46,12 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun testNostrClientSubscriptionAsFlow() =
|
fun testNostrClientSubscriptionAsFlow() =
|
||||||
runTest {
|
runTest {
|
||||||
relayHub.getOrCreate("ws://127.0.0.1:7770/").preload(
|
defaultRelay.preload(SyntheticEvents.batch(20, kind = MetadataEvent.KIND))
|
||||||
SyntheticEvents.batch(20, kind = MetadataEvent.KIND),
|
|
||||||
)
|
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val flow =
|
val flow =
|
||||||
client.subscribeAsFlow(
|
client.subscribeAsFlow(
|
||||||
relay = "ws://127.0.0.1:7770/",
|
relay = defaultRelayUrl,
|
||||||
filter =
|
filter = Filter(kinds = listOf(MetadataEvent.KIND), limit = 10),
|
||||||
Filter(
|
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
|
||||||
limit = 10,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var feedStates = listOf<Event>()
|
var feedStates = listOf<Event>()
|
||||||
@@ -79,11 +68,7 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() {
|
|||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
job.cancel() // Cancel the collection job
|
job.cancel()
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
assertEquals(10, feedStates.size)
|
assertEquals(10, feedStates.size)
|
||||||
}
|
}
|
||||||
@@ -92,20 +77,12 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun testNostrClientSubscriptionAsFlowDebouncing() =
|
fun testNostrClientSubscriptionAsFlowDebouncing() =
|
||||||
runTest {
|
runTest {
|
||||||
relayHub.getOrCreate("ws://127.0.0.1:7770/").preload(
|
defaultRelay.preload(SyntheticEvents.batch(20, kind = MetadataEvent.KIND))
|
||||||
SyntheticEvents.batch(20, kind = MetadataEvent.KIND),
|
|
||||||
)
|
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val flow =
|
val flow =
|
||||||
client.subscribeAsFlow(
|
client.subscribeAsFlow(
|
||||||
relay = "ws://127.0.0.1:7770/",
|
relay = defaultRelayUrl,
|
||||||
filter =
|
filter = Filter(kinds = listOf(MetadataEvent.KIND), limit = 10),
|
||||||
Filter(
|
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
|
||||||
limit = 10,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var feedStates = listOf<Event>()
|
var feedStates = listOf<Event>()
|
||||||
@@ -117,16 +94,11 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance the test dispatcher to ensure emissions are processed
|
|
||||||
while (feedStates.size < 10) {
|
while (feedStates.size < 10) {
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
job.cancel() // Cancel the collection job
|
job.cancel()
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
assertEquals(10, feedStates.size)
|
assertEquals(10, feedStates.size)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-26
@@ -19,17 +19,13 @@
|
|||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.nip01Core.relay
|
package com.vitorpamplona.quartz.nip01Core.relay
|
||||||
|
|
||||||
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
||||||
|
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.StaticSubscription
|
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.StaticSubscription
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -37,15 +33,11 @@ import kotlinx.coroutines.withTimeoutOrNull
|
|||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NostrClientSubscriptionTest : BaseNostrClientTest() {
|
class NostrClientSubscriptionTest : RelayClientTest() {
|
||||||
@Test
|
@Test
|
||||||
fun testNostrClientSubscription() =
|
fun testNostrClientSubscription() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
relayHub.getOrCreate("ws://127.0.0.1:7770/").preload(
|
defaultRelay.preload(SyntheticEvents.batch(150, kind = MetadataEvent.KIND))
|
||||||
SyntheticEvents.batch(150, kind = MetadataEvent.KIND),
|
|
||||||
)
|
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val resultChannel = Channel<Event>(UNLIMITED)
|
val resultChannel = Channel<Event>(UNLIMITED)
|
||||||
val events = mutableSetOf<Event>()
|
val events = mutableSetOf<Event>()
|
||||||
@@ -53,15 +45,7 @@ class NostrClientSubscriptionTest : BaseNostrClientTest() {
|
|||||||
val sub =
|
val sub =
|
||||||
StaticSubscription(
|
StaticSubscription(
|
||||||
client,
|
client,
|
||||||
mapOf(
|
mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(MetadataEvent.KIND), limit = 100))),
|
||||||
RelayUrlNormalizer.normalize("ws://127.0.0.1:7770/") to
|
|
||||||
listOf(
|
|
||||||
Filter(
|
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
|
||||||
limit = 100,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) { event ->
|
) { event ->
|
||||||
assertEquals(MetadataEvent.KIND, event.kind)
|
assertEquals(MetadataEvent.KIND, event.kind)
|
||||||
resultChannel.trySend(event)
|
resultChannel.trySend(event)
|
||||||
@@ -75,13 +59,8 @@ class NostrClientSubscriptionTest : BaseNostrClientTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resultChannel.close()
|
resultChannel.close()
|
||||||
|
|
||||||
sub.close()
|
sub.close()
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
assertEquals(100, events.size)
|
assertEquals(100, events.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-40
@@ -19,19 +19,16 @@
|
|||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.nip01Core.relay
|
package com.vitorpamplona.quartz.nip01Core.relay
|
||||||
|
|
||||||
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
import com.vitorpamplona.geode.fixtures.SyntheticEvents
|
||||||
|
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.fetchAsFlow
|
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.fetchAsFlow
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
@@ -39,7 +36,7 @@ import kotlinx.coroutines.test.runTest
|
|||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NostrClientSubscriptionUntilEoseAsFlowTest : BaseNostrClientTest() {
|
class NostrClientSubscriptionUntilEoseAsFlowTest : RelayClientTest() {
|
||||||
fun List<Event>.printDates(): String {
|
fun List<Event>.printDates(): String {
|
||||||
val starting = this[0].createdAt
|
val starting = this[0].createdAt
|
||||||
return joinToString { (it.createdAt - starting).toString() }
|
return joinToString { (it.createdAt - starting).toString() }
|
||||||
@@ -49,20 +46,12 @@ class NostrClientSubscriptionUntilEoseAsFlowTest : BaseNostrClientTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun testNostrClientSubscriptionUntilEoseAsFlow() =
|
fun testNostrClientSubscriptionUntilEoseAsFlow() =
|
||||||
runTest {
|
runTest {
|
||||||
relayHub.getOrCreate("ws://127.0.0.1:7770/").preload(
|
defaultRelay.preload(SyntheticEvents.batch(20, kind = MetadataEvent.KIND))
|
||||||
SyntheticEvents.batch(20, kind = MetadataEvent.KIND),
|
|
||||||
)
|
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val flow =
|
val flow =
|
||||||
client.fetchAsFlow(
|
client.fetchAsFlow(
|
||||||
relay = "ws://127.0.0.1:7770/",
|
relay = defaultRelayUrl,
|
||||||
filter =
|
filter = Filter(kinds = listOf(MetadataEvent.KIND), limit = 10),
|
||||||
Filter(
|
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
|
||||||
limit = 10,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var feedStates = listOf<Event>()
|
var feedStates = listOf<Event>()
|
||||||
@@ -74,16 +63,11 @@ class NostrClientSubscriptionUntilEoseAsFlowTest : BaseNostrClientTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance the test dispatcher to ensure emissions are processed
|
|
||||||
while (feedStates.size < 10) {
|
while (feedStates.size < 10) {
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
job.cancel() // Cancel the collection job
|
job.cancel()
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
assertEquals(10, feedStates.size)
|
assertEquals(10, feedStates.size)
|
||||||
}
|
}
|
||||||
@@ -92,20 +76,12 @@ class NostrClientSubscriptionUntilEoseAsFlowTest : BaseNostrClientTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun testNostrClientSubscriptionUntilEoseAsFlowDebouncing() =
|
fun testNostrClientSubscriptionUntilEoseAsFlowDebouncing() =
|
||||||
runTest {
|
runTest {
|
||||||
relayHub.getOrCreate("ws://127.0.0.1:7770/").preload(
|
defaultRelay.preload(SyntheticEvents.batch(20, kind = MetadataEvent.KIND))
|
||||||
SyntheticEvents.batch(20, kind = MetadataEvent.KIND),
|
|
||||||
)
|
|
||||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
||||||
val client = NostrClient(socketBuilder, appScope)
|
|
||||||
|
|
||||||
val flow =
|
val flow =
|
||||||
client.fetchAsFlow(
|
client.fetchAsFlow(
|
||||||
relay = "ws://127.0.0.1:7770/",
|
relay = defaultRelayUrl,
|
||||||
filter =
|
filter = Filter(kinds = listOf(MetadataEvent.KIND), limit = 10),
|
||||||
Filter(
|
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
|
||||||
limit = 10,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var feedStates = listOf<Event>()
|
var feedStates = listOf<Event>()
|
||||||
@@ -117,16 +93,11 @@ class NostrClientSubscriptionUntilEoseAsFlowTest : BaseNostrClientTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance the test dispatcher to ensure emissions are processed
|
|
||||||
while (feedStates.size < 10) {
|
while (feedStates.size < 10) {
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
job.cancel() // Cancel the collection job
|
job.cancel()
|
||||||
|
|
||||||
client.disconnect()
|
|
||||||
appScope.cancel()
|
|
||||||
relayHub.close()
|
|
||||||
|
|
||||||
assertEquals(10, feedStates.size)
|
assertEquals(10, feedStates.size)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user