feat(namecoin): distinguish malformed-JSON values from missing-field
When a Namecoin record's value isn't valid JSON (a real failure mode
when an operator hand-builds the value and miscounts braces), the
NIP-05 path used to silently swallow the parser exception and surface
a misleading "no nostr field" message. That sends the publisher
chasing a phantom missing field when the actual problem is the value
itself.
Concrete case that triggered this: a `name_update` published a
474-byte d/testls value with one closing brace short of balanced. The
string parses up to the missing brace, after which kotlinx.serialization
throws "Unfinished JSON term at EOF at line 1, column 474". That error
was previously dropped, leaving the operator to debug "no nostr field"
without ever seeing the underlying JSON parse failure.
Changes:
- New NamecoinResolveOutcome.MalformedRecord(name, error). Distinct
from NoNostrField. The `error` field is the parser's own diagnostic
(e.g. "Unfinished JSON term at EOF at line 1, column 474") so the
publisher can locate the broken byte without spelunking.
- NamecoinNameResolver.performLookupDetailed: parse via a new
parseValueOrError helper and surface MalformedRecord instead of
collapsing into NoNostrField. Also rejects non-object top-level
values (arrays, primitives, null) with a useful diagnostic
("top-level value is JsonArray, expected JSON object").
- DesktopSearchScreen handles the new outcome by surfacing the parser
error verbatim in the Namecoin status banner, so the column number
reaches the publisher's screen.
Tests (commonTest / NamecoinImportTest):
- "NIP-05 lookup surfaces MalformedRecord with parser detail when
value is broken JSON": a deliberately one-brace-short value yields
MalformedRecord with a non-empty diagnostic.
- "NIP-05 lookup surfaces MalformedRecord when top-level value is a
JSON array": ensures non-object top-level values are rejected with
a useful "expected JSON object" message rather than silently
parsing as something unusable.
Tests don't pin the exact parser wording (kotlinx.serialization can
change it across versions); they only pin that the message is
attributed to JSON parsing rather than to a missing field.
This commit is contained in:
+56
@@ -502,6 +502,62 @@ class NamecoinImportTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NIP-05 lookup surfaces MalformedRecord with parser detail when value is broken JSON`() =
|
||||
runTest {
|
||||
// Real-world failure mode: a hand-built `name_update` with one
|
||||
// closing brace short of balanced ends up published as broken
|
||||
// JSON. The publisher then sees "name not found / no nostr
|
||||
// field" and chases a phantom bug. Surface the parser's
|
||||
// diagnostic so they know the value itself is the problem.
|
||||
val truncated =
|
||||
"""{"ip":"1.2.3.4","nostr":{"names":{"_":"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"}}""".trimIndent()
|
||||
val client =
|
||||
FakeElectrumXClient().apply {
|
||||
register("d/broken", truncated)
|
||||
}
|
||||
val resolver = NamecoinNameResolver(client, lookupTimeoutMs = 1_000L)
|
||||
val outcome = resolver.resolveDetailed("_@broken.bit")
|
||||
assertTrue(
|
||||
"expected MalformedRecord, got $outcome",
|
||||
outcome is NamecoinResolveOutcome.MalformedRecord,
|
||||
)
|
||||
outcome as NamecoinResolveOutcome.MalformedRecord
|
||||
assertEquals("d/broken", outcome.name)
|
||||
// Don't pin the exact parser text — different kotlinx.serialization
|
||||
// versions phrase it differently. Just require it's a non-empty
|
||||
// diagnostic.
|
||||
assertTrue(
|
||||
"error must be a non-empty diagnostic: ${outcome.error}",
|
||||
outcome.error.isNotBlank(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NIP-05 lookup surfaces MalformedRecord when top-level value is a JSON array`() =
|
||||
runTest {
|
||||
// Top-level non-object values (arrays, primitives, null) are
|
||||
// not valid Domain Name Objects per ifa-0001. They should be
|
||||
// rejected with a useful diagnostic, not collapsed into
|
||||
// NoNostrField (which implies the JSON parsed fine).
|
||||
val client =
|
||||
FakeElectrumXClient().apply {
|
||||
register("d/arr", """["not","an","object"]""")
|
||||
}
|
||||
val resolver = NamecoinNameResolver(client, lookupTimeoutMs = 1_000L)
|
||||
val outcome = resolver.resolveDetailed("_@arr.bit")
|
||||
assertTrue(
|
||||
"expected MalformedRecord, got $outcome",
|
||||
outcome is NamecoinResolveOutcome.MalformedRecord,
|
||||
)
|
||||
outcome as NamecoinResolveOutcome.MalformedRecord
|
||||
assertEquals("d/arr", outcome.name)
|
||||
assertTrue(
|
||||
"error must mention the unexpected top-level shape: ${outcome.error}",
|
||||
outcome.error.contains("expected JSON object", ignoreCase = true),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
private fun parse(s: String): JsonObject = json.parseToJsonElement(s).jsonObject
|
||||
|
||||
Reference in New Issue
Block a user