Implement strict NIP-01 event validation and canonical reply prefixes
This commit is contained in:
@@ -13,10 +13,10 @@ Implementation checklist for Parrhesia relay.
|
|||||||
|
|
||||||
- [x] Implement websocket endpoint + per-connection process
|
- [x] Implement websocket endpoint + per-connection process
|
||||||
- [x] Implement message decode/encode for `EVENT`, `REQ`, `CLOSE`
|
- [x] Implement message decode/encode for `EVENT`, `REQ`, `CLOSE`
|
||||||
- [ ] Implement strict event validation (`id`, `sig`, shape, timestamps)
|
- [x] Implement strict event validation (`id`, `sig`, shape, timestamps)
|
||||||
- [ ] Implement filter evaluation engine (AND/OR semantics)
|
- [ ] Implement filter evaluation engine (AND/OR semantics)
|
||||||
- [ ] Implement subscription lifecycle + `EOSE` behavior
|
- [ ] Implement subscription lifecycle + `EOSE` behavior
|
||||||
- [ ] Implement canonical `OK`, `NOTICE`, `CLOSED` responses + prefixes
|
- [x] Implement canonical `OK`, `NOTICE`, `CLOSED` responses + prefixes
|
||||||
|
|
||||||
## Phase 2 — storage boundary + postgres adapter
|
## Phase 2 — storage boundary + postgres adapter
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ config :parrhesia,
|
|||||||
max_frame_bytes: 1_048_576,
|
max_frame_bytes: 1_048_576,
|
||||||
max_event_bytes: 262_144,
|
max_event_bytes: 262_144,
|
||||||
max_filters_per_req: 16,
|
max_filters_per_req: 16,
|
||||||
max_subscriptions_per_connection: 32
|
max_subscriptions_per_connection: 32,
|
||||||
|
max_event_future_skew_seconds: 900
|
||||||
],
|
],
|
||||||
policies: [
|
policies: [
|
||||||
auth_required_for_writes: false,
|
auth_required_for_writes: false,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ defmodule Parrhesia.Protocol do
|
|||||||
Nostr protocol message decode/encode helpers.
|
Nostr protocol message decode/encode helpers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Parrhesia.Protocol.EventValidator
|
||||||
|
|
||||||
@type event :: map()
|
@type event :: map()
|
||||||
@type filter :: map()
|
@type filter :: map()
|
||||||
|
|
||||||
@@ -32,6 +34,14 @@ defmodule Parrhesia.Protocol do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec validate_event(event()) :: :ok | {:error, String.t()}
|
||||||
|
def validate_event(event) do
|
||||||
|
case EventValidator.validate(event) do
|
||||||
|
:ok -> :ok
|
||||||
|
{:error, reason} -> {:error, EventValidator.error_message(reason)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@spec encode_relay(relay_message()) :: binary()
|
@spec encode_relay(relay_message()) :: binary()
|
||||||
def encode_relay(message) do
|
def encode_relay(message) do
|
||||||
message
|
message
|
||||||
@@ -42,11 +52,11 @@ defmodule Parrhesia.Protocol do
|
|||||||
@spec decode_error_notice(decode_error()) :: String.t()
|
@spec decode_error_notice(decode_error()) :: String.t()
|
||||||
def decode_error_notice(reason) do
|
def decode_error_notice(reason) do
|
||||||
case reason do
|
case reason do
|
||||||
:invalid_json -> "error:invalid: malformed JSON"
|
:invalid_json -> "invalid: malformed JSON"
|
||||||
:invalid_message -> "error:invalid: unsupported message shape"
|
:invalid_message -> "invalid: unsupported message shape"
|
||||||
:invalid_event -> "error:invalid: invalid EVENT shape"
|
:invalid_event -> "invalid: invalid EVENT shape"
|
||||||
:invalid_subscription_id -> "error:invalid: invalid subscription id"
|
:invalid_subscription_id -> "invalid: invalid subscription id"
|
||||||
:invalid_filters -> "error:invalid: invalid filters"
|
:invalid_filters -> "invalid: invalid filters"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,15 +67,14 @@ defmodule Parrhesia.Protocol do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp decode_message(["EVENT", event]) do
|
defp decode_message(["EVENT", event]) when is_map(event), do: {:ok, {:event, event}}
|
||||||
case valid_event?(event) do
|
defp decode_message(["EVENT", _event]), do: {:error, :invalid_event}
|
||||||
true -> {:ok, {:event, event}}
|
|
||||||
false -> {:error, :invalid_event}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp decode_message(["REQ", subscription_id | filters]) when is_binary(subscription_id) do
|
defp decode_message(["REQ", subscription_id | filters]) when is_binary(subscription_id) do
|
||||||
cond do
|
cond do
|
||||||
|
not valid_subscription_id?(subscription_id) ->
|
||||||
|
{:error, :invalid_subscription_id}
|
||||||
|
|
||||||
filters == [] ->
|
filters == [] ->
|
||||||
{:error, :invalid_filters}
|
{:error, :invalid_filters}
|
||||||
|
|
||||||
@@ -81,33 +90,23 @@ defmodule Parrhesia.Protocol do
|
|||||||
do: {:error, :invalid_subscription_id}
|
do: {:error, :invalid_subscription_id}
|
||||||
|
|
||||||
defp decode_message(["CLOSE", subscription_id]) when is_binary(subscription_id) do
|
defp decode_message(["CLOSE", subscription_id]) when is_binary(subscription_id) do
|
||||||
{:ok, {:close, subscription_id}}
|
if valid_subscription_id?(subscription_id) do
|
||||||
|
{:ok, {:close, subscription_id}}
|
||||||
|
else
|
||||||
|
{:error, :invalid_subscription_id}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp decode_message(["CLOSE", _subscription_id]), do: {:error, :invalid_subscription_id}
|
defp decode_message(["CLOSE", _subscription_id]), do: {:error, :invalid_subscription_id}
|
||||||
defp decode_message(_other), do: {:error, :invalid_message}
|
defp decode_message(_other), do: {:error, :invalid_message}
|
||||||
|
|
||||||
defp valid_event?(%{
|
|
||||||
"id" => id,
|
|
||||||
"pubkey" => pubkey,
|
|
||||||
"created_at" => created_at,
|
|
||||||
"kind" => kind,
|
|
||||||
"tags" => tags,
|
|
||||||
"content" => content,
|
|
||||||
"sig" => sig
|
|
||||||
}) do
|
|
||||||
is_binary(id) and is_binary(pubkey) and is_integer(created_at) and is_integer(kind) and
|
|
||||||
is_list(tags) and Enum.all?(tags, &valid_tag?/1) and is_binary(content) and is_binary(sig)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp valid_event?(_other), do: false
|
|
||||||
|
|
||||||
defp valid_tag?(tag) when is_list(tag), do: Enum.all?(tag, &is_binary/1)
|
|
||||||
defp valid_tag?(_other), do: false
|
|
||||||
|
|
||||||
defp relay_frame({:notice, message}), do: ["NOTICE", message]
|
defp relay_frame({:notice, message}), do: ["NOTICE", message]
|
||||||
defp relay_frame({:ok, event_id, accepted, message}), do: ["OK", event_id, accepted, message]
|
defp relay_frame({:ok, event_id, accepted, message}), do: ["OK", event_id, accepted, message]
|
||||||
defp relay_frame({:closed, subscription_id, message}), do: ["CLOSED", subscription_id, message]
|
defp relay_frame({:closed, subscription_id, message}), do: ["CLOSED", subscription_id, message]
|
||||||
defp relay_frame({:eose, subscription_id}), do: ["EOSE", subscription_id]
|
defp relay_frame({:eose, subscription_id}), do: ["EOSE", subscription_id]
|
||||||
defp relay_frame({:event, subscription_id, event}), do: ["EVENT", subscription_id, event]
|
defp relay_frame({:event, subscription_id, event}), do: ["EVENT", subscription_id, event]
|
||||||
|
|
||||||
|
defp valid_subscription_id?(subscription_id) do
|
||||||
|
subscription_id != "" and String.length(subscription_id) <= 64
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
150
lib/parrhesia/protocol/event_validator.ex
Normal file
150
lib/parrhesia/protocol/event_validator.ex
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
defmodule Parrhesia.Protocol.EventValidator do
|
||||||
|
@moduledoc """
|
||||||
|
Strict NIP-01 event validation helpers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@required_fields ~w[id pubkey created_at kind tags content sig]
|
||||||
|
@max_kind 65_535
|
||||||
|
@default_max_event_future_skew_seconds 900
|
||||||
|
|
||||||
|
@type error_reason ::
|
||||||
|
:invalid_shape
|
||||||
|
| :invalid_id
|
||||||
|
| :invalid_pubkey
|
||||||
|
| :invalid_created_at
|
||||||
|
| :created_at_too_far_in_future
|
||||||
|
| :invalid_kind
|
||||||
|
| :invalid_tags
|
||||||
|
| :invalid_content
|
||||||
|
| :invalid_sig
|
||||||
|
| :invalid_id_hash
|
||||||
|
|
||||||
|
@spec validate(map()) :: :ok | {:error, error_reason()}
|
||||||
|
def validate(event) when is_map(event) do
|
||||||
|
with :ok <- validate_required_fields(event),
|
||||||
|
:ok <- validate_id_shape(event["id"]),
|
||||||
|
:ok <- validate_pubkey(event["pubkey"]),
|
||||||
|
:ok <- validate_created_at(event["created_at"]),
|
||||||
|
:ok <- validate_kind(event["kind"]),
|
||||||
|
:ok <- validate_tags(event["tags"]),
|
||||||
|
:ok <- validate_content(event["content"]),
|
||||||
|
:ok <- validate_sig(event["sig"]) do
|
||||||
|
validate_id_hash(event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate(_event), do: {:error, :invalid_shape}
|
||||||
|
|
||||||
|
@spec compute_id(map()) :: String.t()
|
||||||
|
def compute_id(event) do
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
event["pubkey"],
|
||||||
|
event["created_at"],
|
||||||
|
event["kind"],
|
||||||
|
event["tags"],
|
||||||
|
event["content"]
|
||||||
|
]
|
||||||
|
|> Jason.encode!()
|
||||||
|
|> then(&:crypto.hash(:sha256, &1))
|
||||||
|
|> Base.encode16(case: :lower)
|
||||||
|
end
|
||||||
|
|
||||||
|
@error_messages %{
|
||||||
|
invalid_shape:
|
||||||
|
"invalid: event must include id, pubkey, created_at, kind, tags, content, and sig",
|
||||||
|
invalid_id: "invalid: id must be 32-byte lowercase hex",
|
||||||
|
invalid_pubkey: "invalid: pubkey must be 32-byte lowercase hex",
|
||||||
|
invalid_created_at: "invalid: created_at must be a non-negative integer unix timestamp",
|
||||||
|
created_at_too_far_in_future:
|
||||||
|
"invalid: event creation date is too far off from the current time",
|
||||||
|
invalid_kind: "invalid: kind must be an integer between 0 and 65535",
|
||||||
|
invalid_tags: "invalid: tags must be an array of non-empty string arrays",
|
||||||
|
invalid_content: "invalid: content must be a string",
|
||||||
|
invalid_sig: "invalid: sig must be 64-byte lowercase hex",
|
||||||
|
invalid_id_hash: "invalid: event id does not match serialized event"
|
||||||
|
}
|
||||||
|
|
||||||
|
@spec error_message(error_reason()) :: String.t()
|
||||||
|
def error_message(reason), do: Map.fetch!(@error_messages, reason)
|
||||||
|
|
||||||
|
defp validate_required_fields(event) do
|
||||||
|
if Enum.all?(@required_fields, &Map.has_key?(event, &1)) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error, :invalid_shape}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_id_shape(id) when is_binary(id) do
|
||||||
|
if lowercase_hex?(id, 32), do: :ok, else: {:error, :invalid_id}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_id_shape(_id), do: {:error, :invalid_id}
|
||||||
|
|
||||||
|
defp validate_pubkey(pubkey) when is_binary(pubkey) do
|
||||||
|
if lowercase_hex?(pubkey, 32), do: :ok, else: {:error, :invalid_pubkey}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_pubkey(_pubkey), do: {:error, :invalid_pubkey}
|
||||||
|
|
||||||
|
defp validate_created_at(created_at) when is_integer(created_at) and created_at >= 0 do
|
||||||
|
now = System.system_time(:second)
|
||||||
|
max_future_skew = max_event_future_skew_seconds()
|
||||||
|
|
||||||
|
if created_at <= now + max_future_skew do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error, :created_at_too_far_in_future}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_created_at(_created_at), do: {:error, :invalid_created_at}
|
||||||
|
|
||||||
|
defp validate_kind(kind) when is_integer(kind) and kind >= 0 and kind <= @max_kind, do: :ok
|
||||||
|
defp validate_kind(_kind), do: {:error, :invalid_kind}
|
||||||
|
|
||||||
|
defp validate_tags(tags) when is_list(tags) do
|
||||||
|
if Enum.all?(tags, &valid_tag?/1) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error, :invalid_tags}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_tags(_tags), do: {:error, :invalid_tags}
|
||||||
|
|
||||||
|
defp validate_content(content) when is_binary(content), do: :ok
|
||||||
|
defp validate_content(_content), do: {:error, :invalid_content}
|
||||||
|
|
||||||
|
defp validate_sig(sig) when is_binary(sig) do
|
||||||
|
if lowercase_hex?(sig, 64), do: :ok, else: {:error, :invalid_sig}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_sig(_sig), do: {:error, :invalid_sig}
|
||||||
|
|
||||||
|
defp validate_id_hash(event) do
|
||||||
|
if event["id"] == compute_id(event) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error, :invalid_id_hash}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp valid_tag?(tag) when is_list(tag) do
|
||||||
|
tag != [] and Enum.all?(tag, &is_binary/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp valid_tag?(_tag), do: false
|
||||||
|
|
||||||
|
defp lowercase_hex?(value, bytes) do
|
||||||
|
byte_size(value) == bytes * 2 and
|
||||||
|
match?({:ok, _decoded}, Base.decode16(value, case: :lower))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp max_event_future_skew_seconds do
|
||||||
|
:parrhesia
|
||||||
|
|> Application.get_env(:limits, [])
|
||||||
|
|> Keyword.get(:max_event_future_skew_seconds, @default_max_event_future_skew_seconds)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -26,12 +26,13 @@ defmodule Parrhesia.Web.Connection do
|
|||||||
event_id = Map.get(event, "id", "")
|
event_id = Map.get(event, "id", "")
|
||||||
|
|
||||||
response =
|
response =
|
||||||
Protocol.encode_relay({
|
case Protocol.validate_event(event) do
|
||||||
:ok,
|
:ok ->
|
||||||
event_id,
|
Protocol.encode_relay({:ok, event_id, false, "error: EVENT ingest not implemented"})
|
||||||
false,
|
|
||||||
"error:unsupported: EVENT ingest not implemented"
|
{:error, message} ->
|
||||||
})
|
Protocol.encode_relay({:ok, event_id, false, message})
|
||||||
|
end
|
||||||
|
|
||||||
{:push, {:text, response}, state}
|
{:push, {:text, response}, state}
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ defmodule Parrhesia.Web.Connection do
|
|||||||
next_state = drop_subscription(state, subscription_id)
|
next_state = drop_subscription(state, subscription_id)
|
||||||
|
|
||||||
response =
|
response =
|
||||||
Protocol.encode_relay({:closed, subscription_id, "closed: subscription closed"})
|
Protocol.encode_relay({:closed, subscription_id, "error: subscription closed"})
|
||||||
|
|
||||||
{:push, {:text, response}, next_state}
|
{:push, {:text, response}, next_state}
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ defmodule Parrhesia.Web.Connection do
|
|||||||
@impl true
|
@impl true
|
||||||
def handle_in({_payload, [opcode: :binary]}, %__MODULE__{} = state) do
|
def handle_in({_payload, [opcode: :binary]}, %__MODULE__{} = state) do
|
||||||
response =
|
response =
|
||||||
Protocol.encode_relay({:notice, "error:invalid: binary websocket frames are not supported"})
|
Protocol.encode_relay({:notice, "invalid: binary websocket frames are not supported"})
|
||||||
|
|
||||||
{:push, {:text, response}, state}
|
{:push, {:text, response}, state}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ defmodule Parrhesia.ConfigTest do
|
|||||||
test "returns configured relay limits/policies/features" do
|
test "returns configured relay limits/policies/features" do
|
||||||
assert Parrhesia.Config.get([:limits, :max_frame_bytes]) == 1_048_576
|
assert Parrhesia.Config.get([:limits, :max_frame_bytes]) == 1_048_576
|
||||||
assert Parrhesia.Config.get([:limits, :max_event_bytes]) == 262_144
|
assert Parrhesia.Config.get([:limits, :max_event_bytes]) == 262_144
|
||||||
|
assert Parrhesia.Config.get([:limits, :max_event_future_skew_seconds]) == 900
|
||||||
assert Parrhesia.Config.get([:policies, :auth_required_for_writes]) == false
|
assert Parrhesia.Config.get([:policies, :auth_required_for_writes]) == false
|
||||||
assert Parrhesia.Config.get([:features, :nip_ee_mls]) == false
|
assert Parrhesia.Config.get([:features, :nip_ee_mls]) == false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,21 +2,10 @@ defmodule Parrhesia.ProtocolTest do
|
|||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
alias Parrhesia.Protocol
|
alias Parrhesia.Protocol
|
||||||
|
alias Parrhesia.Protocol.EventValidator
|
||||||
|
|
||||||
test "decodes valid EVENT frame" do
|
test "decodes EVENT frame shape" do
|
||||||
payload =
|
payload = Jason.encode!(["EVENT", valid_event()])
|
||||||
Jason.encode!([
|
|
||||||
"EVENT",
|
|
||||||
%{
|
|
||||||
"id" => String.duplicate("0", 64),
|
|
||||||
"pubkey" => String.duplicate("1", 64),
|
|
||||||
"created_at" => 1_715_000_000,
|
|
||||||
"kind" => 1,
|
|
||||||
"tags" => [["p", String.duplicate("2", 64)]],
|
|
||||||
"content" => "hello",
|
|
||||||
"sig" => String.duplicate("3", 128)
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
assert {:ok, {:event, event}} = Protocol.decode_client(payload)
|
assert {:ok, {:event, event}} = Protocol.decode_client(payload)
|
||||||
assert event["kind"] == 1
|
assert event["kind"] == 1
|
||||||
@@ -33,16 +22,58 @@ defmodule Parrhesia.ProtocolTest do
|
|||||||
assert {:ok, {:close, "sub-1"}} = Protocol.decode_client(close_payload)
|
assert {:ok, {:close, "sub-1"}} = Protocol.decode_client(close_payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "rejects invalid subscription ids" do
|
||||||
|
empty_sub_payload = Jason.encode!(["REQ", "", %{"kinds" => [1]}])
|
||||||
|
long_sub_payload = Jason.encode!(["CLOSE", String.duplicate("x", 65)])
|
||||||
|
|
||||||
|
assert {:error, :invalid_subscription_id} = Protocol.decode_client(empty_sub_payload)
|
||||||
|
assert {:error, :invalid_subscription_id} = Protocol.decode_client(long_sub_payload)
|
||||||
|
end
|
||||||
|
|
||||||
test "returns decode errors for malformed messages" do
|
test "returns decode errors for malformed messages" do
|
||||||
assert {:error, :invalid_json} = Protocol.decode_client("not-json")
|
assert {:error, :invalid_json} = Protocol.decode_client("not-json")
|
||||||
assert {:error, :invalid_filters} = Protocol.decode_client(Jason.encode!(["REQ", "sub-1"]))
|
assert {:error, :invalid_filters} = Protocol.decode_client(Jason.encode!(["REQ", "sub-1"]))
|
||||||
|
|
||||||
assert {:error, :invalid_event} =
|
assert {:error, :invalid_event} =
|
||||||
Protocol.decode_client(Jason.encode!(["EVENT", %{"id" => "nope"}]))
|
Protocol.decode_client(Jason.encode!(["EVENT", "not-a-map"]))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates strict NIP-01 event fields" do
|
||||||
|
event = valid_event()
|
||||||
|
assert :ok = Protocol.validate_event(event)
|
||||||
|
|
||||||
|
future_event = Map.put(event, "created_at", System.system_time(:second) + 3_600)
|
||||||
|
future_event = Map.put(future_event, "id", EventValidator.compute_id(future_event))
|
||||||
|
|
||||||
|
assert {:error, "invalid: event creation date is too far off from the current time"} =
|
||||||
|
Protocol.validate_event(future_event)
|
||||||
|
|
||||||
|
mismatched_id_event = Map.put(event, "id", String.duplicate("f", 64))
|
||||||
|
|
||||||
|
assert {:error, "invalid: event id does not match serialized event"} =
|
||||||
|
Protocol.validate_event(mismatched_id_event)
|
||||||
|
|
||||||
|
bad_sig_event = Map.put(event, "sig", "abc")
|
||||||
|
|
||||||
|
assert {:error, "invalid: sig must be 64-byte lowercase hex"} =
|
||||||
|
Protocol.validate_event(bad_sig_event)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "encodes relay messages" do
|
test "encodes relay messages" do
|
||||||
frame = Protocol.encode_relay({:closed, "sub-1", "closed: subscription closed"})
|
frame = Protocol.encode_relay({:closed, "sub-1", "error: subscription closed"})
|
||||||
assert Jason.decode!(frame) == ["CLOSED", "sub-1", "closed: subscription closed"]
|
assert Jason.decode!(frame) == ["CLOSED", "sub-1", "error: subscription closed"]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp valid_event do
|
||||||
|
base_event = %{
|
||||||
|
"pubkey" => String.duplicate("1", 64),
|
||||||
|
"created_at" => System.system_time(:second),
|
||||||
|
"kind" => 1,
|
||||||
|
"tags" => [["p", String.duplicate("2", 64)]],
|
||||||
|
"content" => "hello",
|
||||||
|
"sig" => String.duplicate("3", 128)
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.put(base_event, "id", EventValidator.compute_id(base_event))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
defmodule Parrhesia.Web.ConnectionTest do
|
defmodule Parrhesia.Web.ConnectionTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Parrhesia.Protocol.EventValidator
|
||||||
alias Parrhesia.Web.Connection
|
alias Parrhesia.Web.Connection
|
||||||
|
|
||||||
test "REQ registers subscription and replies with EOSE" do
|
test "REQ registers subscription and replies with EOSE" do
|
||||||
@@ -27,7 +28,7 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
Connection.handle_in({close_payload, [opcode: :text]}, subscribed_state)
|
Connection.handle_in({close_payload, [opcode: :text]}, subscribed_state)
|
||||||
|
|
||||||
refute MapSet.member?(next_state.subscriptions, "sub-123")
|
refute MapSet.member?(next_state.subscriptions, "sub-123")
|
||||||
assert Jason.decode!(response) == ["CLOSED", "sub-123", "closed: subscription closed"]
|
assert Jason.decode!(response) == ["CLOSED", "sub-123", "error: subscription closed"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "invalid input returns NOTICE" do
|
test "invalid input returns NOTICE" do
|
||||||
@@ -36,34 +37,53 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
assert {:push, {:text, response}, ^state} =
|
assert {:push, {:text, response}, ^state} =
|
||||||
Connection.handle_in({"not-json", [opcode: :text]}, state)
|
Connection.handle_in({"not-json", [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(response) == ["NOTICE", "error:invalid: malformed JSON"]
|
assert Jason.decode!(response) == ["NOTICE", "invalid: malformed JSON"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "EVENT currently replies with unsupported OK" do
|
test "valid EVENT currently replies with unsupported OK" do
|
||||||
{:ok, state} = Connection.init(%{})
|
{:ok, state} = Connection.init(%{})
|
||||||
|
|
||||||
payload =
|
event = valid_event()
|
||||||
Jason.encode!([
|
payload = Jason.encode!(["EVENT", event])
|
||||||
"EVENT",
|
|
||||||
%{
|
|
||||||
"id" => String.duplicate("0", 64),
|
|
||||||
"pubkey" => String.duplicate("1", 64),
|
|
||||||
"created_at" => 1_715_000_000,
|
|
||||||
"kind" => 1,
|
|
||||||
"tags" => [],
|
|
||||||
"content" => "hello",
|
|
||||||
"sig" => String.duplicate("3", 128)
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
assert {:push, {:text, response}, ^state} =
|
assert {:push, {:text, response}, ^state} =
|
||||||
Connection.handle_in({payload, [opcode: :text]}, state)
|
Connection.handle_in({payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(response) == [
|
assert Jason.decode!(response) == [
|
||||||
"OK",
|
"OK",
|
||||||
String.duplicate("0", 64),
|
event["id"],
|
||||||
false,
|
false,
|
||||||
"error:unsupported: EVENT ingest not implemented"
|
"error: EVENT ingest not implemented"
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "invalid EVENT replies with OK false invalid prefix" do
|
||||||
|
{:ok, state} = Connection.init(%{})
|
||||||
|
|
||||||
|
event = valid_event() |> Map.put("sig", "nope")
|
||||||
|
payload = Jason.encode!(["EVENT", event])
|
||||||
|
|
||||||
|
assert {:push, {:text, response}, ^state} =
|
||||||
|
Connection.handle_in({payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
|
assert Jason.decode!(response) == [
|
||||||
|
"OK",
|
||||||
|
event["id"],
|
||||||
|
false,
|
||||||
|
"invalid: sig must be 64-byte lowercase hex"
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp valid_event do
|
||||||
|
base_event = %{
|
||||||
|
"pubkey" => String.duplicate("1", 64),
|
||||||
|
"created_at" => System.system_time(:second),
|
||||||
|
"kind" => 1,
|
||||||
|
"tags" => [],
|
||||||
|
"content" => "hello",
|
||||||
|
"sig" => String.duplicate("3", 128)
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.put(base_event, "id", EventValidator.compute_id(base_event))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user