docs+nix: add deployment README and align release packaging
This commit is contained in:
170
README.md
170
README.md
@@ -1,21 +1,165 @@
|
|||||||
# Parrhesia
|
# Parrhesia
|
||||||
|
|
||||||
**TODO: Add description**
|
Parrhesia is a Nostr relay server written in Elixir/OTP with PostgreSQL storage.
|
||||||
|
|
||||||
## Installation
|
It exposes:
|
||||||
|
- a WebSocket relay endpoint at `/relay`
|
||||||
|
- NIP-11 relay info on `GET /relay` with `Accept: application/nostr+json`
|
||||||
|
- operational HTTP endpoints (`/health`, `/ready`, `/metrics`)
|
||||||
|
- a NIP-86-style management API at `POST /management` (NIP-98 auth)
|
||||||
|
|
||||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
## Supported NIPs
|
||||||
by adding `parrhesia` to your list of dependencies in `mix.exs`:
|
|
||||||
|
|
||||||
```elixir
|
Current `supported_nips` list:
|
||||||
def deps do
|
|
||||||
[
|
`1, 9, 11, 13, 17, 40, 42, 43, 44, 45, 50, 59, 62, 66, 70, 77, 86, 98`
|
||||||
{:parrhesia, "~> 0.1.0"}
|
|
||||||
]
|
## Requirements
|
||||||
end
|
|
||||||
|
- Elixir `~> 1.19`
|
||||||
|
- Erlang/OTP 28
|
||||||
|
- PostgreSQL (18 used in the dev environment; 16+ recommended)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Run locally
|
||||||
|
|
||||||
|
### 1) Prepare the database
|
||||||
|
|
||||||
|
Parrhesia uses these defaults in `dev`:
|
||||||
|
- `PGDATABASE=parrhesia_dev`
|
||||||
|
- `PGHOST=localhost`
|
||||||
|
- `PGPORT=5432`
|
||||||
|
- `PGUSER=$USER`
|
||||||
|
|
||||||
|
Create the DB and run migrations/seeds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mix setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
### 2) Start the server
|
||||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
|
||||||
be found at <https://hexdocs.pm/parrhesia>.
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mix run --no-halt
|
||||||
|
```
|
||||||
|
|
||||||
|
Server listens on `http://localhost:4000` by default.
|
||||||
|
|
||||||
|
WebSocket clients should connect to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ws://localhost:4000/relay
|
||||||
|
```
|
||||||
|
|
||||||
|
### Useful endpoints
|
||||||
|
|
||||||
|
- `GET /health` -> `ok`
|
||||||
|
- `GET /ready` -> readiness status
|
||||||
|
- `GET /metrics` -> Prometheus metrics
|
||||||
|
- `GET /relay` + `Accept: application/nostr+json` -> NIP-11 document
|
||||||
|
- `POST /management` -> management API (requires NIP-98 auth)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production configuration
|
||||||
|
|
||||||
|
In `prod`, these environment variables are used:
|
||||||
|
|
||||||
|
- `DATABASE_URL` (**required**), e.g. `ecto://USER:PASS@HOST/parrhesia_prod`
|
||||||
|
- `POOL_SIZE` (optional, default `10`)
|
||||||
|
- `PORT` (optional, default `4000`)
|
||||||
|
|
||||||
|
`config/runtime.exs` reads these values at runtime in production releases.
|
||||||
|
|
||||||
|
### Typical relay config
|
||||||
|
|
||||||
|
Add/override in config files (for example in `config/prod.exs` or a `config/runtime.exs`):
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
config :parrhesia, Parrhesia.Web.Endpoint,
|
||||||
|
ip: {0, 0, 0, 0},
|
||||||
|
port: 4000
|
||||||
|
|
||||||
|
config :parrhesia,
|
||||||
|
limits: [
|
||||||
|
max_frame_bytes: 1_048_576,
|
||||||
|
max_event_bytes: 262_144,
|
||||||
|
max_filters_per_req: 16,
|
||||||
|
max_filter_limit: 500,
|
||||||
|
max_subscriptions_per_connection: 32,
|
||||||
|
max_event_future_skew_seconds: 900,
|
||||||
|
max_outbound_queue: 256,
|
||||||
|
outbound_drain_batch_size: 64,
|
||||||
|
outbound_overflow_strategy: :close
|
||||||
|
],
|
||||||
|
policies: [
|
||||||
|
auth_required_for_writes: false,
|
||||||
|
auth_required_for_reads: false,
|
||||||
|
min_pow_difficulty: 0,
|
||||||
|
accept_ephemeral_events: true,
|
||||||
|
mls_group_event_ttl_seconds: 300,
|
||||||
|
marmot_require_h_for_group_queries: true,
|
||||||
|
marmot_group_max_h_values_per_filter: 32,
|
||||||
|
marmot_group_max_query_window_seconds: 2_592_000,
|
||||||
|
marmot_media_max_imeta_tags_per_event: 8,
|
||||||
|
marmot_media_max_field_value_bytes: 1024,
|
||||||
|
marmot_media_max_url_bytes: 2048,
|
||||||
|
marmot_media_allowed_mime_prefixes: [],
|
||||||
|
marmot_media_reject_mip04_v1: true,
|
||||||
|
marmot_push_server_pubkeys: [],
|
||||||
|
marmot_push_max_relay_tags: 16,
|
||||||
|
marmot_push_max_payload_bytes: 65_536,
|
||||||
|
marmot_push_max_trigger_age_seconds: 120,
|
||||||
|
marmot_push_require_expiration: true,
|
||||||
|
marmot_push_max_expiration_window_seconds: 120,
|
||||||
|
marmot_push_max_server_recipients: 1
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
nip_45_count: true,
|
||||||
|
nip_50_search: true,
|
||||||
|
nip_77_negentropy: true,
|
||||||
|
marmot_push_notifications: false
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
### Option A: Elixir release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MIX_ENV=prod
|
||||||
|
export DATABASE_URL="ecto://USER:PASS@HOST/parrhesia_prod"
|
||||||
|
export POOL_SIZE=20
|
||||||
|
|
||||||
|
mix deps.get --only prod
|
||||||
|
mix compile
|
||||||
|
mix ecto.migrate
|
||||||
|
mix release
|
||||||
|
|
||||||
|
_build/prod/rel/parrhesia/bin/parrhesia foreground
|
||||||
|
```
|
||||||
|
|
||||||
|
For systemd/process managers, run the release command in foreground mode.
|
||||||
|
|
||||||
|
### Option B: Nix package (`default.nix`)
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix-build
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the built release from `./result/bin/parrhesia` (release command interface).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development quality checks
|
||||||
|
|
||||||
|
Before opening a PR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mix precommit
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
database_url =
|
# Production runtime configuration lives in config/runtime.exs.
|
||||||
System.get_env("DATABASE_URL") ||
|
|
||||||
raise "environment variable DATABASE_URL is missing. Example: ecto://USER:PASS@HOST/DATABASE"
|
|
||||||
|
|
||||||
config :parrhesia, Parrhesia.Repo,
|
|
||||||
url: database_url,
|
|
||||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
|
|
||||||
|
|||||||
14
config/runtime.exs
Normal file
14
config/runtime.exs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Config
|
||||||
|
|
||||||
|
if config_env() == :prod do
|
||||||
|
database_url =
|
||||||
|
System.get_env("DATABASE_URL") ||
|
||||||
|
raise "environment variable DATABASE_URL is missing. Example: ecto://USER:PASS@HOST/DATABASE"
|
||||||
|
|
||||||
|
config :parrhesia, Parrhesia.Repo,
|
||||||
|
url: database_url,
|
||||||
|
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
|
||||||
|
|
||||||
|
config :parrhesia, Parrhesia.Web.Endpoint,
|
||||||
|
port: String.to_integer(System.get_env("PORT") || "4000")
|
||||||
|
end
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"_build"
|
"_build"
|
||||||
"deps"
|
"deps"
|
||||||
"node_modules"
|
"node_modules"
|
||||||
"complement"
|
"marmot-ts"
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
lib.cleanSourceFilter path type
|
lib.cleanSourceFilter path type
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
beamPackages.fetchMixDeps {
|
beamPackages.fetchMixDeps {
|
||||||
pname = "${pname}-mix-deps";
|
pname = "${pname}-mix-deps";
|
||||||
inherit version src;
|
inherit version src;
|
||||||
hash = "sha256-eAk6FVjVPGcJl3adBSoQvPIva7w4xz4GuZ+DZaz6SYY=";
|
hash = "sha256-1v2+Q1MHbu09r5OBaLehiR+JfMP0Q5OHaWuwrQDzZJU=";
|
||||||
}
|
}
|
||||||
else null;
|
else null;
|
||||||
in
|
in
|
||||||
@@ -65,7 +65,7 @@ in
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
meta = with lib; {
|
meta = with lib; {
|
||||||
description = "Parrhesia Matrix homeserver";
|
description = "Parrhesia Nostr relay server";
|
||||||
license = licenses.asl20;
|
license = licenses.asl20;
|
||||||
platforms = platforms.unix;
|
platforms = platforms.unix;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ in {
|
|||||||
vips-mozjpeg
|
vips-mozjpeg
|
||||||
# Mermaid diagram generator
|
# Mermaid diagram generator
|
||||||
mermaid-cli
|
mermaid-cli
|
||||||
|
# Nostr CLI client
|
||||||
|
nak
|
||||||
];
|
];
|
||||||
|
|
||||||
# https://devenv.sh/tests/
|
# https://devenv.sh/tests/
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defmodule Parrhesia.Auth.Nip98 do
|
|||||||
def validate_authorization_header("Nostr " <> encoded_event, method, url)
|
def validate_authorization_header("Nostr " <> encoded_event, method, url)
|
||||||
when is_binary(method) and is_binary(url) do
|
when is_binary(method) and is_binary(url) do
|
||||||
with {:ok, event_json} <- decode_base64(encoded_event),
|
with {:ok, event_json} <- decode_base64(encoded_event),
|
||||||
{:ok, event} <- Jason.decode(event_json),
|
{:ok, event} <- JSON.decode(event_json),
|
||||||
:ok <- validate_event_shape(event),
|
:ok <- validate_event_shape(event),
|
||||||
:ok <- validate_http_binding(event, method, url) do
|
:ok <- validate_http_binding(event, method, url) do
|
||||||
{:ok, event}
|
{:ok, event}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ defmodule Parrhesia.Protocol do
|
|||||||
def encode_relay(message) do
|
def encode_relay(message) do
|
||||||
message
|
message
|
||||||
|> relay_frame()
|
|> relay_frame()
|
||||||
|> Jason.encode!()
|
|> JSON.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec decode_error_notice(decode_error()) :: String.t()
|
@spec decode_error_notice(decode_error()) :: String.t()
|
||||||
@@ -77,7 +77,7 @@ defmodule Parrhesia.Protocol do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp decode_json(payload) do
|
defp decode_json(payload) do
|
||||||
case Jason.decode(payload) do
|
case JSON.decode(payload) do
|
||||||
{:ok, decoded} -> {:ok, decoded}
|
{:ok, decoded} -> {:ok, decoded}
|
||||||
{:error, _reason} -> {:error, :invalid_json}
|
{:error, _reason} -> {:error, :invalid_json}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ defmodule Parrhesia.Protocol.EventValidator do
|
|||||||
event["tags"],
|
event["tags"],
|
||||||
event["content"]
|
event["content"]
|
||||||
]
|
]
|
||||||
|> Jason.encode!()
|
|> JSON.encode!()
|
||||||
|> then(&:crypto.hash(:sha256, &1))
|
|> then(&:crypto.hash(:sha256, &1))
|
||||||
|> Base.encode16(case: :lower)
|
|> Base.encode16(case: :lower)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ defmodule Parrhesia.Web.Connection do
|
|||||||
|
|
||||||
defp generate_hll_payload(filters, count) do
|
defp generate_hll_payload(filters, count) do
|
||||||
filters
|
filters
|
||||||
|> Jason.encode!()
|
|> JSON.encode!()
|
||||||
|> then(&"#{&1}:#{count}")
|
|> then(&"#{&1}:#{count}")
|
||||||
|> then(&:crypto.hash(:sha256, &1))
|
|> then(&:crypto.hash(:sha256, &1))
|
||||||
|> Base.encode64()
|
|> Base.encode64()
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ defmodule Parrhesia.Web.Management do
|
|||||||
defp normalize_result(result), do: %{"value" => inspect(result)}
|
defp normalize_result(result), do: %{"value" => inspect(result)}
|
||||||
|
|
||||||
defp send_json(conn, status, body) do
|
defp send_json(conn, status, body) do
|
||||||
encoded = Jason.encode!(body)
|
encoded = JSON.encode!(body)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/json")
|
|> put_resp_content_type("application/json")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ defmodule Parrhesia.Web.Router do
|
|||||||
plug(Plug.Parsers,
|
plug(Plug.Parsers,
|
||||||
parsers: [:json],
|
parsers: [:json],
|
||||||
pass: ["application/json"],
|
pass: ["application/json"],
|
||||||
json_decoder: Jason
|
json_decoder: JSON
|
||||||
)
|
)
|
||||||
|
|
||||||
plug(:match)
|
plug(:match)
|
||||||
@@ -43,7 +43,7 @@ defmodule Parrhesia.Web.Router do
|
|||||||
|
|
||||||
get "/relay" do
|
get "/relay" do
|
||||||
if accepts_nip11?(conn) do
|
if accepts_nip11?(conn) do
|
||||||
body = Jason.encode!(RelayInfo.document())
|
body = JSON.encode!(RelayInfo.document())
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/nostr+json")
|
|> put_resp_content_type("application/nostr+json")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ defmodule Parrhesia.Auth.Nip98Test do
|
|||||||
test "validates authorization header with matching method and url tags" do
|
test "validates authorization header with matching method and url tags" do
|
||||||
url = "http://example.com/management"
|
url = "http://example.com/management"
|
||||||
event = nip98_event("POST", url)
|
event = nip98_event("POST", url)
|
||||||
header = "Nostr " <> Base.encode64(Jason.encode!(event))
|
header = "Nostr " <> Base.encode64(JSON.encode!(event))
|
||||||
|
|
||||||
assert {:ok, parsed_event} = Nip98.validate_authorization_header(header, "POST", url)
|
assert {:ok, parsed_event} = Nip98.validate_authorization_header(header, "POST", url)
|
||||||
assert parsed_event["id"] == event["id"]
|
assert parsed_event["id"] == event["id"]
|
||||||
@@ -16,7 +16,7 @@ defmodule Parrhesia.Auth.Nip98Test do
|
|||||||
test "rejects mismatched method and url" do
|
test "rejects mismatched method and url" do
|
||||||
url = "http://example.com/management"
|
url = "http://example.com/management"
|
||||||
event = nip98_event("POST", url)
|
event = nip98_event("POST", url)
|
||||||
header = "Nostr " <> Base.encode64(Jason.encode!(event))
|
header = "Nostr " <> Base.encode64(JSON.encode!(event))
|
||||||
|
|
||||||
assert {:error, :invalid_method_tag} =
|
assert {:error, :invalid_method_tag} =
|
||||||
Nip98.validate_authorization_header(header, "GET", url)
|
Nip98.validate_authorization_header(header, "GET", url)
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ defmodule Parrhesia.FaultInjectionGroupFlowTest do
|
|||||||
"content" => Base.encode64("commit")
|
"content" => Base.encode64("commit")
|
||||||
})
|
})
|
||||||
|
|
||||||
payload = Jason.encode!(["EVENT", group_event])
|
payload = JSON.encode!(["EVENT", group_event])
|
||||||
|
|
||||||
assert {:push, {:text, error_response}, ^state} =
|
assert {:push, {:text, error_response}, ^state} =
|
||||||
Connection.handle_in({payload, [opcode: :text]}, state)
|
Connection.handle_in({payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(error_response) == ["OK", group_event["id"], false, "error: :db_down"]
|
assert JSON.decode!(error_response) == ["OK", group_event["id"], false, "error: :db_down"]
|
||||||
|
|
||||||
Application.put_env(
|
Application.put_env(
|
||||||
:parrhesia,
|
:parrhesia,
|
||||||
@@ -57,7 +57,7 @@ defmodule Parrhesia.FaultInjectionGroupFlowTest do
|
|||||||
assert {:push, {:text, ok_response}, ^state} =
|
assert {:push, {:text, ok_response}, ^state} =
|
||||||
Connection.handle_in({payload, [opcode: :text]}, state)
|
Connection.handle_in({payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(ok_response) == ["OK", group_event["id"], true, "ok: event stored"]
|
assert JSON.decode!(ok_response) == ["OK", group_event["id"], true, "ok: event stored"]
|
||||||
|
|
||||||
assert {:ok, persisted_group_event} = Storage.events().get_event(%{}, group_event["id"])
|
assert {:ok, persisted_group_event} = Storage.events().get_event(%{}, group_event["id"])
|
||||||
assert persisted_group_event["id"] == group_event["id"]
|
assert persisted_group_event["id"] == group_event["id"]
|
||||||
@@ -89,11 +89,11 @@ defmodule Parrhesia.FaultInjectionGroupFlowTest do
|
|||||||
|
|
||||||
assert {:push, {:text, outage_response}, ^state} =
|
assert {:push, {:text, outage_response}, ^state} =
|
||||||
Connection.handle_in(
|
Connection.handle_in(
|
||||||
{Jason.encode!(["EVENT", older_event]), [opcode: :text]},
|
{JSON.encode!(["EVENT", older_event]), [opcode: :text]},
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
|
|
||||||
assert Jason.decode!(outage_response) == ["OK", older_event["id"], false, "error: :db_down"]
|
assert JSON.decode!(outage_response) == ["OK", older_event["id"], false, "error: :db_down"]
|
||||||
|
|
||||||
Application.put_env(
|
Application.put_env(
|
||||||
:parrhesia,
|
:parrhesia,
|
||||||
@@ -103,19 +103,19 @@ defmodule Parrhesia.FaultInjectionGroupFlowTest do
|
|||||||
|
|
||||||
assert {:push, {:text, newer_response}, ^state} =
|
assert {:push, {:text, newer_response}, ^state} =
|
||||||
Connection.handle_in(
|
Connection.handle_in(
|
||||||
{Jason.encode!(["EVENT", newer_event]), [opcode: :text]},
|
{JSON.encode!(["EVENT", newer_event]), [opcode: :text]},
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
|
|
||||||
assert Jason.decode!(newer_response) == ["OK", newer_event["id"], true, "ok: event stored"]
|
assert JSON.decode!(newer_response) == ["OK", newer_event["id"], true, "ok: event stored"]
|
||||||
|
|
||||||
assert {:push, {:text, older_response}, ^state} =
|
assert {:push, {:text, older_response}, ^state} =
|
||||||
Connection.handle_in(
|
Connection.handle_in(
|
||||||
{Jason.encode!(["EVENT", older_event]), [opcode: :text]},
|
{JSON.encode!(["EVENT", older_event]), [opcode: :text]},
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
|
|
||||||
assert Jason.decode!(older_response) == ["OK", older_event["id"], true, "ok: event stored"]
|
assert JSON.decode!(older_response) == ["OK", older_event["id"], true, "ok: event stored"]
|
||||||
|
|
||||||
assert {:ok, results} =
|
assert {:ok, results} =
|
||||||
Storage.events().query(
|
Storage.events().query(
|
||||||
|
|||||||
@@ -30,19 +30,19 @@ defmodule Parrhesia.FaultInjectionTest do
|
|||||||
event = valid_event()
|
event = valid_event()
|
||||||
|
|
||||||
assert {:push, {:text, response}, ^state} =
|
assert {:push, {:text, response}, ^state} =
|
||||||
Connection.handle_in({Jason.encode!(["EVENT", event]), [opcode: :text]}, state)
|
Connection.handle_in({JSON.encode!(["EVENT", event]), [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(response) == ["OK", event["id"], false, "error: :db_down"]
|
assert JSON.decode!(response) == ["OK", event["id"], false, "error: :db_down"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "REQ closes with storage error when query fails" do
|
test "REQ closes with storage error when query fails" do
|
||||||
{:ok, state} = Connection.init(subscription_index: nil)
|
{:ok, state} = Connection.init(subscription_index: nil)
|
||||||
payload = Jason.encode!(["REQ", "sub-db-down", %{"kinds" => [1]}])
|
payload = JSON.encode!(["REQ", "sub-db-down", %{"kinds" => [1]}])
|
||||||
|
|
||||||
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) == ["CLOSED", "sub-db-down", "error: :db_down"]
|
assert JSON.decode!(response) == ["CLOSED", "sub-db-down", "error: :db_down"]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp valid_event do
|
defp valid_event do
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ defmodule Parrhesia.Performance.LoadSoakTest do
|
|||||||
|
|
||||||
{:ok, state} = Connection.init(subscription_index: nil, max_outbound_queue: 10_000)
|
{:ok, state} = Connection.init(subscription_index: nil, max_outbound_queue: 10_000)
|
||||||
|
|
||||||
req_payload = Jason.encode!(["REQ", "sub-load", %{"kinds" => [1]}])
|
req_payload = JSON.encode!(["REQ", "sub-load", %{"kinds" => [1]}])
|
||||||
|
|
||||||
assert {:push, _frames, subscribed_state} =
|
assert {:push, _frames, subscribed_state} =
|
||||||
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ defmodule Parrhesia.ProtocolTest do
|
|||||||
alias Parrhesia.Protocol.EventValidator
|
alias Parrhesia.Protocol.EventValidator
|
||||||
|
|
||||||
test "decodes EVENT frame shape" do
|
test "decodes EVENT frame shape" do
|
||||||
payload = Jason.encode!(["EVENT", valid_event()])
|
payload = JSON.encode!(["EVENT", valid_event()])
|
||||||
|
|
||||||
assert {:ok, {:event, event}} = Protocol.decode_client(payload)
|
assert {:ok, {:event, event}} = Protocol.decode_client(payload)
|
||||||
assert event["kind"] == 1
|
assert event["kind"] == 1
|
||||||
@@ -13,9 +13,9 @@ defmodule Parrhesia.ProtocolTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "decodes valid REQ, COUNT and CLOSE frames" do
|
test "decodes valid REQ, COUNT and CLOSE frames" do
|
||||||
req_payload = Jason.encode!(["REQ", "sub-1", %{"authors" => [String.duplicate("a", 64)]}])
|
req_payload = JSON.encode!(["REQ", "sub-1", %{"authors" => [String.duplicate("a", 64)]}])
|
||||||
count_payload = Jason.encode!(["COUNT", "sub-1", %{"kinds" => [1]}, %{"hll" => true}])
|
count_payload = JSON.encode!(["COUNT", "sub-1", %{"kinds" => [1]}, %{"hll" => true}])
|
||||||
close_payload = Jason.encode!(["CLOSE", "sub-1"])
|
close_payload = JSON.encode!(["CLOSE", "sub-1"])
|
||||||
|
|
||||||
assert {:ok, {:req, "sub-1", [%{"authors" => [_author]}]}} =
|
assert {:ok, {:req, "sub-1", [%{"authors" => [_author]}]}} =
|
||||||
Protocol.decode_client(req_payload)
|
Protocol.decode_client(req_payload)
|
||||||
@@ -27,8 +27,8 @@ defmodule Parrhesia.ProtocolTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "rejects invalid subscription ids" do
|
test "rejects invalid subscription ids" do
|
||||||
empty_sub_payload = Jason.encode!(["REQ", "", %{"kinds" => [1]}])
|
empty_sub_payload = JSON.encode!(["REQ", "", %{"kinds" => [1]}])
|
||||||
long_sub_payload = Jason.encode!(["CLOSE", String.duplicate("x", 65)])
|
long_sub_payload = JSON.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(empty_sub_payload)
|
||||||
assert {:error, :invalid_subscription_id} = Protocol.decode_client(long_sub_payload)
|
assert {:error, :invalid_subscription_id} = Protocol.decode_client(long_sub_payload)
|
||||||
@@ -39,25 +39,25 @@ defmodule Parrhesia.ProtocolTest do
|
|||||||
auth_event = Map.put(auth_event, "id", EventValidator.compute_id(auth_event))
|
auth_event = Map.put(auth_event, "id", EventValidator.compute_id(auth_event))
|
||||||
|
|
||||||
assert {:ok, {:auth, ^auth_event}} =
|
assert {:ok, {:auth, ^auth_event}} =
|
||||||
Protocol.decode_client(Jason.encode!(["AUTH", auth_event]))
|
Protocol.decode_client(JSON.encode!(["AUTH", auth_event]))
|
||||||
|
|
||||||
assert {:ok, {:neg_open, "sub-neg", %{"cursor" => 0}}} =
|
assert {:ok, {:neg_open, "sub-neg", %{"cursor" => 0}}} =
|
||||||
Protocol.decode_client(Jason.encode!(["NEG-OPEN", "sub-neg", %{"cursor" => 0}]))
|
Protocol.decode_client(JSON.encode!(["NEG-OPEN", "sub-neg", %{"cursor" => 0}]))
|
||||||
|
|
||||||
assert {:ok, {:neg_msg, "sub-neg", %{"delta" => "abc"}}} =
|
assert {:ok, {:neg_msg, "sub-neg", %{"delta" => "abc"}}} =
|
||||||
Protocol.decode_client(Jason.encode!(["NEG-MSG", "sub-neg", %{"delta" => "abc"}]))
|
Protocol.decode_client(JSON.encode!(["NEG-MSG", "sub-neg", %{"delta" => "abc"}]))
|
||||||
|
|
||||||
assert {:ok, {:neg_close, "sub-neg"}} =
|
assert {:ok, {:neg_close, "sub-neg"}} =
|
||||||
Protocol.decode_client(Jason.encode!(["NEG-CLOSE", "sub-neg"]))
|
Protocol.decode_client(JSON.encode!(["NEG-CLOSE", "sub-neg"]))
|
||||||
end
|
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(JSON.encode!(["REQ", "sub-1"]))
|
||||||
assert {:error, :invalid_count} = Protocol.decode_client(Jason.encode!(["COUNT", "sub-1"]))
|
assert {:error, :invalid_count} = Protocol.decode_client(JSON.encode!(["COUNT", "sub-1"]))
|
||||||
|
|
||||||
assert {:error, :invalid_event} =
|
assert {:error, :invalid_event} =
|
||||||
Protocol.decode_client(Jason.encode!(["EVENT", "not-a-map"]))
|
Protocol.decode_client(JSON.encode!(["EVENT", "not-a-map"]))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validates strict NIP-01 event fields" do
|
test "validates strict NIP-01 event fields" do
|
||||||
@@ -83,13 +83,13 @@ defmodule Parrhesia.ProtocolTest do
|
|||||||
|
|
||||||
test "encodes relay messages" do
|
test "encodes relay messages" do
|
||||||
frame = Protocol.encode_relay({:closed, "sub-1", "error: subscription closed"})
|
frame = Protocol.encode_relay({:closed, "sub-1", "error: subscription closed"})
|
||||||
assert Jason.decode!(frame) == ["CLOSED", "sub-1", "error: subscription closed"]
|
assert JSON.decode!(frame) == ["CLOSED", "sub-1", "error: subscription closed"]
|
||||||
|
|
||||||
auth_frame = Protocol.encode_relay({:auth, "challenge"})
|
auth_frame = Protocol.encode_relay({:auth, "challenge"})
|
||||||
assert Jason.decode!(auth_frame) == ["AUTH", "challenge"]
|
assert JSON.decode!(auth_frame) == ["AUTH", "challenge"]
|
||||||
|
|
||||||
count_frame = Protocol.encode_relay({:count, "sub-1", %{"count" => 1}})
|
count_frame = Protocol.encode_relay({:count, "sub-1", %{"count" => 1}})
|
||||||
assert Jason.decode!(count_frame) == ["COUNT", "sub-1", %{"count" => 1}]
|
assert JSON.decode!(count_frame) == ["COUNT", "sub-1", %{"count" => 1}]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp valid_event do
|
defp valid_event do
|
||||||
|
|||||||
@@ -15,20 +15,20 @@ defmodule Parrhesia.Web.ConformanceTest do
|
|||||||
test "REQ -> EOSE emitted once and CLOSE emits CLOSED" do
|
test "REQ -> EOSE emitted once and CLOSE emits CLOSED" do
|
||||||
{:ok, state} = Connection.init(subscription_index: nil)
|
{:ok, state} = Connection.init(subscription_index: nil)
|
||||||
|
|
||||||
req_payload = Jason.encode!(["REQ", "sub-e2e", %{"kinds" => [1]}])
|
req_payload = JSON.encode!(["REQ", "sub-e2e", %{"kinds" => [1]}])
|
||||||
|
|
||||||
assert {:push, frames, subscribed_state} =
|
assert {:push, frames, subscribed_state} =
|
||||||
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end)
|
decoded = Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end)
|
||||||
assert ["EOSE", "sub-e2e"] = List.last(decoded)
|
assert ["EOSE", "sub-e2e"] = List.last(decoded)
|
||||||
|
|
||||||
close_payload = Jason.encode!(["CLOSE", "sub-e2e"])
|
close_payload = JSON.encode!(["CLOSE", "sub-e2e"])
|
||||||
|
|
||||||
assert {:push, {:text, closed_frame}, closed_state} =
|
assert {:push, {:text, closed_frame}, closed_state} =
|
||||||
Connection.handle_in({close_payload, [opcode: :text]}, subscribed_state)
|
Connection.handle_in({close_payload, [opcode: :text]}, subscribed_state)
|
||||||
|
|
||||||
assert Jason.decode!(closed_frame) == ["CLOSED", "sub-e2e", "error: subscription closed"]
|
assert JSON.decode!(closed_frame) == ["CLOSED", "sub-e2e", "error: subscription closed"]
|
||||||
refute Map.has_key?(closed_state.subscriptions, "sub-e2e")
|
refute Map.has_key?(closed_state.subscriptions, "sub-e2e")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -38,9 +38,9 @@ defmodule Parrhesia.Web.ConformanceTest do
|
|||||||
event = valid_event()
|
event = valid_event()
|
||||||
|
|
||||||
assert {:push, {:text, frame}, ^state} =
|
assert {:push, {:text, frame}, ^state} =
|
||||||
Connection.handle_in({Jason.encode!(["EVENT", event]), [opcode: :text]}, state)
|
Connection.handle_in({JSON.encode!(["EVENT", event]), [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(frame) == ["OK", event["id"], true, "ok: event stored"]
|
assert JSON.decode!(frame) == ["OK", event["id"], true, "ok: event stored"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "wrapped kind 1059 welcome delivery is recipient-gated" do
|
test "wrapped kind 1059 welcome delivery is recipient-gated" do
|
||||||
@@ -56,19 +56,19 @@ defmodule Parrhesia.Web.ConformanceTest do
|
|||||||
|
|
||||||
assert {:push, {:text, ok_frame}, ^state} =
|
assert {:push, {:text, ok_frame}, ^state} =
|
||||||
Connection.handle_in(
|
Connection.handle_in(
|
||||||
{Jason.encode!(["EVENT", wrapped_welcome]), [opcode: :text]},
|
{JSON.encode!(["EVENT", wrapped_welcome]), [opcode: :text]},
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
|
|
||||||
assert Jason.decode!(ok_frame) == ["OK", wrapped_welcome["id"], true, "ok: event stored"]
|
assert JSON.decode!(ok_frame) == ["OK", wrapped_welcome["id"], true, "ok: event stored"]
|
||||||
|
|
||||||
req_payload = Jason.encode!(["REQ", "sub-welcome", %{"kinds" => [1059], "#p" => [recipient]}])
|
req_payload = JSON.encode!(["REQ", "sub-welcome", %{"kinds" => [1059], "#p" => [recipient]}])
|
||||||
|
|
||||||
assert {:push, restricted_frames, ^state} =
|
assert {:push, restricted_frames, ^state} =
|
||||||
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
decoded_restricted =
|
decoded_restricted =
|
||||||
Enum.map(restricted_frames, fn {:text, frame} -> Jason.decode!(frame) end)
|
Enum.map(restricted_frames, fn {:text, frame} -> JSON.decode!(frame) end)
|
||||||
|
|
||||||
assert [
|
assert [
|
||||||
"CLOSED",
|
"CLOSED",
|
||||||
@@ -80,14 +80,14 @@ defmodule Parrhesia.Web.ConformanceTest do
|
|||||||
auth_event = valid_auth_event(state.auth_challenge, recipient)
|
auth_event = valid_auth_event(state.auth_challenge, recipient)
|
||||||
|
|
||||||
assert {:push, {:text, auth_frame}, authed_state} =
|
assert {:push, {:text, auth_frame}, authed_state} =
|
||||||
Connection.handle_in({Jason.encode!(["AUTH", auth_event]), [opcode: :text]}, state)
|
Connection.handle_in({JSON.encode!(["AUTH", auth_event]), [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(auth_frame) == ["OK", auth_event["id"], true, "ok: auth accepted"]
|
assert JSON.decode!(auth_frame) == ["OK", auth_event["id"], true, "ok: auth accepted"]
|
||||||
|
|
||||||
assert {:push, frames, _next_state} =
|
assert {:push, frames, _next_state} =
|
||||||
Connection.handle_in({req_payload, [opcode: :text]}, authed_state)
|
Connection.handle_in({req_payload, [opcode: :text]}, authed_state)
|
||||||
|
|
||||||
decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end)
|
decoded = Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end)
|
||||||
|
|
||||||
assert ["EVENT", "sub-welcome", result_event] =
|
assert ["EVENT", "sub-welcome", result_event] =
|
||||||
Enum.find(decoded, fn frame -> List.first(frame) == "EVENT" end)
|
Enum.find(decoded, fn frame -> List.first(frame) == "EVENT" end)
|
||||||
@@ -108,11 +108,11 @@ defmodule Parrhesia.Web.ConformanceTest do
|
|||||||
|
|
||||||
assert {:push, {:text, commit_ok_frame}, ^state} =
|
assert {:push, {:text, commit_ok_frame}, ^state} =
|
||||||
Connection.handle_in(
|
Connection.handle_in(
|
||||||
{Jason.encode!(["EVENT", commit_event]), [opcode: :text]},
|
{JSON.encode!(["EVENT", commit_event]), [opcode: :text]},
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
|
|
||||||
assert Jason.decode!(commit_ok_frame) == ["OK", commit_event["id"], true, "ok: event stored"]
|
assert JSON.decode!(commit_ok_frame) == ["OK", commit_event["id"], true, "ok: event stored"]
|
||||||
|
|
||||||
assert {:ok, persisted_commit} = Storage.events().get_event(%{}, commit_event["id"])
|
assert {:ok, persisted_commit} = Storage.events().get_event(%{}, commit_event["id"])
|
||||||
assert persisted_commit["id"] == commit_event["id"]
|
assert persisted_commit["id"] == commit_event["id"]
|
||||||
@@ -126,11 +126,11 @@ defmodule Parrhesia.Web.ConformanceTest do
|
|||||||
|
|
||||||
assert {:push, {:text, welcome_ok_frame}, ^state} =
|
assert {:push, {:text, welcome_ok_frame}, ^state} =
|
||||||
Connection.handle_in(
|
Connection.handle_in(
|
||||||
{Jason.encode!(["EVENT", wrapped_welcome]), [opcode: :text]},
|
{JSON.encode!(["EVENT", wrapped_welcome]), [opcode: :text]},
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
|
|
||||||
assert Jason.decode!(welcome_ok_frame) == [
|
assert JSON.decode!(welcome_ok_frame) == [
|
||||||
"OK",
|
"OK",
|
||||||
wrapped_welcome["id"],
|
wrapped_welcome["id"],
|
||||||
true,
|
true,
|
||||||
@@ -189,11 +189,11 @@ defmodule Parrhesia.Web.ConformanceTest do
|
|||||||
|
|
||||||
assert {:push, {:text, relay_ok_frame}, ^state} =
|
assert {:push, {:text, relay_ok_frame}, ^state} =
|
||||||
Connection.handle_in(
|
Connection.handle_in(
|
||||||
{Jason.encode!(["EVENT", relay_list_event]), [opcode: :text]},
|
{JSON.encode!(["EVENT", relay_list_event]), [opcode: :text]},
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
|
|
||||||
assert Jason.decode!(relay_ok_frame) == [
|
assert JSON.decode!(relay_ok_frame) == [
|
||||||
"OK",
|
"OK",
|
||||||
relay_list_event["id"],
|
relay_list_event["id"],
|
||||||
true,
|
true,
|
||||||
@@ -202,11 +202,11 @@ defmodule Parrhesia.Web.ConformanceTest do
|
|||||||
|
|
||||||
assert {:push, {:text, trigger_ok_frame}, ^state} =
|
assert {:push, {:text, trigger_ok_frame}, ^state} =
|
||||||
Connection.handle_in(
|
Connection.handle_in(
|
||||||
{Jason.encode!(["EVENT", push_trigger]), [opcode: :text]},
|
{JSON.encode!(["EVENT", push_trigger]), [opcode: :text]},
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
|
|
||||||
assert Jason.decode!(trigger_ok_frame) == ["OK", push_trigger["id"], true, "ok: event stored"]
|
assert JSON.decode!(trigger_ok_frame) == ["OK", push_trigger["id"], true, "ok: event stored"]
|
||||||
|
|
||||||
assert {:ok, persisted_relay_list} = Storage.events().get_event(%{}, relay_list_event["id"])
|
assert {:ok, persisted_relay_list} = Storage.events().get_event(%{}, relay_list_event["id"])
|
||||||
assert persisted_relay_list["id"] == relay_list_event["id"]
|
assert persisted_relay_list["id"] == relay_list_event["id"]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
test "REQ registers subscription, streams initial events and replies with EOSE" do
|
test "REQ registers subscription, streams initial events and replies with EOSE" do
|
||||||
state = connection_state()
|
state = connection_state()
|
||||||
|
|
||||||
req_payload = Jason.encode!(["REQ", "sub-123", %{"kinds" => [1]}])
|
req_payload = JSON.encode!(["REQ", "sub-123", %{"kinds" => [1]}])
|
||||||
|
|
||||||
assert {:push, responses, next_state} =
|
assert {:push, responses, next_state} =
|
||||||
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
||||||
@@ -23,7 +23,7 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
assert next_state.subscriptions["sub-123"].filters == [%{"kinds" => [1]}]
|
assert next_state.subscriptions["sub-123"].filters == [%{"kinds" => [1]}]
|
||||||
assert next_state.subscriptions["sub-123"].eose_sent?
|
assert next_state.subscriptions["sub-123"].eose_sent?
|
||||||
|
|
||||||
assert List.last(Enum.map(responses, fn {:text, frame} -> Jason.decode!(frame) end)) == [
|
assert List.last(Enum.map(responses, fn {:text, frame} -> JSON.decode!(frame) end)) == [
|
||||||
"EOSE",
|
"EOSE",
|
||||||
"sub-123"
|
"sub-123"
|
||||||
]
|
]
|
||||||
@@ -32,12 +32,12 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
test "COUNT returns exact count payload" do
|
test "COUNT returns exact count payload" do
|
||||||
state = connection_state()
|
state = connection_state()
|
||||||
|
|
||||||
payload = Jason.encode!(["COUNT", "sub-count", %{"kinds" => [1]}])
|
payload = JSON.encode!(["COUNT", "sub-count", %{"kinds" => [1]}])
|
||||||
|
|
||||||
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 ["COUNT", "sub-count", payload] = Jason.decode!(response)
|
assert ["COUNT", "sub-count", payload] = JSON.decode!(response)
|
||||||
assert payload["count"] >= 0
|
assert payload["count"] >= 0
|
||||||
assert payload["approximate"] == false
|
assert payload["approximate"] == false
|
||||||
end
|
end
|
||||||
@@ -46,12 +46,12 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
state = connection_state()
|
state = connection_state()
|
||||||
|
|
||||||
auth_event = valid_auth_event(state.auth_challenge)
|
auth_event = valid_auth_event(state.auth_challenge)
|
||||||
payload = Jason.encode!(["AUTH", auth_event])
|
payload = JSON.encode!(["AUTH", auth_event])
|
||||||
|
|
||||||
assert {:push, {:text, response}, next_state} =
|
assert {:push, {:text, response}, next_state} =
|
||||||
Connection.handle_in({payload, [opcode: :text]}, state)
|
Connection.handle_in({payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(response) == ["OK", auth_event["id"], true, "ok: auth accepted"]
|
assert JSON.decode!(response) == ["OK", auth_event["id"], true, "ok: auth accepted"]
|
||||||
assert MapSet.member?(next_state.authenticated_pubkeys, auth_event["pubkey"])
|
assert MapSet.member?(next_state.authenticated_pubkeys, auth_event["pubkey"])
|
||||||
refute next_state.auth_challenge == state.auth_challenge
|
refute next_state.auth_challenge == state.auth_challenge
|
||||||
end
|
end
|
||||||
@@ -60,11 +60,11 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
state = connection_state()
|
state = connection_state()
|
||||||
|
|
||||||
auth_event = valid_auth_event("wrong-challenge")
|
auth_event = valid_auth_event("wrong-challenge")
|
||||||
payload = Jason.encode!(["AUTH", auth_event])
|
payload = JSON.encode!(["AUTH", auth_event])
|
||||||
|
|
||||||
assert {:push, frames, ^state} = Connection.handle_in({payload, [opcode: :text]}, state)
|
assert {:push, frames, ^state} = Connection.handle_in({payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end)
|
decoded = Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end)
|
||||||
|
|
||||||
assert Enum.any?(decoded, fn frame -> frame == ["AUTH", state.auth_challenge] end)
|
assert Enum.any?(decoded, fn frame -> frame == ["AUTH", state.auth_challenge] end)
|
||||||
|
|
||||||
@@ -81,11 +81,11 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
|> Map.put("tags", [["-"]])
|
|> Map.put("tags", [["-"]])
|
||||||
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
||||||
|
|
||||||
payload = Jason.encode!(["EVENT", event])
|
payload = JSON.encode!(["EVENT", event])
|
||||||
|
|
||||||
assert {:push, frames, ^state} = Connection.handle_in({payload, [opcode: :text]}, state)
|
assert {:push, frames, ^state} = Connection.handle_in({payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end)
|
decoded = Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end)
|
||||||
|
|
||||||
assert ["OK", _, false, "auth-required: protected events require authenticated pubkey"] =
|
assert ["OK", _, false, "auth-required: protected events require authenticated pubkey"] =
|
||||||
Enum.find(decoded, fn frame -> List.first(frame) == "OK" end)
|
Enum.find(decoded, fn frame -> List.first(frame) == "OK" end)
|
||||||
@@ -96,11 +96,11 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
test "kind 445 REQ without #h is rejected" do
|
test "kind 445 REQ without #h is rejected" do
|
||||||
state = connection_state()
|
state = connection_state()
|
||||||
|
|
||||||
req_payload = Jason.encode!(["REQ", "sub-445", %{"kinds" => [445]}])
|
req_payload = JSON.encode!(["REQ", "sub-445", %{"kinds" => [445]}])
|
||||||
|
|
||||||
assert {:push, frames, ^state} = Connection.handle_in({req_payload, [opcode: :text]}, state)
|
assert {:push, frames, ^state} = Connection.handle_in({req_payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end)
|
decoded = Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end)
|
||||||
|
|
||||||
assert ["CLOSED", "sub-445", "restricted: kind 445 queries must include a #h tag"] =
|
assert ["CLOSED", "sub-445", "restricted: kind 445 queries must include a #h tag"] =
|
||||||
Enum.find(decoded, fn frame -> List.first(frame) == "CLOSED" end)
|
Enum.find(decoded, fn frame -> List.first(frame) == "CLOSED" end)
|
||||||
@@ -110,24 +110,24 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
state = connection_state()
|
state = connection_state()
|
||||||
|
|
||||||
event = valid_event()
|
event = valid_event()
|
||||||
payload = Jason.encode!(["EVENT", event])
|
payload = JSON.encode!(["EVENT", event])
|
||||||
|
|
||||||
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) == ["OK", event["id"], true, "ok: event stored"]
|
assert JSON.decode!(response) == ["OK", event["id"], true, "ok: event stored"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "invalid EVENT replies with OK false invalid prefix" do
|
test "invalid EVENT replies with OK false invalid prefix" do
|
||||||
state = connection_state()
|
state = connection_state()
|
||||||
|
|
||||||
event = valid_event() |> Map.put("sig", "nope")
|
event = valid_event() |> Map.put("sig", "nope")
|
||||||
payload = Jason.encode!(["EVENT", event])
|
payload = JSON.encode!(["EVENT", event])
|
||||||
|
|
||||||
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 JSON.decode!(response) == [
|
||||||
"OK",
|
"OK",
|
||||||
event["id"],
|
event["id"],
|
||||||
false,
|
false,
|
||||||
@@ -145,12 +145,12 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
|> Map.put("content", "ciphertext")
|
|> Map.put("content", "ciphertext")
|
||||||
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
||||||
|
|
||||||
payload = Jason.encode!(["EVENT", event])
|
payload = JSON.encode!(["EVENT", event])
|
||||||
|
|
||||||
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 JSON.decode!(response) == [
|
||||||
"OK",
|
"OK",
|
||||||
event["id"],
|
event["id"],
|
||||||
false,
|
false,
|
||||||
@@ -168,12 +168,12 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
|> Map.put("content", "not-base64")
|
|> Map.put("content", "not-base64")
|
||||||
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
||||||
|
|
||||||
payload = Jason.encode!(["EVENT", event])
|
payload = JSON.encode!(["EVENT", event])
|
||||||
|
|
||||||
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 JSON.decode!(response) == [
|
||||||
"OK",
|
"OK",
|
||||||
event["id"],
|
event["id"],
|
||||||
false,
|
false,
|
||||||
@@ -202,12 +202,12 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
])
|
])
|
||||||
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
||||||
|
|
||||||
payload = Jason.encode!(["EVENT", event])
|
payload = JSON.encode!(["EVENT", event])
|
||||||
|
|
||||||
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 JSON.decode!(response) == [
|
||||||
"OK",
|
"OK",
|
||||||
event["id"],
|
event["id"],
|
||||||
false,
|
false,
|
||||||
@@ -253,12 +253,12 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
|> Map.put("content", "encrypted")
|
|> Map.put("content", "encrypted")
|
||||||
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
||||||
|
|
||||||
payload = Jason.encode!(["EVENT", event])
|
payload = JSON.encode!(["EVENT", event])
|
||||||
|
|
||||||
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 JSON.decode!(response) == [
|
||||||
"OK",
|
"OK",
|
||||||
event["id"],
|
event["id"],
|
||||||
false,
|
false,
|
||||||
@@ -304,17 +304,17 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
|> Map.put("content", "encrypted")
|
|> Map.put("content", "encrypted")
|
||||||
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
|> then(&Map.put(&1, "id", EventValidator.compute_id(&1)))
|
||||||
|
|
||||||
payload = Jason.encode!(["EVENT", event])
|
payload = JSON.encode!(["EVENT", event])
|
||||||
|
|
||||||
assert {:push, {:text, first_response}, ^state} =
|
assert {:push, {:text, first_response}, ^state} =
|
||||||
Connection.handle_in({payload, [opcode: :text]}, state)
|
Connection.handle_in({payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(first_response) == ["OK", event["id"], true, "ok: event stored"]
|
assert JSON.decode!(first_response) == ["OK", event["id"], true, "ok: event stored"]
|
||||||
|
|
||||||
assert {:push, {:text, second_response}, ^state} =
|
assert {:push, {:text, second_response}, ^state} =
|
||||||
Connection.handle_in({payload, [opcode: :text]}, state)
|
Connection.handle_in({payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(second_response) == [
|
assert JSON.decode!(second_response) == [
|
||||||
"OK",
|
"OK",
|
||||||
event["id"],
|
event["id"],
|
||||||
false,
|
false,
|
||||||
@@ -325,32 +325,32 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
test "NEG sessions open and close" do
|
test "NEG sessions open and close" do
|
||||||
state = connection_state()
|
state = connection_state()
|
||||||
|
|
||||||
open_payload = Jason.encode!(["NEG-OPEN", "neg-1", %{"cursor" => 0}])
|
open_payload = JSON.encode!(["NEG-OPEN", "neg-1", %{"cursor" => 0}])
|
||||||
|
|
||||||
assert {:push, {:text, open_response}, ^state} =
|
assert {:push, {:text, open_response}, ^state} =
|
||||||
Connection.handle_in({open_payload, [opcode: :text]}, state)
|
Connection.handle_in({open_payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
assert ["NEG-MSG", "neg-1", %{"status" => "open", "cursor" => 0}] =
|
assert ["NEG-MSG", "neg-1", %{"status" => "open", "cursor" => 0}] =
|
||||||
Jason.decode!(open_response)
|
JSON.decode!(open_response)
|
||||||
|
|
||||||
close_payload = Jason.encode!(["NEG-CLOSE", "neg-1"])
|
close_payload = JSON.encode!(["NEG-CLOSE", "neg-1"])
|
||||||
|
|
||||||
assert {:push, {:text, close_response}, ^state} =
|
assert {:push, {:text, close_response}, ^state} =
|
||||||
Connection.handle_in({close_payload, [opcode: :text]}, state)
|
Connection.handle_in({close_payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
assert Jason.decode!(close_response) == ["NEG-MSG", "neg-1", %{"status" => "closed"}]
|
assert JSON.decode!(close_response) == ["NEG-MSG", "neg-1", %{"status" => "closed"}]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "CLOSE removes subscription and replies with CLOSED" do
|
test "CLOSE removes subscription and replies with CLOSED" do
|
||||||
state = subscribed_connection_state([])
|
state = subscribed_connection_state([])
|
||||||
|
|
||||||
close_payload = Jason.encode!(["CLOSE", "sub-1"])
|
close_payload = JSON.encode!(["CLOSE", "sub-1"])
|
||||||
|
|
||||||
assert {:push, {:text, response}, next_state} =
|
assert {:push, {:text, response}, next_state} =
|
||||||
Connection.handle_in({close_payload, [opcode: :text]}, state)
|
Connection.handle_in({close_payload, [opcode: :text]}, state)
|
||||||
|
|
||||||
refute Map.has_key?(next_state.subscriptions, "sub-1")
|
refute Map.has_key?(next_state.subscriptions, "sub-1")
|
||||||
assert Jason.decode!(response) == ["CLOSED", "sub-1", "error: subscription closed"]
|
assert JSON.decode!(response) == ["CLOSED", "sub-1", "error: subscription closed"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "fanout_event enqueues and drains matching events" do
|
test "fanout_event enqueues and drains matching events" do
|
||||||
@@ -366,7 +366,7 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
Connection.handle_info(:drain_outbound_queue, queued_state)
|
Connection.handle_info(:drain_outbound_queue, queued_state)
|
||||||
|
|
||||||
assert drained_state.outbound_queue_size == 0
|
assert drained_state.outbound_queue_size == 0
|
||||||
assert Jason.decode!(payload) == ["EVENT", "sub-1", event]
|
assert JSON.decode!(payload) == ["EVENT", "sub-1", event]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "high-volume kind 445 fanout drains in order across batches" do
|
test "high-volume kind 445 fanout drains in order across batches" do
|
||||||
@@ -392,7 +392,7 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
|
|
||||||
delivered_ids =
|
delivered_ids =
|
||||||
frames
|
frames
|
||||||
|> Enum.map(fn {:text, payload} -> Jason.decode!(payload) end)
|
|> Enum.map(fn {:text, payload} -> JSON.decode!(payload) end)
|
||||||
|> Enum.map(fn ["EVENT", "sub-group", event] -> event["id"] end)
|
|> Enum.map(fn ["EVENT", "sub-group", event] -> event["id"] end)
|
||||||
|
|
||||||
assert delivered_ids == Enum.map(events, & &1["id"])
|
assert delivered_ids == Enum.map(events, & &1["id"])
|
||||||
@@ -418,12 +418,12 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert message == "rate-limited: outbound queue overflow"
|
assert message == "rate-limited: outbound queue overflow"
|
||||||
assert Jason.decode!(notice_payload) == ["NOTICE", message]
|
assert JSON.decode!(notice_payload) == ["NOTICE", message]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp subscribed_connection_state(opts) do
|
defp subscribed_connection_state(opts) do
|
||||||
state = connection_state(opts)
|
state = connection_state(opts)
|
||||||
req_payload = Jason.encode!(["REQ", "sub-1", %{"kinds" => [1]}])
|
req_payload = JSON.encode!(["REQ", "sub-1", %{"kinds" => [1]}])
|
||||||
|
|
||||||
assert {:push, _, subscribed_state} =
|
assert {:push, _, subscribed_state} =
|
||||||
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
||||||
@@ -433,7 +433,7 @@ defmodule Parrhesia.Web.ConnectionTest do
|
|||||||
|
|
||||||
defp subscribed_group_connection_state(group_id, opts) do
|
defp subscribed_group_connection_state(group_id, opts) do
|
||||||
state = connection_state(opts)
|
state = connection_state(opts)
|
||||||
req_payload = Jason.encode!(["REQ", "sub-group", %{"kinds" => [445], "#h" => [group_id]}])
|
req_payload = JSON.encode!(["REQ", "sub-group", %{"kinds" => [445], "#h" => [group_id]}])
|
||||||
|
|
||||||
assert {:push, _, subscribed_state} =
|
assert {:push, _, subscribed_state} =
|
||||||
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
Connection.handle_in({req_payload, [opcode: :text]}, state)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ defmodule Parrhesia.Web.RouterTest do
|
|||||||
assert conn.status == 200
|
assert conn.status == 200
|
||||||
assert get_resp_header(conn, "content-type") == ["application/nostr+json; charset=utf-8"]
|
assert get_resp_header(conn, "content-type") == ["application/nostr+json; charset=utf-8"]
|
||||||
|
|
||||||
body = Jason.decode!(conn.resp_body)
|
body = JSON.decode!(conn.resp_body)
|
||||||
|
|
||||||
assert body["name"] == "Parrhesia"
|
assert body["name"] == "Parrhesia"
|
||||||
assert 11 in body["supported_nips"]
|
assert 11 in body["supported_nips"]
|
||||||
@@ -52,29 +52,29 @@ defmodule Parrhesia.Web.RouterTest do
|
|||||||
|
|
||||||
test "POST /management requires authorization" do
|
test "POST /management requires authorization" do
|
||||||
conn =
|
conn =
|
||||||
conn(:post, "/management", Jason.encode!(%{"method" => "ping", "params" => %{}}))
|
conn(:post, "/management", JSON.encode!(%{"method" => "ping", "params" => %{}}))
|
||||||
|> put_req_header("content-type", "application/json")
|
|> put_req_header("content-type", "application/json")
|
||||||
|> Router.call([])
|
|> Router.call([])
|
||||||
|
|
||||||
assert conn.status == 401
|
assert conn.status == 401
|
||||||
assert Jason.decode!(conn.resp_body) == %{"ok" => false, "error" => "auth-required"}
|
assert JSON.decode!(conn.resp_body) == %{"ok" => false, "error" => "auth-required"}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "POST /management accepts valid NIP-98 header" do
|
test "POST /management accepts valid NIP-98 header" do
|
||||||
management_url = "http://www.example.com/management"
|
management_url = "http://www.example.com/management"
|
||||||
auth_event = nip98_event("POST", management_url)
|
auth_event = nip98_event("POST", management_url)
|
||||||
|
|
||||||
authorization = "Nostr " <> Base.encode64(Jason.encode!(auth_event))
|
authorization = "Nostr " <> Base.encode64(JSON.encode!(auth_event))
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn(:post, "/management", Jason.encode!(%{"method" => "ping", "params" => %{}}))
|
conn(:post, "/management", JSON.encode!(%{"method" => "ping", "params" => %{}}))
|
||||||
|> put_req_header("content-type", "application/json")
|
|> put_req_header("content-type", "application/json")
|
||||||
|> put_req_header("authorization", authorization)
|
|> put_req_header("authorization", authorization)
|
||||||
|> Router.call([])
|
|> Router.call([])
|
||||||
|
|
||||||
assert conn.status == 200
|
assert conn.status == 200
|
||||||
|
|
||||||
assert Jason.decode!(conn.resp_body) == %{
|
assert JSON.decode!(conn.resp_body) == %{
|
||||||
"ok" => true,
|
"ok" => true,
|
||||||
"result" => %{"status" => "ok"}
|
"result" => %{"status" => "ok"}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user