diff --git a/lib/subscription/actor_function.ex b/lib/subscription/actor_function.ex index 6cd3e42f..a24a6708 100644 --- a/lib/subscription/actor_function.ex +++ b/lib/subscription/actor_function.ex @@ -1,7 +1,7 @@ defmodule AshGraphql.Subscription.ActorFunction do @moduledoc false - @behaviour AshGraphql.Resource.Subscription.Actor + @behaviour AshGraphql.Subscription.Actor @impl true def actor(actor, [{:fun, {m, f, a}}]) do diff --git a/test/subscription_test.exs b/test/subscription_test.exs index 3e12b1f9..58a97c2d 100644 --- a/test/subscription_test.exs +++ b/test/subscription_test.exs @@ -3,38 +3,54 @@ defmodule AshGraphql.SubscriptionTest do alias AshGraphql.Test.PubSub alias AshGraphql.Test.Schema + alias AshGraphql.Test.Subscribable + + def assert_down(pid) do + ref = Process.monitor(pid) + + assert_receive {:DOWN, ^ref, _, _, _} + end setup do Application.put_env(PubSub, :notifier_test_pid, self()) - {:ok, _} = PubSub.start_link() - {:ok, _} = Absinthe.Subscription.start_link(PubSub) + {:ok, pubsub} = PubSub.start_link() + {:ok, absinthe_sub} = Absinthe.Subscription.start_link(PubSub) :ok + + on_exit(fn -> + Process.exit(pubsub, :normal) + Process.exit(absinthe_sub, :normal) + # block until the processes have exited + assert_down(pubsub) + assert_down(absinthe_sub) + end) end - @query """ - subscription { - subscribableEvents { - created { - id - text - } - updated { - id - text - } - destroyed - } + @admin %{ + id: 1, + role: :admin } - """ - @tag :wip - test "can subscribe to a resource" do - id = "1" + test "can subscribe to all action types resource" do assert {:ok, %{"subscribed" => topic}} = Absinthe.run( - @query, + """ + subscription { + subscribableEvents { + created { + id + text + } + updated { + id + text + } + destroyed + } + } + """, Schema, - context: %{actor: %{id: id}, pubsub: PubSub} + context: %{actor: @admin, pubsub: PubSub} ) create_mutation = """ @@ -52,7 +68,10 @@ defmodule AshGraphql.SubscriptionTest do """ assert {:ok, %{data: mutation_result}} = - Absinthe.run(create_mutation, Schema, variables: %{"input" => %{"text" => "foo"}}) + Absinthe.run(create_mutation, Schema, + variables: %{"input" => %{"text" => "foo"}}, + context: %{actor: @admin} + ) assert Enum.empty?(mutation_result["createSubscribable"]["errors"]) @@ -80,7 +99,8 @@ defmodule AshGraphql.SubscriptionTest do assert {:ok, %{data: mutation_result}} = Absinthe.run(update_mutation, Schema, - variables: %{"id" => subscribable_id, "input" => %{"text" => "bar"}} + variables: %{"id" => subscribable_id, "input" => %{"text" => "bar"}}, + context: %{actor: @admin} ) assert Enum.empty?(mutation_result["updateSubscribable"]["errors"]) @@ -103,7 +123,10 @@ defmodule AshGraphql.SubscriptionTest do """ assert {:ok, %{data: mutation_result}} = - Absinthe.run(destroy_mutation, Schema, variables: %{"id" => subscribable_id}) + Absinthe.run(destroy_mutation, Schema, + variables: %{"id" => subscribable_id}, + context: %{actor: @admin} + ) assert Enum.empty?(mutation_result["destroySubscribable"]["errors"]) @@ -111,4 +134,127 @@ defmodule AshGraphql.SubscriptionTest do assert subscription_data["subscribableEvents"]["destroyed"] == subscribable_id end + + test "policies are applied to subscriptions" do + actor1 = %{ + id: 1, + role: :user + } + + actor2 = %{ + id: 2, + role: :user + } + + assert {:ok, %{"subscribed" => topic1}} = + Absinthe.run( + """ + subscription { + subscribableEvents { + created { + id + text + } + updated { + id + text + } + destroyed + } + } + """, + Schema, + context: %{actor: actor1, pubsub: PubSub} + ) + + assert {:ok, %{"subscribed" => topic2}} = + Absinthe.run( + """ + subscription { + subscribableEvents { + created { + id + text + } + updated { + id + text + } + destroyed + } + } + """, + Schema, + context: %{actor: actor2, pubsub: PubSub} + ) + + assert topic1 != topic2 + + subscribable = + Subscribable + |> Ash.Changeset.for_create(:create, %{text: "foo", actor_id: 1}, actor: @admin) + |> Ash.create!() + + # actor1 will get data because it can see the resource + assert_receive {^topic1, %{data: subscription_data}} + # actor 2 will not get data because it cannot see the resource + refute_receive({^topic2, _}) + + assert subscribable.id == + subscription_data["subscribableEvents"]["created"]["id"] + end + + test "can dedup with actor fun" do + actor1 = %{ + id: 1, + role: :user + } + + actor2 = %{ + id: 2, + role: :user + } + + subscription = """ + subscription { + dedupedSubscribableEvents { + created { + id + text + } + updated { + id + text + } + destroyed + } + } + """ + + assert {:ok, %{"subscribed" => topic1}} = + Absinthe.run( + subscription, + Schema, + context: %{actor: actor1, pubsub: PubSub} + ) + + assert {:ok, %{"subscribed" => topic2}} = + Absinthe.run( + subscription, + Schema, + context: %{actor: actor2, pubsub: PubSub} + ) + + assert topic1 == topic2 + + subscribable = + Subscribable + |> Ash.Changeset.for_create(:create, %{text: "foo", actor_id: 1}, actor: @admin) + |> Ash.create!() + + assert_receive {^topic1, %{data: subscription_data}} + + assert subscribable.id == + subscription_data["dedupedSubscribableEvents"]["created"]["id"] + end end diff --git a/test/support/resources/subscribable.ex b/test/support/resources/subscribable.ex index 5940db9a..81159f7f 100644 --- a/test/support/resources/subscribable.ex +++ b/test/support/resources/subscribable.ex @@ -3,6 +3,7 @@ defmodule AshGraphql.Test.Subscribable do use Ash.Resource, domain: AshGraphql.Test.Domain, data_layer: Ash.DataLayer.Ets, + authorizers: [Ash.Policy.Authorizer], extensions: [AshGraphql.Resource] require Ash.Query @@ -26,18 +27,44 @@ defmodule AshGraphql.Test.Subscribable do subscribe(:subscribable_events) do actions([:create, :update, :destroy]) end + + subscribe(:deduped_subscribable_events) do + actions([:create, :update, :destroy]) + read_action(:open_read) + + actor(fn _ -> + %{id: -1, role: :deduped_actor} + end) + end + end + end + + policies do + bypass actor_attribute_equals(:role, :admin) do + authorize_if(always()) + end + + policy action(:read) do + authorize_if(expr(actor_id == ^actor(:id))) + end + + policy action(:open_read) do + authorize_if(always()) end end actions do default_accept(:*) defaults([:create, :read, :update, :destroy]) + + read(:open_read) end attributes do uuid_primary_key(:id) attribute(:text, :string, public?: true) + attribute(:actor_id, :integer, public?: true) create_timestamp(:created_at) update_timestamp(:updated_at) end