@ -1,10 +1,10 @@
defmodule GcIndexRelayWeb.RelayIntegrationTest do
defmodule GcIndexRelayWeb.RelayIntegrationTest do
@moduledoc """
@moduledoc """
Integration probe of the relay REST API , covering the scenarios a browser - based
Integration tests for relay REST API scenarios that require a live database .
Nostr client ( e . g . Jumble at https :/ / jumble . imwald . eu / ) would exercise .
Covers event publishing , deletion , and filter - based querying .
Scenarios are mapped 1 : 1 to test / features / relay_api . feature .
Scenarios are mapped 1 : 1 to test / features / relay_api . feature .
Run with : mix test . integration test / gc_index_relay_web / relay_integration_test . exs
Run with : source . env && mix test . integration test / gc_index_relay_web / relay_integration_test . exs
"""
"""
use GcIndexRelayWeb.ConnCase
use GcIndexRelayWeb.ConnCase
@ -22,37 +22,16 @@ defmodule GcIndexRelayWeb.RelayIntegrationTest do
{ :ok , conn : conn }
{ :ok , conn : conn }
end
end
# ---------------------------------------------------------------------------
# Discovery
# ---------------------------------------------------------------------------
describe " GET /api — discovery " do
test " client discovers available endpoints " , %{ conn : conn } do
# When
conn = get ( conn , ~p" /api " )
# Then
assert %{ " endpoints " = > endpoints } = json_response ( conn , 200 )
paths = Enum . map ( endpoints , & &1 [ " path " ] )
assert " /api/events " in paths
assert " /api/events/:id " in paths
assert " /api/events/filter " in paths or " /api/events/:id " in paths
end
end
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Publishing events (POST /api/events)
# Publishing events (POST /api/events)
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
describe " POST /api/events — publishing " do
describe " POST /api/events — publishing " do
test " client publishes a valid kind 1 note " , %{ conn : conn } do
test " client publishes a valid kind 1 note " , %{ conn : conn } do
# Given
event = valid_pub_event_fixture ( %{ kind : 1 , content : " hello nostr " } )
event = valid_pub_event_fixture ( %{ kind : 1 , content : " hello nostr " } )
# When
conn = post ( conn , ~p" /api/events " , %{ " event " = > Map . from_struct ( event ) } )
conn = post ( conn , ~p" /api/events " , %{ " event " = > Map . from_struct ( event ) } )
# Then
assert %{ " data " = > data } = json_response ( conn , 201 )
assert %{ " data " = > data } = json_response ( conn , 201 )
assert data [ " id " ] == event . id
assert data [ " id " ] == event . id
assert data [ " kind " ] == 1
assert data [ " kind " ] == 1
@ -60,54 +39,24 @@ defmodule GcIndexRelayWeb.RelayIntegrationTest do
end
end
test " client publishes a kind 0 profile event with metadata content " , %{ conn : conn } do
test " client publishes a kind 0 profile event with metadata content " , %{ conn : conn } do
# Given — kind 0 content is a JSON string per NIP-01
metadata = Jason . encode! ( %{ name : " testuser " , about : " a test profile " , picture : " " } )
metadata = Jason . encode! ( %{ name : " testuser " , about : " a test profile " , picture : " " } )
event = valid_pub_event_fixture ( %{ kind : 0 , content : metadata } )
event = valid_pub_event_fixture ( %{ kind : 0 , content : metadata } )
# When
conn = post ( conn , ~p" /api/events " , %{ " event " = > Map . from_struct ( event ) } )
conn = post ( conn , ~p" /api/events " , %{ " event " = > Map . from_struct ( event ) } )
# Then — event was stored and returned
assert %{ " data " = > data } = json_response ( conn , 201 )
assert %{ " data " = > data } = json_response ( conn , 201 )
assert data [ " kind " ] == 0
assert data [ " kind " ] == 0
assert data [ " content " ] == metadata
assert data [ " content " ] == metadata
end
end
test " relay rejects a duplicate event with 409 " , %{ conn : conn } do
test " relay rejects a duplicate event with 409 " , %{ conn : conn } do
# Given
event = valid_pub_event_fixture ( )
event = valid_pub_event_fixture ( )
post ( conn , ~p" /api/events " , %{ " event " = > Map . from_struct ( event ) } )
post ( conn , ~p" /api/events " , %{ " event " = > Map . from_struct ( event ) } )
# When — same event again
conn = post ( conn , ~p" /api/events " , %{ " event " = > Map . from_struct ( event ) } )
conn = post ( conn , ~p" /api/events " , %{ " event " = > Map . from_struct ( event ) } )
# Then
assert json_response ( conn , 409 )
assert json_response ( conn , 409 )
end
end
test " relay rejects an event with a tampered ID with 400 " , %{ conn : conn } do
# Given — ID does not match hash of content
event = invalid_id_pub_event_fixture ( )
# When
conn = post ( conn , ~p" /api/events " , %{ " event " = > Map . from_struct ( event ) } )
# Then
assert %{ " errors " = > %{ " detail " = > detail } } = json_response ( conn , 400 )
assert detail =~ " invalid "
end
test " relay rejects a NIP-70 protected event with 400 " , %{ conn : conn } do
# Given — event has the ["-"] protection tag
event = valid_pub_event_fixture ( %{ tags : [ [ " - " ] ] } )
# When
conn = post ( conn , ~p" /api/events " , %{ " event " = > Map . from_struct ( event ) } )
# Then
assert %{ " errors " = > %{ " detail " = > detail } } = json_response ( conn , 400 )
assert detail =~ " auth-required "
end
end
end
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
@ -116,24 +65,19 @@ defmodule GcIndexRelayWeb.RelayIntegrationTest do
describe " DELETE /api/events/:id — deletion " do
describe " DELETE /api/events/:id — deletion " do
test " client deletes an existing event and gets 204 " , %{ conn : conn } do
test " client deletes an existing event and gets 204 " , %{ conn : conn } do
# Given
event = valid_pub_event_fixture ( )
event = valid_pub_event_fixture ( )
{ :ok , db_event } = GcIndexRelay.Nostr . create_event ( event )
{ :ok , db_event } = GcIndexRelay.Nostr . create_event ( event )
hex_id = Base . encode16 ( db_event . id , case : :lower )
hex_id = Base . encode16 ( db_event . id , case : :lower )
# When
conn = delete ( conn , ~p" /api/events/ #{ hex_id } " )
conn = delete ( conn , ~p" /api/events/ #{ hex_id } " )
# Then
assert conn . status == 204
assert conn . status == 204
assert conn . resp_body == " "
assert conn . resp_body == " "
end
end
test " deleting a non-existent event returns 404 " , %{ conn : conn } do
test " deleting a non-existent event returns 404 " , %{ conn : conn } do
# When
conn = delete ( conn , ~p" /api/events/ #{ String . duplicate ( " b " , 64 ) } " )
conn = delete ( conn , ~p" /api/events/ #{ String . duplicate ( " b " , 64 ) } " )
# Then
assert json_response ( conn , 404 )
assert json_response ( conn , 404 )
end
end
end
end
@ -144,29 +88,18 @@ defmodule GcIndexRelayWeb.RelayIntegrationTest do
describe " GET /api/events — cacheable query " do
describe " GET /api/events — cacheable query " do
test " client fetches events with since/until/limit query params " , %{ conn : conn } do
test " client fetches events with since/until/limit query params " , %{ conn : conn } do
# Given
_e1 =
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_640_000_001 } )
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_640_000_001 } )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
_e2 =
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_640_000_002 , keypair : :keypair2 } )
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_640_000_002 , keypair : :keypair2 } )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
# When
conn = get ( conn , " /api/events?since=0&until=9999999999&limit=10 " )
conn = get ( conn , " /api/events?since=0&until=9999999999&limit=10 " )
# Then
assert %{ " data " = > events } = json_response ( conn , 200 )
assert %{ " data " = > events } = json_response ( conn , 200 )
assert length ( events ) == 2
assert length ( events ) == 2
end
[ first , second ] = events
assert first [ " created_at " ] >= second [ " created_at " ]
test " GET /api/events without required params is rejected with 400 " , %{ conn : conn } do
# When — missing since, until, limit
conn = get ( conn , " /api/events?kinds=1 " )
# Then
assert json_response ( conn , 400 )
end
end
end
end
@ -176,19 +109,14 @@ defmodule GcIndexRelayWeb.RelayIntegrationTest do
describe " POST /api/events/filter — querying " do
describe " POST /api/events/filter — querying " do
test " client fetches recent kind 1 notes, newest first " , %{ conn : conn } do
test " client fetches recent kind 1 notes, newest first " , %{ conn : conn } do
# Given
_older =
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_640_000_001 } )
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_640_000_001 } )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
_newer =
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_640_000_002 , keypair : :keypair2 } )
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_640_000_002 , keypair : :keypair2 } )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
# When
conn = post ( conn , ~p" /api/events/filter " , %{ " kinds " = > [ 1 ] , " limit " = > 10 } )
conn = post ( conn , ~p" /api/events/filter " , %{ " kinds " = > [ 1 ] , " limit " = > 10 } )
# Then
assert %{ " data " = > events } = json_response ( conn , 200 )
assert %{ " data " = > events } = json_response ( conn , 200 )
assert length ( events ) == 2
assert length ( events ) == 2
[ first , second ] = events
[ first , second ] = events
@ -196,13 +124,11 @@ defmodule GcIndexRelayWeb.RelayIntegrationTest do
end
end
test " client fetches a user profile (kind 0) by author " , %{ conn : conn } do
test " client fetches a user profile (kind 0) by author " , %{ conn : conn } do
# Given
%{ keypair1 : kp } = test_keypairs ( )
%{ keypair1 : kp } = test_keypairs ( )
metadata = Jason . encode! ( %{ name : " alice " } )
metadata = Jason . encode! ( %{ name : " alice " } )
event = valid_pub_event_fixture ( %{ kind : 0 , content : metadata } )
event = valid_pub_event_fixture ( %{ kind : 0 , content : metadata } )
{ :ok , _ } = GcIndexRelay.Nostr . create_event ( event )
{ :ok , _ } = GcIndexRelay.Nostr . create_event ( event )
# When
conn =
conn =
post ( conn , ~p" /api/events/filter " , %{
post ( conn , ~p" /api/events/filter " , %{
" kinds " = > [ 0 ] ,
" kinds " = > [ 0 ] ,
@ -210,69 +136,43 @@ defmodule GcIndexRelayWeb.RelayIntegrationTest do
" limit " = > 1
" limit " = > 1
} )
} )
# Then
assert %{ " data " = > [ profile ] } = json_response ( conn , 200 )
assert %{ " data " = > [ profile ] } = json_response ( conn , 200 )
assert profile [ " kind " ] == 0
assert profile [ " kind " ] == 0
assert profile [ " pubkey " ] == kp . public_key_hex
assert profile [ " pubkey " ] == kp . public_key_hex
end
end
test " client fetches events mentioning a pubkey via # p tag " , %{ conn : conn } do
test " client fetches events mentioning a pubkey via # p tag " , %{ conn : conn } do
# Given — an event tagging another user
mentioned_pubkey = String . duplicate ( " ab " , 32 )
mentioned_pubkey = String . duplicate ( " ab " , 32 )
event = valid_pub_event_fixture ( %{ tags : [ [ " p " , mentioned_pubkey ] ] } )
event = valid_pub_event_fixture ( %{ tags : [ [ " p " , mentioned_pubkey ] ] } )
{ :ok , _ } = GcIndexRelay.Nostr . create_event ( event )
{ :ok , _ } = GcIndexRelay.Nostr . create_event ( event )
_unrelated =
valid_pub_event_fixture ( %{ created_at : 1_640_000_001 , keypair : :keypair2 } )
valid_pub_event_fixture ( %{ created_at : 1_640_000_001 , keypair : :keypair2 } )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
# When
conn = post ( conn , ~p" /api/events/filter " , %{ " # p " = > [ mentioned_pubkey ] , " limit " = > 10 } )
conn = post ( conn , ~p" /api/events/filter " , %{ " # p " = > [ mentioned_pubkey ] , " limit " = > 10 } )
# Then — only the tagged event is returned
assert %{ " data " = > events } = json_response ( conn , 200 )
assert %{ " data " = > events } = json_response ( conn , 200 )
assert length ( events ) == 1
assert length ( events ) == 1
assert hd ( events ) [ " id " ] == event . id
assert hd ( events ) [ " id " ] == event . id
end
end
test " client fetches events within a time window " , %{ conn : conn } do
test " client fetches events within a time window " , %{ conn : conn } do
# Given
_old =
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_500_000_000 } )
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_500_000_000 } )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
_new =
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_640_000_001 , keypair : :keypair2 } )
valid_pub_event_fixture ( %{ kind : 1 , created_at : 1_640_000_001 , keypair : :keypair2 } )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
|> then ( & GcIndexRelay.Nostr . create_event / 1 )
# When — only events after 1_600_000_000
conn =
conn =
post ( conn , ~p" /api/events/filter " , %{
post ( conn , ~p" /api/events/filter " , %{
" since " = > 1_600_000_000 ,
" since " = > 1_600_000_000 ,
" limit " = > 10
" limit " = > 10
} )
} )
# Then
assert %{ " data " = > events } = json_response ( conn , 200 )
assert %{ " data " = > events } = json_response ( conn , 200 )
assert length ( events ) == 1
assert length ( events ) == 1
assert hd ( events ) [ " created_at " ] == 1_640_000_001
assert hd ( events ) [ " created_at " ] == 1_640_000_001
end
end
test " filter without a limit is rejected with 400 " , %{ conn : conn } do
# When
conn = post ( conn , ~p" /api/events/filter " , %{ " kinds " = > [ 1 ] } )
# Then
assert json_response ( conn , 400 )
end
test " filter with limit over 100 is rejected with 400 " , %{ conn : conn } do
# When
conn = post ( conn , ~p" /api/events/filter " , %{ " limit " = > 101 } )
# Then
assert json_response ( conn , 400 )
end
end
end
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
@ -281,160 +181,21 @@ defmodule GcIndexRelayWeb.RelayIntegrationTest do
describe " GET /api/events/:id — single event lookup " do
describe " GET /api/events/:id — single event lookup " do
test " client fetches a specific event by ID " , %{ conn : conn } do
test " client fetches a specific event by ID " , %{ conn : conn } do
# Given
event = valid_pub_event_fixture ( )
event = valid_pub_event_fixture ( )
{ :ok , db_event } = GcIndexRelay.Nostr . create_event ( event )
{ :ok , db_event } = GcIndexRelay.Nostr . create_event ( event )
hex_id = Base . encode16 ( db_event . id , case : :lower )
hex_id = Base . encode16 ( db_event . id , case : :lower )
# When
conn = get ( conn , ~p" /api/events/ #{ hex_id } " )
conn = get ( conn , ~p" /api/events/ #{ hex_id } " )
# Then
assert %{ " data " = > data } = json_response ( conn , 200 )
assert %{ " data " = > data } = json_response ( conn , 200 )
assert data [ " id " ] == hex_id
assert data [ " id " ] == hex_id
assert data [ " content " ] == event . content
assert data [ " content " ] == event . content
end
end
test " fetching a non-existent event returns 404 " , %{ conn : conn } do
test " fetching a non-existent event returns 404 " , %{ conn : conn } do
# When
conn = get ( conn , ~p" /api/events/ #{ String . duplicate ( " a " , 64 ) } " )
conn = get ( conn , ~p" /api/events/ #{ String . duplicate ( " a " , 64 ) } " )
# Then
assert json_response ( conn , 404 )
assert json_response ( conn , 404 )
end
end
end
end
# ---------------------------------------------------------------------------
# CORS — required for browser-based clients like Jumble
# ---------------------------------------------------------------------------
describe " CORS headers — browser client compatibility " do
test " relay includes CORS headers on a GET response " , %{ conn : conn } do
# When
conn = get ( conn , ~p" /api " )
# Then
assert get_resp_header ( conn , " access-control-allow-origin " ) == [ " * " ]
methods = get_resp_header ( conn , " access-control-allow-methods " ) |> List . first ( " " )
assert methods =~ " GET "
assert methods =~ " POST "
assert methods =~ " DELETE "
end
test " relay includes CORS headers on a POST response " , %{ conn : conn } do
# When
conn = post ( conn , ~p" /api/events/filter " , %{ " limit " = > 10 } )
# Then
assert get_resp_header ( conn , " access-control-allow-origin " ) == [ " * " ]
end
test " relay responds 200 to a browser preflight OPTIONS request " , %{ conn : conn } do
# When — browser sends preflight before the real request
conn = options ( conn , " /api/events " )
# Then
assert conn . status == 200
assert conn . resp_body == " "
assert get_resp_header ( conn , " access-control-allow-origin " ) == [ " * " ]
methods = get_resp_header ( conn , " access-control-allow-methods " ) |> List . first ( " " )
assert methods =~ " OPTIONS "
end
end
# ---------------------------------------------------------------------------
# NIP-11 relay information document
# ---------------------------------------------------------------------------
describe " GET / with Accept: application/nostr+json — NIP-11 " do
test " returns 200 with application/nostr+json content-type " , %{ conn : conn } do
# When
conn =
conn
|> put_req_header ( " accept " , " application/nostr+json " )
|> get ( " / " )
# Then
assert conn . status == 200
[ content_type | _ ] = get_resp_header ( conn , " content-type " )
assert content_type =~ " application/nostr+json "
end
test " response body is valid JSON with required NIP-11 fields " , %{ conn : conn } do
# When
conn =
conn
|> put_req_header ( " accept " , " application/nostr+json " )
|> get ( " / " )
# Then
assert { :ok , body } = Jason . decode ( conn . resp_body )
assert is_binary ( body [ " name " ] )
assert is_list ( body [ " supported_nips " ] )
assert is_map ( body [ " limitation " ] )
assert is_binary ( body [ " icon " ] ) and String . starts_with? ( body [ " icon " ] , " http " )
end
test " supported_nips list includes NIP-11 and NIP-70 " , %{ conn : conn } do
# When
conn =
conn
|> put_req_header ( " accept " , " application/nostr+json " )
|> get ( " / " )
# Then
assert { :ok , %{ " supported_nips " = > nips } } = Jason . decode ( conn . resp_body )
assert 11 in nips
assert 70 in nips
end
test " limitation object contains expected fields " , %{ conn : conn } do
# When
conn =
conn
|> put_req_header ( " accept " , " application/nostr+json " )
|> get ( " / " )
# Then
assert { :ok , %{ " limitation " = > limitation } } = Jason . decode ( conn . resp_body )
assert Map . has_key? ( limitation , " max_limit " )
assert Map . has_key? ( limitation , " auth_required " )
assert Map . has_key? ( limitation , " payment_required " )
end
test " NIP-11 response includes CORS headers " , %{ conn : conn } do
# When
conn =
conn
|> put_req_header ( " accept " , " application/nostr+json " )
|> get ( " / " )
# Then
assert get_resp_header ( conn , " access-control-allow-origin " ) == [ " * " ]
end
test " regular browser request to GET / still returns HTML " , %{ conn : conn } do
# When — no special Accept header, browser default
conn =
conn
|> put_req_header ( " accept " , " text/html,application/xhtml+xml " )
|> get ( " / " )
# Then
assert conn . status == 200
[ content_type | _ ] = get_resp_header ( conn , " content-type " )
assert content_type =~ " text/html "
end
end
# ---------------------------------------------------------------------------
# Health check
# ---------------------------------------------------------------------------
describe " GET /health — health check " do
test " health check returns 200 " , %{ conn : conn } do
conn = get ( conn , " /health " )
assert conn . status == 200
end
end
end
end