mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 09:40:40 +00:00
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]>
154 lines
4.7 KiB
Python
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"))
|