test(nostr): improve and unit test Blossom.parseServerList (#613)
* chore(test): use version catalog for test deps * refactor(nostr): require event kind 10063 in `Blossom.parseServerList` `Blossom.parseServerList` currently has a single caller: `BlossomRepository.updateFromEvent`. That caller already ignores non-kind 10063 events before invoking the method, so the added `require` does not change repository behavior. The method already assumed kind 10063 input; by making this explicit, the requirement is documented, testable, and prevents future callers missing it. * feat(nostr): ignore invalid urls in `Blossom.parseServerList` `Blossom.parseServerList` returns a list of advertised servers which must be valid urls as per BUD3. * feat(nostr): return distinct blossom server urls `Blossom.parseServerList` returns distinct server urls parsed from the events tags. Duplicated list items are not useful and can lead to bugs or unnecessary overhead if not anticipated. * chore(nostr): document `Blossom.parseServerList` * test(nostr): unit test Blossom.parseServerList
This commit is contained in:
@@ -84,7 +84,8 @@ android {
|
||||
|
||||
dependencies {
|
||||
testImplementation(libs.kotlinx.serialization.json)
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.kotlin.test)
|
||||
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.ui)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.wisp.app.nostr
|
||||
|
||||
import android.util.Base64
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import java.security.MessageDigest
|
||||
|
||||
object Blossom {
|
||||
@@ -8,10 +9,28 @@ object Blossom {
|
||||
const val KIND_AUTH = 24242
|
||||
const val DEFAULT_SERVER = "https://blossom.primal.net"
|
||||
|
||||
/**
|
||||
* Extract Blossom server URLs from the events tags and returns them as a list.
|
||||
*
|
||||
* The event must be a Blossom server list event (BUD-03)(10063). Tags that are not defined by
|
||||
* BUD-03 or are invalid (i.e. invalid URL) are ignored. The returned list is deduplicated and
|
||||
* ordered by first occurrence in the event.
|
||||
*
|
||||
* @throws IllegalArgumentException if [event] is not kind [KIND_SERVER_LIST]
|
||||
*/
|
||||
fun parseServerList(event: NostrEvent): List<String> {
|
||||
return event.tags.mapNotNull { tag ->
|
||||
if (tag.size >= 2 && tag[0] == "server") tag[1] else null
|
||||
require(event.kind == KIND_SERVER_LIST) {
|
||||
"Expected kind $KIND_SERVER_LIST event, got kind ${event.kind}"
|
||||
}
|
||||
return event.tags.mapNotNull { tag ->
|
||||
if (tag.size >= 2 && tag[0] == "server") {
|
||||
val url = tag[1].toHttpUrlOrNull()
|
||||
url?.toString()
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
}.distinct()
|
||||
}
|
||||
|
||||
fun buildServerListTags(urls: List<String>): List<List<String>> {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.wisp.app.nostr
|
||||
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class BlossomTest {
|
||||
private fun eventWith(
|
||||
kind: Int = Blossom.KIND_SERVER_LIST,
|
||||
tags: List<List<String>> = emptyList()
|
||||
): NostrEvent {
|
||||
return NostrEvent.createUnsigned(
|
||||
pubkeyHex = "0".repeat(64),
|
||||
kind = kind,
|
||||
content = "",
|
||||
tags = tags,
|
||||
createdAt = 1L
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws IllegalArgumentException when event kind is not server list`() {
|
||||
val event = eventWith(
|
||||
kind = 1,
|
||||
tags = emptyList()
|
||||
)
|
||||
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
Blossom.parseServerList(event)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns empty list when event has no tags`() {
|
||||
val event = eventWith(
|
||||
kind = Blossom.KIND_SERVER_LIST,
|
||||
tags = emptyList()
|
||||
)
|
||||
|
||||
val result = Blossom.parseServerList(event)
|
||||
|
||||
assertEquals(emptyList(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ignores empty too short and non server tags`() {
|
||||
val event = eventWith(
|
||||
kind = Blossom.KIND_SERVER_LIST,
|
||||
tags = listOf(
|
||||
emptyList(),
|
||||
listOf("server"),
|
||||
listOf("p", "abc"),
|
||||
listOf("serverish", "https://example.com")
|
||||
)
|
||||
)
|
||||
|
||||
val result = Blossom.parseServerList(event)
|
||||
|
||||
assertEquals(emptyList(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ignores server tags with invalid urls`() {
|
||||
val event = eventWith(
|
||||
kind = Blossom.KIND_SERVER_LIST,
|
||||
tags = listOf(
|
||||
listOf("server", ""),
|
||||
listOf("server", "not a url"),
|
||||
listOf("server", "ftp://example.com")
|
||||
)
|
||||
)
|
||||
|
||||
val result = Blossom.parseServerList(event)
|
||||
|
||||
assertEquals(emptyList(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns url from valid server tag`() {
|
||||
val event = eventWith(
|
||||
kind = Blossom.KIND_SERVER_LIST,
|
||||
tags = listOf(
|
||||
listOf("server", "https://example.com")
|
||||
)
|
||||
)
|
||||
|
||||
val result = Blossom.parseServerList(event)
|
||||
|
||||
assertEquals(listOf("https://example.com/"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns url from valid server tag with extra fields`() {
|
||||
val event = eventWith(
|
||||
kind = Blossom.KIND_SERVER_LIST,
|
||||
tags = listOf(
|
||||
listOf("server", "https://example.com", "extra")
|
||||
)
|
||||
)
|
||||
|
||||
val result = Blossom.parseServerList(event)
|
||||
|
||||
assertEquals(listOf("https://example.com/"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns distinct valid urls in first occurrence order`() {
|
||||
val event = eventWith(
|
||||
kind = Blossom.KIND_SERVER_LIST,
|
||||
tags = listOf(
|
||||
listOf("server", "https://b.com"),
|
||||
listOf("p", "abc"),
|
||||
listOf("server", "not a url"),
|
||||
listOf("server", "https://a.com"),
|
||||
listOf("server", "https://b.com"),
|
||||
listOf("server", "https://c.com")
|
||||
)
|
||||
)
|
||||
|
||||
val result = Blossom.parseServerList(event)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
"https://b.com/",
|
||||
"https://a.com/",
|
||||
"https://c.com/"
|
||||
),
|
||||
result
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
[versions]
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.0.21"
|
||||
agp = "8.13.2"
|
||||
compose-bom = "2024.12.01"
|
||||
@@ -34,6 +35,8 @@ compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
compose-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" }
|
||||
lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
|
||||
|
||||
Reference in New Issue
Block a user