From 8fc8cb9924c61e1a3286037ce20fb18abcecec1a Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Wed, 18 Feb 2026 22:18:19 -0600 Subject: [PATCH] Set up integration tests for REST endpoints --- config/test.exs | 5 +- .../controllers/event_controller_test.exs | 134 ++++----- .../controllers/filter_controller_test.exs | 283 ++++++++++++++++++ 3 files changed, 348 insertions(+), 74 deletions(-) create mode 100644 test/gc_index_relay_web/controllers/filter_controller_test.exs diff --git a/config/test.exs b/config/test.exs index b00832e..67cdf17 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,8 +1,6 @@ import Config -# Allow tests to run without database by default -# Set REQUIRE_DB=true to enable database for integration tests -config :gc_index_relay, :start_repo, System.get_env("REQUIRE_DB") == "true" +config :gc_index_relay, :start_repo, true # Configure your database # @@ -10,6 +8,7 @@ config :gc_index_relay, :start_repo, System.get_env("REQUIRE_DB") == "true" # to provide built-in test partitioning in CI environment. # Run `mix help test` for more information. config :gc_index_relay, GcIndexRelay.Repo, + port: 5455, username: "postgres", password: "postgres", hostname: "localhost", diff --git a/test/gc_index_relay_web/controllers/event_controller_test.exs b/test/gc_index_relay_web/controllers/event_controller_test.exs index 5220f33..7980064 100644 --- a/test/gc_index_relay_web/controllers/event_controller_test.exs +++ b/test/gc_index_relay_web/controllers/event_controller_test.exs @@ -2,101 +2,93 @@ defmodule GcIndexRelayWeb.EventControllerTest do use GcIndexRelayWeb.ConnCase import GcIndexRelay.NostrFixtures - alias GcIndexRelay.Nostr.Event @moduletag :integration - @create_attrs %{ - sig: "some sig", - kind: 42, - pubkey: "some pubkey", - created_at: ~U[2026-01-28 03:51:00Z], - content: "some content" - } - @update_attrs %{ - sig: "some updated sig", - kind: 43, - pubkey: "some updated pubkey", - created_at: ~U[2026-01-29 03:51:00Z], - content: "some updated content" - } - @invalid_attrs %{sig: nil, kind: nil, pubkey: nil, created_at: nil, content: nil} - setup %{conn: conn} do - {:ok, conn: put_req_header(conn, "accept", "application/json")} - end + conn = + conn + |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") - describe "index" do - test "lists all events", %{conn: conn} do - conn = get(conn, ~p"/api/events") - assert json_response(conn, 200)["data"] == [] - end + {:ok, conn: conn} end - describe "create event" do - test "renders event when data is valid", %{conn: conn} do - conn = post(conn, ~p"/api/events", event: @create_attrs) - assert %{"id" => id} = json_response(conn, 201)["data"] - - conn = get(conn, ~p"/api/events/#{id}") - - assert %{ - "id" => ^id, - "content" => "some content", - "created_at" => "2026-01-28T03:51:00Z", - "kind" => 42, - "pubkey" => "some pubkey", - "sig" => "some sig" - } = json_response(conn, 200)["data"] + describe "POST /api/events" do + test "returns published event when request succeeds", %{conn: conn} do + pub_event = valid_pub_event_fixture() + conn = post(conn, ~p"/api/events", %{"event" => Map.from_struct(pub_event)}) + + assert %{"data" => data} = json_response(conn, 201) + assert data["id"] == pub_event.id + assert data["pubkey"] == pub_event.pubkey + assert data["kind"] == pub_event.kind + assert data["content"] == pub_event.content + assert data["sig"] == pub_event.sig end - test "renders errors when data is invalid", %{conn: conn} do - conn = post(conn, ~p"/api/events", event: @invalid_attrs) - assert json_response(conn, 422)["errors"] != %{} + test "returns 409 Conflict for duplicate event", %{conn: conn} do + pub_event = valid_pub_event_fixture() + post(conn, ~p"/api/events", %{"event" => Map.from_struct(pub_event)}) + conn = post(conn, ~p"/api/events", %{"event" => Map.from_struct(pub_event)}) + + assert json_response(conn, 409) end - end - describe "update event" do - setup [:create_event] + test "returns 422 for event with invalid kind", %{conn: conn} do + pub_event = valid_pub_event_fixture(%{kind: -1}) + conn = post(conn, ~p"/api/events", %{"event" => Map.from_struct(pub_event)}) - test "renders event when data is valid", %{conn: conn, event: %Event{id: id} = event} do - conn = put(conn, ~p"/api/events/#{event}", event: @update_attrs) - assert %{"id" => ^id} = json_response(conn, 200)["data"] + assert json_response(conn, 422) + end - conn = get(conn, ~p"/api/events/#{id}") + test "returns 400 for invalid event ID", %{conn: conn} do + pub_event = invalid_id_pub_event_fixture() + conn = post(conn, ~p"/api/events", %{"event" => Map.from_struct(pub_event)}) - assert %{ - "id" => ^id, - "content" => "some updated content", - "created_at" => "2026-01-29T03:51:00Z", - "kind" => 43, - "pubkey" => "some updated pubkey", - "sig" => "some updated sig" - } = json_response(conn, 200)["data"] + assert json_response(conn, 400) end - test "renders errors when data is invalid", %{conn: conn, event: event} do - conn = put(conn, ~p"/api/events/#{event}", event: @invalid_attrs) - assert json_response(conn, 422)["errors"] != %{} + test "returns 400 for invalid signature", %{conn: conn} do + pub_event = invalid_sig_pub_event_fixture() + conn = post(conn, ~p"/api/events", %{"event" => Map.from_struct(pub_event)}) + + assert json_response(conn, 400) end end - describe "delete event" do - setup [:create_event] + describe "GET /api/events/:id" do + test "returns event when found", %{conn: conn} do + event = event_fixture() + hex_id = Base.encode16(event.id, case: :lower) + conn = get(conn, ~p"/api/events/#{hex_id}") - test "deletes chosen event", %{conn: conn, event: event} do - conn = delete(conn, ~p"/api/events/#{event}") - assert response(conn, 204) + assert %{"data" => data} = json_response(conn, 200) + assert data["id"] == hex_id + end + + test "returns 404 when event not found", %{conn: conn} do + nonexistent_id = String.duplicate("a", 64) + conn = get(conn, ~p"/api/events/#{nonexistent_id}") - assert_error_sent 404, fn -> - get(conn, ~p"/api/events/#{event}") - end + assert json_response(conn, 404) end end - defp create_event(_) do - event = event_fixture(%{}) + describe "DELETE /api/events/:id" do + test "deletes event and returns 204", %{conn: conn} do + event = event_fixture() + hex_id = Base.encode16(event.id, case: :lower) + conn = delete(conn, ~p"/api/events/#{hex_id}") - %{event: event} + assert response(conn, 204) + end + + test "returns 404 when event not found", %{conn: conn} do + nonexistent_id = String.duplicate("a", 64) + conn = delete(conn, ~p"/api/events/#{nonexistent_id}") + + assert json_response(conn, 404) + end end end diff --git a/test/gc_index_relay_web/controllers/filter_controller_test.exs b/test/gc_index_relay_web/controllers/filter_controller_test.exs new file mode 100644 index 0000000..8841bca --- /dev/null +++ b/test/gc_index_relay_web/controllers/filter_controller_test.exs @@ -0,0 +1,283 @@ +defmodule GcIndexRelayWeb.FilterControllerTest do + use GcIndexRelayWeb.ConnCase + + import GcIndexRelay.NostrFixtures + + @moduletag :integration + + setup %{conn: conn} do + conn = + conn + |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") + + {:ok, conn: conn} + end + + describe "GET /api/events (index)" do + test "returns empty list when no events exist", %{conn: conn} do + conn = get(conn, ~p"/api/events?since=0&until=9999999999&limit=10") + + assert %{"data" => []} = json_response(conn, 200) + end + + test "returns events within time range", %{conn: conn} do + event_fixture(%{created_at: 1_640_000_100}) + conn = get(conn, ~p"/api/events?since=0&until=9999999999&limit=10") + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + end + + test "filters by authors", %{conn: conn} do + %{keypair1: keypair1, keypair2: keypair2} = test_keypairs() + event_fixture(%{keypair: :keypair1}) + event_fixture(%{keypair: :keypair2}) + + conn = + get( + conn, + ~p"/api/events?since=0&until=9999999999&limit=10&authors=#{keypair1.public_key_hex}" + ) + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + assert hd(events)["pubkey"] == keypair1.public_key_hex + assert hd(events)["pubkey"] != keypair2.public_key_hex + end + + test "filters by kinds", %{conn: conn} do + event_fixture(%{kind: 1}) + event_fixture(%{kind: 2}) + conn = get(conn, ~p"/api/events?since=0&until=9999999999&limit=10&kinds=1") + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + assert hd(events)["kind"] == 1 + end + + test "filters by ids", %{conn: conn} do + event1 = event_fixture(%{kind: 1}) + _event2 = event_fixture(%{kind: 2, created_at: 1_640_000_001}) + hex_id1 = Base.encode16(event1.id, case: :lower) + + conn = get(conn, ~p"/api/events?since=0&until=9999999999&limit=10&ids=#{hex_id1}") + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + assert hd(events)["id"] == hex_id1 + end + + test "filters by tags", %{conn: conn} do + tagged_pub_event = valid_pub_event_fixture(%{tags: [["p", "abc123def456"]]}) + {:ok, _} = GcIndexRelay.Nostr.create_event(tagged_pub_event) + event_fixture(%{kind: 2, created_at: 1_640_000_001}) + + conn = get(conn, ~p"/api/events?since=0&until=9999999999&limit=10&#p=abc123def456") + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + assert hd(events)["id"] == tagged_pub_event.id + end + + test "respects limit parameter", %{conn: conn} do + event_fixture(%{kind: 1}) + event_fixture(%{kind: 2, created_at: 1_640_000_001}) + event_fixture(%{kind: 3, created_at: 1_640_000_002}) + conn = get(conn, ~p"/api/events?since=0&until=9999999999&limit=2") + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 2 + end + + test "returns 400 when since is missing", %{conn: conn} do + conn = get(conn, ~p"/api/events?until=9999999999&limit=10") + + assert json_response(conn, 400) + end + + test "returns 400 when until is missing", %{conn: conn} do + conn = get(conn, ~p"/api/events?since=0&limit=10") + + assert json_response(conn, 400) + end + + test "returns 400 when limit is missing", %{conn: conn} do + conn = get(conn, ~p"/api/events?since=0&until=9999999999") + + assert json_response(conn, 400) + end + + test "returns 400 when limit is too large", %{conn: conn} do + conn = get(conn, ~p"/api/events?since=0&until=9999999999&limit=101") + + assert json_response(conn, 400) + end + + test "returns 400 for unknown query parameter", %{conn: conn} do + conn = get(conn, ~p"/api/events?since=0&until=9999999999&limit=10&unknown=foo") + + assert json_response(conn, 400) + end + + test "returns 400 for invalid (non-integer) limit value", %{conn: conn} do + conn = get(conn, ~p"/api/events?since=0&until=9999999999&limit=abc") + + assert json_response(conn, 400) + end + + test "returns 400 for invalid (non-integer) since value", %{conn: conn} do + conn = get(conn, ~p"/api/events?since=not_a_number&until=9999999999&limit=10") + + assert json_response(conn, 400) + end + end + + describe "POST /api/events/filter (query)" do + test "returns empty list when no events exist", %{conn: conn} do + conn = post(conn, ~p"/api/events/filter", %{"limit" => 10}) + + assert %{"data" => []} = json_response(conn, 200) + end + + test "returns events with only limit specified", %{conn: conn} do + event_fixture() + conn = post(conn, ~p"/api/events/filter", %{"limit" => 10}) + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) >= 1 + assert length(events) <= 10 + end + + test "filters by authors", %{conn: conn} do + %{keypair1: keypair1, keypair2: keypair2} = test_keypairs() + event_fixture(%{keypair: :keypair1}) + event_fixture(%{keypair: :keypair2}) + + conn = + post(conn, ~p"/api/events/filter", %{ + "authors" => [keypair1.public_key_hex], + "limit" => 10 + }) + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + assert hd(events)["pubkey"] == keypair1.public_key_hex + assert hd(events)["pubkey"] != keypair2.public_key_hex + end + + test "filters by kinds", %{conn: conn} do + event_fixture(%{kind: 1}) + event_fixture(%{kind: 2, created_at: 1_640_000_001}) + + conn = post(conn, ~p"/api/events/filter", %{"kinds" => [1], "limit" => 10}) + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + assert hd(events)["kind"] == 1 + end + + test "filters by ids", %{conn: conn} do + pub_event = valid_pub_event_fixture() + event_fixture(%{kind: 2, created_at: 1_640_000_001}) + {:ok, _} = GcIndexRelay.Nostr.create_event(pub_event) + + conn = + post(conn, ~p"/api/events/filter", %{ + "ids" => [pub_event.id], + "limit" => 10 + }) + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + assert hd(events)["id"] == pub_event.id + end + + test "filters by since timestamp", %{conn: conn} do + event_fixture(%{created_at: 1_639_999_999}) + event_fixture(%{kind: 2, created_at: 1_640_000_100}) + + conn = + post(conn, ~p"/api/events/filter", %{ + "since" => 1_640_000_000, + "limit" => 10 + }) + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + assert hd(events)["created_at"] == 1_640_000_100 + end + + test "filters by until timestamp", %{conn: conn} do + event_fixture(%{created_at: 1_639_999_999}) + event_fixture(%{kind: 2, created_at: 1_640_000_100}) + + conn = + post(conn, ~p"/api/events/filter", %{ + "until" => 1_640_000_000, + "limit" => 10 + }) + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + assert hd(events)["created_at"] == 1_639_999_999 + end + + test "filters by tags", %{conn: conn} do + tagged_pub_event = valid_pub_event_fixture(%{tags: [["p", "abc123def456"]]}) + {:ok, _} = GcIndexRelay.Nostr.create_event(tagged_pub_event) + event_fixture(%{kind: 2, created_at: 1_640_000_001}) + + conn = + post(conn, ~p"/api/events/filter", %{ + "#p" => ["abc123def456"], + "limit" => 10 + }) + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 1 + assert hd(events)["id"] == tagged_pub_event.id + end + + test "respects limit parameter", %{conn: conn} do + event_fixture(%{kind: 1}) + event_fixture(%{kind: 2, created_at: 1_640_000_001}) + event_fixture(%{kind: 3, created_at: 1_640_000_002}) + + conn = post(conn, ~p"/api/events/filter", %{"limit" => 2}) + + assert %{"data" => events} = json_response(conn, 200) + assert length(events) == 2 + end + + test "returns results in descending created_at order", %{conn: conn} do + event_fixture(%{kind: 1, created_at: 1_640_000_001}) + event_fixture(%{kind: 2, created_at: 1_640_000_002}) + event_fixture(%{kind: 3, created_at: 1_640_000_003}) + + conn = post(conn, ~p"/api/events/filter", %{"limit" => 10}) + + assert %{"data" => events} = json_response(conn, 200) + created_ats = Enum.map(events, & &1["created_at"]) + assert created_ats == Enum.sort(created_ats, :desc) + end + + test "returns 400 when limit is missing", %{conn: conn} do + conn = post(conn, ~p"/api/events/filter", %{"kinds" => [1]}) + + assert json_response(conn, 400) + end + + test "returns 400 when limit is 0", %{conn: conn} do + conn = post(conn, ~p"/api/events/filter", %{"limit" => 0}) + + assert json_response(conn, 400) + end + + test "returns 400 when limit exceeds 100", %{conn: conn} do + conn = post(conn, ~p"/api/events/filter", %{"limit" => 101}) + + assert json_response(conn, 400) + end + end +end