ash/test/actions/aggregate_test.exs
Zach Daniel 34d6f229c5 improvement: allow policy conditions to be applied inside their block
```elixir
policy do
  condition [...]
  authorize_if ...
end
```
2024-07-22 07:10:09 -04:00

396 lines
10 KiB
Elixir

defmodule Ash.Test.Actions.AggregateTest do
@moduledoc false
use ExUnit.Case, async: true
require Ash.Query
alias Ash.Test.Domain, as: Domain
defmodule Comment do
use Ash.Resource,
domain: Domain,
data_layer: Ash.DataLayer.Ets,
authorizers: [Ash.Policy.Authorizer]
actions do
default_accept :*
defaults [:read, :destroy, create: :*, update: :*]
end
attributes do
uuid_primary_key :id
attribute :public, :boolean do
public?(true)
default false
end
attribute :thing, :string do
public?(true)
end
attribute :thing2, :decimal do
public?(true)
end
attribute :thing3, :integer do
public?(true)
end
end
relationships do
belongs_to :post, Ash.Test.Actions.AggregateTest.Post do
public?(true)
end
end
policies do
policy do
condition(always())
authorize_if expr(public == true)
end
end
end
defmodule Post do
@moduledoc false
use Ash.Resource,
domain: Domain,
data_layer: Ash.DataLayer.Ets,
authorizers: [Ash.Policy.Authorizer]
ets do
private?(true)
end
actions do
default_accept :*
defaults [:read, :destroy, create: :*, update: :*]
read :unpublic
read :with_foo do
prepare build(filter: [foo: true])
end
end
attributes do
uuid_primary_key :id
attribute :title, :string do
public?(true)
end
attribute :public, :boolean do
public?(true)
default false
end
attribute :tenant, :string do
public?(true)
end
attribute :thing, :string do
public?(true)
end
attribute :foo, :boolean, default: false, public?: true
end
multitenancy do
global? true
strategy :attribute
attribute :tenant
end
aggregates do
count :count_of_comments, :comments do
public? false
end
count :count_of_posts, [:comments, :post] do
public? false
end
count :count_of_comment_posts_with_matching_things, [:comments, :post] do
public? false
join_filter(:comments, expr(parent(thing) == thing))
end
count :count_of_comments_unauthorized, :comments do
public? true
authorize? false
end
min :min_of_thing2, :comments, :thing2 do
public? true
authorize? false
end
max :max_of_thing2, :comments, :thing2 do
public? true
authorize? false
end
avg :average_of_thing2, :comments, :thing2 do
public? true
authorize? false
end
min :min_of_thing3, :comments, :thing3 do
public? true
authorize? false
end
max :max_of_thing3, :comments, :thing3 do
public? true
authorize? false
end
avg :average_of_thing3, :comments, :thing3 do
public? true
authorize? false
end
end
relationships do
has_many :comments, Comment do
public?(true)
end
end
policies do
policy action(:read) do
authorize_if expr(public == true)
end
policy action(:unpublic) do
authorize_if expr(public == false)
end
end
end
describe "Ash.aggregate" do
test "allows counting records" do
assert %{count: 0} = Ash.aggregate!(Post, {:count, :count}, authorize?: false)
Post
|> Ash.Changeset.for_create(:create, %{title: "title"})
|> Ash.create!(authorize?: false)
assert %{count: 1} = Ash.aggregate!(Post, {:count, :count}, authorize?: false)
Post
|> Ash.Changeset.for_create(:create, %{title: "title"})
|> Ash.create!(authorize?: false)
assert %{count: 2} = Ash.aggregate!(Post, {:count, :count}, authorize?: false)
end
test "honors tenant" do
assert %{count: 0} = Ash.aggregate!(Post, {:count, :count}, authorize?: false)
Post
|> Ash.Changeset.for_create(:create, %{title: "title", tenant: "foo"})
|> Ash.create!(authorize?: false)
assert %{count: 1} =
Ash.aggregate!(Post, {:count, :count}, tenant: "foo", authorize?: false)
Post
|> Ash.Changeset.for_create(:create, %{title: "title", tenant: "foo"})
|> Ash.create!(authorize?: false)
Post
|> Ash.Changeset.for_create(:create, %{title: "title", tenant: "bar"})
|> Ash.create!(authorize?: false)
assert %{count: 2} =
Ash.aggregate!(Post, {:count, :count}, tenant: "foo", authorize?: false)
assert %{count: 3} = Ash.aggregate!(Post, {:count, :count}, authorize?: false)
end
test "runs authorization" do
assert %{count: 0} = Ash.aggregate!(Post, {:count, :count}, authorize?: true)
assert 0 = Ash.count!(Post, authorize?: true)
Post
|> Ash.Changeset.for_create(:create, %{title: "title"})
|> Ash.create!(authorize?: false)
assert %{count: 0} = Ash.aggregate!(Post, {:count, :count}, authorize?: true)
assert 0 = Ash.count!(Post, authorize?: true)
assert %{count: 1} =
Post
|> Ash.Query.for_read(:unpublic)
|> Ash.aggregate!({:count, :count}, actor: nil)
assert %{count: 1} =
Ash.aggregate!(Post, {:count, :count}, actor: nil, action: :unpublic)
assert 1 = Ash.count!(Post, actor: nil, action: :unpublic, authorize?: true)
assert 0 = Ash.count!(Post, authorize?: true)
Post
|> Ash.Changeset.for_create(:create, %{title: "title", public: true})
|> Ash.create!(authorize?: false)
assert %{count: 1} = Ash.aggregate!(Post, {:count, :count}, authorize?: true)
assert 1 = Ash.count!(Post, authorize?: true)
end
test "use custom actions from the query" do
Post
|> Ash.Changeset.for_create(:create, %{title: "without foo", foo: false})
|> Ash.create!(authorize?: false)
assert false ==
Post
|> Ash.Query.for_read(:with_foo)
|> Ash.exists?(authorize?: false)
Post
|> Ash.Changeset.for_create(:create, %{title: "with foo", foo: true})
|> Ash.create!(authorize?: false)
assert 1 ==
Post
|> Ash.Query.for_read(:with_foo)
|> Ash.count!(authorize?: false)
assert ["with foo"] ==
Post
|> Ash.Query.for_read(:with_foo)
|> Ash.list!(:title, authorize?: false)
end
test "returns error for invalid input" do
assert {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Invalid.NoSuchInput{input: :bar}]}} =
Post
|> Ash.Query.for_read(:with_foo, %{bar: "no such input"})
|> Ash.exists(authorize?: false)
end
end
describe "aggregate loading" do
test "loading aggregates can be authorized or not" do
post =
Post
|> Ash.Changeset.for_create(:create, %{title: "title", public: true})
|> Ash.create!(authorize?: false)
Comment
|> Ash.Changeset.for_create(:create, %{post_id: post.id, public: true})
|> Ash.create!(authorize?: false)
Comment
|> Ash.Changeset.for_create(:create, %{post_id: post.id, public: false})
|> Ash.create!(authorize?: false)
assert %{count_of_comments: 2, count_of_comments_unauthorized: 2} =
Ash.load!(post, [:count_of_comments, :count_of_comments_unauthorized],
authorize?: false
)
assert %{count_of_comments: 1, count_of_comments_unauthorized: 2} =
Ash.load!(post, [:count_of_comments, :count_of_comments_unauthorized],
authorize?: true
)
end
test "join filters are applied" do
post =
Post
|> Ash.Changeset.for_create(:create, %{
title: "title",
public: true,
thing: "not the same"
})
|> Ash.create!(authorize?: false)
Comment
|> Ash.Changeset.for_create(:create, %{
post_id: post.id,
public: true,
thing: "doesnt match"
})
|> Ash.create!(authorize?: false)
assert Ash.load!(post, :count_of_comment_posts_with_matching_things, authorize?: false).count_of_comment_posts_with_matching_things ==
0
Comment
|> Ash.Changeset.for_create(:create, %{
post_id: post.id,
public: true,
thing: "not the same"
})
|> Ash.create!(authorize?: false)
assert Ash.load!(post, :count_of_comment_posts_with_matching_things, authorize?: false).count_of_comment_posts_with_matching_things ==
1
end
test "aggregations on decimal fields succeed" do
post =
Post
|> Ash.Changeset.for_create(:create, %{
title: "title",
public: true,
thing: "not the same"
})
|> Ash.create!(authorize?: false)
Comment
|> Ash.Changeset.for_create(:create, %{
post_id: post.id,
public: true,
thing: "doesnt match",
thing2: 10,
thing3: 100
})
|> Ash.create!(authorize?: false)
Comment
|> Ash.Changeset.for_create(:create, %{
post_id: post.id,
public: true,
thing: "doesnt match",
thing2: 20,
thing3: 200
})
|> Ash.create!(authorize?: false)
Comment
|> Ash.Changeset.for_create(:create, %{
post_id: post.id,
public: true,
thing: "doesnt match",
thing2: nil,
thing3: nil
})
|> Ash.create!(authorize?: false)
assert Decimal.eq?(
Ash.load!(post, :min_of_thing2, authorize?: false).min_of_thing2,
Decimal.new(10)
)
assert Decimal.eq?(
Ash.load!(post, :max_of_thing2, authorize?: false).max_of_thing2,
Decimal.new(20)
)
assert Decimal.eq?(
Ash.load!(post, :average_of_thing2, authorize?: false).average_of_thing2,
Decimal.new(15)
)
assert Ash.load!(post, :min_of_thing3, authorize?: false).min_of_thing3 == 100
assert Ash.load!(post, :max_of_thing3, authorize?: false).max_of_thing3 == 200
assert Ash.load!(post, :average_of_thing3, authorize?: false).average_of_thing3 == 150
end
end
end