mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 21:43:02 +12:00
251 lines
6.5 KiB
Markdown
251 lines
6.5 KiB
Markdown
<!-- livebook:{"persist_outputs":true} -->
|
|
|
|
# Test Resources
|
|
|
|
```elixir
|
|
Mix.install([{:ash, "~> 3.0}, {:simple_sat, "~> 0.1"}],
|
|
consolidate_protocols: false
|
|
)
|
|
|
|
Logger.configure(level: :warning)
|
|
ExUnit.start()
|
|
```
|
|
|
|
## Introduction
|
|
|
|
We recommend testing your resources *thoroughly*. Often, folks think that testing an `Ash.Resource` is "testing the framework", and in some very simple cases this may be true, like a simple create that just accepts a few attributes.
|
|
|
|
However, testing has two primary roles:
|
|
|
|
1. Confirming our understanding of the way that our application behaves *now*
|
|
2. Ensuring that our application does not change in unintended ways *later*
|
|
|
|
To this end, we highly recommend writing tests even for your simple actions. A single test that confirms that, with simple inputs, the action returns what you expect, can be very powerful.
|
|
|
|
Additionally, Ash offers unique ways of testing individual components of our resources, similar to a unit test.
|
|
|
|
While you don't necessarily need to follow all steps below, we show the various ways you may want to go about testing your resources.
|
|
|
|
## Testing Resources
|
|
|
|
* Add tests for action inputs using property testing
|
|
* Add tests for calling action invocation using property testing
|
|
* Add explicit tests for action inputs/invocation.
|
|
* Add tests for our calculations
|
|
* Test policies "in isolation" using `Ash.can?` (or `Domain.can_*`, provided by code interfaces)
|
|
|
|
## Examples
|
|
|
|
<!-- livebook:{"disable_formatting":true} -->
|
|
|
|
```elixir
|
|
defmodule User do
|
|
use Ash.Resource,
|
|
domain: Domain,
|
|
data_layer: Ash.DataLayer.Ets
|
|
|
|
actions do
|
|
defaults [:read, create: [:admin?]]
|
|
end
|
|
|
|
attributes do
|
|
uuid_primary_key :id
|
|
attribute :admin?, :boolean do
|
|
allow_nil? false
|
|
default false
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule Tweet do
|
|
use Ash.Resource,
|
|
domain: Domain,
|
|
data_layer: Ash.DataLayer.Ets,
|
|
authorizers: [Ash.Policy.Authorizer]
|
|
|
|
attributes do
|
|
uuid_primary_key :id
|
|
attribute :text, :string do
|
|
allow_nil? false
|
|
constraints max_length: 144
|
|
public? true
|
|
end
|
|
|
|
attribute :hidden?, :boolean do
|
|
allow_nil? false
|
|
default false
|
|
public? true
|
|
end
|
|
end
|
|
|
|
calculations do
|
|
calculate :tweet_length, :integer, expr(string_length(text))
|
|
end
|
|
|
|
relationships do
|
|
belongs_to :user, User, allow_nil?: false
|
|
end
|
|
|
|
actions do
|
|
defaults [:read, update: [:text]]
|
|
|
|
create :create do
|
|
primary? true
|
|
accept [:text]
|
|
change relate_actor(:user)
|
|
end
|
|
end
|
|
|
|
policies do
|
|
policy action_type(:read) do
|
|
description "If a tweet is hidden, only the author can read it. Otherwise, anyone can."
|
|
authorize_if relates_to_actor_via(:user)
|
|
forbid_if expr(hidden? == true)
|
|
authorize_if always()
|
|
end
|
|
|
|
policy action_type(:create) do
|
|
description "Anyone can create a tweet"
|
|
authorize_if always()
|
|
end
|
|
|
|
policy action_type(:update) do
|
|
description "Only an admin or the user who tweeted can edit their tweet"
|
|
authorize_if actor_attribute_equals(:admin?, true)
|
|
authorize_if relates_to_actor_via(:user)
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule Domain do
|
|
use Ash.Domain,
|
|
validate_config_inclusion?: false
|
|
|
|
resources do
|
|
resource Tweet do
|
|
define :create_tweet, action: :create, args: [:text]
|
|
define :update_tweet, action: :update, args: [:text]
|
|
define :list_tweets, action: :read
|
|
end
|
|
|
|
resource User do
|
|
define :create_user, action: :create
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
<!-- livebook:{"output":true} -->
|
|
|
|
```
|
|
{:module, Domain, <<70, 79, 82, 49, 0, 2, 46, ...>>,
|
|
[
|
|
Ash.Domain.Dsl.Resources.Resource,
|
|
Ash.Domain.Dsl.Resources.Options,
|
|
Ash.Domain.Dsl,
|
|
%{opts: [], entities: [...]},
|
|
Ash.Domain.Dsl,
|
|
Ash.Domain.Dsl.Resources.Options,
|
|
...
|
|
]}
|
|
```
|
|
|
|
## Write some tests
|
|
|
|
```elixir
|
|
defmodule ActionInvocationTest do
|
|
use ExUnit.Case
|
|
import ExUnitProperties
|
|
|
|
describe "valid inputs" do
|
|
# now if our action inputs are invalid when we think they should be valid, we will find out here
|
|
property "accepts all valid input" do
|
|
user = Domain.create_user!()
|
|
|
|
check all(input <- Ash.Generator.action_input(Tweet, :create)) do
|
|
{text, other_inputs} = Map.pop!(input, :text)
|
|
|
|
assert Domain.changeset_to_create_tweet(
|
|
text,
|
|
other_inputs,
|
|
authorize?: false,
|
|
actor: user
|
|
).valid?
|
|
end
|
|
end
|
|
|
|
# same as the above, but actually call the action. This tests the underlying action implementation
|
|
# not just intiail validation
|
|
property "succeeds on all valid input" do
|
|
user = Domain.create_user!()
|
|
|
|
check all(input <- Ash.Generator.action_input(Tweet, :create)) do
|
|
{text, other_inputs} = Map.pop!(input, :text)
|
|
Domain.create_tweet!(text, other_inputs, authorize?: false, actor: user)
|
|
end
|
|
end
|
|
|
|
test "can tweet some specific text, in addition to any other valid inputs" do
|
|
user = Domain.create_user!()
|
|
|
|
check all(
|
|
input <- Ash.Generator.action_input(Tweet, :create, %{text: "some specific text"})
|
|
) do
|
|
{text, other_inputs} = Map.pop!(input, :text)
|
|
Domain.create_tweet!(text, other_inputs, actor: user)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "authorization" do
|
|
test "allows a user to update their own tweet" do
|
|
user = Domain.create_user!()
|
|
tweet = Domain.create_tweet!("Hello world!", actor: user)
|
|
|
|
assert Domain.can_update_tweet?(user, tweet, "Goodbye world!")
|
|
end
|
|
|
|
test "does not allow a user to update someone elses tweet" do
|
|
user = Domain.create_user!()
|
|
user2 = Domain.create_user!()
|
|
tweet = Domain.create_tweet!("Hello world!", actor: user)
|
|
|
|
refute Domain.can_update_tweet?(user2, tweet, "Goodbye world!")
|
|
end
|
|
|
|
test "allows an admin user to update someone elses tweet" do
|
|
user = Domain.create_user!()
|
|
user2 = Domain.create_user!(%{admin?: true})
|
|
tweet = Domain.create_tweet!("Hello world!", actor: user)
|
|
|
|
assert Domain.can_update_tweet?(user2, tweet, "Goodbye world!")
|
|
end
|
|
end
|
|
|
|
describe "calculations" do
|
|
test "text length calculation computes the length of the text" do
|
|
user = Domain.create_user!()
|
|
tweet = Domain.create_tweet!("Hello world!", actor: user)
|
|
assert Ash.calculate!(tweet, :tweet_length) == 12
|
|
end
|
|
end
|
|
end
|
|
|
|
ExUnit.run()
|
|
```
|
|
|
|
<!-- livebook:{"output":true} -->
|
|
|
|
```
|
|
.......
|
|
Finished in 0.07 seconds (0.00s async, 0.07s sync)
|
|
2 properties, 5 tests, 0 failures
|
|
|
|
Randomized with seed 42546
|
|
```
|
|
|
|
<!-- livebook:{"output":true} -->
|
|
|
|
```
|
|
%{total: 7, failures: 0, skipped: 0, excluded: 0}
|
|
```
|