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:
2026-03-14 00:15:06 +01:00
parent cc9c18b38c
commit 1199369dd9
7 changed files with 366 additions and 3 deletions

View 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

View File

@@ -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)