test: NAME:
nak - the nostr army knife command-line tool USAGE: nak [global options] [command [command options]] VERSION: 0.17.3 COMMANDS: event generates an encoded event and either prints it or sends it to a set of relays req generates encoded REQ messages and optionally use them to talk to relays filter applies an event filter to an event to see if it matches. fetch fetches events related to the given nip19 or nip05 code from the included relay hints or the author's outbox relays. count generates encoded COUNT messages and optionally use them to talk to relays decode decodes nip19, nip21, nip05 or hex entities encode encodes notes and other stuff to nip19 entities key operations on secret keys: generate, derive, encrypt, decrypt verify checks the hash and signature of an event given through stdin or as the first argument relay gets the relay information document for the given relay, as JSON admin manage relays using the relay management API bunker starts a nip46 signer daemon with the given --sec key serve starts an in-memory relay for testing purposes blossom an army knife for blossom things dekey handles NIP-4E decoupled encryption keys encrypt encrypts a string with nip44 (or nip04 if specified using a flag) and returns the resulting ciphertext as base64 decrypt decrypts a base64 nip44 ciphertext (or nip04 if specified using a flag) and returns the resulting plaintext gift gift-wraps (or unwraps) an event according to NIP-59 outbox manage outbox relay hints database wallet displays the current wallet balance mcp pander to the AI gods curl calls curl but with a nip98 header fs mount a FUSE filesystem that exposes Nostr events as files. publish publishes a note with content from stdin git git-related operations nip list NIPs or get the description of a NIP from its number sync sync events between two relays using negentropy spell downloads a spell event and executes its REQ request help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --quiet, -q do not print logs and info messages to stderr, use -qq to also not print anything to stdout (default: false) --verbose, -v print more stuff than normally (default: false) --help, -h show help --version prints the version (default: false) based E2E tests
This commit is contained in:
284
test/parrhesia/e2e/nak_cli_test.exs
Normal file
284
test/parrhesia/e2e/nak_cli_test.exs
Normal file
@@ -0,0 +1,284 @@
|
||||
defmodule Parrhesia.E2E.NakCliTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
@moduletag :nak_e2e
|
||||
|
||||
setup_all do
|
||||
if is_nil(System.find_executable("nak")) do
|
||||
raise "nak executable not found in PATH"
|
||||
end
|
||||
|
||||
{:ok, _apps} = Application.ensure_all_started(:req)
|
||||
:ok = wait_for_server()
|
||||
:ok
|
||||
end
|
||||
|
||||
setup do
|
||||
relay_http_url = relay_http_url()
|
||||
|
||||
{:ok,
|
||||
relay_url: relay_ws_url(relay_http_url),
|
||||
relay_http_url: relay_http_url,
|
||||
author_secret_key: random_secret_key()}
|
||||
end
|
||||
|
||||
test "nak relay returns NIP-11 metadata", %{
|
||||
relay_url: relay_url,
|
||||
relay_http_url: relay_http_url
|
||||
} do
|
||||
{ws_output, 0} = run_nak(["-q", "relay", relay_url])
|
||||
{http_output, 0} = run_nak(["-q", "relay", relay_http_url])
|
||||
|
||||
ws_metadata = JSON.decode!(ws_output)
|
||||
http_metadata = JSON.decode!(http_output)
|
||||
|
||||
assert ws_metadata["name"] == "Parrhesia"
|
||||
assert 11 in ws_metadata["supported_nips"]
|
||||
assert 98 in ws_metadata["supported_nips"]
|
||||
assert http_metadata["name"] == ws_metadata["name"]
|
||||
end
|
||||
|
||||
test "nak event/req/count/search round-trip", %{
|
||||
relay_url: relay_url,
|
||||
author_secret_key: author_secret_key
|
||||
} do
|
||||
nonce = System.unique_integer([:positive])
|
||||
|
||||
first =
|
||||
publish_event(relay_url,
|
||||
sec: author_secret_key,
|
||||
content: "nak-e2e-alpha-#{nonce}"
|
||||
)
|
||||
|
||||
second =
|
||||
publish_event(relay_url,
|
||||
sec: author_secret_key,
|
||||
content: "nak-e2e-beta-#{nonce}"
|
||||
)
|
||||
|
||||
events = req_events(relay_url, ["-k", "1", "-a", first["pubkey"], "-l", "10"])
|
||||
|
||||
assert Enum.any?(events, fn event -> event["id"] == first["id"] end)
|
||||
assert Enum.any?(events, fn event -> event["id"] == second["id"] end)
|
||||
|
||||
count = count_events(relay_url, ["-k", "1", "-a", first["pubkey"]])
|
||||
assert count >= 2
|
||||
|
||||
[search_result] = req_events(relay_url, ["--search", "alpha-#{nonce}", "-k", "1", "-l", "5"])
|
||||
assert search_result["id"] == first["id"]
|
||||
end
|
||||
|
||||
test "nak user a post -> user b reply -> user a refresh sees reply", %{
|
||||
relay_url: relay_url,
|
||||
author_secret_key: user_a_secret_key
|
||||
} do
|
||||
nonce = System.unique_integer([:positive])
|
||||
|
||||
post =
|
||||
publish_event(relay_url,
|
||||
sec: user_a_secret_key,
|
||||
content: "nak-e2e-parent-#{nonce}"
|
||||
)
|
||||
|
||||
user_b_secret_key = random_secret_key()
|
||||
|
||||
reply =
|
||||
publish_event(relay_url,
|
||||
sec: user_b_secret_key,
|
||||
content: "nak-e2e-reply-#{nonce}",
|
||||
tags: ["e=#{post["id"]}", "p=#{post["pubkey"]}"]
|
||||
)
|
||||
|
||||
refresh_results = req_events(relay_url, ["-k", "1", "-e", post["id"], "-l", "10"])
|
||||
|
||||
assert Enum.any?(refresh_results, fn event -> event["id"] == reply["id"] end)
|
||||
assert Enum.any?(refresh_results, fn event -> event["pubkey"] == reply["pubkey"] end)
|
||||
end
|
||||
|
||||
test "nak duplicate publish does not create additional events", %{
|
||||
relay_url: relay_url,
|
||||
author_secret_key: author_secret_key
|
||||
} do
|
||||
created_at = System.system_time(:second)
|
||||
content = "nak-e2e-dup-#{System.unique_integer([:positive])}"
|
||||
|
||||
event =
|
||||
publish_event(relay_url,
|
||||
sec: author_secret_key,
|
||||
content: content,
|
||||
created_at: created_at
|
||||
)
|
||||
|
||||
duplicate =
|
||||
publish_event(relay_url,
|
||||
sec: author_secret_key,
|
||||
content: content,
|
||||
created_at: created_at
|
||||
)
|
||||
|
||||
assert duplicate["id"] == event["id"]
|
||||
|
||||
assert req_events(relay_url, ["-i", event["id"], "-l", "5"]) |> Enum.map(& &1["id"]) == [
|
||||
event["id"]
|
||||
]
|
||||
end
|
||||
|
||||
test "nak kind 445 event query with #h filter", %{
|
||||
relay_url: relay_url,
|
||||
author_secret_key: author_secret_key
|
||||
} do
|
||||
group_id = String.duplicate("a", 64)
|
||||
|
||||
group_event =
|
||||
publish_event(relay_url,
|
||||
sec: author_secret_key,
|
||||
kind: 445,
|
||||
tags: ["h=#{group_id}"],
|
||||
content: Base.encode64("nak-e2e-group")
|
||||
)
|
||||
|
||||
assert req_events(relay_url, ["-k", "445", "-t", "h=#{group_id}", "-l", "5"])
|
||||
|> Enum.map(& &1["id"]) == [group_event["id"]]
|
||||
end
|
||||
|
||||
test "nak deletion and vanish lifecycle", %{
|
||||
relay_url: relay_url,
|
||||
author_secret_key: author_secret_key
|
||||
} do
|
||||
first =
|
||||
publish_event(relay_url,
|
||||
sec: author_secret_key,
|
||||
content: "nak-e2e-delete-first-#{System.unique_integer([:positive])}"
|
||||
)
|
||||
|
||||
second =
|
||||
publish_event(relay_url,
|
||||
sec: author_secret_key,
|
||||
content: "nak-e2e-delete-second-#{System.unique_integer([:positive])}"
|
||||
)
|
||||
|
||||
_delete_request =
|
||||
publish_event(relay_url,
|
||||
sec: author_secret_key,
|
||||
kind: 5,
|
||||
tags: ["e=#{first["id"]}"],
|
||||
content: "delete"
|
||||
)
|
||||
|
||||
assert req_events(relay_url, ["-i", first["id"], "-l", "5"]) == []
|
||||
|
||||
assert req_events(relay_url, ["-i", second["id"], "-l", "5"]) |> Enum.map(& &1["id"]) == [
|
||||
second["id"]
|
||||
]
|
||||
|
||||
_vanish_request =
|
||||
publish_event(relay_url,
|
||||
sec: author_secret_key,
|
||||
kind: 62,
|
||||
content: "vanish"
|
||||
)
|
||||
|
||||
assert req_events(relay_url, ["-a", first["pubkey"], "-l", "20"]) == []
|
||||
end
|
||||
|
||||
defp publish_event(relay_url, opts) do
|
||||
sec = Keyword.fetch!(opts, :sec)
|
||||
kind = Keyword.get(opts, :kind, 1)
|
||||
content = Keyword.get(opts, :content, "")
|
||||
tags = Keyword.get(opts, :tags, [])
|
||||
|
||||
created_at_args =
|
||||
case Keyword.get(opts, :created_at) do
|
||||
created_at when is_integer(created_at) -> ["--created-at", Integer.to_string(created_at)]
|
||||
_other -> []
|
||||
end
|
||||
|
||||
tag_args = Enum.flat_map(tags, fn tag -> ["-t", tag] end)
|
||||
|
||||
args =
|
||||
["-q", "event", "--sec", sec, "-k", Integer.to_string(kind), "-c", content] ++
|
||||
created_at_args ++ tag_args ++ [relay_url]
|
||||
|
||||
{output, 0} = run_nak(args)
|
||||
JSON.decode!(output)
|
||||
end
|
||||
|
||||
defp req_events(relay_url, filter_args) do
|
||||
{output, 0} = run_nak(["-q", "req" | filter_args] ++ [relay_url])
|
||||
|
||||
output
|
||||
|> String.split("\n", trim: true)
|
||||
|> Enum.map(&JSON.decode!/1)
|
||||
end
|
||||
|
||||
defp count_events(relay_url, filter_args) do
|
||||
{output, 0} = run_nak(["-q", "count" | filter_args] ++ [relay_url])
|
||||
|
||||
case Regex.run(~r/:\s*(\d+)\s*$/, String.trim(output)) do
|
||||
[_, count] -> String.to_integer(count)
|
||||
_other -> flunk("unexpected nak count output: #{inspect(output)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp run_nak(args, timeout_ms \\ 5_000) do
|
||||
shell_command =
|
||||
["nak" | args]
|
||||
|> Enum.map_join(" ", &shell_escape/1)
|
||||
|> Kernel.<>(" </dev/null 2>&1")
|
||||
|
||||
task = Task.async(fn -> System.cmd("bash", ["-lc", shell_command]) end)
|
||||
|
||||
case Task.yield(task, timeout_ms) || Task.shutdown(task, :brutal_kill) do
|
||||
{:ok, {_output, 0} = success} ->
|
||||
success
|
||||
|
||||
{:ok, {output, status}} ->
|
||||
flunk("nak command failed with status #{status}: #{shell_command}\n#{output}")
|
||||
|
||||
nil ->
|
||||
flunk("nak command timed out after #{timeout_ms}ms: #{shell_command}")
|
||||
end
|
||||
end
|
||||
|
||||
defp shell_escape(value) when is_binary(value) do
|
||||
"'" <> String.replace(value, "'", "'\\''") <> "'"
|
||||
end
|
||||
|
||||
defp wait_for_server do
|
||||
health_url = base_http_url() <> "/health"
|
||||
|
||||
1..100
|
||||
|> Enum.reduce_while(:error, fn _attempt, _acc ->
|
||||
case Req.get(health_url, receive_timeout: 500, connect_options: [timeout: 500]) do
|
||||
{:ok, %{status: 200, body: "ok"}} ->
|
||||
{:halt, :ok}
|
||||
|
||||
_other ->
|
||||
Process.sleep(100)
|
||||
{:cont, :error}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
:ok -> :ok
|
||||
:error -> raise "server was not ready at #{health_url}"
|
||||
end
|
||||
end
|
||||
|
||||
defp relay_http_url do
|
||||
base_http_url() <> "/relay"
|
||||
end
|
||||
|
||||
defp base_http_url do
|
||||
port = System.get_env("PARRHESIA_NAK_E2E_RELAY_PORT") || "4050"
|
||||
"http://127.0.0.1:#{port}"
|
||||
end
|
||||
|
||||
defp relay_ws_url(relay_http_url) do
|
||||
String.replace_prefix(relay_http_url, "http://", "ws://")
|
||||
end
|
||||
|
||||
defp random_secret_key do
|
||||
:crypto.strong_rand_bytes(32)
|
||||
|> Base.encode16(case: :lower)
|
||||
end
|
||||
end
|
||||
@@ -1 +1,8 @@
|
||||
ExUnit.start()
|
||||
exclude_tags =
|
||||
if System.get_env("PARRHESIA_NAK_E2E") == "1" do
|
||||
[]
|
||||
else
|
||||
[:nak_e2e]
|
||||
end
|
||||
|
||||
ExUnit.start(exclude: exclude_tags)
|
||||
|
||||
Reference in New Issue
Block a user