From 62ae0d7066e0982540c256989179627a88c3e021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Buszkiewicz?= Date: Mon, 2 Feb 2026 10:30:02 +0100 Subject: [PATCH] Fix semantic of has-many association permissions to match Permit.Ecto behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: MichaƂ Buszkiewicz --- lib/permit/permissions/parsed_condition.ex | 2 +- test/permit/permissions/condition_test.exs | 31 ++++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/permit/permissions/parsed_condition.ex b/lib/permit/permissions/parsed_condition.ex index c8b211f..b14bc49 100644 --- a/lib/permit/permissions/parsed_condition.ex +++ b/lib/permit/permissions/parsed_condition.ex @@ -115,7 +115,7 @@ defmodule Permit.Permissions.ParsedCondition do check_conditions(sub_assoc, assoc_conditions) sub_assoc when is_list(sub_assoc) -> - Enum.all?(sub_assoc, &check_conditions(&1, assoc_conditions)) + Enum.any?(sub_assoc, &check_conditions(&1, assoc_conditions)) sub_assoc -> sub_assoc == assoc_conditions diff --git a/test/permit/permissions/condition_test.exs b/test/permit/permissions/condition_test.exs index 42fa5f8..5881421 100644 --- a/test/permit/permissions/condition_test.exs +++ b/test/permit/permissions/condition_test.exs @@ -78,7 +78,8 @@ defmodule Permit.Permissions.ConditionTest do |> ParsedCondition.satisfied?(test_object, nil) end - test "should not satisfy nested has-many associations" do + test "should use ANY semantics for has-many associations" do + # None of the actors have age 123, so should fail condition = {:actors, {:==, [age: 123]}} test_object = %Movie{actors: [%Actor{age: 666}]} @@ -86,19 +87,45 @@ defmodule Permit.Permissions.ConditionTest do refute ConditionParser.build(condition) |> ParsedCondition.satisfied?(test_object, nil) + # With ANY semantics: at least one actor has age 666, so should PASS condition = {:actors, {:==, [age: 666]}} test_object = %Movie{actors: [%Actor{age: 123}, %Actor{age: 666}]} - refute ConditionParser.build(condition) + assert ConditionParser.build(condition) |> ParsedCondition.satisfied?(test_object, nil) + # At least one actor matches both age AND name conditions condition = {:actors, {:==, [age: 123, name: "test"]}} test_object = %Movie{ actors: [%Actor{age: 123, name: "test"}, %Actor{age: 123, name: "test_666"}] } + # With ANY semantics: first actor matches both conditions + assert ConditionParser.build(condition) + |> ParsedCondition.satisfied?(test_object, nil) + + # No actor matches both conditions simultaneously + condition = {:actors, {:==, [age: 123, name: "test"]}} + + test_object = %Movie{ + actors: [%Actor{age: 666, name: "test"}, %Actor{age: 123, name: "wrong"}] + } + + # No single actor has both age 123 AND name "test" + refute ConditionParser.build(condition) + |> ParsedCondition.satisfied?(test_object, nil) + end + + test "should not satisfy nested has-many associations when association is empty" do + condition = {:actors, {:==, [age: 123]}} + + # An empty list of actors should NOT satisfy the condition + # because there is no actor with age 123 + test_object = %Movie{actors: []} + + # Changed to any? semantic (PR #22 in permit_ecto) refute ConditionParser.build(condition) |> ParsedCondition.satisfied?(test_object, nil) end