Skip to content

test: multitenancy aggregate validation tests #535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
208 changes: 208 additions & 0 deletions test/multitenancy_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,214 @@
assert [_] = CompositeKeyPost |> Ash.Query.set_tenant(org1) |> Ash.read!()
end

test "aggregate validation prevents update with linked posts", %{org1: org1} do
# Create a post in org1
post =
Post
|> Ash.Changeset.for_create(:create, %{name: "foo"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

# Create a linked post for the post in org1
linked_post =
Post
|> Ash.Changeset.for_create(:create, %{name: "linked post"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

# Link the posts in org1
post
|> Ash.Changeset.new()
|> Ash.Changeset.manage_relationship(:linked_posts, linked_post, type: :append_and_remove)
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.update!()

# Test that aggregate validation works with tenant context
assert_raise Ash.Error.Invalid, ~r/Can only update if Post has no linked posts/, fn ->
post
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(:update_if_no_linked_posts, %{name: "updated"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.update!()
end
end

test "non-atomic aggregate validation prevents update with linked posts", %{org1: org1} do

Check failure on line 170 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (14) / mix test

test non-atomic aggregate validation prevents update with linked posts (AshPostgres.Test.MultitenancyTest)

Check failure on line 170 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (15) / mix test

test non-atomic aggregate validation prevents update with linked posts (AshPostgres.Test.MultitenancyTest)

Check failure on line 170 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (16) / mix test

test non-atomic aggregate validation prevents update with linked posts (AshPostgres.Test.MultitenancyTest)
# Create a post in org1
post =
Post
|> Ash.Changeset.for_create(:create, %{name: "foo"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

# Create a linked post for the post in org1
linked_post =
Post
|> Ash.Changeset.for_create(:create, %{name: "linked post"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

# Link the posts in org1
post
|> Ash.Changeset.new()
|> Ash.Changeset.manage_relationship(:linked_posts, linked_post, type: :append_and_remove)
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.update!()

# Test non-atomic validation
assert_raise Ash.Error.Invalid, ~r/Can only update if Post has no linked posts/, fn ->
post
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(:update_if_no_linked_posts_non_atomic, %{name: "updated"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.update!()
end
end

test "aggregate validation prevents destroy with linked posts", %{org1: org1} do

Check failure on line 202 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (14) / mix test

test aggregate validation prevents destroy with linked posts (AshPostgres.Test.MultitenancyTest)

Check failure on line 202 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (15) / mix test

test aggregate validation prevents destroy with linked posts (AshPostgres.Test.MultitenancyTest)

Check failure on line 202 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (16) / mix test

test aggregate validation prevents destroy with linked posts (AshPostgres.Test.MultitenancyTest)
# Create a post in org1
post =
Post
|> Ash.Changeset.for_create(:create, %{name: "foo"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

# Create a linked post for the post in org1
linked_post =
Post
|> Ash.Changeset.for_create(:create, %{name: "linked post"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

# Link the posts in org1
post
|> Ash.Changeset.new()
|> Ash.Changeset.manage_relationship(:linked_posts, linked_post, type: :append_and_remove)
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.update!()

# Test destroy with atomic validation
assert_raise Ash.Error.Invalid, ~r/Can only delete if Post has no linked posts/, fn ->
post
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(:destroy_if_no_linked_posts, %{})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.destroy!()
end
end

test "non-atomic aggregate validation prevents destroy with linked posts", %{org1: org1} do

Check failure on line 234 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (14) / mix test

test non-atomic aggregate validation prevents destroy with linked posts (AshPostgres.Test.MultitenancyTest)

Check failure on line 234 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (15) / mix test

test non-atomic aggregate validation prevents destroy with linked posts (AshPostgres.Test.MultitenancyTest)

Check failure on line 234 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (16) / mix test

test non-atomic aggregate validation prevents destroy with linked posts (AshPostgres.Test.MultitenancyTest)
# Create a post in org1
post =
Post
|> Ash.Changeset.for_create(:create, %{name: "foo"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

# Create a linked post for the post in org1
linked_post =
Post
|> Ash.Changeset.for_create(:create, %{name: "linked post"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

# Link the posts in org1
post
|> Ash.Changeset.new()
|> Ash.Changeset.manage_relationship(:linked_posts, linked_post, type: :append_and_remove)
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.update!()

# Test destroy with non-atomic validation
assert_raise Ash.Error.Invalid, ~r/Can only delete if Post has no linked posts/, fn ->
post
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(:destroy_if_no_linked_posts_non_atomic, %{})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.destroy!()
end
end

test "post with no linked posts can be updated in another tenant", %{org1: org1, org2: org2} do
# Create a post in org1 with a linked post
post_in_org1 =
Post
|> Ash.Changeset.for_create(:create, %{name: "foo"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

linked_post =
Post
|> Ash.Changeset.for_create(:create, %{name: "linked post"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

post_in_org1
|> Ash.Changeset.new()
|> Ash.Changeset.manage_relationship(:linked_posts, linked_post, type: :append_and_remove)
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.update!()

# Create a post in org2 with no linked posts
org2_post =
Post
|> Ash.Changeset.for_create(:create, %{name: "updateable"})
|> Ash.Changeset.set_tenant("org_" <> org2.id)
|> Ash.create!()

# This should succeed since the post has no linked posts in org2
updated_post =
org2_post
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(:update_if_no_linked_posts, %{name: "updated"})
|> Ash.Changeset.set_tenant("org_" <> org2.id)
|> Ash.update!()

assert updated_post.name == "updated"
end

test "post with no linked posts can be destroyed in another tenant", %{org1: org1, org2: org2} do

Check failure on line 304 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (14) / mix test

test post with no linked posts can be destroyed in another tenant (AshPostgres.Test.MultitenancyTest)

Check failure on line 304 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (15) / mix test

test post with no linked posts can be destroyed in another tenant (AshPostgres.Test.MultitenancyTest)

Check failure on line 304 in test/multitenancy_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (16) / mix test

test post with no linked posts can be destroyed in another tenant (AshPostgres.Test.MultitenancyTest)
# Create a post in org1 with a linked post
post_in_org1 =
Post
|> Ash.Changeset.for_create(:create, %{name: "foo"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

linked_post =
Post
|> Ash.Changeset.for_create(:create, %{name: "linked post"})
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.create!()

post_in_org1
|> Ash.Changeset.new()
|> Ash.Changeset.manage_relationship(:linked_posts, linked_post, type: :append_and_remove)
|> Ash.Changeset.set_tenant("org_" <> org1.id)
|> Ash.update!()

# Create a post in org2 with no linked posts
org2_post_for_destroy =
Post
|> Ash.Changeset.for_create(:create, %{name: "destroyable"})
|> Ash.Changeset.set_tenant("org_" <> org2.id)
|> Ash.create!()

# This should succeed since the post has no linked posts in org2
org2_post_for_destroy
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(:destroy_if_no_linked_posts, %{})
|> Ash.Changeset.set_tenant("org_" <> org2.id)
|> Ash.destroy!()

# Verify the post was destroyed
assert [] =
Post
|> Ash.Query.filter(id == ^org2_post_for_destroy.id)
|> Ash.Query.set_tenant("org_" <> org2.id)
|> Ash.read!()
end

test "loading attribute multitenant resources from context multitenant resources works" do
org =
Org
Expand Down
46 changes: 46 additions & 0 deletions test/support/multitenancy/resources/post.ex
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
defmodule HasNoLinkedPosts do
@moduledoc false
use Ash.Resource.Validation

def atomic(_changeset, _opts, context) do
condition = expr(exists(linked_posts, true))

[
{:atomic, [], condition,
expr(
error(^Ash.Error.Changes.InvalidChanges, %{
message: ^context.message || "Post has linked posts"
})
)}
]
end
end

defmodule AshPostgres.MultitenancyTest.Post do
@moduledoc false
use Ash.Resource,
Expand Down Expand Up @@ -27,6 +45,34 @@ defmodule AshPostgres.MultitenancyTest.Post do
defaults([:create, :read, :update, :destroy])

update(:update_with_policy)

update :update_if_no_linked_posts do
validate HasNoLinkedPosts do
message "Can only update if Post has no linked posts"
end
end

update :update_if_no_linked_posts_non_atomic do
require_atomic?(false)

validate HasNoLinkedPosts do
message "Can only update if Post has no linked posts"
end
end

destroy :destroy_if_no_linked_posts do
validate HasNoLinkedPosts do
message "Can only delete if Post has no linked posts"
end
end

destroy :destroy_if_no_linked_posts_non_atomic do
require_atomic?(false)

validate HasNoLinkedPosts do
message "Can only delete if Post has no linked posts"
end
end
end

postgres do
Expand Down
Loading