[
  {
    "path": ".credo.exs",
    "content": "# This file contains the configuration for Credo and you are probably reading\n# this after creating it with `mix credo.gen.config`.\n#\n# If you find anything wrong or unclear in this file, please report an\n# issue on GitHub: https://github.com/rrrene/credo/issues\n#\n%{\n  #\n  # You can have as many configs as you like in the `configs:` field.\n  configs: [\n    %{\n      #\n      # Run any config using `mix credo -C <name>`. If no config name is given\n      # \"default\" is used.\n      name: \"default\",\n      #\n      # These are the files included in the analysis:\n      files: %{\n        #\n        # You can give explicit globs or simply directories.\n        # In the latter case `**/*.{ex,exs}` will be used.\n        included: [\"lib/\", \"src/\", \"web/\", \"apps/\"],\n        excluded: [~r\"/_build/\", ~r\"/deps/\"]\n      },\n      #\n      # If you create your own checks, you must specify the source files for\n      # them here, so they can be loaded by Credo before running the analysis.\n      requires: [],\n      #\n      # Credo automatically checks for updates, like e.g. Hex does.\n      # You can disable this behaviour below:\n      check_for_updates: true,\n      #\n      # If you want to enforce a style guide and need a more traditional linting\n      # experience, you can change `strict` to `true` below:\n      strict: false,\n      #\n      # If you want to use uncolored output by default, you can change `color`\n      # to `false` below:\n      color: true,\n      #\n      # You can customize the parameters of any check by adding a second element\n      # to the tuple.\n      #\n      # To disable a check put `false` as second element:\n      #\n      #     {Credo.Check.Design.DuplicatedCode, false}\n      #\n      checks: [\n        {Credo.Check.Consistency.ExceptionNames},\n        {Credo.Check.Consistency.LineEndings},\n        {Credo.Check.Consistency.MultiAliasImportRequireUse},\n        {Credo.Check.Consistency.ParameterPatternMatching},\n        {Credo.Check.Consistency.SpaceAroundOperators},\n        {Credo.Check.Consistency.SpaceInParentheses},\n        {Credo.Check.Consistency.TabsOrSpaces},\n\n        # For some checks, like AliasUsage, you can only customize the priority\n        # Priority values are: `low, normal, high, higher`\n        {Credo.Check.Design.AliasUsage, priority: :low},\n\n        # For others you can set parameters\n\n        # If you don't want the `setup` and `test` macro calls in ExUnit tests\n        # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just\n        # set the `excluded_macros` parameter to `[:schema, :setup, :test]`.\n        {Credo.Check.Design.DuplicatedCode, excluded_macros: []},\n\n        # You can also customize the exit_status of each check.\n        # If you don't want TODO comments to cause `mix credo` to fail, just\n        # set this value to 0 (zero).\n        {Credo.Check.Design.TagTODO, exit_status: 2},\n        {Credo.Check.Design.TagFIXME},\n\n        {Credo.Check.Readability.FunctionNames},\n        {Credo.Check.Readability.LargeNumbers},\n        {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80},\n        {Credo.Check.Readability.ModuleAttributeNames},\n        {Credo.Check.Readability.ModuleDoc},\n        {Credo.Check.Readability.ModuleNames},\n        {Credo.Check.Readability.NoParenthesesWhenZeroArity},\n        {Credo.Check.Readability.ParenthesesInCondition},\n        {Credo.Check.Readability.PredicateFunctionNames},\n        {Credo.Check.Readability.PreferImplicitTry},\n        {Credo.Check.Readability.RedundantBlankLines},\n        {Credo.Check.Readability.Specs, false},\n        {Credo.Check.Readability.StringSigils},\n        {Credo.Check.Readability.TrailingBlankLine},\n        {Credo.Check.Readability.TrailingWhiteSpace},\n        {Credo.Check.Readability.VariableNames},\n        {Credo.Check.Refactor.DoubleBooleanNegation},\n\n        # {Credo.Check.Refactor.CaseTrivialMatches}, # deprecated in 0.4.0\n        {Credo.Check.Refactor.ABCSize},\n        {Credo.Check.Refactor.CondStatements},\n        {Credo.Check.Refactor.CyclomaticComplexity},\n        {Credo.Check.Refactor.FunctionArity},\n        {Credo.Check.Refactor.MatchInCondition},\n        {Credo.Check.Refactor.NegatedConditionsInUnless},\n        {Credo.Check.Refactor.NegatedConditionsWithElse},\n        {Credo.Check.Refactor.Nesting},\n        {Credo.Check.Refactor.PipeChainStart, false},\n        {Credo.Check.Refactor.UnlessWithElse},\n        {Credo.Check.Refactor.VariableRebinding},\n\n        {Credo.Check.Warning.BoolOperationOnSameValues},\n        {Credo.Check.Warning.IExPry},\n        {Credo.Check.Warning.IoInspect},\n        {Credo.Check.Warning.NameRedeclarationByAssignment},\n        {Credo.Check.Warning.NameRedeclarationByCase},\n        {Credo.Check.Warning.NameRedeclarationByDef},\n        {Credo.Check.Warning.NameRedeclarationByFn},\n        {Credo.Check.Warning.OperationOnSameValues},\n        {Credo.Check.Warning.OperationWithConstantResult},\n        {Credo.Check.Warning.UnusedEnumOperation},\n        {Credo.Check.Warning.UnusedFileOperation},\n        {Credo.Check.Warning.UnusedKeywordOperation},\n        {Credo.Check.Warning.UnusedListOperation},\n        {Credo.Check.Warning.UnusedPathOperation},\n        {Credo.Check.Warning.UnusedRegexOperation},\n        {Credo.Check.Warning.UnusedStringOperation},\n        {Credo.Check.Warning.UnusedTupleOperation},\n\n        # Custom checks can be created using `mix credo.gen.check`.\n        #\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": ".formatter.exs",
    "content": "[\n  import_deps: [:plug, :phoenix, :phoenix_live_view],\n  inputs: [\n    \"lib/**/*.ex\",\n    \"config/*.exs\",\n    \"test/**/*.exs\",\n    \"mix.exs\"\n  ]\n]\n"
  },
  {
    "path": ".github/workflows/elixir.yml",
    "content": "name: CI\n\non:\n  push:\n  pull_request:\n    branches:\n      - master\n\njobs:\n  mix_test:\n    name: mix test (OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}})\n\n    strategy:\n      matrix:\n        include:\n          - elixir: \"1.18\"\n            otp: \"27.2\"\n          - elixir: \"1.14\"\n            otp: \"25.3\"\n\n    runs-on: ubuntu-20.04\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install Erlang and Elixir\n        uses: erlef/setup-beam@v1\n        with:\n          otp-version: ${{ matrix.otp }}\n          elixir-version: ${{ matrix.elixir }}\n\n      - name: Restore deps and _build cache\n        uses: actions/cache@v4\n        with:\n          path: |\n            deps\n            _build\n          key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}\n          restore-keys: |\n            deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}\n\n      - name: Install dependencies\n        run: mix deps.get --only test\n\n      - name: Remove compiled application files\n        run: mix clean\n\n      - name: Compile & lint dependencies\n        run: mix compile --warnings-as-errors\n        env:\n          MIX_ENV: test\n\n      - name: Run tests\n        run: mix test\n"
  },
  {
    "path": ".gitignore",
    "content": "/_build\n/deps\nerl_crash.dump\n*.ez\ntags\n/doc\n/cover\n*.beam\n.history\n.tool-version"
  },
  {
    "path": ".travis.yml",
    "content": "language: elixir\nelixir:\n  - 1.8\n  - 1.7\n  - 1.6\n  - 1.5\n  - 1.4\n  \notp_release:\n  - 21.1\n  - 20.3\n  - 19.3\n  - 18.3\n\nmatrix:\n  exclude:\n    - elixir: 1.8\n      otp_release: 19.3\n    - elixir: 1.8\n      otp_release: 18.3\n    - elixir: 1.7\n      otp_release: 18.3\n    - elixir: 1.6\n      otp_release: 18.3\n    - elixir: 1.5\n      otp_release: 21.1\n    - elixir: 1.4\n      otp_release: 21.1\n    - elixir: 1.3\n      otp_release: 21.1\n    - elixir: 1.3\n      otp_release: 20.3\nscript: mix test && mix credo\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## Changelog\n\n## v2.0.0-dev\n  Canary 2.0.0 introduces authorization hooks for Phoenix LiveView. The Plug based authorization was refactored a bit to make the API cosistent. Please follow the [Upgrade guide to 2.0.0](docs/upgrade.md#upgrading-from-canary-1-2-0-to-2-0-0) for more details.\n\n  * Enhancements\n    * added support for authorization LiveView with `Canary.Hooks`\n    * added `:error_handler` and ErrorHandler behaviour\n    * added `:required` option, default to true\n\n  * Dependency changes\n    * Elixir ~> 1.14 is now required\n\n  * Deprecations\n    * The `:non_id_actions` option is deprecated and will be removed in Canary 2.1.0. Use separate `:authorize_resource` plug for `non_id_actions` and `:except` to exclude non_in_actions.\n    * The `:persisted` option is deprecated and will be removed in Canary 2.1.0. Use `:required` instead.\n\n## v1.2.0\n  * Enhancements\n    * Add `required` opt\n\n## v1.1.0\n  * Enhancements\n    * Add `non_id_actions` opt\n\n## v1.0\n  * Bug fixes\n    * Do not clobber resources in the `Conn` on index action if they are of the same model\n\n## v0.14.2\n  * Relax Ecto version requirements\n\n## v0.14.1\n  * Bug fixes\n    * Use Macro.underscore/1 instead of Mix.Utils.underscore/1 to avoid :mix dependency on production\n\n## v0.14.0\n  * Enhancements\n    * You can now tell Canary to search for a resource using a field other than the default `:id` by using the `:id_field` option. Note that the specified field must be able to uniquely identify any resource in the specified table.\n  * Dependency changes\n    * Elixir ~> 1.2 is now required\n    * Ecto ~> 1.1 is now required\n\n## v0.13.1\n\n  * Enhancements\n    * If both an `:unauthorized_handler` and a `:not_found_handler` are specified for `load_and_authorize_resource`, and the request meets the criteria for both, the `:unauthorized_handler` will be called first.\n  * Bug Fixes\n    * If more than one handler are specified and the first handler halts the request, the second handler will be skipped.\n\n## v0.13.0\n\n  * Enhancements\n    * Canary can now be configured to call a user-defined function when a resource is not found. The function is specified and used in a similar manner to `:unauthorized_handler`.\n  * Bug Fixes\n    * Disabled protocol consolidation in order for tests to work on Elixir 1.2\n\n## v0.12.2\n\n  * Deprecations\n    * Canary now looks for the current action in `conn.assigns.canary_action` rather than `conn.assigns.action` in order to avoid conflicts. The `action` key is deprecated.\n\n## v0.12.0\n\n* Enhancements\n  * Canary can now be configured to call a user-defined function when authorization fails. Canary will pass the `Plug.Conn` for the request to the given function. The handler should accept a `Plug.Conn` as its only argument, and should return a `Plug.Conn`.\n    * For example, to have Canary call `Helpers.handle_unauthorized/1`:\n    ```elixir\n    config :canary, unauthorized_handler: {Helpers, :handle_unauthorized}\n    ```\n    * You can also specify the `:unauthorized_handler` on an individual basis by specifying the `:unauthorized_handler`   `opt` in the plug call like so:\n    ```elixir\n    plug :load_and_authorize_resource Post, unauthorized_handler: {Helpers, :handle_unauthorized}\n    ```\n\n## v0.11.0\n\n* Enhancements\n  * Resources can now be loaded on `:new` and `:create` actions, when `persisted: true` is specified in the plug call. This allows parent resources to be loaded when a child is created. For example, if a `Post` resource has multiple `Comment` children, you may want to load the parent `Post` when creating a new `Comment`. You can load the parent `Post` with a separate\n  ```elixir\n  plug :load_and_authorize_resource, model: Post, id_name: \"post_id\", persisted: true, only: [:create]\n  ```\n  This will cause Canary to try to load the corresponding `Post` from the database when creating a `Comment` at the URL `/posts/:post_id/comments`\n\n## v0.10.0\n\n* Bug fix\n  * Correctly checks `conn.assigns` for pre-existing resource\n\n* Deprecations\n  * Canary now favours looking for the current action in `conn.assigns.canary_action` rather than `conn.assigns.action` in order to avoid conflicts. The `action` key is deprecated\n\n* Enhancements\n  * The name of the id in `conn.params` can now be specified with the `id_name` opt\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2016 Chris Kelly\n\n\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\n\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\n\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "Canary\n======\n[![Actions Status](https://github.com/cpjk/canary/workflows/CI/badge.svg)](https://github.com/runhyve/canary/actions?query=workflow%3ACI)\n[![Hex pm](https://img.shields.io/hexpm/v/canary.svg?style=flat)](https://hex.pm/packages/canary)\n\nAn authorization library in Elixir for `Plug` and `Phoenix.LiveView` applications that restricts what resources the current user is allowed to access, and automatically load and assigns resources.\n\nInspired by [CanCan](https://github.com/CanCanCommunity/cancancan) for Ruby on Rails.\n\n[Read the docs](https://hexdocs.pm/canary/2.0.0-dev/getting-started.html)\n\n# Canary 2.0.0\n\nThe `master` branch is for the development of Canary 2.0.0. Check out [branch 1.2.x](https://github.com/cpjk/canary/tree/1.2.x) if you are looking Canary 1 (only plug authentication).\n\n## Installation\n\nFor the latest master (2.0.0-dev):\n\n```elixir\ndefp deps do\n  {:canary, github: \"cpjk/canary\"}\nend\n```\n\nFor the latest release:\n\n```elixir\ndefp deps do\n  {:canary, \"~> 2.0.0-dev\"}\nend\n```\n\nThen run `mix deps.get` to fetch the dependencies.\n\n## Quick start\n\nCanary provides functions to be used as plugs or LiveView hooks to load and authorize resources:\n\n`load_resource`, `authorize_resource`, `authorize_controller`*, and `load_and_authorize_resource`.\n\n`load_resource` and `authorize_resource` can be used by themselves, while `load_and_authorize_resource` combines them both.\n\n*Available only in plug based authentication*\n\nIn order to use Canary, you will need, at minimum:\n\n- A [Canada.Can protocol](https://github.com/jarednorman/canada) implementation (a good place would be `lib/abilities.ex`)\n\n- An Ecto record struct containing the user to authorize in `assigns.current_user` (the key can be customized - [see more](#overriding-the-default-user)).\n\n- Your Ecto repo specified in your `config/config.exs`: `config :canary, repo: YourApp.Repo`\n\nFor the plugs just `import Canary.Plugs`. In a Phoenix app the best place would probably be inside `controller/0` in your `web/web.ex`, in order to make the functions available in all of your controllers.\n\nFor the liveview hooks just `use Canary.Hooks`. In a Phoenix app the best place would probably be inside `live_view/0` in your `web/web.ex`, in order to make the functions available in all of your controllers.\n\n\n### load_resource\n\nLoads the resource having the id given in `params[\"id\"]` from the database using the given Ecto repo and model, and assigns the resource to `assigns.<resource_name>`, where `resource_name` is inferred from the model name.\n\n<!-- tabs-open -->\n### Conn Plugs example\n```elixir\nplug :load_resource, model: Project.Post\n```\n\nWill load the `Project.Post` having the id given in `conn.params[\"id\"]` through `YourApp.Repo`, and assign it to `conn.assigns.post`.\n\n### LiveView Hooks example\n```elixir\nmount_canary :load_resource, model: Project.Post\n```\n\nWill load the `Project.Post` having the id given in `params[\"id\"]` through `YourApp.Repo`, and assign it to `socket.assigns.post`\n<!-- tabs-close -->\n\n### authorize_resource\n\nChecks whether or not the `current_user` for the request can perform the given action on the given resource and assigns the result (true/false) to `assigns.authorized`. It is up to you to decide what to do with the result.\n\nFor Phoenix applications, Canary determines the action automatically.\nFor non-Phoenix applications, or to override the action provided by Phoenix, simply ensure that `assigns.canary_action` contains an atom specifying the action.\n\nFor the LiveView on `handle_params` it uses `socket.assigns.live_action` as action, on `handle_event` it uses the event name as action.\n\n\n\nIn order to authorize resources, you must specify permissions by implementing the [Canada.Can protocol](https://github.com/jarednorman/canada) for your `User` model (Canada is included as a light weight dependency).\n\n### load_and_authorize_resource\n\nAuthorizes the resource and then loads it if authorization succeeds. Again, the resource is loaded into `assigns.<resource_name>`.\n\nIn the following example, the `Post` with the same `user_id` as the `current_user` is only loaded if authorization succeeds.\n\n## Usage Example\n\nLet's say you have a Phoenix application with a `Post` model, and you want to authorize the `current_user` for accessing `Post` resources.\n\nLet's suppose that you have a file named `lib/abilities.ex` that contains your Canada authorization rules like so:\n\n```elixir\ndefimpl Canada.Can, for: User do\n  def can?(%User{ id: user_id }, action, %Post{ user_id: user_id })\n    when action in [:show], do: true\n\n  def can?(%User{ id: user_id }, _, _), do: false\nend\n```\n\n### Example for Conn Plugs\n\nIn your `web/router.ex:` you have:\n\n```elixir\nget \"/posts/:id\", PostController, :show\ndelete \"/posts/:id\", PostController, :delete\n```\n\nTo automatically load and authorize on the `Post` having the `id` given in the params, you would add the following plug to your `PostController`:\n\n```elixir\nplug :load_and_authorize_resource, model: Post\n```\n\nIn this case, on `GET /posts/12` authorization succeeds, and the `Post` specified by `conn.params[\"id]` will be loaded into `conn.assigns.post`.\n\nHowever, on `DELETE /posts/12`, authorization fails and the `Post` resource is not loaded.\n\n### Example for LiveView Hooks\n\nIn your `web/router.ex:` you have:\n\n```elixir\nlive \"/posts/:id\", PostLive, :show\n```\n\nand in your PostLive module `web/live/post_live.ex`:\n\n```elixir\ndefmodule MyAppWeb.PostLive do\n  use MyAppWeb, :live_view\n\n  def render(assigns) do\n    ~H\"\"\"\n    Post id: {@post.id}\n    <button phx-click=\"delete\">Delete</button>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket), do: {:ok, socket}\n\n  def handle_event(\"delete\", _params, socket) do\n    # Do the action\n    {:noreply, update(socket, :temperature, &(&1 + 1))}\n  end\nend\n```\n\nTo automatically load and authorize on the `Post` having the `id` given in the params, you would add the following hook to your `PostLive`:\n\n```elixir\nmount_hook :load_and_authorize_resource, model: Post\n```\n\nIn this case, once opening `/posts/12` the `load_and_authorize_resource` on `handle_params` stage will be performed. The the `Post` specified by `params[\"id]` will be loaded into `socket.assigns.post`.\n\nHowever, when the `delete` event will be triggered, authorization fails and the `Post` resource is not loaded. Socket will be halted.\n\n### Excluding actions\n\nTo exclude an action from any of the plugs, pass the `:except` key, with a single action or list of actions.\n\nFor example,\n\nSingle action form:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, except: :show\n\nmount_canary :load_and_authorize_resource, model: Post, except: :show\n```\n\nList form:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, except: [:show, :create]\n\nmount_canary :load_and_authorize_resource, model: Post, except: [:show, :create]\n```\n\n### Authorizing only specific actions\n\nTo specify that a plug should be run only for a specific list of actions, pass the `:only` key, with a single action or list of actions.\n\nFor example,\n\nSingle action form:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, only: :show\n\nmount_canary :load_and_authorize_resource, model: Post, only: :show\n```\n\nList form:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, only: [:show, :create]\n\nmount_canary :load_and_authorize_resource, model: Post, only: [:show, :create]\n```\n\n> Note: Having both `:only` and `:except` in opts is invalid. Canary will raise `ArgumentError` \"You can't use both :except and :only options\"\n\n### Overriding the default user\n\nGlobally, the default key for finding the user to authorize can be set in your configuration as follows:\n\n```elixir\nconfig :canary, current_user: :some_current_user\n```\n\nIn this case, canary will look for the current user record in `assigns.some_current_user`.\n\nThe current user key can also be overridden for individual plugs as follows:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, current_user: :current_admin\n\nmount_canary :load_and_authorize_resource, model: Post, current_user: :current_admin\n```\n\n### Specifying resource_name\n\nTo specify the name under which the loaded resource is stored, pass the `:as` flag in the plug declaration.\n\nFor example,\n\n```elixir\nplug :load_and_authorize_resource, model: Post, as: :new_post\n\nmount_canary :load_and_authorize_resource, model: Post, as: :new_post\n```\n\nwill load the post into `assigns.new_post`\n\n### Preloading associations\n\nAssociations can be preloaded with `Repo.preload` by passing the `:preload` option with the name of the association:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, preload: :comments\n\nmount_canary :load_and_authorize_resource, model: Post, preload: :comments\n```\n\n### Non-id actions\n\nTo authorize actions where there is no loaded resource, the resource passed to the `Canada.Can` implementation should be the module name of the model rather than a struct.\n\nTo authorize such actions use `authorize_resource` plug with `required: false` option\n\n```elixir\nplug :authorize_resource, model: Post, only: [:index, :new, :create], required: false\n\nmount_canary :authorize_resource, model: Post, only: [:index, :new, :create], required: false\n```\n\nFor example, when authorizing access to the `Post` resource, you should use\n\n```elixir\ndef can?(%User{}, :index, Post), do: true\n```\n\ninstead of\n\n```elixir\ndef can?(%User{}, :index, %Post{}), do: true\n```\n\n> ### Deprecated {: .warning}\n>\n> The `:non_id_actions` is deprecated as of 2.0.0-dev and will be removed in Canary 2.1.0\n> Please follow the [Upgrade guide to 2.0.0](docs/upgrade.md#upgrading-from-canary-1-2-0-to-2-0-0) for more details.\n\n### Nested associations\n\nSometimes you need to load and authorize a parent resource when you have\na relationship between two resources and you are creating a new one or\nlisting all the children of that parent. Depending on your authorization\nmodel you migth authorize against the parent resource or against the child.\n\n```elixir\ndefmodule MyAppWeb.CommentController do\n\n  plug :load_and_authorize_resource,\n    model: Post,\n    id_name: \"post_id\",\n    only: [:new_comment, :create_comment]\n\n  # get /posts/:post_id/comments/new\n  def new_comment(conn, _params) do\n    # ...\n  end\n\n  # post /posts/:post_id/comments\n  def new_comment(conn, _params) do\n    # ...\n  end\nend\n```\n\nIt will authorize using `Canada.Can` with following arguments:\n1. subject is `conn.assigns.current_user`\n2. action is `:new_comment` or `:create_comment`\n3. resource is `%Post{}` with `conn.params[\"post_id\"]`\n\nThanks to the `:requried` set to true by default this plug will call `not_found_handler` if the `Post` with given `post_id` does not exists.\nIf for some reason you want to disable it, set `required: false` in opts.\n\n> ### Deprecated {: .warning}\n>\n> The `:persisted` is deprecated as of 2.0.0-dev and will be removed in Canary 2.1.0\n> Please follow the [Upgrade guide to 2.0.0](docs/upgrade.md#upgrading-from-canary-1-2-0-to-2-0-0) for more details.\n\n### Implementing Canada.Can for an anonymous user\n\nYou may wish to define permissions for when there is no logged in current user (when `conn.assigns.current_user` is `nil`).\nIn this case, you should implement `Canada.Can` for `nil` like so:\n\n```elixir\ndefimpl Canada.Can, for: Atom do\n  # When the user is not logged in, all they can do is read Posts\n  def can?(nil, :show, %Post{}), do: true\n  def can?(nil, _, _), do: false\nend\n```\n\n### Specifing database field\n\nYou can tell Canary to search for a resource using a field other than the default `:id` by using the `:id_field` option. Note that the specified field must be able to uniquely identify any resource in the specified table.\n\nFor example, if you want to access your posts using a string field called `slug`, you can use\n\n```elixir\nplug :load_and_authorize_resource, model: Post, id_name: \"slug\", id_field: \"slug\"\n```\n\nto load and authorize the resource `Post` with the slug specified by `conn.params[\"slug\"]` value.\n\nIf you are using Phoenix, your `web/router.ex` should contain something like:\n\n```elixir\nresources \"/posts\", PostController, param: \"slug\"\n```\n\nThen your URLs will look like:\n\n```\n/posts/my-new-post\n```\n\ninstead of\n\n```\n/posts/1\n```\n\n### Handling unauthorized actions\n\nBy default, when an action is unauthorized, Canary simply sets `conn.assigns.authorized` to `false`.\nHowever, you can configure a handler function to be called when authorization fails. Canary will pass the `Plug.Conn` to the given function. The handler should accept a `Plug.Conn` as its only argument, and should return a `Plug.Conn`.\n\nFor example, to have Canary call `Helpers.handle_unauthorized/1`:\n\n```elixir\nconfig :canary, unauthorized_handler: {Helpers, :handle_unauthorized}\n```\n\n### Handling resource not found\n\nBy default, when a resource is not found, Canary simply sets the resource in `conn.assigns` to `nil`. Like unauthorized action handling , you can configure a function to which Canary will pass the `conn` when a resource is not found:\n\n```elixir\nconfig :canary, not_found_handler: {Helpers, :handle_not_found}\n```\n\nYou can also specify handlers on an individual basis (which will override the corresponding configured handler, if any) by specifying the corresponding `opt` in the plug call:\n\n```elixir\nplug :load_and_authorize_resource Post,\n  unauthorized_handler: {Helpers, :handle_unauthorized},\n  not_found_handler: {Helpers, :handle_not_found}\n```\n\nTip: If you would like the request handling to stop after the handler function exits, e.g. when redirecting, be sure to call `Plug.Conn.halt/1` within your handler like so:\n\n```elixir\ndef handle_unauthorized(conn) do\n  conn\n  |> put_flash(:error, \"You can't access that page!\")\n  |> redirect(to: \"/\")\n  |> halt\nend\n```\n\nNote: If both an `:unauthorized_handler` and a `:not_found_handler` are specified for `load_and_authorize_resource`, and the request meets the criteria for both, the `:unauthorized_handler` will be called first.\n\n## License\nMIT License. Copyright 2016 Chris Kelly.\n"
  },
  {
    "path": "config/config.exs",
    "content": "# This file is responsible for configuring your application\n# and its dependencies with the aid of the Mix.Config module.\nimport Config\n\n# This configuration is loaded before any dependency and is restricted\n# to this project. If another project depends on this project, this\n# file won't be loaded nor affect the parent project. For this reason,\n# if you want to provide default values for your application for third-\n# party users, it should be done in your mix.exs file.\n\n# Sample configuration:\n#\n#     config :logger, :console,\n#       level: :info,\n#       format: \"$date $time [$level] $metadata$message\\n\",\n#       metadata: [:user_id]\n\n# It is also possible to import configuration files, relative to this\n# directory. For example, you can emulate configuration per environment\n# by uncommenting the line below and defining dev.exs, test.exs and such.\n# Configuration from the imported file will override the ones defined\n# here (which is why it is important to import them last).\n#\n#     import_config \"#{Mix.env}.exs\"\n"
  },
  {
    "path": "docs/getting-started.md",
    "content": "# Getting Started\n\nThis guide introduces **Canary**, an authorization library for **Elixir** applications using `Plug` and `Phoenix.LiveView`. It restricts resource access based on user permissions and automatically loads and assigns resources.\n\nCanary provides three primary functions to be used as *plugs* or *LiveView hooks* to manage resources:\n\n- `load_resource`\n- `authorize_resource`\n- `load_and_authorize_resource`\n\n## Glossary\n\n### Subject\n\nThe key name used to fetch the subject from `assigns`. This subject is passed to `Canada.Can` to evaluate permissions. By default, it is `:current_user`.\n\nTo configure this in your module:\n\n```elixir\nconfig :canary, current_user: :user\n```\n\nYou can override this setting per plug/mounted hook by specifying `:current_user`.\n\n### Action\n\nFor **Phoenix applications and Plug-based pages**, Canary determines the action automatically from `conn.private.phoenix_action`. In **non-Phoenix applications**, or when overriding Phoenix's default action behavior, set `conn.assigns.canary_action` with an atom specifying the action.\n\nFor **LiveView**:\n\n- In `handle_params`, Canary uses `socket.assigns.live_action`.\n- In `handle_event`, Canary uses the `event_name` (converted from a string to an atom for consistency).\n\nActions can be limited using `:only` or `:except` options; otherwise, they apply to all actions.\n\n### Resource\nFor `load_resource` and `load_and_authorize_resource`, Canary checks if the resource is already assigned. If not, it fetches the resource from the repository using:\n\n- `:id_name` from `params` (default: `\"id\"`).\n- `:id_field` in the struct (default: `:id`).\n\nBy default, **a resource is required**. That means the resource must be present in `conn.assigns` or `socket.assigns`. It's fetched using the `:model` name, which can be overridden with the `:as` option.\n\nIf it cannot be found, an error is handled. To make it optional, set `:required` to `false`. In this case, the resource module name is used instead of a loaded struct.\n\nYou can also use `:preload` to preload associations. See `Ecto.Query.preload/3` for more details.\n\nFor `authorize_resource`, the resource must be present in `conn.assigns` or `socket.assigns`. By default, it fetches the resource using the `:model` name, which can be overridden with the `:as` option.\n\n### Load Resource\n\nLoads a resource from the database using the specified **Ecto repo** and model. It assigns the result to `assigns.<resource_name>`, where `resource_name` is inferred from the model.\n\n### Authorize Resource\n\nChecks if the **subject** can perform a given action on a resource. The result (`true`/`false`) is assigned to `assigns.authorized`. The developer decides how to handle this result.\n\n### Load and Authorize Resource\n\nA combination of **Load Resource** and **Authorize Resource** in a single function.\n\n## Configuration\n\nTo use Canary, you need to configure it in `config/config.exs`. All settings, except for `:repo`, can be overridden when using the plug or hook.\n\n### Available Configuration Options\n\n| Name | Description | Example |\n| --- | --- | --- |\n| `:repo` | The Repo module used in your application. | `YourApp.Repo` |\n| `:current_user` | The key name used to fetch the user from assigns. This value will be used as the `subject` for `Canada.Can` to evaluate permissions. Defaults to `:current_user`. | `:current_member` |\n| `:error_handler` | A module that implements the `Canary.ErrorHandler` behavior. It is used to handle `:not_found` and `:unauthorized` errors. Defaults to `Canary.DefaultHandler`. | `YourApp.ErrorHandler` |\n\n### Deprecated Options\n\n| Name | Description | Example |\n| --- | --- | --- |\n| `:not_found_handler` | A `{mod, fun}` tuple for handling not found errors. | `{YourApp.ErrorHandler, :handle_not_found}` |\n| `:unauthorized_handler` | A `{mod, fun}` tuple for handling unauthorized errors. | `{YourApp.ErrorHandler, :handle_unauthorized}` |\n\n> #### Info {: .info}\n>\n> The `:error_handler` option should be used instead of separate handlers for `:not_found` and `:unauthorized` errors.\n> Handlers can still be overridden using plug or `mount_canary` options.\n\n### Example Configuration\n\n```elixir\nconfig :canary,\n  repo: YourApp.Repo,\n  current_user: :current_user,\n  error_handler: YourApp.ErrorHandler\n```\n\n### Overriding configuration\n\n#### Authorize different subject\nSometimes, you may need to perform authorization for a different subject. You can override `:current_user` by passing options to the plug or hook.\n\n<!-- tabs-open -->\n### Conn Plugs\n```elixir\nimport Canary.Plugs\n\nplug :load_and_authorize_resource,\n  model: Team,\n  current_user: :current_member\n```\n\nWith this override, the authorization check will use `conn.assigns.current_member` as the subject.\n\n\n### LiveView Hooks\n```elixir\nuse Canary.Hooks\n\nmount_canary :load_and_authorize_resource,\n  on: :handle_event,\n  current_user: :current_member,\n  model: Team\n```\n\nWith this override, the authorization check for the `:handle_event` stage hook will use `socket.assigns.current_member` as the subject.\n<!-- tabs-close -->\n\n\n### Different error handler\n\nIf you want to override the global Canary error handler, you can override one of the functions: `:not_found_handler` or `:unauthorized_handler`.\n\n<!-- tabs-open -->\n### Conn Plugs\n```elixir\nplug :load_and_authorize_resource,\n  model: Team,\n  current_user: :current_member,\n  not_found_handler: {CustomErrorHandler, :custom_not_found_handler},\n  unauthorized_handler: {CustomErrorHandler, :custom_unauthorized_handler}\n```\n\n### LiveView Hooks\n```elixir\nuse Canary.Hooks\n\nmount_canary :load_and_authorize_resource,\n  model: Team,\n  current_user: :current_member,\n  only: [:special_action]\n  unauthorized_handler: {CustomErrorHandler, :special_unauthorized_handler}\n```\n<!-- tabs-close -->\n\nThe error handler should implement the `Canary.ErrorHandler` behavior.\nRefer to the default implementation in `Canary.DefaultHandler`.\n\n## Canary options\nCanary Plugs and Hooks use the same configuration options.\n\n### Available Options\n\n| Name | Description | Example |\n| --- | --- | --- |\n| `:model` | The model module name used in your app. **Required** | `Post` |\n| `:only` | Specifies the actions for which the plug/hook is enabled. | `[:show, :edit, :update]` |\n| `:except` | Specifies the actions for which the plug/hook is disabled. | `[:delete]` |\n| `:current_user` | The key name used to fetch the user from assigns. This value will be used as the `subject` for `Canada.Can` to evaluate permissions. Defaults to `:current_user`. Applies only to `authorize_resource` or `load_and_authorize_resource`. | `:current_member` |\n| `:on` | Specifies the LiveView lifecycle stages where the hook should be attached. Defaults to `:handle_params`. **Available only in Canary.Hooks** | `[:handle_params, :handle_event]` |\n| `:as` | Specifies the key name under which the resource will be stored in assigns. | `:team_post` |\n| `:id_name` | Specifies the name of the ID in params. *Defaults to `\"id\"`*. | `:post_id` |\n| `:id_field` | Specifies the database field name used to search for the `id_name` value. *Defaults to `\"id\"`*. | `:post_id` |\n| `:required` | Determines if the resource is required. If not found, it triggers a not found error. *Defaults to `true`*. | `false` |\n| `:not_found_handler` | A `{mod, fun}` tuple that overrides the default error handler for not found errors. | `{YourApp.ErrorHandler, :custom_handle_not_found}` |\n| `:unauthorized_handler` | A `{mod, fun}` tuple that overrides the default error handler for unauthorized errors. | `{YourApp.ErrorHandler, :custom_handle_unauthorized}` |\n\n### Deprecated Options\n\n| Name | Description | Example |\n| --- | --- | --- |\n| `:non_id_actions` | Additional actions for which Canary will authorize based on the model name. | `[:index, :new, :create]` |\n| `:persisted` | Forces the resource to always be loaded from the database. Defaults to `false`. **Available only in Canary.Plugs** | `true` |\n\n### Examples\n\n```elixir\n  plug :load_and_authorize_resource,\n    current_user: :current_member,\n    model: Machine,\n    preload: [:plan, :networks, :distribution, :job, ipv4: [:ip_pool], hypervisor: :region]\n\n  plug :load_resource,\n    model: Hypervisor,\n    id_name: \"hypervisor_id\",\n    only: [:new, :create],\n    preload: [:region, :hypervisor_type, machines: [:networks, :plan, :distribution]],\n\n  plug :load_and_authorize_resource,\n    model: Hypervisor,\n    preload: [\n      :region,\n      :hypervisor_type,\n      machines:\n        {Hypervisors.preload_active_machines, [:plan, :distribution, :hypervisor, :networks]}\n    ]\n\n  mount_canary :authorize_resource,\n    on: [:handle_params, :handle_event],\n    current_user: :current_member,\n    model: Machine,\n    only: [:index, :new],\n    required: false\n\n  mount_canary :load_and_authorize_resource,\n    on: [:handle_event],\n    current_user: :current_member,\n    model: Machine,\n    only: [:start, :stop, :restart, :poweroff]\n```\n\n## Plug and Hooks\n\n\n`Canary.Plugs` and `Canary.Hooks` should work the same way in most cases, providing a unified approach to authorization for both Plug-based controllers and LiveView.\n\n- **Shared Functionality:**\n  Both Plugs and Hooks allow for resource loading and authorization using similar configuration options. This ensures consistency across different parts of your application.\n\n- **Differences:**\n  - `Canary.Plugs` is designed for use in traditional Phoenix controllers and pipelines.\n  - `Canary.Hooks` is specifically built for LiveView and integrates with lifecycle events such as `:handle_params` and `:handle_event`.\n\n- **Configuration Compatibility:**\n  Most options, such as `:model`, `:current_user`, `:only`, `:except`, and error handlers, function identically in both Plugs and Hooks. However, `Canary.Hooks` includes the `:on` option, allowing you to specify which LiveView lifecycle stage the authorization should run on.\n\nBy keeping their behavior aligned, Canary ensures a seamless developer experience, whether you're working with traditional controller-based actions or real-time LiveView interactions.\n### Authorize Resource\n\nThe `authorize_resource` function checks whether the subject, typically stored in `assigns` under `:current_user`, is authorized to access a given resource. If the `:current_user` is not authorized, it sets `assigns.authorized` to `false` and calls the `handle_unauthorized/1` function from the `:error_handler` module configured in `config.exs` or the `:unauthorized_handler` specified in the options.\n\n#### Authorization Logic\n\nThe authorization check is performed using the `can?/3` function from the `Canada.Can` protocol implemeted for `subject`:\n\n```elixir\ncan?(subject, action, resource)\n```\n\nwhere:\n\n1. **Subject** – The entity being authorized, typically fetched from `assigns.current_user`.\n   - By default, Canary looks for `:current_user`.\n   - This key can be overridden via the `opts` or globally in `Application.get_env(:canary, :current_user, :current_user)`.\n\n2. **Action** – The current action being performed.\n\n3. **Resource** – The resource being accessed.\n   - If the resource is already loaded, it is taken from `assigns`.\n   - If the resource is not loaded and not required, the model name is used instead.\n\n#### Example Usage\n\n```elixir\n# Replace `plug` with `mount_canary` for LiveView Hooks\nplug :authorize_resource,\n  current_user: :current_member,\n  model: Event,\n  as: :public_event\n```\n\nIn this example:\n\n1. The `authorize_resource` function checks whether `:current_member` (instead of the default `:current_user`) is authorized to access the `Event` resource.\n2. The resource is expected to be available in `assigns.public_event`.\n3. If the user is unauthorized, `assigns.authorized` is set to false, and the `unauthorized_handler` is triggered.\n\n### Load Resource\n\nThe `load_resource` function fetches a resource based on an ID provided in `params` and assigns it to `assigns`. By default, it uses the `\"id\"` key from `params` and retrieves the resource from the database using the `:id` field of the model specified in `opts[:model]`. The loaded resource is stored under `assigns` using a key derived from the model module name.\n\n#### Customizing the Load Behavior\n\nYou can modify the default behavior with the following options:\n\n- **`:id_name`** – Override the default `\"id\"` param key.\n- **`:id_field`** – Change the field used to query the resource in the database.\n- **`:as`** – Override the default `assigns` key where the resource is stored.\n- **`:required`** - When set to `false` it will assign `nil` instad calling the `not_found_handler`.\n\n#### Example Usage\n\n```elixir\n# Replace `plug` with `mount_canary` for LiveView Hooks\nplug :load_resource,\n  model: Event,\n  as: :public_event,\n  id_name: \"uuid\",\n  id_field: :uuid,\n  required: false\n```\n\nIn this example:\n\n1. `load_resource` fetches the `\"uuid\"` from `params`.\n2. It queries `Event` using the `:uuid` field in the database.\n3. The result is assigned to `assigns.public_event`.\n4. If no matching `Event` is found, `assigns.public_event` will be set to `nil`.\n\nTo trigger the `not_found_handler` when the resource is missing, ensure the `:required` flag is **not explicitly set to** `false` (it defaults to `true`).\n\n\n### Load and Authorize Resource\n\nThe `load_and_authorize_resource` function combines two operations:\n\n1. **Loading the Resource** – Fetches the resource based on an ID from `params` and assigns it to `assigns`, similar to `load_resource`.\n2. **Authorizing the Resource** – Checks whether the subject (by default, `:current_user`) is authorized to access the resource, using `authorize_resource`.\n\nThis function ensures that resources are both retrieved and access-controlled within a single step.\n\n> #### Error handler order {: .info}\n>\n> If both `:unauthorized_handler` and `:not_found_handler` are specified for `load_and_authorize_resource`, and the request meets the criteria for both, the `:unauthorized_handler` will be called first.\n\n\n## Non-ID Actions\n\nFor actions that do not require loading a specific resource (such as `:index`, `:new`, and `:create`), use `:authorize_resource` instead of `:load_resource` or `:load_and_authorize_resource`.\nEnsure that these functions are limited to actions where resource loading is necessary.\n\nBy default, the `:required` option is set to `true`, meaning that if the resource cannot be found in the repository, the `not_found_handler` will be called.\nSetting `:required` to `false` allows the resource to be assigned as `nil`, in which case the model module name will be used as the resource when calling `can?/3`.\n\n### Example Usage\n\n```elixir\nplug :authorize_resource,\n  model: Post,\n  only: [:index, :new, :create],\n  required: false\n\nplug :load_and_authorize_resource,\n  model: Post,\n  except: [:index, :create, :new]\n```\n\n### Loading All Resources in `:index` Action\nIf you need to load multiple resources for the `:index` action, you can either use a plug or load the resources directly within the `index/2` controller action.\n\n#### Option 1: Using a Plug\n```elixir\nplug :load_all_resources when action in [:index]\n\ndefp load_all_resources(conn, _opts) do\n  assign(conn, :posts, Posts.list_posts())\nend\n```\n\n#### Option 2: Loading Directly in the Controller Action\n```elixir\ndef index(conn, _params) do\n  posts = Posts.list_posts()\n  render(conn, \"index.html\", posts: posts)\nend\n```\n\n## Nested Resources\n\nSometimes, you need to load and authorize a parent resource when dealing with nested relationships—such as when creating a child resource or listing all children of a parent. With the default `:required` set to true, if the parent resource is not found, the `not_found_handler` will be called.\n\n### Example Usage\n\nWhen loading and authorizing a `Post` resource that `has_many` `Comment` resources:\n\n```elixir\n# Load and authorize the parent (Post)\nplug :load_and_authorize_resource,\n  model: Post,\n  id_name: \"post_id\",\n  only: [:create_comment]\n\n# Authorize action the child (Comment)\nplug :authorize_resource,\n  model: Comment,\n  only: [:create_comment, :save_comment],\n  required: false\n```\n\n#### Explanation\n\n1. The first plug loads and authorizes the parent `Post` resource using the `post_id` from `params` in the URL (`/posts/:post_id/comments`).\n   - The `:required` option ensures that if the Post is missing, the `not_found_handler` is called.\n2. The second plug authorizes actions on the child `Comment` resource.\n  - Since this is a **non-ID action**, `authorize_resource` is used.\n  - The `Comment` module name is passed as the resource to `can?/3` since no specific `Comment` does not exists yet.\n\nThis approach ensures that authorization is enforced correctly in nested resource scenarios.\n\n## Defining Permissions\n\nTo perform authorization checks, you need to implement the [`Canada.Can` protocol](https://github.com/jarednorman/canada) for each subject that requires permission validation.\nBy default, Canary uses `:current_user` from Plug or LiveView assigns as the subject.\n\n### Example: Defining Permissions for an Authenticated User\n\nAssume your application has a `User` module for authentication.\nYou can define permissions in `lib/abilities/user.ex`:\n\n```elixir\ndefimpl Canada.Can, for: User do\n  # Super admin can do everything\n  def can?(%User{role: \"superadmin\"}, _action, _resource), do: true\n\n  # Post owner can view and modify their own posts\n  def can?(%User{id: user_id}, action, %Post{user_id: user_id})\n    when action in [:show, :edit, :update], do: true\n\n  # Deny all other actions by default\n  def can?(%User{id: user_id}, _, _), do: false\nend\n```\n\n### Handling Anonymous Users\n\nIf the subject (`:current_user` in assigns) is `nil`, and the authorization check is performed then `can/3` will be performed against `Atom`.\n\nFor anonymous users, define permissions, for example: `lib/abilities/anonymous.ex`:\n```elixir\ndefimpl Canada.Can, for: Atom do\n  # Allow anonymous users to register\n  def can?(nil, :new, User), do: true\n  def can?(nil, :create, User), do: true\n  def can?(nil, :confirm, User), do: true\n\n  # Allow anonymous users to create sessions\n  def can?(nil, :new, Session), do: true\n  def can?(nil, :create, Session), do: true\n\n  # Deny all other actions\n  def can?(_, _action, _model), do: false\nend\n```\n\nDefining permissions for `Atom` and `nil` subjects is optional.\nIf your application enforces authentication using a plug like `:require_authenticated_user` in the router pipeline, this may not be necessary.\n\n\n## Error handling\n\n### Handling Unauthorized Actions\n\nBy default, when subject is unauthorized to access an action, Canary sets `assigns.authorized` to `false`.\nHowever, you can configure a custom handler function to be called when authorization fails.\nCanary will pass the `Plug.Conn` or `Phoenix.LiveView.Socket` to the specified function, which should accept `conn` or `socket` as its only argument and return a `Plug.Conn` or tuple `{:halt, socket}`.\n\nThe error handler should implement the `Canary.ErrorHandler` behavior.\nRefer to the default implementation in `Canary.DefaultHandler`.\n\nFor example, to have Canary call `ErrorHandler.handle_unauthorized/1`:\n\n```elixir\nconfig :canary, error_handler: ErrorHandler\n```\n\n> #### LiveView Hook handlers\n>\n> In LiveView, the error handler should return `{:halt, socket}`.\n> For `handle_params`, it should also perform a redirect.\n\n\n### Handling Resource Not Found\n\nBy default, when a resource is not found, Canary sets the resource in `assigns` to `nil`.\nSimilar to unauthorized action handling, you can configure a function that Canary will call when a resource is missing. This function will receive the `conn` (for Plugs) or `socket` (for LiveView).\n\n```elixir\nconfig :canary, error_handler: ErrorHandler\n```\n\n### Overriding Handlers Per Action\n\nYou can specify custom handlers per action using `opts` in the `plug` or `mount_canary` call.\nThese handlers will override any globally configured error handlers.\n\n<!-- tabs-open -->\n\n### Conn Plugs\n\n```elixir\nplug :load_and_authorize_resource Post,\n  unauthorized_handler: {Helpers, :handle_unauthorized},\n  not_found_handler: {Helpers, :handle_not_found}\n```\n\n> **Tip:** If you want to stop request handling after the handler function executes (e.g., for a redirect),\n> be sure to call `Plug.Conn.halt/1` within your handler:\n\n```elixir\ndef handle_unauthorized(conn) do\n  conn\n  |> put_flash(:error, \"You can't access that page!\")\n  |> redirect(to: \"/\")\n  |> halt()\nend\n```\n\n### LiveView Hooks\n\n```elixir\nmount_canary :load_and_authorize_resource Post,\n  unauthorized_handler: {Helpers, :handle_unauthorized},\n  not_found_handler: {Helpers, :handle_not_found}\n```\n\n> **Tip:** If you want to stop request handling after the handler function executes (e.g., for a redirect),\n> be sure to call `Plug.Conn.halt/1` within your handler:\n\n```elixir\ndef handle_unauthorized(socket) do\n  {:halt, Phoenix.LiveView.redirect(socket, to: \"/\")}\nend\n```\n<!-- tabs-close -->\n\n> #### Error handler order {: .info}\n>\n> If both an `:unauthorized_handler` and a `:not_found_handler` are specified for `load_and_authorize_resource`,\n> and the request meets the criteria for both, the `:unauthorized_handler` will be called first."
  },
  {
    "path": "docs/upgrade.md",
    "content": "# Upgrade guides\n\n## Upgrading from Canary 1.2.0 to 2.0.0\n\n### Update Your Non-ID Actions\n\n> Since 2.0.0, the `:persisted` and `:non_id_actions` options have been deprecated and will be removed in Canary 2.1.0.\n\nYou need to update plug calls. Using `:authorize_resource` for actions where there is no actual load action is more explicit.\n\nLet's assume you currently have the following plug call:\n```elixir\n  plug :load_and_authorize_resource,\n    model: Network,\n    non_id_actions: [:index, :create, :new],\n    preload: [:hypervisor]\n```\n\nNow let's break it apart:\n```elixir\n  plug :authorize_resource,\n    model: Network,\n    only: [:index, :create, :new],\n    required: false\n\n  plug :load_and_authorize_resource,\n    model: Network,\n    except: [:index, :create, :new],\n    preload: [:hypervisor]\n```\n\nFor non-ID actions, there is a separate plug for authorization. The `required: false` option marks the resource as optional during authorization checks, and the model module name is used. Essentially, this is how :non_id_actions worked.\nFor actions other than `:index`, `:create`, and `:new`, it will load and authorize resources as usual.\nTo load all resources in the `:index` action, you can set up a plug or add the load function directly in `index/2`.\n\n```elixir\n  # using a plug\n\n  plug :load_all_resources when action in [:index]\n  defp load_all_resources(conn, _opts) do\n    assign(:networks, Hypervisors.list_hypervisor_networks(hypervisor))\n  end\n\n  # directly in the controller action\n\n  def index(conn, _params) do\n    networks = Hypervisors.list_hypervisor_networks(hypervisor)\n    render(conn, \"index.html\", networks: networks)\n  end\n```\n\n### Remove `:persisted` option\n\nWith the [update non-id action](#update-your-non-id-actions) the `:persisted` is no longer required."
  },
  {
    "path": "lib/canary/default_handler.ex",
    "content": "defmodule Canary.DefaultHandler do\n  @moduledoc \"\"\"\n  The fallback Canary handler.\n\n  This module is used primarily as a backwards compatibility for the `:not_found_handler` and `:unauthorized_handler`.\n  It uses old configuration values to determine how to handle the error.\n\n  If you are using `Canary` only with `Plug` based authorization then you can still\n  use the `:not_found_handler` and `:unauthorized_handler` configuration values.\n  Otherwise, you should implement the `Canary.ErrorHandler` behaviour in your own module.\n  \"\"\"\n  @moduledoc since: \"2.0.0\"\n\n  @behaviour Canary.ErrorHandler\n\n  @doc \"\"\"\n  The default handler for when a resource is not found.\n  For Plug based authorization it will use the global `:not_found_handler` or return the conn.\n\n  For LiveView base authorization it will halt socket.\n  \"\"\"\n  @impl true\n  def not_found_handler(%Plug.Conn{} = conn) do\n    case Application.get_env(:canary, :not_found_handler) do\n      {mod, fun} -> apply(mod, fun, [conn])\n      _ -> conn\n    end\n  end\n  def not_found_handler(%Phoenix.LiveView.Socket{} = socket) do\n    {:halt, Phoenix.LiveView.redirect(socket, to: \"/\")}\n  end\n\n  @doc \"\"\"\n  The default handler for when a resource is not authorized.\n  For Plug based authorization it will use the global `:unauthorized_handler` or return the conn.\n\n  For LiveView base authorization it will halt socket.\n  \"\"\"\n  @impl true\n  def unauthorized_handler(%Plug.Conn{} = conn) do\n    case Application.get_env(:canary, :unauthorized_handler) do\n      {mod, fun} -> apply(mod, fun, [conn])\n      _ -> conn\n    end\n  end\n  def unauthorized_handler(%Phoenix.LiveView.Socket{} = socket) do\n    {:halt, Phoenix.LiveView.redirect(socket, to: \"/\")}\n  end\nend\n"
  },
  {
    "path": "lib/canary/error_handler.ex",
    "content": "defmodule Canary.ErrorHandler do\n  @moduledoc \"\"\"\n  Specifies the behavior for handling errors in Canary.\n\n\n  \"\"\"\n  @moduledoc since: \"2.0.0\"\n\n  @doc \"\"\"\n  Handles the case where a resource is not found.\n  \"\"\"\n  @callback not_found_handler(Plug.Conn.t) :: Plug.Conn.t\n  @callback not_found_handler(Phoenix.LiveView.Socket.t) :: {:halt, Phoenix.LiveView.Socket.t}\n\n  @doc \"\"\"\n  Handles the case where a resource is not authorized.\n  \"\"\"\n  @callback unauthorized_handler(Plug.Conn.t) :: Plug.Conn.t\n  @callback unauthorized_handler(Phoenix.LiveView.Socket.t) :: {:halt, Phoenix.LiveView.Socket.t}\nend\n"
  },
  {
    "path": "lib/canary/hooks.ex",
    "content": "if Code.ensure_loaded?(Phoenix.LiveView) do\n  defmodule Canary.Hooks do\n    @moduledoc \"\"\"\n\n    Hooks functions for loading and authorizing resources for the LiveView events.\n    If you want to authorize `handle_params` and `handle_event` LiveView callbacks\n    you can use `mount_canary` macro to attach the hooks.\n\n    You can think about the `mount_canary` as something similar to `plug` but for LiveView events.\n\n    For `handle_params` it uses `socket.assigns.live_action` as `:action`.\n    For `handle_event` it uses the event name as `:action`.\n\n    > Note that the `event_name` is a string - but in Canary it's converted to an atom for consistency.\n\n    `Canary.Hooks` and `Canary.Plugs` are separate modules but they share the same API.\n    You can define plugs for standard pages and hooks for LiveView events with the same opts.\n\n    For the authorization actions, when the `:required` is false (by default it's true) it might be nil.\n    Then the `Canada.Can` implementation should be the module name of the model rather than a struct.\n\n    ## Example\n      ```elixir\n      use Canary.Hooks\n\n      mount_canary :load_and_authorize_resource,\n        on: [:handle_params, :handle_event],\n        model: Post,\n        only: [:show, :edit, :update]\n\n      mount_canary :authorize_resource,\n        on: [:handle_event],\n        model: Post,\n        only: [:my_event],\n        required: false\n\n      # ...\n\n      def handle_params(params, _uri, socket) do\n        # resource is already loaded and authorized\n        post = socket.assigns.post\n      end\n\n      def handle_event(\"my_event\", _unsigned_params, socket) do\n        # Only admin is allowed to perform my_event\n      end\n\n      ```\n\n      `lib/abilities/user.ex`:\n\n      ```elixir\n      defimpl Canada.Can, for: User do\n\n        def can?(%User{} = user, :my_event, Post), do: user.role == \"admin\"\n\n        def can?(%User{id: id}, _, %Post{user_id: user_id}), do: id == user_id\n      end\n      ```\n    \"\"\"\n    # Copyright 2025 Piotr Baj\n    @moduledoc since: \"2.0.0\"\n\n    import Canary.Utils\n    import Canada.Can, only: [can?: 3]\n    import Phoenix.LiveView, only: [attach_hook: 4]\n    import Phoenix.Component, only: [assign: 3]\n\n    alias Phoenix.LiveView.Socket\n\n    @doc false\n    defmacro __using__(_opts) do\n      quote do\n        import Canary.Hooks\n\n        Module.register_attribute(__MODULE__, :canary_hooks, accumulate: true)\n\n        # Register the Canary.Hooks.__before_compile__/1 callback to be called before Phoenix.LiveView.\n        hooks = Module.delete_attribute(__MODULE__, :before_compile)\n        @before_compile Canary.Hooks\n        Enum.each(hooks, fn {mod, fun} ->\n          @before_compile {mod, fun}\n        end)\n      end\n    end\n\n    @doc false\n    defmacro __before_compile__(env) do\n      stages =\n        Module.get_attribute(env.module, :canary_hooks, [])\n        |> Enum.reverse()\n\n      wrapped_hooks = Enum.with_index(stages, &wrap_hooks/2)\n      mount_attach = attach_hooks_on_mount(env, stages)\n\n      [wrapped_hooks, mount_attach]\n    end\n\n    defp wrap_hooks({stage, hook, opts}, id) do\n      name = hook_name(stage, hook, id)\n\n      quote do\n        def unquote(name)(hook_arg_1, hook_arg_2, %Socket{} = socket) do\n          metadata = %{hook: unquote(hook), stage: unquote(stage), opts: unquote(opts)}\n          handle_hook(metadata, [hook_arg_1, hook_arg_2, socket])\n        end\n      end\n    end\n\n    defp attach_hooks_on_mount(env, stages) do\n      hooks =\n        Enum.with_index(stages)\n        |> Enum.map(fn {{stage, hook, _opts}, id} ->\n          name = hook_name(stage, hook, id)\n          {name, stage}\n        end)\n\n      quote bind_quoted: [module: env.module, hooks: hooks] do\n        on_mount {Canary.Hooks, {:initialize, module, hooks}}\n      end\n    end\n\n    defp hook_name(stage, hook, id) do\n      String.to_atom(\"#{stage}_#{hook}_#{id}\")\n    end\n\n    @doc false\n    def handle_hook(metadata, [hook_arg_1, hook_arg_2, socket]) do\n      %{hook: hook, stage: stage, opts: opts} = metadata\n\n      case hook do\n        :load_resource ->\n          load_resource(stage, hook_arg_1, hook_arg_2, socket, opts)\n\n        :authorize_resource ->\n          authorize_resource(stage, hook_arg_1, hook_arg_2, socket, opts)\n\n        :load_and_authorize_resource ->\n          load_and_authorize_resource(stage, hook_arg_1, hook_arg_2, socket, opts)\n\n        _ ->\n          IO.warn(\n            \"Invalid type #{inspect(hook)} for Canary hook call. Please review defined hooks with mount_canary/2.\",\n            module: __MODULE__\n          )\n\n          {:cont, socket}\n      end\n    end\n\n    @doc \"\"\"\n    Mount canary authorization hooks on the current module.\n    It creates a wrapper function to handle_params and handle_event,\n    and attaches the hooks to the Live View.\n\n    ## Example\n      ```\n      mount_canary :load_and_authorize_resource,\n        model: Post,\n        only: [:edit: :update]\n      ```\n    \"\"\"\n    defmacro mount_canary(type, opts) do\n      stages = get_stages(opts)\n\n      if Enum.empty?(stages),\n        do:\n          IO.warn(\"mount_canary called with empty :on stages\",\n            module: __CALLER__.module,\n            file: __CALLER__.file,\n            line: __CALLER__.line\n          )\n\n      Enum.reduce(stages, [], fn stage, acc ->\n        [put_canary_hook(__CALLER__.module, stage, type, opts) | acc]\n      end)\n      |> Enum.reverse()\n    end\n\n    defp put_canary_hook(module, stage, type, opts) do\n      quote do\n        Module.put_attribute(\n          unquote(module),\n          :canary_hooks,\n          {unquote(stage), unquote(type), unquote(opts)}\n        )\n      end\n    end\n\n    @doc false\n    def on_mount({:initialize, mod, stages}, _params, _session, %Socket{} = socket) do\n      socket =\n        Enum.reduce(stages, socket, fn {name, stage}, socket ->\n          fun = Function.capture(mod, name, 3)\n          attach_hook(socket, name, stage, fun)\n        end)\n\n      {:cont, socket}\n    end\n\n    def on_mount(_, :not_mounted_at_router, _session, socket), do: {:cont, socket}\n\n    @doc \"\"\"\n    Authorize the `:current_user` for the ginve resource. If the `:current_user` is not authorized it will halt the socket.\n\n    For the authorization check, it uses the `can?/3` function from the `Canada.Can` module -\n\n    `can?(subject, action, resource)` where:\n\n    1. The subject is the `:current_user` from the socket assigns. The `:current_user` key can be changed in the `opts` or in the `Application.get_env(:canary, :current_user, :current_user)`. By default it's `:current_user`.\n    2. The action for `handle_params` is `socket.assigns.live_action`, for `handle_event` it uses the event name.\n    3. The resource is the loaded resource from the socket assigns or the model name if the resource is not loaded and not required.\n\n    Required opts:\n\n    * `:model` - Specifies the module name of the model to load resources from\n    * `:on` - Specifies the LiveView lifecycle stages to attach the hook. Default :handle_params\n\n    Optional opts:\n\n    * `:only` - Specifies which actions to authorize\n    * `:except` - Specifies which actions for which to skip authorization\n    >  For `handle_params` it uses `socket.assigns.live_action` as `:action`.\n    >  For `handle_event` it uses the event name as `:action`.\n\n    * `:as` - Specifies the `resource_name` to get from assigns\n    * `:current_user` - Specifies the key in the socket assigns to get the current user\n    * `:required` - Specifies if the resource is required, when it's not assigned in socket it will halt the socket\n    * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized\n\n    Example:\n\n    ```elixir\n\n    mount_canary :authorize_resource,\n      model: Post,\n      only: [:show, :edit, :update]\n      current_user: :current_user\n\n    mount_canary :authorize_resource,\n      model: Post,\n      as: :custom_resource_name,\n      except: [:new, :create],\n      unauthorized_handler: {ErrorHandler, :unauthorized_handler}\n\n    ```\n    \"\"\"\n    def authorize_resource(:handle_params, _params, _uri, %Socket{} = socket, opts) do\n      action = socket.assigns.live_action\n      do_authorize_resource(action, socket, opts)\n    end\n\n    def authorize_resource(:handle_event, event_name, _unsigned_params, %Socket{} = socket, opts) do\n      action = String.to_atom(event_name)\n      do_authorize_resource(action, socket, opts)\n    end\n\n    @doc \"\"\"\n    Loads the resource and assigns it to the socket. When resource is required it will\n    halt the socket if the resource is not found.\n\n    `load_resource` wrapper for attached hook functions, similar to the `load_resource/2` plug\n    but for LiveView events on `:handle_params` and `:handle_event` stages.\n\n    Required opts:\n\n    * `:model` - Specifies the module name of the model to load resources from\n    * `:on` - Specifies the LiveView lifecycle stages to attach the hook. Default :handle_params\n\n    Optional opts:\n    * `:only` - Specifies which actions to authorize\n    * `:except` - Specifies which actions for which to skip authorization\n    >  For `handle_params` it uses `socket.assigns.live_action` as `:action`.\n    >  For `handle_event` it uses the event name as `:action`.\n\n    * `:as` - Specifies the `resource_name` to use in assigns\n    * `:preload` - Specifies association(s) to preload\n    * `:id_name` - Specifies the name of the id in `params`, defaults to \"id\"\n    * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to \"id\".\n    * `:required` - Specifies if the resource is required, when it's not found it will halt the socket\n    * `:not_found_handler` - Specify a handler function to be called if the resource is not found\n\n    Example:\n\n    ```elixir\n\n    mount_canary :load_resource,\n      model: Post,\n      only: [:show, :edit, :update],\n      preload: [:comments]\n\n    mount_canary :load_resource,\n      on: [:handle_params, :handle_event]\n      model: Post,\n      as: :custom_name,\n      except: [:new, :create],\n      preload: [:comments],\n      not_found_handler: {ErrorHandler, :not_found_handler}\n\n    ```\n    \"\"\"\n    def load_resource(:handle_params, params, _uri, %Socket{} = socket, opts) do\n      action = socket.assigns.live_action\n      do_load_resource(action, socket, params, opts)\n    end\n\n    def load_resource(:handle_event, event_name, unsigned_params, %Socket{} = socket, opts) do\n      action = String.to_atom(event_name)\n      do_load_resource(action, socket, unsigned_params, opts)\n    end\n\n    @doc \"\"\"\n    Loads and autorize resource and assigns it to the socket. When resource is required it will\n    halt the socket if the resource is not found. If the user is not authorized it will halt the socket.\n\n    It combines `load_resource` and `authorize_resource` functions.\n\n    Required opts:\n\n    * `:model` - Specifies the module name of the model to load resources from\n    * `:on` - Specifies the LiveView lifecycle stages to attach the hook. Default :handle_params\n\n    Optional opts:\n    * `:only` - Specifies which actions to authorize\n    * `:except` - Specifies which actions for which to skip authorization\n    >  For `handle_params` it uses `socket.assigns.live_action` as `:action`.\n    >  For `handle_event` it uses the event name as `:action`.\n\n    * `:as` - Specifies the `resource_name` to use in assigns\n    * `:current_user` - Specifies the key in the socket assigns to get the current user\n    * `:preload` - Specifies association(s) to preload\n    * `:id_name` - Specifies the name of the id in `params`, defaults to \"id\"\n    * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to \"id\".\n    * `:required` - Specifies if the resource is required, when it's not found it will halt the socket, default true\n    * `:not_found_handler` - Specify a handler function to be called if the resource is not found\n    * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized\n\n    Example:\n\n    ```elixir\n\n    mount_canary :load_and_authorize_resource,\n      model: Comments,\n      id_name: :post_id,\n      id_field: :post_id,\n      only: [:comments]\n\n    mount_canary :load_and_authorize_resource,\n      model: Post,\n      as: :custom_name,\n      except: [:new, :create],\n      preload: [:comments],\n      error_handler: CustomErrorHandler\n    ```\n\n    \"\"\"\n    def load_and_authorize_resource(:handle_params, params, _uri, %Socket{} = socket, opts) do\n      action = socket.assigns.live_action\n      do_load_and_authorize_resource(action, params, socket, opts)\n    end\n\n    def load_and_authorize_resource(\n          :handle_event,\n          event_name,\n          unsigned_params,\n          %Socket{} = socket,\n          opts\n        ) do\n      action = String.to_atom(event_name)\n      do_load_and_authorize_resource(action, unsigned_params, socket, opts)\n    end\n\n    defp do_load_resource(action, socket, params, opts) do\n      if action_valid?(action, opts) do\n        assign(socket, :canary_action, action)\n        |> load_resource(params, opts)\n        |> verify_resource(opts)\n      else\n        {:cont, socket}\n      end\n    end\n\n    defp do_load_and_authorize_resource(action, params, socket, opts) do\n      if action_valid?(action, opts) do\n        assign(socket, :canary_action, action)\n        |> load_resource(params, opts)\n        |> check_authorization(action, opts)\n        |> verify_authorized_resource(opts)\n      else\n        {:cont, socket}\n      end\n    end\n\n    defp do_authorize_resource(action, socket, opts) do\n      if action_valid?(action, opts) do\n        assign(socket, :canary_action, action)\n        |> check_authorization(action, opts)\n        |> verify_authorized_resource(opts)\n      else\n        {:cont, socket}\n      end\n    end\n\n    # Check if the resource is already loaded in the socket assigns\n    # If not we need to load and assign it\n    defp load_resource(%Socket{} = socket, params, opts) do\n      resource =\n        case fetch_resource(socket, opts) do\n          {:ok, resource} ->\n            resource\n\n          _ ->\n            repo_get_resource(params, opts)\n        end\n\n      action = socket.assigns.canary_action\n      assign(socket, get_resource_name(action, opts), resource)\n    end\n\n    # Fetch the resource from the socket assigns or nil\n    defp fetch_resource(%Socket{} = socket, opts) do\n      action = socket.assigns.canary_action\n      case Map.get(socket.assigns, get_resource_name(action, opts), nil) do\n        resource when is_struct(resource) ->\n          if resource.__struct__ == opts[:model] do\n            {:ok, resource}\n          else\n            nil\n          end\n\n        _ ->\n          nil\n      end\n    end\n\n    # Load the resource from the repo\n    defp repo_get_resource(params, opts) do\n      repo = Application.get_env(:canary, :repo)\n      field_name = Keyword.get(opts, :id_field, \"id\")\n      get_map_args = %{String.to_atom(field_name) => get_resource_id(params, opts)}\n\n      repo.get_by(opts[:model], get_map_args)\n      |> preload_if_needed(repo, opts)\n    end\n\n    # Perform the authorization check\n    defp check_authorization(%Socket{} = socket, action, opts) do\n      current_user_name =\n        opts[:current_user] || Application.get_env(:canary, :current_user, :current_user)\n\n      current_user = Map.fetch(socket.assigns, current_user_name)\n      resource = fetch_resoruce_or_model(socket, opts)\n\n      case {current_user, resource} do\n        {{:ok, _current_user}, nil} ->\n          assign(socket, :authorized, false)\n        {{:ok, current_user}, _} ->\n          assign(socket, :authorized, can?(current_user, action, resource))\n        _ ->\n          assign(socket, :authorized, false)\n      end\n    end\n\n    # Fetch resource form assigns or model name if empty and not required\n    defp fetch_resoruce_or_model(%Socket{} = socket, opts) do\n      case fetch_resource(socket, opts) do\n        {:ok, resource} ->\n          resource\n\n        _ ->\n          if required?(opts) do\n            nil\n          else\n            opts[:model]\n          end\n      end\n    end\n\n    # Verify if subject is authorized to perform action on resource\n    defp verify_authorized_resource(%Socket{} = socket, opts) do\n      authorized = Map.get(socket.assigns, :authorized, false)\n\n      if authorized do\n        verify_resource(socket, opts)\n      else\n        apply_error_handler(socket, :unauthorized_handler, opts)\n      end\n    end\n\n    # Verify if the resource is loaded and if it is required\n    defp verify_resource(%Socket{} = socket, opts) do\n      is_required = required?(opts)\n      resource = fetch_resource(socket, opts)\n\n      if is_nil(resource) && is_required do\n        apply_error_handler(socket, :not_found_handler, opts)\n      else\n        {:cont, socket}\n      end\n    end\n\n     defp get_stages(opts) do\n      Keyword.get(opts, :on, :handle_params)\n      |> validate_stages()\n    end\n\n    defp validate_stages(stage) when is_atom(stage), do: validate_stages([stage])\n\n    defp validate_stages(stages) when is_list(stages) do\n      allowed_satges = [:handle_params, :handle_event]\n      Enum.filter(stages, &Enum.member?(allowed_satges, &1))\n    end\n  end\nend\n"
  },
  {
    "path": "lib/canary/plugs.ex",
    "content": "defmodule Canary.Plugs do\n  import Canary.Utils\n  import Canada.Can, only: [can?: 3]\n  import Ecto.Query\n\n  @moduledoc \"\"\"\n  Plug functions for loading and authorizing resources for the current request.\n\n  The plugs all store data in conn.assigns (in Phoenix applications, keys in conn.assigns can be accessed with `@key_name` in templates)\n\n  In order to use the plug functions, you must `import Canary.Plugs`.\n\n  You must also specify the Ecto repo to use in your configuration:\n  ```\n  config :canary, repo: Project.Repo\n  ```\n  If you wish, you may also specify the key where Canary will look for the current user record to authorize against:\n  ```\n  config :canary, current_user: :some_current_user\n  ```\n\n  You can specify a error handler module (in this case, `Helpers`) to be called when an action is unauthorized like so:\n  ```elixir\n  config :canary, error_handler: Helpers\n  ```\n\n  Module should implement the `Canary.ErrorHandler` behaviour.\n\n  Canary will pass the `conn` to the handler function.\n  \"\"\"\n\n  @doc \"\"\"\n  Load the given resource.\n\n  Load the resource with id given by `conn.params[\"id\"]` (or `conn.params[opts[:id_name]]` if `opts[:id_name]` is specified)\n  and ecto model given by `opts[:model]` into `conn.assigns.resource_name`.\n\n  `resource_name` is either inferred from the model name or specified in the plug declaration with the `:as` key.\n  To infer the `resource_name`, the most specific(right most) name in the model's\n  module name will be used, converted to underscore case.\n\n  For example, `load_resource model: Some.Project.BlogPost` will load the resource into\n  `conn.assigns.blog_post`\n\n  If the resource cannot be fetched, `conn.assigns.resource_name` is set\n  to nil.\n\n  By default, when the action is `:index`, all records from the specified model will be loaded. This can\n  be overridden to fetch a single record from the database by using the `:persisted` key.\n\n  Currently, `:new` and `:create` actions are ignored, and `conn.assigns.resource_name`\n  will be set to nil for these actions. This can be overridden to fetch a single record from the database\n  by using the `:persisted` key.\n\n  The `:persisted` key can override how a resource is loaded and can be useful when dealing\n  with nested resources.\n\n  Required opts:\n\n  * `:model` - Specifies the module name of the model to load resources from\n\n  Optional opts:\n\n  * `:as` - Specifies the `resource_name` to use\n  * `:only` - Specifies which actions to authorize\n  * `:except` - Specifies which actions for which to skip authorization\n  * `:preload` - Specifies association(s) to preload\n  * `:id_name` - Specifies the name of the id in `conn.params`, defaults to \"id\"\n  * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to \"id\".\n  * `:persisted` - Specifies the resource should always be loaded from the database, defaults to false\n  * `:required` - Same as `:persisted` but with not found handler - even for :index, :new or :create action\n  * `:not_found_handler` - Specify a handler function to be called if the resource is not found\n\n\n  Examples:\n  ```\n  plug :load_resource, model: Post\n\n  plug :load_resource, model: User, preload: :posts, as: :the_user\n\n  plug :load_resource, model: User, only: [:index, :show], preload: :posts, as: :person\n\n  plug :load_resource, model: User, except: [:destroy]\n\n  plug :load_resource, model: Post, id_name: \"post_id\", only: [:new, :create], persisted: true\n\n  plug :load_resource, model: Post, id_name: \"slug\", id_field: \"slug\", only: [:show], persisted: true\n  ```\n  \"\"\"\n  @spec load_resource(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()\n  def load_resource(conn, opts) do\n    action = get_action(conn)\n    validate_opts(opts)\n\n    if action_valid?(action, opts) do\n      conn\n      |> do_load_resource(opts)\n      |> handle_not_found(opts)\n    else\n      conn\n    end\n  end\n\n  defp do_load_resource(conn, opts) do\n    action = get_action(conn)\n    is_persisted = persisted?(opts)\n    validate_opts(opts)\n\n    loaded_resource =\n      cond do\n        is_persisted ->\n          fetch_resource(conn, opts)\n\n        action == :index ->\n          fetch_all(conn, opts)\n\n        action in [:new, :create] ->\n          nil\n\n        true ->\n          fetch_resource(conn, opts)\n      end\n\n    Plug.Conn.assign(conn, get_resource_name(action, opts), loaded_resource)\n  end\n\n  @doc \"\"\"\n  Authorize the current user against the calling controller.\n\n  In order to use this function,\n\n    1) `conn.assigns[Application.get_env(:canary, :current_user, :current_user)]` must be an ecto\n    struct representing the current user\n\n    2) `conn.private` must be a map (this should not be a problem unless you explicitly modified it)\n\n  authorize_controller checks for the name of the current controller in one of the following places\n    1) :phoenix_controller in conn.private\n    2) :canary_controller in conn.assigns\n\n  In case you are not using phoenix, make sure you set the controller name in the conn.assigns\n  Note that in case neither of `:phoenix_controller` or `:canary_controller` are found the requested\n    authorization won't necessarily fail, rather it will trigger a `.can?` function with a `nil` controller\n\n  If authorization succeeds, sets `conn.assigns.authorized` to true.\n\n  If authorization fails, sets `conn.assigns.authorized` to false.\n\n  Optional opts:\n\n  * `:only` - Specifies which actions to authorize\n  * `:except` - Specifies which actions for which to skip authorization\n  * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized\n\n  Examples:\n  ```\n  plug :authorize_controller\n\n  plug :authorize_controller, only: [:index, :show]\n\n  plug :authorize_controller, except: [:destroy]\n  ```\n  \"\"\"\n  @spec authorize_controller(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()\n  def authorize_controller(conn, opts) do\n    action = get_action(conn)\n    validate_opts(opts)\n\n    if action_valid?(action, opts) do\n      do_authorize_controller(conn, opts) |> handle_unauthorized(opts)\n    else\n      conn\n    end\n  end\n\n  defp do_authorize_controller(conn, opts) do\n    controller = conn.assigns[:canary_controller] || conn.private[:phoenix_controller]\n\n    current_user_name =\n      opts[:current_user] ||\n        Application.get_env(:canary, :current_user, :current_user)\n\n    current_user = Map.fetch!(conn.assigns, current_user_name)\n    action = get_action(conn)\n\n    Plug.Conn.assign(conn, :authorized, can?(current_user, action, controller))\n  end\n\n  @doc \"\"\"\n  Authorize the current user for the given resource.\n\n  In order to use this function,\n\n    1) `conn.assigns[Application.get_env(:canary, :current_user, :current_user)]` must be an ecto\n    struct representing the current user\n\n    2) `conn.private` must be a map (this should not be a problem unless you explicitly modified it)\n\n  If authorization succeeds, sets `conn.assigns.authorized` to true.\n\n  If authorization fails, sets `conn.assigns.authorized` to false.\n\n  For the `:index`, `:new`, and `:create` actions, the resource in the `Canada.Can` implementation\n  should be the module name of the model rather than a struct. A struct should be used instead of\n  the module name only if the `:persisted` key is used and you want to override the default\n  authorization behavior.  This can be useful when dealing with nested resources.\n\n  For example:\n\n    use\n    ```\n    def can?(%User{}, :index, Post), do: true\n    ```\n    instead of\n    ```\n    def can?(%User{}, :index, %Post{}), do: true\n    ```\n\n    or\n\n    use\n    ```\n    def can?(%User{id: user_id}, :index, %Post{user_id: user_id}), do: true\n    ```\n    if you are dealing with a nested resource, such as, \"/post/post_id/comments\"\n\n\n    You can specify additional actions for which Canary will authorize based on the model name, by passing the `non_id_actions` opt to the plug.\n\n    For example,\n    ```elixir\n    plug :authorize_resource, model: Post, non_id_actions: [:find_by_name]\n    ```\n\n  Required opts:\n\n  * `:model` - Specifies the module name of the model to authorize access to\n\n  Optional opts:\n\n  * `:only` - Specifies which actions to authorize\n  * `:except` - Specifies which actions for which to skip authorization\n  * `:preload` - Specifies association(s) to preload\n  * `:id_name` - Specifies the name of the id in `conn.params`, defaults to \"id\"\n  * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to \"id\".\n  * `:persisted` - Specifies the resource should always be loaded from the database, defaults to false\n  * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized\n\n  Examples:\n  ```\n  plug :authorize_resource, model: Post\n\n  plug :authorize_resource, model: User, preload: :posts\n\n  plug :authorize_resource, model: User, only: [:index, :show], preload: :posts\n\n  plug :load_resource, model: Post, id_name: \"post_id\", only: [:index], persisted: true, preload: :comments\n\n  plug :load_resource, model: Post, id_name: \"slug\", id_field: \"slug\", only: [:show], persisted: true\n  ```\n  \"\"\"\n  @spec authorize_resource(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()\n  def authorize_resource(conn, opts) do\n    action = get_action(conn)\n\n    if action_valid?(action, opts) do\n      do_authorize_resource(conn, opts) |> handle_unauthorized(opts)\n    else\n      conn\n    end\n  end\n\n  defp do_authorize_resource(conn, opts) do\n    current_user_name =\n      opts[:current_user] || Application.get_env(:canary, :current_user, :current_user)\n\n    current_user = Map.fetch!(conn.assigns, current_user_name)\n    action = get_action(conn)\n    is_persisted = persisted?(opts)\n\n    non_id_actions =\n      if opts[:non_id_actions] do\n        Enum.concat([:index, :new, :create], opts[:non_id_actions])\n      else\n        [:index, :new, :create]\n      end\n\n    resource =\n      cond do\n        is_persisted ->\n          fetch_resource(conn, opts)\n\n        action in non_id_actions ->\n          opts[:model]\n\n        true ->\n          fetch_resource(conn, opts)\n      end\n\n    Plug.Conn.assign(conn, :authorized, can?(current_user, action, resource))\n  end\n\n  @doc \"\"\"\n  Authorize the given resource and then load it if\n  authorization succeeds.\n\n  If the resource cannot be loaded or authorization\n  fails, conn.assigns.resource_name is set to nil.\n\n  The result of the authorization (true/false) is\n  assigned to conn.assigns.authorized.\n\n  Also, see the documentation for load_resource/2 and\n  authorize_resource/2.\n\n  Required opts:\n\n  * `:model` - Specifies the module name of the model to load resources from\n\n  Optional opts:\n\n  * `:as` - Specifies the `resource_name` to use\n  * `:only` - Specifies which actions to authorize\n  * `:except` - Specifies which actions for which to skip authorization\n  * `:preload` - Specifies association(s) to preload\n  * `:id_name` - Specifies the name of the id in `conn.params`, defaults to \"id\"\n  * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to \"id\".\n  * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized\n  * `:not_found_handler` - Specify a handler function to be called if the resource is not found\n\n  Note: If both an `:unauthorized_handler` and a `:not_found_handler` are specified for `load_and_authorize_resource`,\n  and the request meets the criteria for both, the `:unauthorized_handler` will be called first.\n\n  Examples:\n  ```\n  plug :load_and_authorize_resource, model: Post\n\n  plug :load_and_authorize_resource, model: User, preload: :posts, as: :the_user\n\n  plug :load_and_authorize_resource, model: User, only: [:index, :show], preload: :posts, as: :person\n\n  plug :load_and_authorize_resource, model: User, except: [:destroy]\n\n  plug :load_and_authorize_resource, model: Post, id_name: \"slug\", id_field: \"slug\", only: [:show], persisted: true\n  ```\n  \"\"\"\n  def load_and_authorize_resource(conn, opts) do\n    action = get_action(conn)\n\n    if action_valid?(action, opts) do\n      do_load_and_authorize_resource(conn, opts)\n    else\n      conn\n    end\n  end\n\n  defp do_load_and_authorize_resource(conn, opts) do\n    conn\n    |> do_load_resource(opts)\n    |> authorize_resource(opts)\n    |> maybe_handle_not_found(opts)\n    |> purge_resource_if_unauthorized(opts)\n  end\n\n  # Only try to handle 404 if the response has not been sent during authorization handling\n  defp maybe_handle_not_found(%{state: :sent} = conn, _opts), do: conn\n  defp maybe_handle_not_found(conn, opts), do: handle_not_found(conn, opts)\n\n  defp purge_resource_if_unauthorized(%{assigns: %{authorized: true}} = conn, _opts),\n    do: conn\n\n  defp purge_resource_if_unauthorized(%{assigns: %{authorized: false}} = conn, opts) do\n    action = get_action(conn)\n    Plug.Conn.assign(conn, get_resource_name(action, opts), nil)\n  end\n\n  defp fetch_resource(conn, opts) do\n    repo = Application.get_env(:canary, :repo)\n    action = get_action(conn)\n    field_name = Keyword.get(opts, :id_field, \"id\")\n\n    get_map_args = %{String.to_atom(field_name) => get_resource_id(conn, opts)}\n\n    case Map.fetch(conn.assigns, get_resource_name(action, opts)) do\n      :error ->\n        repo.get_by(opts[:model], get_map_args)\n        |> preload_if_needed(repo, opts)\n\n      {:ok, nil} ->\n        repo.get_by(opts[:model], get_map_args)\n        |> preload_if_needed(repo, opts)\n\n      {:ok, resource} ->\n        if resource.__struct__ == opts[:model] do\n          # A resource of the type passed as opts[:model] is already loaded; do not clobber it\n          resource\n        else\n          opts[:model]\n          |> repo.get_by(get_map_args)\n          |> preload_if_needed(repo, opts)\n        end\n    end\n  end\n\n  defp fetch_all(conn, opts) do\n    repo = Application.get_env(:canary, :repo)\n    action = get_action(conn)\n    resource_name = get_resource_name(action, opts)\n\n    # check if a resource is already loaded at the key\n    case Map.fetch(conn.assigns, resource_name) do\n      :error ->\n        from(m in opts[:model]) |> select([m], m) |> repo.all |> preload_if_needed(repo, opts)\n\n      {:ok, resources} ->\n        if Enum.at(resources, 0).__struct__ == opts[:model] do\n          resources\n        else\n          from(m in opts[:model]) |> select([m], m) |> repo.all |> preload_if_needed(repo, opts)\n        end\n    end\n  end\n\n  defp get_action(conn) do\n    case Map.fetch(conn.assigns, :canary_action) do\n      {:ok, action} -> action\n      _ -> conn.private.phoenix_action\n    end\n  end\n\n  defp handle_unauthorized(%{assigns: %{authorized: true}} = conn, _opts),\n    do: conn\n\n  defp handle_unauthorized(%{assigns: %{authorized: false}} = conn, opts),\n    do: apply_error_handler(conn, :unauthorized_handler, opts)\n\n  defp handle_not_found(conn, opts) do\n    action = get_action(conn)\n\n    if apply_handle_not_found?(action, conn.assigns, opts) do\n      apply_error_handler(conn, :not_found_handler, opts)\n    else\n      conn\n    end\n  end\nend\n"
  },
  {
    "path": "lib/canary/utils.ex",
    "content": "defmodule Canary.Utils do\n  @moduledoc \"\"\"\n  Common utils functions for `Canary.Plugs` and `Canary.Hooks`\n  \"\"\"\n\n  @doc \"\"\"\n  Get the resource id from the connection params\n\n      iex> Canary.Utils.get_resource_id(%{\"id\" => \"9\"}, [])\n      \"9\"\n\n      iex> Canary.Utils.get_resource_id(%Plug.Conn{params: %{\"custom_id\" => \"1\"}}, id_name: \"custom_id\")\n      \"1\"\n\n      iex> Canary.Utils.get_resource_id(%{\"user_id\" => \"7\"}, id_name: \"user_id\")\n      \"7\"\n\n      iex> Canary.Utils.get_resource_id(%{\"other_id\" => \"9\"}, id_name: \"id\")\n      nil\n  \"\"\"\n  @moduledoc since: \"2.0.0\"\n\n  @spec get_resource_id(Plug.Conn.t(), Keyword.t()) :: String.t() | nil\n  def get_resource_id(%Plug.Conn{params: params}, opts) do\n    get_resource_id(params, opts)\n  end\n\n  @spec get_resource_id(map(), Keyword.t()) :: String.t() | nil\n  def get_resource_id(params, opts) when is_map(params) do\n    case opts[:id_name] do\n      nil ->\n        params[\"id\"]\n\n      id_name ->\n        params[id_name]\n    end\n  end\n\n  @doc \"\"\"\n  Preload associations if needed\n  \"\"\"\n  @spec preload_if_needed(nil, Ecto.Repo.t(), Keyword.t()) :: nil\n  def preload_if_needed(nil, _repo, _opts), do: nil\n\n  @spec preload_if_needed([Ecto.Schema.t()], Ecto.Repo.t(), Keyword.t()) :: [Ecto.Schema.t()]\n  def preload_if_needed(records, repo, opts) do\n    case opts[:preload] do\n      nil ->\n        records\n\n      models ->\n        repo.preload(records, models)\n    end\n  end\n\n  @doc ~S\"\"\"\n  Check if an action is valid based on the options.\n\n      iex> Canary.Utils.action_valid?(:index, only: [:index, :show])\n        true\n\n      iex> Canary.Utils.action_valid?(:index, except: :index)\n        false\n\n      iex> Canary.Utils.action_valid?(:show, except: :index, only: :show)\n        ** (ArgumentError) You can't use both :except and :only options\n  \"\"\"\n  @spec action_valid?(atom, Keyword.t()) :: boolean\n  def action_valid?(action, opts) do\n    cond do\n      Keyword.has_key?(opts, :except) && Keyword.has_key?(opts, :only) ->\n        raise ArgumentError, \"You can't use both :except and :only options\"\n\n      Keyword.has_key?(opts, :except) ->\n        !action_exempt?(action, opts)\n\n      Keyword.has_key?(opts, :only) ->\n        action_included?(action, opts)\n\n      true ->\n        true\n    end\n  end\n\n  defp action_exempt?(action, opts) do\n    if is_list(opts[:except]) && action in opts[:except] do\n      true\n    else\n      action == opts[:except]\n    end\n  end\n\n  defp action_included?(action, opts) do\n    if is_list(opts[:only]) && action in opts[:only] do\n      true\n    else\n      action == opts[:only]\n    end\n  end\n\n  @doc \"\"\"\n  Check if a key is present in a keyword list\n  \"\"\"\n  @spec required?(Keyword.t()) :: boolean\n  def required?(opts) do\n    !!Keyword.get(opts, :required, true)\n  end\n\n  @doc \"\"\"\n  Apply the error handler to the connection or socket\n  \"\"\"\n  @spec apply_error_handler(Plug.Conn.t(), atom, Keyword.t()) :: Plug.Conn.t()\n  @spec apply_error_handler(Phoenix.LiveView.Socket.t(), atom, Keyword.t()) ::\n          {:halt, Phoenix.LiveView.Socket.t()}\n  def apply_error_handler(conn_or_socket, handler_key, opts) do\n    get_handler(handler_key, opts)\n    |> apply([conn_or_socket])\n  end\n\n  defp get_handler(handler_key, opts) do\n    mod_or_mod_fun =\n      Keyword.get(opts, handler_key) ||\n        Application.get_env(:canary, :error_handler, Canary.DefaultHandler)\n\n    case mod_or_mod_fun do\n      {mod, fun} ->\n        Function.capture(mod, fun, 1)\n\n      mod when is_atom(mod) ->\n        Function.capture(mod, handler_key, 1)\n\n      _ ->\n        raise ArgumentError, \"\n            Invalid error handler, expected a module or a tuple with a module and a function,\n            got: #{inspect(mod_or_mod_fun)}\"\n    end\n  end\n\n  @doc \"\"\"\n  Get the resource name from the options, covert it to atom and pluralize\n  it if needed - only for :index action.\n\n  If the `:as` option is provided, it will be used as the resource name.\n\n        iex> Canary.Utils.get_resource_name(:show, model: MyApp.Post)\n        :post\n\n        iex> Canary.Utils.get_resource_name(:index, model: MyApp.Post)\n        :posts\n\n        iex> Canary.Utils.get_resource_name(:index, model: MyApp.Post, as: :my_posts)\n        :my_posts\n  \"\"\"\n  def get_resource_name(action, opts) do\n    case opts[:as] do\n      nil ->\n        opts[:model]\n        |> Module.split()\n        |> List.last()\n        |> Macro.underscore()\n        |> pluralize_if_needed(action, opts)\n        |> String.to_atom()\n\n      as ->\n        as\n    end\n  end\n\n  def persisted?(opts) do\n    !!Keyword.get(opts, :persisted, false) || !!Keyword.get(opts, :required, false)\n  end\n\n  defp pluralize_if_needed(name, action, opts) do\n    if action in [:index] and not persisted?(opts) do\n      name <> \"s\"\n    else\n      name\n    end\n  end\n\n  @doc \"\"\"\n  Returns the :non_id_actions option if it is present\n  \"\"\"\n  def non_id_actions(opts) do\n    if opts[:non_id_actions] do\n      Enum.concat([:index, :new, :create], opts[:non_id_actions])\n    else\n      [:index, :new, :create]\n    end\n  end\n\n  @doc \"\"\"\n  Check if the not_found handler should be applied for given action, assigns and options\n  \"\"\"\n  @spec apply_handle_not_found?(action :: atom(), assigns :: map(), opts :: Keyword.t()) ::\n          boolean()\n  def apply_handle_not_found?(action, assigns, opts) do\n    non_id_actions = non_id_actions(opts)\n    is_required = required?(opts)\n\n    resource_name = Map.get(assigns, get_resource_name(action, opts))\n\n    if is_nil(resource_name) and (is_required or action not in non_id_actions) do\n      true\n    else\n      false\n    end\n  end\n\n  @doc false\n  def validate_opts(opts) do\n    opts\n    |> warn_deprecated_opts()\n  end\n\n  defp warn_deprecated_opts(opts) do\n    if Keyword.has_key?(opts, :persisted) do\n      IO.warn(\"The `:persisted` option is deprecated and will be removed in Canary 2.1.0. Use `:required` instead. Check the documentation for more information.\")\n    end\n\n    if Keyword.has_key?(opts, :non_id_actions) do\n      IO.warn(\"The `:non_id_actions` option is deprecated and will be removed in Canary 2.1.0. Use separate :authorize_resource plug for non_id_actions and `:except` to exclude non_in_actions. Check the documentation for more information.\")\n    end\n\n    opts\n  end\nend\n"
  },
  {
    "path": "lib/canary.ex",
    "content": "defmodule Canary do\n  @moduledoc \"\"\"\n  An authorization library in Elixir for `Plug` and `Phoenix.LiveView` applications that restricts what resources the current user is allowed to access, and automatically load and assigns resources.\n\n  `load_resource/2` and `authorize_resource/2` can be used by themselves, while `load_and_authorize_resource/2` combines them both.\n\n  The plug functions are defined in `Canary.Plugs`\n\n  In order to use `Canary` authorization in standard pages with plug, just `import Canary.Plugs` and use plugs, for example:\n\n  ```elixir\n  defmodule MyAppWeb.PostController do\n    use MyAppWeb, :controller\n    import Canary.Plugs\n\n    plug :load_and_authorize_resource,\n      model: Post,\n      current_user: :current_user,\n      only: [:show, :edit, :update]\n  end\n  ```\n\n  The LiveView hooks are defined in `Canary.Hooks`\n\n  In order to use `Canary` authorization in LiveView, just `use Canary.Hooks` and mount hooks, for example:\n\n  ```elixir\n  defmodule MyAppWeb.PostLive do\n    use MyAppWeb, :live_view\n    use Canary.Hooks\n\n    mount_canary :load_and_authorize_resource,\n      on: [:handle_params, :handle_event],\n      current_user: :current_user,\n      model: Post,\n      only: [:show, :edit, :update]\n\n  end\n  ```\n\n  This will attach hooks to the LiveView module with `Phoenix.LiveView.attach_hook/4`.\n  In the example above hooks will be attached to `handle_params` and `handle_event` stages of the LiveView lifecycle.\n\n  Please read the documentation for `Canary.Plugs` and `Canary.Hooks` for more information.\n  \"\"\"\nend\n"
  },
  {
    "path": "mix.exs",
    "content": "defmodule Canary.Mixfile do\n  use Mix.Project\n\n  def project do\n    [\n      app: :canary,\n      version: \"2.0.0-dev\",\n      elixir: \"~> 1.14\",\n      package: package(),\n      description: \"\"\"\n      An authorization library to restrict what resources the current user is\n      allowed to access, and load those resources for you.\n      \"\"\",\n      build_embedded: Mix.env() == :prod,\n      start_permanent: Mix.env() == :prod,\n      deps: deps(),\n      consolidate_protocols: false,\n      elixirc_paths: elixirc_paths(Mix.env()),\n      test_options: [docs: true],\n      test_coverage: [summary: [threshold: 85], ignore_modules: coverage_ignore_modules()],\n      docs: [\n        extras: [\n          \"docs/getting-started.md\",\n          \"README.md\",\n          \"CHANGELOG.md\",\n          \"docs/upgrade.md\",\n        ],\n        groups_for_modules: [\n          \"Error Handler\": [Canary.ErrorHandler, Canary.DefaultHandler],\n        ]\n      ]\n    ]\n  end\n\n  defp elixirc_paths(:test), do: [\"lib\", \"test/support\"]\n  defp elixirc_paths(_), do: [\"lib\"]\n\n  def application do\n    [extra_applications: [:logger]]\n  end\n\n  defp package do\n    [\n      maintainers: [\"Chris Kelly\"],\n      licenses: [\"MIT\"],\n      links: %{\"GitHub\" => \"https://github.com/cpjk/canary\"}\n    ]\n  end\n\n  defp deps do\n    [\n      {:ecto, \">= 1.1.0\"},\n      {:canada, \"~> 2.0.0\"},\n      {:plug, \"~> 1.10\"},\n      {:ex_doc, \"~> 0.7\", only: :dev},\n      {:earmark, \">= 0.0.0\", only: :dev},\n      {:mock, \">= 0.0.0\", only: :test},\n      {:credo, \"~> 1.0\", only: [:dev, :test]},\n      {:phoenix, \"~> 1.6\", optional: true},\n      {:phoenix_live_view, \"~> 0.20 or ~> 1.0\", optional: true},\n      {:floki, \">= 0.30.0\", only: :test}\n    ]\n  end\n\n  defp coverage_ignore_modules do\n    [\n      ~r/Canary\\.HooksHelper\\..*/\n    ]\n  end\nend\n"
  },
  {
    "path": "test/canary/default_handler_test.exs",
    "content": "defmodule CustomHandlers do\n  def not_found_handler(conn) do\n    conn\n    |> Plug.Conn.assign(:legacy_error_handler, true)\n  end\n\n  def unauthorized_handler(conn) do\n    conn\n    |> Plug.Conn.assign(:legacy_error_handler, true)\n  end\nend\n\ndefmodule DefaultHandlerTest do\n  use ExUnit.Case, async: false\n  import Plug.Adapters.Test.Conn, only: [conn: 4]\n\n  describe \"not_found_handler/1\" do\n    test \"calls the global :not_found_handler for plug based authorization\" do\n      Application.put_env(:canary, :not_found_handler, {CustomHandlers, :not_found_handler})\n\n      params = %{\"id\" => \"30\"}\n\n      conn =\n        conn(\n          %Plug.Conn{private: %{phoenix_action: :show}},\n          :get,\n          \"/posts/30\",\n          params\n        )\n        |> Canary.DefaultHandler.not_found_handler()\n\n      assert conn.assigns[:legacy_error_handler] == true\n      assert conn.assigns[:post] == nil\n    end\n\n    test \"returns conn when error_handler is not defined\" do\n      Application.put_env(:canary, :not_found_handler, nil)\n\n      params = %{\"id\" => \"30\"}\n\n      conn =\n        conn(\n          %Plug.Conn{private: %{phoenix_action: :show}},\n          :get,\n          \"/posts/30\",\n          params\n        )\n        |> Canary.DefaultHandler.not_found_handler()\n\n      assert conn.assigns[:post] == nil\n      refute conn.assigns[:legacy_error_handler] == true\n    end\n\n    test \"halts the socket for liveview based authorization\" do\n      assert {:halt, socket} =\n               %Phoenix.LiveView.Socket{assigns: %{}}\n               |> Canary.DefaultHandler.not_found_handler()\n\n      assert {:redirect, %{to: \"/\"}} = socket.redirected\n    end\n  end\n\n  describe \"unauthorized_handler/1\" do\n    test \"calls the global :not_found_handler for plug based authorization\" do\n      Application.put_env(:canary, :unauthorized_handler, {CustomHandlers, :unauthorized_handler})\n\n      params = %{\"id\" => \"30\"}\n\n      conn =\n        conn(\n          %Plug.Conn{private: %{phoenix_action: :show}},\n          :get,\n          \"/posts/30\",\n          params\n        )\n        |> Canary.DefaultHandler.unauthorized_handler()\n\n      assert conn.assigns[:legacy_error_handler] == true\n      assert conn.assigns[:post] == nil\n    end\n\n    test \"returns conn when error_handler is not defined\" do\n      Application.put_env(:canary, :unauthorized_handler, nil)\n\n      params = %{\"id\" => \"30\"}\n\n      conn =\n        conn(\n          %Plug.Conn{private: %{phoenix_action: :show}},\n          :get,\n          \"/posts/30\",\n          params\n        )\n        |> Canary.DefaultHandler.unauthorized_handler()\n\n      assert conn.assigns[:post] == nil\n      refute conn.assigns[:legacy_error_handler] == true\n    end\n\n    test \"halts the socket for liveview based authorization\" do\n      assert {:halt, socket} =\n               %Phoenix.LiveView.Socket{assigns: %{}}\n               |> Canary.DefaultHandler.unauthorized_handler()\n\n      assert {:redirect, %{to: \"/\"}} = socket.redirected\n    end\n  end\nend\n"
  },
  {
    "path": "test/canary/hooks_test.exs",
    "content": "defmodule Canary.HooksTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n\n  alias Canary.HooksHelper.{PageLive, PostLive}\n  @endpoint Canary.HooksHelper.Endpoint\n\n  setup_all do\n    Application.put_env(:canary, Canary.HooksHelper.Endpoint,\n      live_view: [signing_salt: \"eTh8jeshoe2Bie4e\"],\n      secret_key_base: String.duplicate(\"57689\", 50)\n    )\n\n    Application.put_env(:canary, :repo, Repo)\n\n    start_supervised!(Canary.HooksHelper.Endpoint)\n    |> Process.link()\n\n    conn =\n      Plug.Test.init_test_session(build_conn(), %{})\n      |> Plug.Conn.assign(:current_user, %User{id: 1})\n\n    {:ok, conn: conn}\n  end\n\n  describe \"handle_hook/2\" do\n    test \"load_resource hook on handle_params loads resource when is available\" do\n      uri = \"http://localhost/post\"\n      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post, required: false]}\n      params = %{}\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])\n\n      assert socket.assigns.post == nil\n\n      params = %{\"id\" => \"1\"}\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])\n\n      assert socket.assigns.post == %Post{id: 1}\n    end\n\n    test \"load_resource accepts already assigned resource when it matches\" do\n      uri = \"http://localhost/post\"\n      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post]}\n      params = %{\"id\" => \"2\"}\n\n      socket =\n        build_socket()\n        |> put_assigns(%{post: %Post{id: 2}})\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, socket])\n\n      assert socket.assigns.post == %Post{id: 2}\n\n      socket =\n        build_socket()\n        |> put_assigns(%{post: %User{id: 1}})\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, socket])\n\n      assert socket.assigns.post == %Post{id: 2, user_id: 2}\n    end\n\n    test \"load_resource hook on handle_params assigns nil when resource is not available\" do\n      uri = \"http://localhost/post\"\n      params = %{\"id\" => \"13\"}\n\n      metadata = %{\n        hook: :load_resource,\n        stage: :handle_params,\n        opts: [model: Post]\n      }\n\n      assert {:halt, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])\n\n      assert socket.assigns.post == nil\n    end\n\n    test \"load_resource hook on handle_event\" do\n      metadata = %{\n        hook: :load_resource,\n        stage: :handle_event,\n        opts: [model: Post]\n      }\n\n      params = %{\"id\" => \"1\"}\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [\"my_event\", params, build_socket()])\n\n      assert socket.assigns.post == %Post{id: 1}\n\n      params = %{\"id\" => \"13\"}\n\n      assert {:halt, socket} =\n               Canary.Hooks.handle_hook(metadata, [\"my_event\", params, build_socket()])\n\n      assert socket.assigns.post == nil\n    end\n\n    test \"authorize_resource hook on handle_params\" do\n      uri = \"http://localhost/post\"\n      metadata = %{hook: :authorize_resource, stage: :handle_params, opts: [model: Post, required: false]}\n      params = %{}\n\n      socket =\n        build_socket()\n        |> put_assigns(%{post: %Post{id: 1}, current_user: %User{id: 1}})\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, socket])\n\n      assert socket.assigns.authorized == true\n\n      socket =\n        build_socket(:delete)\n        |> put_assigns(%{post: %Post{id: 1}, current_user: %User{id: 1}})\n\n      assert {:halt, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, socket])\n\n      assert socket.assigns.authorized == false\n\n      socket =\n        build_socket(:create)\n        |> put_assigns(%{current_user: %User{id: 1}})\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, socket])\n\n      assert socket.assigns.authorized == true\n    end\n\n    test \"authorize_resource hook on handle_event\" do\n      metadata = %{hook: :authorize_resource, stage: :handle_event, opts: [model: Post]}\n      params = %{}\n\n      socket =\n        build_socket()\n        |> put_assigns(%{post: %Post{id: 1}, current_user: %User{id: 1}})\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [\"create\", params, socket])\n\n      assert socket.assigns.authorized == true\n\n      socket =\n        build_socket(:delete)\n        |> put_assigns(%{post: %Post{id: 1}, current_user: %User{id: 1}})\n\n      assert {:halt, socket} =\n               Canary.Hooks.handle_hook(metadata, [\"delete\", params, socket])\n\n      assert socket.assigns.authorized == false\n    end\n\n    test \"load_and_authorize_resource on handle_params\" do\n      uri = \"http://localhost/post\"\n\n      metadata = %{\n        hook: :load_and_authorize_resource,\n        stage: :handle_params,\n        opts: [model: Post, preload: :user]\n      }\n\n      params = %{\"id\" => \"1\"}\n\n      socket =\n        build_socket(:edit)\n        |> put_assigns(%{current_user: %User{id: 1}})\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, socket])\n\n      assert socket.assigns.post == %Post{id: 1} |> Repo.preload(:user)\n      assert socket.assigns.authorized == true\n\n      socket =\n        build_socket()\n        |> put_assigns(%{current_user: %User{id: 1}})\n\n      params = %{\"id\" => \"13\"}\n\n      assert {:halt, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, socket])\n\n      assert socket.assigns.post == nil\n      assert socket.assigns.authorized == false\n    end\n\n    test \"load_and_authorize_resource on handle_event\" do\n      metadata = %{\n        hook: :load_and_authorize_resource,\n        stage: :handle_event,\n        opts: [model: Post, preload: :user]\n      }\n\n      params = %{\"id\" => \"1\"}\n\n      socket =\n        build_socket()\n        |> put_assigns(%{current_user: %User{id: 1}})\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [\"edit\", params, socket])\n\n      assert socket.assigns.post == %Post{id: 1} |> Repo.preload(:user)\n      assert socket.assigns.authorized == true\n\n      socket =\n        build_socket()\n        |> put_assigns(%{current_user: %User{id: 1}})\n\n      params = %{\"id\" => \"13\"}\n\n      assert {:halt, socket} =\n               Canary.Hooks.handle_hook(metadata, [\"update\", params, socket])\n\n      assert socket.assigns.post == nil\n      assert socket.assigns.authorized == false\n    end\n\n    test \"accepts :id_field to override the default id field\" do\n      uri = \"http://localhost/post\"\n      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post, id_field: \"slug\"]}\n      params = %{\"id\" => \"slug1\"}\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])\n\n      assert socket.assigns.post == %Post{id: 1, slug: \"slug1\"}\n\n      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post]}\n      params = %{\"id\" => \"1\"}\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])\n\n      assert socket.assigns.post == %Post{id: 1}\n    end\n\n    test \"accepts :id_name to override the default id field\" do\n      uri = \"http://localhost/post\"\n      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post, id_name: \"blog_post_id\"]}\n      params = %{\"blog_post_id\" => \"2\"}\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])\n\n      assert socket.assigns.post == Repo.get(Post, 2)\n\n      params = %{\"id\" => \"1\"}\n      assert {:halt, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])\n\n      assert socket.assigns.post == nil\n    end\n\n\n    test \"accepts :preload to preload the resource\" do\n      uri = \"http://localhost/post\"\n      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post, preload: :user]}\n      params = %{\"id\" => \"1\"}\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])\n\n      assert socket.assigns.post == %Post{id: 1} |> Repo.preload(:user)\n\n      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post]}\n      params = %{\"id\" => \"1\"}\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])\n\n      assert socket.assigns.post != %Post{id: 1} |> Repo.preload(:user)\n\n    end\n\n    test \"accepts :as to override the default assign name\" do\n      uri = \"http://localhost/post\"\n      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post, as: :my_post]}\n      params = %{\"id\" => \"1\"}\n\n      assert {:cont, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])\n\n      assert socket.assigns.my_post == %Post{id: 1}\n    end\n\n    test \"accepts :current_user to override the default subject assign name\" do\n      uri = \"http://localhost/post\"\n\n      metadata = %{\n        hook: :authorize_resource,\n        stage: :handle_params,\n        opts: [model: Post, current_user: :my_user]\n      }\n\n      params = %{}\n\n      socket =\n        build_socket()\n        |> put_assigns(%{post: %Post{id: 1}, current_user: %User{id: 1}})\n\n      assert {:halt, socket} =\n               Canary.Hooks.handle_hook(metadata, [params, uri, socket])\n\n      assert socket.assigns.authorized == false\n    end\n\n    test \"emits a warning when the hook is not defined\" do\n      metadata = %{hook: :invalid_hook, stage: :handle_params, opts: [model: Post]}\n\n      assert ExUnit.CaptureIO.capture_io(:stderr, fn ->\n               {:cont, socket} =\n                 Canary.Hooks.handle_hook(metadata, [%{}, \"http://localhost/post\", build_socket()])\n\n               assert_raise KeyError, ~r/key :post not found in/, fn ->\n                 Map.fetch!(socket.assigns, :post)\n               end\n             end) =~\n               \"Invalid type :invalid_hook for Canary hook call. Please review defined hooks with mount_canary/2\"\n    end\n  end\n\n  describe \"integration for :load_resource\" do\n    test \"it loads the resource correctly\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/post/1\")\n      assert %{post: %Post{id: 1}} = PostLive.fetch_assigns(lv)\n    end\n\n    test \"it halt the socket when the resource is required\", %{conn: conn} do\n      assert {:error, {:redirect, %{to: \"/\"}}} = live(conn, \"/post/13/edit\")\n      assert {:error, {:redirect, %{to: \"/\"}}} = live(conn, \"/post/15/update\")\n    end\n  end\n\n  describe \"integration for on_mount/4\" do\n    test \"it attaches defined hooks to the socket\", %{conn: conn} do\n      {:ok, lv, _html} = live(conn, \"/page\")\n\n      {:ok, lifecycle} = PageLive.fetch_lifecycle(lv)\n\n      expected_handle_params = [\n        %{\n          id: :handle_params_load_resource_0,\n          function: &PageLive.handle_params_load_resource_0/3,\n          stage: :handle_params\n        },\n        %{\n          id: :handle_params_load_and_authorize_resource_1,\n          function: &PageLive.handle_params_load_and_authorize_resource_1/3,\n          stage: :handle_params\n        }\n      ]\n\n      assert Enum.all?(expected_handle_params, &(&1 in lifecycle.handle_params)),\n             \"Expected Enum: #{inspect(lifecycle.handle_params)} \\n to include: #{inspect(expected_handle_params)}\"\n\n      expected_handle_events = [\n        %{\n          id: :handle_event_load_and_authorize_resource_2,\n          function: &PageLive.handle_event_load_and_authorize_resource_2/3,\n          stage: :handle_event\n        }\n      ]\n\n      assert Enum.all?(expected_handle_events, &(&1 in lifecycle.handle_event)),\n             \"Expected Enum: #{inspect(lifecycle.handle_event)} \\n to include: #{inspect(expected_handle_events)}\"\n    end\n  end\n\n  describe \"mount_canary/2\" do\n    defmodule TestLive do\n      use Phoenix.LiveView\n      use Canary.Hooks\n\n      mount_canary(:load_resource,\n        model: Post\n      )\n\n      mount_canary(:load_and_authorize_resource,\n        on: [:handle_params, :handle_event],\n        model: User,\n        only: [:show]\n      )\n\n      def render(assigns) do\n        ~H\"\"\"\n        <div>Test</div>\n        \"\"\"\n      end\n\n      def mount(_params, _session, socket) do\n        {:ok, socket}\n      end\n    end\n\n    test \"defines wrapper function for events\" do\n      expected_fun = [\n        {:handle_params_load_resource_0, 3},\n        {:handle_params_load_and_authorize_resource_1, 3},\n        {:handle_event_load_and_authorize_resource_2, 3}\n      ]\n\n      assert Enum.all?(expected_fun, &(&1 in TestLive.__info__(:functions)))\n    end\n\n    test \"adds on_mount hook for attaching event hooks\" do\n      %{lifecycle: %{mount: mount}} = TestLive.__live__()\n\n      expected_mount = %{\n        function: &Canary.Hooks.on_mount/4,\n        id:\n          {Canary.Hooks,\n           {:initialize, TestLive,\n            [\n              handle_params_load_resource_0: :handle_params,\n              handle_params_load_and_authorize_resource_1: :handle_params,\n              handle_event_load_and_authorize_resource_2: :handle_event\n            ]}},\n        stage: :mount\n      }\n\n      assert Enum.any?(mount, &(&1 == expected_mount))\n    end\n\n    test \"emits a warning when no valid stage is provided\" do\n      assert ExUnit.CaptureIO.capture_io(:stderr, fn ->\n               defmodule InvalidLive do\n                 use Phoenix.LiveView\n                 use Canary.Hooks\n\n                 mount_canary(:load_resource,\n                   model: Post,\n                   on: [:invalid_stage]\n                 )\n\n                 def mount(_params, _session, socket) do\n                   {:ok, socket}\n                 end\n               end\n             end) =~\n               \"mount_canary called with empty :on stages\"\n    end\n  end\n\n  defp build_socket(action \\\\ :show) do\n    %Phoenix.LiveView.Socket{assigns: %{__changed__: %{}, live_action: action}}\n  end\n\n  defp put_assigns(socket, assigns) do\n    %{socket | assigns: Map.merge(socket.assigns, assigns)}\n  end\nend\n"
  },
  {
    "path": "test/canary/plugs_test.exs",
    "content": "defmodule Canary.PlugsTest do\n  import Canary.Plugs\n\n  import Plug.Adapters.Test.Conn, only: [conn: 4]\n\n  use ExUnit.Case, async: true\n\n  @moduletag timeout: 100_000_000\n\n  Application.put_env(:canary, :repo, Repo)\n  Application.delete_env(:canary, :error_handler)\n\n  test \"it loads the resource correctly\" do\n    opts = [model: Post]\n\n    # when the resource with the id can be fetched\n    params = %{\"id\" => \"1\"}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/1\", params)\n    expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 1})}\n\n    assert load_resource(conn, opts) == expected\n\n    # when a resource of the desired type is already present in conn.assigns\n    # it does not clobber the old resource\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{private: %{phoenix_action: :show}, assigns: %{post: %Post{id: 2}}},\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :post, %Post{id: 2})\n\n    assert load_resource(conn, opts) == expected\n\n    # when a resource of the desired type is already present in conn.assigns and the action is :index\n    # it does not clobber the old resource\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{private: %{phoenix_action: :index}, assigns: %{posts: [%Post{id: 2}]}},\n        :get,\n        \"/posts\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :posts, [%Post{id: 2}])\n\n    assert load_resource(conn, opts) == expected\n\n    # when a resource of a different type is already present in conn.assigns\n    # it replaces that resource with the desired resource\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{private: %{phoenix_action: :show}, assigns: %{post: %User{id: 2}}},\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :post, %Post{id: 1})\n\n    assert load_resource(conn, opts) == expected\n\n    # when a resource of a different type is already present in conn.assigns and the action is :index\n    # it replaces that resource with the desired resource\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{private: %{phoenix_action: :index}, assigns: %{posts: [%User{id: 2}]}},\n        :get,\n        \"/posts\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :posts, [%Post{id: 1}, %Post{id: 2, user_id: 2}])\n\n    assert load_resource(conn, opts) == expected\n\n    # when the resource with the id cannot be fetched\n    params = %{\"id\" => \"3\"}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/3\", params)\n    expected = Plug.Conn.assign(conn, :post, nil)\n\n    assert load_resource(conn, opts) == expected\n\n    # when the action is \"index\"\n    params = %{}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :index}}, :get, \"/posts\", params)\n    expected = Plug.Conn.assign(conn, :posts, [%Post{id: 1}, %Post{id: 2, user_id: 2}])\n\n    assert load_resource(conn, opts) == expected\n\n    # when the action is \"new\"\n    params = %{}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :new}}, :get, \"/posts/new\", params)\n    expected = Plug.Conn.assign(conn, :post, nil)\n\n    assert load_resource(conn, opts) == expected\n\n    # when the action is \"create\"\n    params = %{}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :create}}, :post, \"/posts/create\", params)\n    expected = Plug.Conn.assign(conn, :post, nil)\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it loads the resource correctly with opts[:id_name] specified\" do\n    opts = [model: Post, id_name: \"post_id\"]\n\n    # when id param is correct\n    params = %{\"post_id\" => 1}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/1\", params)\n    expected = Plug.Conn.assign(conn, :post, %Post{id: 1})\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it loads the resource correctly with opts[:id_field] specified\" do\n    opts = [model: Post, id_name: \"slug\", id_field: \"slug\"]\n\n    # when slug param is correct\n    params = %{\"slug\" => \"slug1\"}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/slug1\", params)\n    expected = Plug.Conn.assign(conn, :post, %Post{id: 1, slug: \"slug1\"})\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it loads the resource correctly with opts[:persisted] specified on :index action\" do\n    opts = [model: User, id_name: \"user_id\", persisted: true]\n\n    params = %{\"user_id\" => 1}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :index}}, :get, \"/users/1/posts\", params)\n    expected = Plug.Conn.assign(conn, :user, %User{id: 1})\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it loads the resource correctly with opts[:persisted] specified on :new action\" do\n    opts = [model: User, id_name: \"user_id\", persisted: true]\n\n    params = %{\"user_id\" => 1}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :new}}, :get, \"/users/1/posts/new\", params)\n    expected = Plug.Conn.assign(conn, :user, %User{id: 1})\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it loads the resource correctly with opts[:persisted] specified on :create action\" do\n    opts = [model: User, id_name: \"user_id\", persisted: true]\n\n    params = %{\"user_id\" => \"1\"}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :create}}, :post, \"/users/1/posts\", params)\n    expected = Plug.Conn.assign(conn, :user, %User{id: 1})\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it calls the specified action when not_found with opts[:required] specified on :new action\" do\n    opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}, required: true]\n\n    params = %{\"id\" => \"3\"}\n\n    conn =\n      conn(\n        %Plug.Conn{assigns: %{post: nil}, private: %{phoenix_action: :new}},\n        :get,\n        \"/posts/3/new\",\n        params\n      )\n\n    expected = Helpers.not_found_handler(conn)\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it calls the specified action when not_found with opts[:required] specified on :create action\" do\n    opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}, required: true]\n\n    params = %{\"id\" => \"3\"}\n\n    conn =\n      conn(\n        %Plug.Conn{assigns: %{post: nil}, private: %{phoenix_action: :index}},\n        :post,\n        \"/posts/3/new\",\n        params\n      )\n\n    expected = Helpers.not_found_handler(conn)\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it calls the specified action when not_found with opts[:required] specified on :index action\" do\n    opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}, required: true]\n\n    params = %{\"id\" => \"3\"}\n\n    conn =\n      conn(\n        %Plug.Conn{assigns: %{post: nil}, private: %{phoenix_action: :index}},\n        :get,\n        \"/posts/3\",\n        params\n      )\n\n    expected = Helpers.not_found_handler(conn)\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it authorizes the resource correctly\" do\n    opts = [model: Post]\n\n    # when the action is \"new\"\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :new},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/new\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the action is \"create\"\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :create},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/create\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the action is \"index\"\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :index},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the action is a phoenix action\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the current user can access the given resource\n    # and the action is specified in conn.assigns.canary_action\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when both conn.assigns.canary_action and conn.private.phoenix_action are defined\n    # it uses conn.assigns.canary_action for authorization\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}, canary_action: :unauthorized}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the current user cannot access the given resource\n    params = %{\"id\" => \"2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show}\n        },\n        :get,\n        \"/posts/2\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the resource of the desired type already exists in conn.assigns,\n    # it authorizes for that resource\n    params = %{\"id\" => \"2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}}\n        },\n        :get,\n        \"/posts/2\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the resource of a different type already exists in conn.assigns,\n    # it authorizes for the desired resource\n    params = %{\"id\" => \"2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{}}\n        },\n        :get,\n        \"/posts/2\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when current_user is nil\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: nil, canary_action: :create}\n        },\n        :post,\n        \"/posts\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_resource(conn, opts) == expected\n  end\n\n  test \"it authorizes the resource correctly when using :id_field option\" do\n    opts = [model: Post, id_field: \"slug\", id_name: \"slug\"]\n\n    # when the action is \"new\"\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :new},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/new\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the action is \"create\"\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :create},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/create\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the action is \"index\"\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :index},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the action is a phoenix action\n    params = %{\"slug\" => \"slug1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/slug1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the current user can access the given resource\n    # and the action is specified in conn.assigns.canary_action\n    params = %{\"slug\" => \"slug1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show}\n        },\n        :get,\n        \"/posts/slug1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when both conn.assigns.canary_action and conn.private.phoenix_action are defined\n    # it uses conn.assigns.canary_action for authorization\n    params = %{\"slug\" => \"slug1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}, canary_action: :unauthorized}\n        },\n        :get,\n        \"/posts/slug1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the current user cannot access the given resource\n    params = %{\"slug\" => \"slug2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show}\n        },\n        :get,\n        \"/posts/slug2\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the resource of the desired type already exists in conn.assigns,\n    # it authorizes for that resource\n    params = %{\"slug\" => \"slug2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}}\n        },\n        :get,\n        \"/posts/slug2\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the resource of a different type already exists in conn.assigns,\n    # it authorizes for the desired resource\n    params = %{\"slug\" => \"slug2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{}}\n        },\n        :get,\n        \"/posts/slug2\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when current_user is nil\n    params = %{\"slug\" => \"slug1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: nil, canary_action: :create}\n        },\n        :post,\n        \"/posts\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_resource(conn, opts) == expected\n  end\n\n  test \"it authorizes the resource correctly with opts[:persisted] specified on :index action\" do\n    opts = [model: Post, id_name: \"post_id\", persisted: true]\n\n    params = %{\"post_id\" => 2}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :index},\n          assigns: %{current_user: %User{id: 2}}\n        },\n        :get,\n        \"/posts/post_id/comments\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n  end\n\n  test \"it authorizes the resource correctly with opts[:persisted] specified on :new action\" do\n    opts = [model: Post, id_name: \"post_id\", persisted: true]\n\n    params = %{\"post_id\" => 2}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :new},\n          assigns: %{current_user: %User{id: 2}}\n        },\n        :get,\n        \"/posts/post_id/comments/new\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n  end\n\n  test \"it authorizes the resource correctly with opts[:persisted] specified on :create action\" do\n    opts = [model: Post, id_name: \"post_id\", persisted: true]\n\n    params = %{\"post_id\" => \"2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :create},\n          assigns: %{current_user: %User{id: 2}}\n        },\n        :post,\n        \"/posts/post_id/comments\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n  end\n\n  test \"it loads and authorizes the resource correctly\" do\n    opts = [model: Post]\n\n    # when the current user can access the given resource\n    # and the resource can be loaded\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, true)\n      |> Plug.Conn.assign(:post, %Post{id: 1, user_id: 1})\n\n    assert load_and_authorize_resource(conn, opts) == expected\n\n    # when the current user cannot access the given resource\n    params = %{\"id\" => \"2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show}\n        },\n        :get,\n        \"/posts/2\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, false)\n      |> Plug.Conn.assign(:post, nil)\n\n    assert load_and_authorize_resource(conn, opts) == expected\n\n    # when a resource of the desired type is already present in conn.assigns\n    # it does not load a new resource\n    params = %{\"id\" => \"2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}}\n        },\n        :get,\n        \"/posts/2\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, true)\n      |> Plug.Conn.assign(:post, %Post{user_id: 1})\n\n    assert load_and_authorize_resource(conn, opts) == expected\n\n    # when a resource of the a different type is already present in conn.assigns\n    # it loads and authorizes for the desired resource\n    params = %{\"id\" => \"2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{id: 1}}\n        },\n        :get,\n        \"/posts/2\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, false)\n      |> Plug.Conn.assign(:post, nil)\n\n    assert load_and_authorize_resource(conn, opts) == expected\n\n    # when the given resource cannot be loaded\n    params = %{\"id\" => \"3\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, false)\n      |> Plug.Conn.assign(:post, nil)\n\n    assert load_and_authorize_resource(conn, opts) == expected\n  end\n\n  test \"it loads and authorizes the resource correctly when using :id_field option\" do\n    opts = [model: Post, id_field: \"slug\", id_name: \"slug\"]\n\n    # when the current user can access the given resource\n    # and the resource can be loaded\n    params = %{\"slug\" => \"slug1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/slug1\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, true)\n      |> Plug.Conn.assign(:post, %Post{id: 1, slug: \"slug1\", user_id: 1})\n\n    assert load_and_authorize_resource(conn, opts) == expected\n\n    # when the current user cannot access the given resource\n    params = %{\"slug\" => \"slug2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show}\n        },\n        :get,\n        \"/posts/slug2\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, false)\n      |> Plug.Conn.assign(:post, nil)\n\n    assert load_and_authorize_resource(conn, opts) == expected\n\n    # when a resource of the desired type is already present in conn.assigns\n    # it does not load a new resource\n    params = %{\"slug\" => \"slug2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}}\n        },\n        :get,\n        \"/posts/slug2\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, true)\n      |> Plug.Conn.assign(:post, %Post{user_id: 1})\n\n    assert load_and_authorize_resource(conn, opts) == expected\n\n    # when a resource of the a different type is already present in conn.assigns\n    # it loads and authorizes for the desired resource\n    params = %{\"slug\" => \"slug2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{id: 1}}\n        },\n        :get,\n        \"/posts/slug2\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, false)\n      |> Plug.Conn.assign(:post, nil)\n\n    assert load_and_authorize_resource(conn, opts) == expected\n\n    # when the given resource cannot be loaded\n    params = %{\"slug\" => \"slug3\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show}\n        },\n        :get,\n        \"/posts/slug3\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, false)\n      |> Plug.Conn.assign(:post, nil)\n\n    assert load_and_authorize_resource(conn, opts) == expected\n  end\n\n  test \"it loads and authorizes the resource correctly with opts[:persisted] specified on :index action\" do\n    opts = [model: Post, id_name: \"post_id\", persisted: true]\n\n    params = %{\"post_id\" => 2}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :index},\n          assigns: %{current_user: %User{id: 2}}\n        },\n        :get,\n        \"/posts/2/comments\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, true)\n      |> Plug.Conn.assign(:post, %Post{id: 2, user_id: 2})\n\n    assert load_and_authorize_resource(conn, opts) == expected\n  end\n\n  test \"it loads and authorizes the resource correctly with opts[:persisted] specified on :new action\" do\n    opts = [model: Post, id_name: \"post_id\", persisted: true]\n\n    params = %{\"post_id\" => 2}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :new},\n          assigns: %{current_user: %User{id: 2}}\n        },\n        :get,\n        \"/posts/2/comments/new\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, true)\n      |> Plug.Conn.assign(:post, %Post{id: 2, user_id: 2})\n\n    assert load_and_authorize_resource(conn, opts) == expected\n  end\n\n  test \"it loads and authorizes the resource correctly with opts[:persisted] specified on :create action\" do\n    opts = [model: Post, id_name: \"post_id\", persisted: true]\n\n    params = %{\"post_id\" => \"2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :create},\n          assigns: %{current_user: %User{id: 2}}\n        },\n        :create,\n        \"/posts/2/comments\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, true)\n      |> Plug.Conn.assign(:post, %Post{id: 2, user_id: 2})\n\n    assert load_and_authorize_resource(conn, opts) == expected\n  end\n\n  test \"it only loads the resource when the action is in opts[:only]\" do\n    # when the action is in opts[:only]\n    opts = [model: Post, only: :show]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :post, %Post{id: 1})\n\n    assert load_resource(conn, opts) == expected\n\n    # when the action is not opts[:only]\n    opts = [model: Post, only: :other]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = conn\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it only authorizes actions in opts[:only]\" do\n    # when the action is in opts[:only]\n    opts = [model: Post, only: :show]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n    assert authorize_resource(conn, opts) == expected\n\n    # when the action is not opts[:only]\n    opts = [model: Post, only: :other]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = conn\n\n    assert authorize_resource(conn, opts) == expected\n  end\n\n  test \"it only loads and authorizes the resource for actions in opts[:only]\" do\n    # when the action is in opts[:only]\n    opts = [model: Post, only: :show]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, true)\n      |> Plug.Conn.assign(:post, %Post{id: 1, user_id: 1})\n\n    assert load_and_authorize_resource(conn, opts) == expected\n\n    # when the action is not opts[:only]\n    opts = [model: Post, only: :other]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = conn\n\n    assert load_and_authorize_resource(conn, opts) == expected\n  end\n\n  test \"it raises when both opts[:only] and opts[:except] are specified\" do\n    # when the plug is load_resource\n    opts = [model: Post, only: :show, except: :index]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = conn\n\n    assert_raise ArgumentError, fn ->\n      load_resource(conn, opts) == expected\n    end\n\n    # when the plug is authorize_resource\n    opts = [model: Post, only: :show, except: :index]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = conn\n\n\n    assert_raise ArgumentError, fn ->\n      authorize_resource(conn, opts) == expected\n    end\n\n    # when the plug is load_and_authorize_resource\n    opts = [model: Post, only: :show, except: :index]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = conn\n\n    assert_raise ArgumentError, fn ->\n      load_and_authorize_resource(conn, opts) == expected\n    end\n  end\n\n  test \"it correctly skips authorization for exempt actions\" do\n    # when the action is exempt\n    opts = [model: Post, except: :show]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = conn\n\n    assert authorize_resource(conn, opts) == expected\n\n    # when the action is not exempt\n    opts = [model: Post]\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n  end\n\n  test \"it correctly skips loading resources for exempt actions\" do\n    # when the action is exempt\n    opts = [model: Post, except: :show]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = conn\n    assert load_resource(conn, opts) == expected\n\n    # when the action is not exempt\n    opts = [model: Post]\n    expected = Plug.Conn.assign(conn, :post, %Post{id: 1, user_id: 1})\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it correctly skips load_and_authorize_resource for exempt actions\" do\n    # when the action is exempt\n    opts = [model: Post, except: :show]\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = conn\n    assert load_and_authorize_resource(conn, opts) == expected\n\n    # when the action is not exempt\n    opts = [model: Post]\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, true)\n      |> Plug.Conn.assign(:post, %Post{id: 1, user_id: 1})\n\n    assert load_and_authorize_resource(conn, opts) == expected\n  end\n\n  test \"it loads the resource into a key specified by the :as option\" do\n    opts = [model: Post, as: :some_key]\n\n    # when the resource with the id can be fetched\n    params = %{\"id\" => \"1\"}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/1\", params)\n    expected = Plug.Conn.assign(conn, :some_key, %Post{id: 1})\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"it authorizes the resource correctly when the :as key is specified\" do\n    opts = [model: Post, as: :some_key]\n\n    # when the action is \"new\"\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :new},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/new\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_resource(conn, opts) == expected\n    # need to check that it works for authorization as well, and for load_and_authorize_resource\n  end\n\n  test \"it loads and authorizes the resource correctly when the :as key is specified\" do\n    opts = [model: Post, as: :some_key]\n\n    # when the current user can access the given resource\n    # and the resource can be loaded\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, true)\n      |> Plug.Conn.assign(:some_key, %Post{id: 1, user_id: 1})\n\n    assert load_and_authorize_resource(conn, opts) == expected\n  end\n\n  test \"when the :as key is not specified, it loads the resource into a key inferred from the model name\" do\n    opts = [model: Post]\n\n    # when the resource with the id can be fetched\n    params = %{\"id\" => \"1\"}\n    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/1\", params)\n    expected = Plug.Conn.assign(conn, :post, %Post{id: 1})\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"when unauthorized, it calls the specified action\" do\n    opts = [model: Post, unauthorized_handler: {Helpers, :unauthorized_handler}]\n\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, false)\n      |> Helpers.unauthorized_handler()\n\n    assert authorize_resource(conn, opts) == expected\n  end\n\n  test \"when not_found, it calls the specified action\" do\n    opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}]\n\n    params = %{\"id\" => \"3\"}\n\n    conn =\n      conn(\n        %Plug.Conn{assigns: %{post: nil}, private: %{phoenix_action: :show}},\n        :get,\n        \"/posts/3\",\n        params\n      )\n\n    expected = Helpers.not_found_handler(conn)\n\n    assert load_resource(conn, opts) == expected\n  end\n\n  test \"when unauthorized and resource not found, it calls the specified authorization handler first\" do\n    opts = [\n      model: Post,\n      not_found_handler: {Helpers, :not_found_handler},\n      unauthorized_handler: {Helpers, :unauthorized_handler}\n    ]\n\n    params = %{\"id\" => \"3\"}\n\n    conn =\n      conn(\n        %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},\n        :get,\n        \"/posts/3\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, false)\n      |> Plug.Conn.assign(:post, nil)\n      |> Helpers.unauthorized_handler()\n\n    assert load_and_authorize_resource(conn, opts) == expected\n  end\n\n  test \"when the authorization handler does not halt the request, it calls the not found handler if specified\" do\n    opts = [\n      model: Post,\n      not_found_handler: {Helpers, :not_found_handler},\n      unauthorized_handler: {Helpers, :non_halting_unauthorized_handler}\n    ]\n\n    params = %{\"id\" => \"3\"}\n\n    conn =\n      conn(\n        %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},\n        :get,\n        \"/posts/3\",\n        params\n      )\n\n    expected =\n      conn\n      |> Plug.Conn.assign(:authorized, false)\n      |> Plug.Conn.assign(:post, nil)\n      |> Helpers.non_halting_unauthorized_handler()\n      |> Helpers.not_found_handler()\n\n    assert load_and_authorize_resource(conn, opts) == expected\n  end\n\n  defmodule UnauthorizedHandlerConfigured do\n    use ExUnit.Case, async: false\n\n    test \"when unauthorized, it calls the configured action\" do\n      Application.put_env(:canary, :unauthorized_handler, {Helpers, :unauthorized_handler})\n      opts = [model: Post]\n\n      params = %{\"id\" => \"1\"}\n\n      conn =\n        conn(\n          %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},\n          :get,\n          \"/posts/1\",\n          params\n        )\n\n      expected =\n        conn\n        |> Plug.Conn.assign(:authorized, false)\n        |> Helpers.unauthorized_handler()\n\n      assert authorize_resource(conn, opts) == expected\n    end\n\n    test \"when unauthorized and resource not found, it calls the configured authorization handler first\" do\n      Application.put_env(:canary, :unauthorized_handler, {Helpers, :unauthorized_handler})\n      opts = [model: Post]\n\n      params = %{\"id\" => \"3\"}\n\n      conn =\n        conn(\n          %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},\n          :get,\n          \"/posts/3\",\n          params\n        )\n\n      expected =\n        conn\n        |> Plug.Conn.assign(:authorized, false)\n        |> Plug.Conn.assign(:post, nil)\n        |> Helpers.unauthorized_handler()\n\n      assert load_and_authorize_resource(conn, opts) == expected\n    end\n  end\n\n  defmodule UnauthorizedHandlerConfiguredAndSpecified do\n    use ExUnit.Case, async: false\n\n    test \"when unauthorized, it calls the opt-specified action rather than the configured action\" do\n      # should not be called\n      Application.put_env(:canary, :unauthorized_handler, {Helpers, :does_not_exist})\n      opts = [model: Post, unauthorized_handler: {Helpers, :unauthorized_handler}]\n\n      params = %{\"id\" => \"1\"}\n\n      conn =\n        conn(\n          %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},\n          :get,\n          \"/posts/1\",\n          params\n        )\n\n      expected =\n        conn\n        |> Helpers.unauthorized_handler()\n        |> Plug.Conn.assign(:authorized, false)\n\n      assert authorize_resource(conn, opts) == expected\n    end\n  end\n\n  defmodule NotFoundHandlerConfigured do\n    use ExUnit.Case, async: false\n\n    test \"when not_found, it calls the configured action\" do\n      Application.put_env(:canary, :error_handler, Helpers)\n      opts = [model: Post]\n\n      params = %{\"id\" => \"4\"}\n      conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/4\", params)\n\n      expected =\n        conn\n        |> Helpers.not_found_handler()\n        |> Plug.Conn.assign(:post, nil)\n\n      assert load_resource(conn, opts) == expected\n    end\n  end\n\n  defmodule NotFoundHandlerConfiguredAndSpecified do\n    use ExUnit.Case, async: false\n\n    test \"when not_found, it calls the opt-specified action rather than the configured action\" do\n      # should not be called\n      Application.put_env(:canary, :not_found_handler, {Helpers, :does_not_exist})\n      opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}]\n\n      params = %{\"id\" => \"4\"}\n      conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/4\", params)\n\n      expected =\n        conn\n        |> Helpers.not_found_handler()\n        |> Plug.Conn.assign(:post, nil)\n\n      assert load_resource(conn, opts) == expected\n    end\n  end\n\n  defmodule CurrentUser do\n    use ExUnit.Case, async: true\n\n    defmodule ApplicationConfig do\n      use ExUnit.Case, async: false\n      import Mock\n\n      test_with_mock \"it uses the current_user name configured\", Application, [:passthrough],\n        get_env: fn _, _, _ -> :current_admin end do\n        # when the user configured with opts\n        opts = [model: Post, except: :show]\n        params = %{\"id\" => \"1\"}\n\n        conn =\n          conn(\n            %Plug.Conn{\n              private: %{phoenix_action: :show},\n              assigns: %{current_admin: %User{id: 1}}\n            },\n            :get,\n            \"/posts/1\",\n            params\n          )\n\n        expected = conn\n\n        assert authorize_resource(conn, opts) == expected\n      end\n    end\n\n    test \"it uses the current_user name in options\" do\n      # when the user configured with opts\n      opts = [model: Post, current_user: :user]\n      params = %{\"id\" => \"1\"}\n\n      conn =\n        conn(\n          %Plug.Conn{\n            private: %{phoenix_action: :show},\n            assigns: %{user: %User{id: 1}, authorized: true}\n          },\n          :get,\n          \"/posts/1\",\n          params\n        )\n\n      expected = conn\n\n      assert authorize_resource(conn, opts) == expected\n    end\n\n    test \"it throws an error when the wrong current_user name is used\" do\n      # when the user configured with opts\n      opts = [model: Post, current_user: :configured_current_user]\n      params = %{\"id\" => \"1\"}\n\n      conn =\n        conn(\n          %Plug.Conn{\n            private: %{phoenix_action: :show},\n            assigns: %{user: %User{id: 1}, authorized: true}\n          },\n          :get,\n          \"/posts/1\",\n          params\n        )\n\n      assert_raise KeyError, ~r/^key :configured_current_user not found in: %{/, fn ->\n        authorize_resource(conn, opts)\n      end\n    end\n  end\n\n  defmodule Preload do\n    use ExUnit.Case, async: true\n\n    test \"it loads the resource correctly when the :preload key is specified\" do\n      opts = [model: Post, preload: :user]\n\n      # when the resource with the id can be fetched and the association exists\n      params = %{\"id\" => \"2\"}\n      conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/1\", params)\n      expected = Plug.Conn.assign(conn, :post, %Post{id: 2, user_id: 2, user: %User{id: 2}})\n\n      assert load_resource(conn, opts) == expected\n\n      # when the resource with the id can be fetched and the association does not exist\n      params = %{\"id\" => \"1\"}\n      conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/1\", params)\n      expected = Plug.Conn.assign(conn, :post, %Post{id: 1, user_id: 1, user: %User{id: 1}})\n\n      assert load_resource(conn, opts) == expected\n\n      # when the resource with the id cannot be fetched\n      params = %{\"id\" => \"3\"}\n      conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, \"/posts/3\", params)\n      expected = Plug.Conn.assign(conn, :post, nil)\n\n      assert load_resource(conn, opts) == expected\n\n      # when the action is \"index\"\n      params = %{}\n      conn = conn(%Plug.Conn{private: %{phoenix_action: :index}}, :get, \"/posts\", params)\n\n      expected =\n        Plug.Conn.assign(conn, :posts, [\n          %Post{id: 1},\n          %Post{id: 2, user_id: 2, user: %User{id: 2}}\n        ])\n\n      assert load_resource(conn, opts) == expected\n\n      # when the action is \"new\"\n      params = %{}\n      conn = conn(%Plug.Conn{private: %{phoenix_action: :new}}, :get, \"/posts/new\", params)\n      expected = Plug.Conn.assign(conn, :post, nil)\n\n      assert load_resource(conn, opts) == expected\n    end\n\n    test \"it authorizes the resource correctly when the :preload key is specified\" do\n      opts = [model: Post, preload: :user]\n\n      # when the action is \"edit\"\n      params = %{\"id\" => \"2\"}\n\n      conn =\n        conn(\n          %Plug.Conn{\n            private: %{phoenix_action: :edit},\n            assigns: %{current_user: %User{id: 2}}\n          },\n          :get,\n          \"/posts/edit/2\",\n          params\n        )\n\n      expected = Plug.Conn.assign(conn, :authorized, true)\n\n      assert authorize_resource(conn, opts) == expected\n\n      # when the action is \"index\"\n      params = %{}\n\n      conn =\n        conn(\n          %Plug.Conn{\n            private: %{phoenix_action: :index},\n            assigns: %{current_user: %User{id: 1}}\n          },\n          :get,\n          \"/posts\",\n          params\n        )\n\n      expected = Plug.Conn.assign(conn, :authorized, true)\n\n      assert authorize_resource(conn, opts) == expected\n    end\n\n    test \"it loads and authorizes the resource correctly when the :preload key is specified\" do\n      opts = [model: Post, preload: :user]\n\n      # when the current user can access the given resource\n      # and the resource can be loaded and the association exists\n      params = %{\"id\" => \"2\"}\n\n      conn =\n        conn(\n          %Plug.Conn{\n            private: %{phoenix_action: :show},\n            assigns: %{current_user: %User{id: 2}}\n          },\n          :get,\n          \"/posts/2\",\n          params\n        )\n\n      expected =\n        conn\n        |> Plug.Conn.assign(:authorized, true)\n        |> Plug.Conn.assign(:post, %Post{id: 2, user_id: 2, user: %User{id: 2}})\n\n     assert load_and_authorize_resource(conn, opts) == expected\n\n      # when the current user can access the given resource\n      # and the resource can be loaded and the association does not exist\n      params = %{\"id\" => \"1\"}\n\n      conn =\n        conn(\n          %Plug.Conn{\n            private: %{phoenix_action: :show},\n            assigns: %{current_user: %User{id: 1}}\n          },\n          :get,\n          \"/posts/1\",\n          params\n        )\n\n      expected =\n        conn\n        |> Plug.Conn.assign(:authorized, true)\n        |> Plug.Conn.assign(:post, %Post{id: 1, user_id: 1, user: %User{id: 1}})\n\n      assert load_and_authorize_resource(conn, opts) == expected\n\n      # when the action is \"edit\"\n      params = %{\"id\" => \"2\"}\n\n      conn =\n        conn(\n          %Plug.Conn{\n            private: %{phoenix_action: :edit},\n            assigns: %{current_user: %User{id: 2}}\n          },\n          :get,\n          \"/posts/edit/2\",\n          params\n        )\n\n      expected =\n        conn\n        |> Plug.Conn.assign(:authorized, true)\n        |> Plug.Conn.assign(:post, %Post{id: 2, user_id: 2, user: %User{id: 2}})\n\n      assert load_and_authorize_resource(conn, opts) == expected\n    end\n  end\n\n  defmodule NonIdActions do\n    use ExUnit.Case, async: true\n\n    test \"it throws an error when the non_id_actions is not a list\" do\n      # when opts[:non_id_actions] is set but not as a list\n      opts = [model: Post, non_id_actions: :other_action]\n      params = %{\"id\" => \"1\"}\n\n      conn =\n        conn(\n          %Plug.Conn{\n            private: %{phoenix_action: :other_action},\n            assigns: %{current_user: %User{id: 1}, authorized: true}\n          },\n          :get,\n          \"/posts/other-action\",\n          params\n        )\n\n      assert_raise Protocol.UndefinedError, ~r/protocol Enumerable not implemented for /, fn ->\n        authorize_resource(conn, opts)\n      end\n    end\n\n    test \"it authorizes the resource correctly when non_id_actions is a list\" do\n      # when opts[:non_id_actions] is set as a list\n      opts = [model: Post, non_id_actions: [:other_action]]\n\n      params = %{\"id\" => \"1\"}\n\n      conn =\n        conn(\n          %Plug.Conn{\n            private: %{phoenix_action: :other_action},\n            assigns: %{current_user: %User{id: 1}, authorized: true}\n          },\n          :get,\n          \"/posts/other-action\",\n          params\n        )\n\n      expected = Plug.Conn.assign(conn, :authorized, true)\n\n      assert authorize_resource(conn, opts) == expected\n    end\n  end\n\n  test \"it authorizes the controller correctly\" do\n    opts = [model: Post]\n\n    # when the action is \"new\"\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :new, phoenix_controller: Myproject.SampleController},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/new\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_controller(conn, opts) == expected\n\n    # when the action is \"create\"\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :create, phoenix_controller: Myproject.SampleController},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/create\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_controller(conn, opts) == expected\n\n    # when the action is \"index\"\n    params = %{}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :index, phoenix_controller: Myproject.SampleController},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_controller(conn, opts) == expected\n\n    # when the action is a phoenix action\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show, phoenix_controller: Myproject.SampleController},\n          assigns: %{current_user: %User{id: 1}}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_controller(conn, opts) == expected\n\n    # when the current user can access the given resource\n    # and the action and controller are specified in conn.assigns.canary_action\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{},\n          assigns: %{\n            current_user: %User{id: 1},\n            canary_action: :show,\n            canary_controller: Myproject.SampleController\n          }\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_controller(conn, opts) == expected\n\n    # when both conn.assigns.canary_action and conn.private.phoenix_action are defined\n    # it uses conn.assigns.canary_action for authorization\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_action: :show, phoenix_controller: Myproject.SampleController},\n          assigns: %{current_user: %User{id: 1}, canary_action: :unauthorized}\n        },\n        :get,\n        \"/posts/1\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_controller(conn, opts) == expected\n\n    # when the current user cannot access the given action\n    params = %{\"id\" => \"2\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_controller: Myproject.SampleController},\n          assigns: %{current_user: %User{id: 1}, canary_action: :someaction}\n        },\n        :get,\n        \"/posts/2\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_controller(conn, opts) == expected\n\n    # when current_user is nil\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_controller: Myproject.SampleController},\n          assigns: %{current_user: nil, canary_action: :create}\n        },\n        :post,\n        \"/posts\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_controller(conn, opts) == expected\n\n    # when an action is restricted on a controller\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_controller: Myproject.PartialAccessController},\n          assigns: %{current_user: %User{id: 1}, canary_action: :new}\n        },\n        :post,\n        \"/posts\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, false)\n\n    assert authorize_controller(conn, opts) == expected\n\n    # when an action is authorized on a controller\n    params = %{\"id\" => \"1\"}\n\n    conn =\n      conn(\n        %Plug.Conn{\n          private: %{phoenix_controller: Myproject.PartialAccessController},\n          assigns: %{current_user: %User{id: 1}, canary_action: :show}\n        },\n        :post,\n        \"/posts\",\n        params\n      )\n\n    expected = Plug.Conn.assign(conn, :authorized, true)\n\n    assert authorize_controller(conn, opts) == expected\n  end\nend\n"
  },
  {
    "path": "test/canary/utils_test.exs",
    "content": "defmodule UtilsTest do\n  import Canary.Utils\n\n  use ExUnit.Case, async: true\n\n  describe \"get_resource_id/2\" do\n    test \"returns the id from the params\" do\n      assert get_resource_id(%{\"id\" => \"9\"}, []) == \"9\"\n      assert get_resource_id(%{\"user_id\" => \"7\"}, id_name: \"user_id\") == \"7\"\n    end\n\n    test \"returns the id form conn.params\" do\n      conn = %Plug.Conn{params: %{\"id\" => \"9\"}}\n      assert get_resource_id(conn, []) == \"9\"\n\n      conn = %Plug.Conn{params: %{\"custom_id\" => \"1\"}}\n      assert get_resource_id(conn, id_name: \"custom_id\") == \"1\"\n    end\n\n    test \"returns nil if the id is not found\" do\n      assert get_resource_id(%{\"other_id\" => \"9\"}, id_name: \"id\") == nil\n\n      conn = %Plug.Conn{params: %{\"other_id\" => \"9\"}}\n      assert get_resource_id(conn, id_name: \"id\") == nil\n    end\n  end\n\n  describe \"action_valid?/2\" do\n    test \"returns true if the action is valid\" do\n      assert action_valid?(:index, only: [:index, :show]) == true\n      assert action_valid?(:show, except: :index) == true\n    end\n\n    test \"returns false if the action is not valid\" do\n      assert action_valid?(:index, except: :index) == false\n      assert action_valid?(:edit, only: [:index, :show]) == false\n    end\n\n    test \"raise when both :only and :except are provided\" do\n      assert_raise ArgumentError, fn ->\n        action_valid?(:index, only: [:index], except: :index)\n      end\n    end\n  end\n\n  test \"required?/1 returns true if the resource is required\" do\n    assert required?(required: true) == true\n    assert required?(required: false) == false\n    assert required?([]) == true\n  end\n\n  describe \"apply_error_handler/3\" do\n    defmodule CustomErrorHandler do\n      @behaviour Canary.ErrorHandler\n\n      def not_found_handler(%Plug.Conn{} = conn) do\n        %{conn | assigns: %{ok_custom_not_found_handler: true}}\n      end\n      def unauthorized_handler(%Plug.Conn{} = conn) do\n        %{conn | assigns: %{ok_custom_unauthorized_handler: true}}\n      end\n\n      def custom_handler(%Plug.Conn{} = conn) do\n        %{conn | assigns: %{ok_custom_handler: true}}\n      end\n    end\n\n    test \"raises if the error_handler is undefined\" do\n      assert_raise UndefinedFunctionError, ~r/function UnknownCustomErrorHandler.wrong_function\\/1 is undefined/, fn ->\n        apply_error_handler(%Plug.Conn{}, :unauthorized_handler, [\n          unauthorized_handler: {UnknownCustomErrorHandler, :wrong_function}\n        ])\n      end\n\n      assert_raise UndefinedFunctionError, ~r/function OtherErrorHandler.custom_function\\/1 is undefined/, fn ->\n        apply_error_handler(%Plug.Conn{}, :unauthorized_handler, [\n          unauthorized_handler: {OtherErrorHandler, :custom_function}\n        ])\n      end\n    end\n\n    test \"raises if the error_handler is not a module\" do\n      Application.put_env(:canary, :error_handler, 42)\n\n      assert_raise ArgumentError, ~r/Invalid error handler, expected a module or a tuple with a module and a function/, fn ->\n        apply_error_handler(%Plug.Conn{}, :not_found_handler, [])\n      end\n    end\n\n    test \"allows overriding the error handler\" do\n      Application.put_env(:canary, :error_handler, CustomErrorHandler)\n\n      conn = apply_error_handler(%Plug.Conn{}, :not_found_handler, [])\n      assert conn.assigns[:ok_custom_not_found_handler] == true\n\n      conn = apply_error_handler(%Plug.Conn{}, :unauthorized_handler, [])\n      assert conn.assigns[:ok_custom_unauthorized_handler] == true\n\n\n      conn = apply_error_handler(%Plug.Conn{}, :unauthorized_handler, [unauthorized_handler: {Canary.DefaultHandler, :unauthorized_handler}])\n      assert conn.assigns[:ok_custom_unauthorized_handler] == nil\n\n      conn = apply_error_handler(%Plug.Conn{}, :not_found_handler, [\n        not_found_handler: {CustomErrorHandler, :custom_handler}\n        ])\n      assert conn.assigns[:ok_custom_handler] == true\n    end\n  end\nend\n"
  },
  {
    "path": "test/support/endpoint.ex",
    "content": "defmodule Canary.HooksHelper.Endpoint do\n  use Phoenix.Endpoint, otp_app: :canary\n\n  socket \"/live\", Phoenix.LiveView.Socket\n\n  plug Canary.HooksHelper.Router\nend\n"
  },
  {
    "path": "test/support/page_live.ex",
    "content": "defmodule Canary.HooksHelper.PageLive do\n  use Phoenix.LiveView\n  use Canary.Hooks\n\n  mount_canary :load_resource,\n    model: Post,\n    required: false\n\n  mount_canary :load_and_authorize_resource,\n    on: [:handle_params, :handle_event],\n    model: User,\n    only: [:show],\n    required: false\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>Page</div>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  ## test helpers\n\n  def handle_call({:run, func}, _, socket), do: func.(socket)\n\n  def run(lv, func) do\n    GenServer.call(lv.pid, {:run, func})\n  end\n\n  def fetch_lifecycle(lv) do\n    run(lv, fn socket ->\n      {:reply, Map.fetch(socket.private, :lifecycle), socket}\n    end)\n  end\nend\n"
  },
  {
    "path": "test/support/post_live.ex",
    "content": "defmodule Canary.HooksHelper.PostLive do\n  use Phoenix.LiveView\n  use Canary.Hooks\n\n  mount_canary :load_resource,\n    model: Post,\n    only: [:show]\n\n  mount_canary :load_resource,\n    model: Post,\n    only: [:edit, :update]\n\n  def render(assigns) do\n    ~H\"\"\"\n    <div>Post</div>\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  ## test helpers\n\n  def handle_call({:run, func}, _, socket), do: func.(socket)\n\n  def run(lv, func) do\n    GenServer.call(lv.pid, {:run, func})\n  end\n\n  def fetch_assigns(lv) do\n    run(lv, fn socket ->\n      {:reply, socket.assigns, socket}\n    end)\n  end\n\n  def fetch_socket(lv) do\n    run(lv, fn socket ->\n      {:reply, socket, socket}\n    end)\n  end\nend\n"
  },
  {
    "path": "test/support/router.ex",
    "content": "defmodule Canary.HooksHelper.Router do\n  use Phoenix.Router\n  import Plug.Conn\n  import Phoenix.LiveView.Router\n\n  pipeline :browser do\n    plug :fetch_session\n    plug :accepts, [\"html\"]\n    plug :fetch_live_flash\n  end\n\n  scope \"/\" do\n    pipe_through :browser\n\n    live \"/page\", Canary.HooksHelper.PageLive\n    live \"/post\", Canary.HooksHelper.PostLive\n    live \"/post/:id\", Canary.HooksHelper.PostLive, :show\n    live \"/post/:id/edit\", Canary.HooksHelper.PostLive, :edit\n    live \"/post/:id/update\", Canary.HooksHelper.PostLive, :update\n  end\nend\n"
  },
  {
    "path": "test/test_helper.exs",
    "content": "ExUnit.start()\n\n\ndefmodule User do\n  defstruct id: 1\nend\n\ndefmodule Post do\n  use Ecto.Schema\n\n  schema \"posts\" do\n    # :defaults not working so define own field with default value\n    belongs_to(:user, :integer, define_field: false)\n\n    field(:user_id, :integer, default: 1)\n    field(:slug, :string)\n  end\nend\n\ndefmodule Repo do\n  def get(User, 1), do: %User{}\n  def get(User, _id), do: nil\n\n  def get(Post, 1), do: %Post{id: 1}\n  def get(Post, 2), do: %Post{id: 2, user_id: 2}\n  def get(Post, _), do: nil\n\n  def all(_), do: [%Post{id: 1}, %Post{id: 2, user_id: 2}]\n\n  def preload(%Post{id: post_id, user_id: user_id}, :user) do\n    %Post{id: post_id, user_id: user_id, user: %User{id: user_id}}\n  end\n  #def preload(%Post{id: 2, user_id: 2}, :user), do: %Post{id: 2, user_id: 2, user: %User{id: 2}}\n\n  def preload([%Post{id: 1}, %Post{id: 2, user_id: 2}], :user),\n    do: [%Post{id: 1}, %Post{id: 2, user_id: 2, user: %User{id: 2}}]\n\n  def preload(resources, _), do: resources\n\n  def get_by(User, %{id: \"1\"}), do: %User{}\n  def get_by(User, _), do: nil\n\n  def get_by(Post, %{id: \"1\"}), do: %Post{id: 1}\n  def get_by(Post, %{id: \"2\"}), do: %Post{id: 2, user_id: 2}\n  def get_by(Post, %{id: _}), do: nil\n\n  def get_by(Post, %{slug: \"slug1\"}), do: %Post{id: 1, slug: \"slug1\"}\n  def get_by(Post, %{slug: \"slug2\"}), do: %Post{id: 2, slug: \"slug2\", user_id: 2}\n  def get_by(Post, %{slug: _}), do: nil\nend\n\ndefimpl Canada.Can, for: User do\n  def can?(%User{}, action, Myproject.PartialAccessController)\n      when action in [:index, :show],\n      do: true\n\n  def can?(%User{}, action, Myproject.PartialAccessController)\n      when action in [:new, :create, :update, :delete],\n      do: false\n\n  def can?(%User{}, :index, Myproject.SampleController), do: true\n\n  def can?(%User{id: _user_id}, action, Myproject.SampleController)\n      when action in [:index, :show, :new, :create, :update, :delete],\n      do: true\n\n  def can?(%User{id: user_id}, action, %Post{user_id: user_id})\n      when action in [:index, :show, :new, :create],\n      do: true\n\n  def can?(%User{}, :index, Post), do: true\n\n  def can?(%User{}, action, Post)\n      when action in [:new, :create, :other_action],\n      do: true\n\n  def can?(%User{id: user_id}, action, %Post{user: %User{id: user_id}})\n      when action in [:edit, :update],\n      do: true\n\n  def can?(%User{}, _, _), do: false\nend\n\ndefimpl Canada.Can, for: Atom do\n  def can?(nil, :create, Post), do: false\n  def can?(nil, :create, Myproject.SampleController), do: false\nend\n\ndefmodule Helpers do\n  def unauthorized_handler(conn) do\n    conn\n    |> Plug.Conn.resp(403, \"I'm sorry Dave. I'm afraid I can't do that.\")\n    |> Plug.Conn.send_resp()\n  end\n\n  def not_found_handler(conn) do\n    conn\n    |> Map.put(:not_found_handler_called, true)\n    |> Plug.Conn.resp(404, \"Resource not found.\")\n    |> Plug.Conn.send_resp()\n  end\n\n  def non_halting_unauthorized_handler(conn) do\n    conn\n  end\nend\n\ndefmodule ErrorHandler do\n  @behaviour Canary.ErrorHandler\n\n  def not_found_handler(%Plug.Conn{} = conn) do\n    Helpers.not_found_handler(conn)\n  end\n\n  def not_found_handler(%Phoenix.LiveView.Socket{} = socket) do\n    {:halt, Phoenix.LiveView.redirect(socket, to: \"/\")}\n  end\n\n  def unauthorized_handler(%Plug.Conn{} = conn) do\n    Helpers.unauthorized_handler(conn)\n  end\n\n  def unauthorized_handler(%Phoenix.LiveView.Socket{} = socket) do\n    {:halt, Phoenix.LiveView.redirect(socket, to: \"/\")}\n  end\nend\n"
  }
]