Files
VoxVera/tests/test_nostr_schema.py
thePR0M3TH3ANandClaude Opus 4.8 9b03ada43a Flyers: edit, re-publish (keep-alive), and delete (tombstone + NIP-09)
Authors can now manage a published flyer from the editor when it's loaded from
an event (author-only — the active signing key must match the flyer's author):

- Edit: change fields and publish again; the same d-tag replaces in place.
- Re-publish (keep-alive): re-sign + re-broadcast the current content so relays
  that prune app-data don't age the flyer out. Partially addresses roadmap #4.
- Delete: belt-and-suspenders across relay behaviors —
  1) a replaceable TOMBSTONE (same kind+d, payload deleted:true + a ["deleted"]
     tag) that overwrites the flyer's content, so the board/viewer hide it even
     on relays that ignore deletions, and
  2) a NIP-09 deletion request (kind 5) for the addressable a-tag + event id.
  Confirmed via a modal; framed as best-effort, not a guarantee.

Tombstone recognition: the board's parseFlyers keeps the newest event per
(pubkey, d) and skips it when that newest version is a tombstone (so a delete
can't be reversed by an older copy arriving later); the viewer renders a
localized "This flyer was removed." state.

Adds the loaded-flyer actions UI + a delete-confirm modal, 11 new strings ×
14 languages, a Python schema test for the tombstone payload, and e2e for the
delete flow, board hiding, and the viewer removed state. 216 e2e across all six
engines; docs (spec/roadmap/README) updated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-10 19:39:23 -04:00

154 lines
4.7 KiB
Python

import json
import pytest
from voxvera.nostr import NostrValidationError, normalize_event_source, validate_event_source
def _payload(**overrides):
payload = {
"type": "voxvera_flyer",
"version": 1,
"name": "Nostr Flyer",
"folder_name": "nostr-flyer",
"lang": "en",
"title": "NOSTR SOURCE TEST",
"subtitle": "A static source event",
"headline": "Publish once, render anywhere",
"content": "This flyer came from a Nostr event.",
"url": "https://voxvera.org",
"tear_off_link": "https://client.example/nostr/?addr=naddr1example",
"url_message": "Open this poster",
"footer_message": "share truth",
}
payload.update(overrides)
return payload
def _event(payload=None, **overrides):
event = {
"kind": 30078,
"content": json.dumps(payload or _payload()),
"tags": [
["d", "voxvera:nostr-flyer"],
["t", "voxvera"],
["t", "flyer"],
["language", "en"],
["L", "ISO-639-1"],
["l", "en", "ISO-639-1"],
],
}
event.update(overrides)
return event
def test_validate_accepts_direct_payload():
assert validate_event_source(_payload())["title"] == "NOSTR SOURCE TEST"
def test_validate_accepts_nostr_event_payload():
assert validate_event_source(_event())["content"] == "This flyer came from a Nostr event."
def test_validate_accepts_deletion_tombstone():
# A deleted flyer is replaced (same d-tag) with a minimal tombstone payload
# carrying deleted: true. It is still a valid voxvera_flyer event.
tombstone = {
"type": "voxvera_flyer",
"version": 1,
"deleted": True,
"folder_name": "nostr-flyer",
"lang": "en",
}
event = _event(tombstone, tags=[
["d", "voxvera:nostr-flyer"],
["t", "voxvera"],
["t", "flyer"],
["deleted", ""],
["language", "en"],
])
payload = validate_event_source(event)
assert payload["deleted"] is True
def test_validate_uses_language_tag_when_payload_lang_is_missing():
payload = _payload()
payload.pop("lang")
event = _event(payload, tags=[
["d", "voxvera:nostr-flyer"],
["t", "voxvera"],
["t", "flyer"],
["language", "es"],
["L", "ISO-639-1"],
["l", "es", "ISO-639-1"],
])
assert validate_event_source(event)["lang"] == "es"
def test_normalize_maps_to_flyer_config_defaults():
defaults = {
"folder_name": "voxvera",
"lang": "en",
"tear_off_link": "",
"url": "",
}
config = normalize_event_source(_event(), defaults)
assert config["folder_name"] == "nostr-flyer"
assert config["lang"] == "en"
assert config["url"] == "https://voxvera.org"
assert config["tear_off_link"] == "https://client.example/nostr/?addr=naddr1example"
def test_normalize_keeps_content_url_separate_from_tear_off_link():
defaults = {"folder_name": "voxvera", "lang": "en", "tear_off_link": ""}
event = _event(_payload(
url="https://creator.example/action",
tear_off_link="https://client.example/nostr/?addr=naddr1example",
))
config = normalize_event_source(event, defaults)
assert config["url"] == "https://creator.example/action"
assert config["tear_off_link"] == "https://client.example/nostr/?addr=naddr1example"
def test_normalize_supports_content_url_qr_target():
defaults = {"folder_name": "voxvera", "lang": "en", "tear_off_link": ""}
event = _event(_payload(qr_target="content_url"))
config = normalize_event_source(event, defaults)
assert config["tear_off_link"] == "https://voxvera.org"
def test_normalize_supports_nostr_event_qr_target():
defaults = {"folder_name": "voxvera", "lang": "en", "tear_off_link": ""}
event = _event(_payload(qr_target="nostr_event"))
config = normalize_event_source(event, defaults, source_identifier="note1example")
assert config["tear_off_link"] == "note1example"
def test_validate_rejects_unsafe_url_scheme():
with pytest.raises(NostrValidationError, match="Unsupported URL scheme"):
validate_event_source(_payload(url="javascript:alert(1)"))
def test_validate_rejects_raw_html():
with pytest.raises(NostrValidationError, match="contains raw HTML"):
validate_event_source(_payload(content="<script>alert(1)</script>"))
def test_validate_rejects_missing_tags():
with pytest.raises(NostrValidationError, match="#voxvera and #flyer"):
validate_event_source(_event(tags=[["d", "voxvera:nostr-flyer"]]))
def test_validate_rejects_attachments():
with pytest.raises(NostrValidationError, match="Attachments are not supported"):
validate_event_source(_payload(attachment_path="file.zip"))