AshGrant provides a DSL-based testing framework for verifying policy configurations without requiring a database. This tests policy configuration, not data — no database records needed.

Resource Setup

Policy tests verify how your resolver converts roles to permissions. Use any resource with an ash_grant block configured (see the Getting Started guide).

DSL-Based Tests

Write policy tests to verify the resolver and scope configuration:

defmodule MyApp.PolicyTests.PostPolicyTest do
  use AshGrant.PolicyTest

  resource MyApp.Post

  actor :admin, %{role: :admin}
  actor :editor, %{role: :editor, id: "editor_001"}
  actor :viewer, %{role: :viewer}

  describe "read access" do
    test "editor can read all posts" do
      assert_can :editor, :read
    end

    test "viewer can read published posts" do
      assert_can :viewer, :read, %{status: :published}
    end

    test "viewer cannot read drafts" do
      assert_cannot :viewer, :read, %{status: :draft}
    end
  end

  describe "write access" do
    test "editor can update own posts" do
      assert_can :editor, :update, %{author_id: "editor_001"}
    end

    test "editor cannot update others posts" do
      assert_cannot :editor, :update, %{author_id: "other_user"}
    end

    test "viewer cannot update any posts" do
      assert_cannot :viewer, :update
    end
  end
end

Assertion Macros

MacroDescription
assert_can(actor, action)Actor can perform action
assert_can(actor, action, record)Actor can access specific record (bare map)
assert_can(actor, action, [record: ..., arguments: ...])Actor can access record with action arguments
assert_cannot(actor, action)Actor cannot perform action
assert_cannot(actor, action, record)Actor cannot access specific record (bare map)
assert_cannot(actor, action, [record: ..., arguments: ...])Actor cannot access record with action arguments

Action can be specified as:

  • Atom: :read (shorthand for action: :read)
  • Keyword: action: :approve (specific action name)
  • Keyword: action_type: :update (all actions of type)

Testing argument-based scopes

For scopes that reference ^arg(:name) (see Argument-Based Scope), pass an arguments: map alongside record: using the keyword-list form:

assert_can :manager, :update,
  record: %{author_id: "u1"},
  arguments: %{center_id: "center_A"}

assert_cannot :manager, :update,
  record: %{author_id: "u1"},
  arguments: %{center_id: "outside_unit"}

The arguments map is forwarded to Ash.Expr.fill_template as :args, so the scope's ^arg(:center_id) resolves to the supplied value before evaluation against the record.

YAML Format

Policy tests can also be written in YAML for non-developers or interchange:

resource: MyApp.Post

actors:
  editor:
    role: editor
    id: "editor_001"
  viewer:
    role: viewer

tests:
  - name: "editor can read all posts"
    assert_can:
      actor: editor
      action: read

  - name: "viewer can read published posts"
    assert_can:
      actor: viewer
      action: read
      record:
        status: published

  - name: "viewer cannot read drafts"
    assert_cannot:
      actor: viewer
      action: read
      record:
        status: draft

  - name: "editor can update own posts"
    assert_can:
      actor: editor
      action: update
      record:
        author_id: "editor_001"

  - name: "editor cannot update others posts"
    assert_cannot:
      actor: editor
      action: update
      record:
        author_id: "other_user"

YAML — arguments: field for argument-based scopes

YAML tests can provide action arguments for scopes that use ^arg(:name):

  - name: "manager can update refund in their unit"
    assert_can:
      actor: unit_manager
      action: update
      record:
        author_id: "u1"
      arguments:
        center_id: "center_A"

  - name: "manager cannot update refund outside their unit"
    assert_cannot:
      actor: unit_manager
      action: update
      record:
        author_id: "u1"
      arguments:
        center_id: "center_Z"

See Argument-Based Scope for when to use this pattern.

Mix Tasks

Run policy tests:

# Run DSL tests
mix ash_grant.verify test/policy_tests/

# Run YAML tests
mix ash_grant.verify priv/policy_tests/document.yaml

# Verbose output
mix ash_grant.verify test/policy_tests/ --verbose

Export policies:

# Export to YAML
mix ash_grant.export MyApp.Document --format=yaml

# Export to Mermaid diagram
mix ash_grant.export MyApp.Document --format=mermaid

# Export to Markdown documentation
mix ash_grant.export MyApp.Document --format=markdown

# Export to file
mix ash_grant.export MyApp.Document --format=markdown --output=docs/document.md

Import YAML to DSL:

# Generate DSL code from YAML (output to stdout)
mix ash_grant.import priv/policy_tests/document.yaml

# Generate and write to file
mix ash_grant.import priv/policy_tests/document.yaml --output=test/policy_tests/document_test.exs

Running Policy Tests Programmatically

Use the AshGrant.PolicyTest.Runner module:

# Run a single module
results = AshGrant.PolicyTest.Runner.run_module(MyApp.PolicyTests.DocumentPolicyTest)

# Run all policy test modules
summary = AshGrant.PolicyTest.Runner.run_all()
# => %{passed: 10, failed: 0, results: [...]}

# Run specific modules
summary = AshGrant.PolicyTest.Runner.run_all(modules: [DocumentPolicyTest, PostPolicyTest])

Dependencies

To use YAML format, add yaml_elixir to your dependencies:

def deps do
  [
    {:yaml_elixir, "~> 2.9"}
  ]
end