Skip to content
Draft
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