diff --git a/README.md b/README.md
index 890b9fdf..dbf816ff 100644
--- a/README.md
+++ b/README.md
@@ -2567,12 +2567,12 @@ e.g:
-### 12 (Bonus!) Adding authentication
+### 12. Authentication (Optional)
Currently, the application allows *anyone*
-to access it and manage todo items.
-However, wouldn't it be awesome if
-we added *authentication* so each user
+to access it and manage todo `items`.
+Wouldn't it be great if
+we added *authentication* so each `person`
could check their own list?
We created a dedicated authentication guide:
@@ -2600,6 +2600,23 @@ e.g: https://phxtodo.fly.dev
xs
+### 13. REST API (Optional)
+
+Our `Phoenix` server currently
+only returns **`HTML` pages**
+that are **_server-side_ rendered**.
+This is already *awesome*
+but we can make use of `Phoenix`
+to extend its capabilities.
+
+What if our server also responded
+with `JSON`?
+You're in luck!
+We've created small guide
+for creating a `REST API`:
+[**`api.md`**](./api.md)
+
+
### Done!
diff --git a/api.md b/api.md
new file mode 100644
index 00000000..f2e71c4e
--- /dev/null
+++ b/api.md
@@ -0,0 +1,470 @@
+
+
+# Create a Basic `REST` API to Return `JSON` Data
+
+
+
+This guide demonstrates
+how to *extend* a `Phoenix` App
+so it also acts as an **API**
+returning `JSON` data.
+
+## Add `/api` scope and pipeline
+
+Open the
+`lib/router.ex` file.
+There is already a
+`pipeline :browser`
+that is used inside the `scope "/"`.
+
+```elixir
+ pipeline :browser do
+ plug :accepts, ["html"]
+ plug :fetch_session
+ plug :fetch_live_flash
+ plug :put_root_layout, {AppWeb.Layouts, :root}
+ plug :protect_from_forgery
+ plug :put_secure_browser_headers
+ end
+
+ scope "/", AppWeb do
+ pipe_through :browser
+
+ get "/", ItemController, :index
+ get "/items/toggle/:id", ItemController, :toggle
+ get "/items/clear", ItemController, :clear_completed
+ get "/items/:filter", ItemController, :index
+ resources "/items", ItemController
+ end
+```
+
+What this means is that
+every time a request is made
+to any of the aforementioned endpoints,
+the pipeline `:browser` acts as a *middleware*,
+going through each `plug` defined.
+Check the first plug.
+It says `plug :accepts, ["html"]`.
+It means it only accepts requests
+to return an HTML page.
+
+We want to do something similar to all of this,
+but to return a `JSON` object.
+For this,
+add the following piece of code.
+
+```elixir
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/api", AppWeb do
+ pipe_through :api
+
+ put "/items/:id/status", ApiController, :update_status
+ resources "items", ApiController, only: [:create, :update, :index]
+ end
+```
+
+This creates a pipeline `:api`
+that only accepts requests for `JSON` data.
+
+We have also added a scope.
+All routes starting with `/api`
+will be piped through with the
+`:api` pipeline
+and handled by the `ApiController`.
+Speaking of which,
+let's create it!
+
+## Create the API Controller
+
+Before creating our controller,
+let's define our requirements.
+We want the API to:
+
+- list `item`s
+- create an `item`
+- edit an `item`
+- update an `item`'s status
+
+We want each endpoint to respond appropriately
+if any data is invalid,
+the response body and status
+should inform the user what went wrong.
+We can leverage `changesets`
+to validate the `Item`
+and check if it's correctly formatted.
+
+Since we now know what to do,
+let's create our tests.
+
+### Adding Tests
+
+Before writing tests,
+we need to change the
+`test/support/fixtures/todo_fixtures.ex` file.
+This file contains a function
+that is used to create an `Item` for testing.
+
+Currently, the returned default `Item` is invalid.
+This is because it returns a `status: 42`,
+which according to our requirements,
+doesn't make sense.
+The `status` field can only be `0`, `1` or `2`.
+
+Change the `item_fixture/1` function
+so it looks like this:
+
+```elixir
+ def item_fixture(attrs \\ %{}) do
+ {:ok, item} =
+ attrs
+ |> Enum.into(%{
+ person_id: 42,
+ status: 0,
+ text: "some text"
+ })
+ |> App.Todo.create_item()
+ item
+ end
+```
+
+Now, let's create
+our controller tests.
+Create a file with the path:
+`test/app_web/controllers/api_controller_test.exs`
+and add the following code:
+
+```elixir
+defmodule AppWeb.ApiControllerTest do
+ use AppWeb.ConnCase
+ alias App.Todo
+
+ @create_attrs %{person_id: 42, status: 0, text: "some text"}
+ @update_attrs %{person_id: 43, status: 0, text: "some updated text"}
+ @update_status_attrs %{status: 1}
+ @invalid_attrs %{person_id: nil, status: nil, text: nil}
+ @invalid_status_attrs %{status: 6}
+
+ describe "list" do
+ test "all items", %{conn: conn} do
+ {:ok, item} = Todo.create_item(@create_attrs)
+ conn = get(conn, ~p"/api/items")
+
+ assert conn.status == 200
+ assert length(Jason.decode!(response(conn, 200))) == 1
+ end
+ end
+
+ describe "create" do
+ test "a valid item", %{conn: conn} do
+ conn = post(conn, ~p"/api/items", @create_attrs)
+
+ assert conn.status == 200
+ assert Map.get(Jason.decode!(response(conn, 200)), :text) == Map.get(@create_attrs, "text")
+
+ assert Map.get(Jason.decode!(response(conn, 200)), :status) ==
+ Map.get(@create_attrs, "status")
+
+ assert Map.get(Jason.decode!(response(conn, 200)), :person_id) ==
+ Map.get(@create_attrs, "person_id")
+ end
+
+ test "an invalid item", %{conn: conn} do
+ conn = post(conn, ~p"/api/items", @invalid_attrs)
+
+ assert conn.status == 400
+
+ error_text = response(conn, 400) |> Jason.decode!() |> Map.get("text")
+ assert error_text == ["can't be blank"]
+ end
+ end
+
+ describe "update" do
+ test "item with valid attributes", %{conn: conn} do
+ {:ok, item} = Todo.create_item(@create_attrs)
+
+ conn = put(conn, ~p"/api/items/#{item.id}", @update_attrs)
+ assert conn.status == 200
+ assert Map.get(Jason.decode!(response(conn, 200)), :text) == Map.get(@update_attrs, "text")
+ end
+
+ test "item with invalid attributes", %{conn: conn} do
+ {:ok, item} = Todo.create_item(@create_attrs)
+
+ conn = put(conn, ~p"/api/items/#{item.id}", @invalid_attrs)
+
+ assert conn.status == 400
+ error_text = response(conn, 400) |> Jason.decode!() |> Map.get("text")
+ assert error_text == ["can't be blank"]
+ end
+ end
+
+ describe "update item status" do
+ test "with valid attributes", %{conn: conn} do
+ {:ok, item} = Todo.create_item(@create_attrs)
+
+ conn = put(conn, ~p"/api/items/#{item.id}/status", @update_status_attrs)
+ assert conn.status == 200
+
+ assert Map.get(Jason.decode!(response(conn, 200)), :status) ==
+ Map.get(@update_status_attrs, "status")
+ end
+
+ test "with invalid attributes", %{conn: conn} do
+ {:ok, item} = Todo.create_item(@create_attrs)
+
+ conn = put(conn, ~p"/api/items/#{item.id}/status", @invalid_status_attrs)
+
+ assert conn.status == 400
+ error_text = response(conn, 400) |> Jason.decode!() |> Map.get("status")
+ assert error_text == ["must be less than or equal to 2"]
+ end
+ end
+end
+```
+
+Let's break down what we just wrote.
+We've created constants for each scenario we want:
+testing valid or invalid attributes
+for each endpoint:
+- `create`, referring to creating an `Item`.
+- `update`, referring to updating an `Item`'s text.
+- `update_status`, referring to updating an `Item`'s status.
+
+The `ApiController` will have these three functions.
+Let's look at the `describe "create"` suite.
+The first test checks if a **valid `item`** is created.
+If it is created,
+the item should be returned to the user in `JSON` format.
+The second test checks if an **_invalid_ `item`**
+was attempted to be created.
+It should return a response with
+[HTTP Status Code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
+`400` (meaning the client made a bad request)
+and an error text accompanying the body.
+These tests are replicated
+on the other two functions.
+
+If you run the tests `mix test`,
+they will fail,
+because these functions aren't defined.
+
+### Create API Controller
+
+Let's create our API controller.
+Inside `lib/app_web/controllers`,
+create a file called
+`api_controller.ex`.
+
+Use the following code.
+
+```elixir
+defmodule AppWeb.ApiController do
+ use AppWeb, :controller
+ alias App.Todo
+ import Ecto.Changeset
+
+ def index(conn, params) do
+ items = Todo.list_items()
+ json(conn, items)
+ end
+
+ def create(conn, params) do
+ case Todo.create_item(params) do
+ # Successfully creates item
+ {:ok, item} ->
+ json(conn, item)
+
+ # Error creating item
+ {:error, %Ecto.Changeset{} = changeset} ->
+ errors = make_errors_readable(changeset)
+
+ json(
+ conn |> put_status(400),
+ errors
+ )
+ end
+ end
+
+ def update(conn, params) do
+ id = Map.get(params, "id")
+ text = Map.get(params, "text", "")
+
+ item = Todo.get_item!(id)
+
+ case Todo.update_item(item, %{text: text}) do
+ # Successfully updates item
+ {:ok, item} ->
+ json(conn, item)
+
+ # Error creating item
+ {:error, %Ecto.Changeset{} = changeset} ->
+ errors = make_errors_readable(changeset)
+
+ json(
+ conn |> put_status(400),
+ errors
+ )
+ end
+ end
+
+ def update_status(conn, params) do
+ id = Map.get(params, "id")
+ status = Map.get(params, "status", "")
+
+ item = Todo.get_item!(id)
+
+ case Todo.update_item(item, %{status: status}) do
+ # Successfully updates item
+ {:ok, item} ->
+ json(conn, item)
+
+ # Error creating item
+ {:error, %Ecto.Changeset{} = changeset} ->
+ errors = make_errors_readable(changeset)
+
+ json(
+ conn |> put_status(400),
+ errors
+ )
+ end
+ end
+
+ defp make_errors_readable(changeset) do
+ traverse_errors(changeset, fn {msg, opts} ->
+ Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
+ opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
+ end)
+ end)
+ end
+end
+```
+
+We have created three functions,
+each coresponding to the endpoints
+`[:create, :update, :index]`
+we defined earlier in `router.ex`.
+They all follow the same flow:
+try to do an action;
+if it fails, return an error to the user.
+
+Let's review the `update/2` function.
+The `params` parameter gives us information
+about the body of the request
+and the URL parameter.
+Since the user accesses `/api/items/:id`,
+an `id` field is present in the `params` map.
+Similarly, the user sends a body
+with the new text.
+
+```json
+{
+ "text": "new text"
+}
+```
+
+which can be accessed as `params.text`.
+
+Next, we use the passed `id`
+to check and fetch the item from the database.
+After this, we pass the fetched item
+with the new `text` to update
+(calling `Todo.update_item`).
+
+Depending on the success of the operation,
+different results are returned to the user.
+If it succeeds,
+the updated item is returned to the user,
+alongside an HTTP status code of `200`.
+On the other hand, if there's an error,
+an error is returned to the user,
+alongside an HTTP status code of `400`.
+
+The error is fetched from the changeset
+(that validates the passed attributes)
+and made readable by the
+`make_errors_readable/1` function.
+You don't need to know about the details,
+it just fetches the errors from the `changeset` struct
+and converts it to a map that can be
+serializable to JSON format.
+
+If we use
+[Postman](https://www.postman.com/)
+to make an API call,
+you will see the API in action.
+
+> Postman is a tool
+that makes it easy to test API requests.
+
+
+
+## Adding validations to `Item` changeset
+
+There's one last thing we need to change.
+We want to add validations to the `Item` changeset
+so we can make sure the `Item` is valid
+before adding it to the database.
+We also want the user to receive useful information
+if he unwillingly passed invalid attributes.
+
+Open `lib/app/todo/item.ex`
+and change `changeset/2`
+to the following.
+
+```elixir
+ def changeset(item, attrs) do
+ item
+ |> cast(attrs, [:text, :person_id, :status])
+ |> validate_required([:text])
+ |> validate_number(:status, greater_than_or_equal_to: 0, less_than_or_equal_to: 2)
+ |> validate_length(:text, min: 0)
+ end
+```
+
+We are now verifying
+if the `status` number is between `0` and `1`
+and checking the length of the `text` to be updated.
+
+This is great.
+*However*, a `changeset` struct
+is not serializable to `JSON`.
+We need to tell the `JSON` serializer
+which fields we want to retain in the `Item` schema.
+
+For this,
+we need to add
+the following annotation
+on top of the schema definition
+inside `lib/app/todo/item.ex`.
+
+```elixir
+ @derive {Jason.Encoder, only: [:id, :person_id, :status, :text]}
+ schema "items" do
+ field :person_id, :integer, default: 0
+ field :status, :integer, default: 0
+ field :text, :string
+ timestamps()
+ end
+```
+
+We are telling the `Jason` library
+that when encoding or decoding `Item` structs,
+we are only interested in the
+`id`, `person_id`, `status` and `text` fields
+(instead of other fields like
+`updated_at`, `inserted_at` or `__meta__`).
+
+## Congratulations!
+
+Congratulations,
+you just added REST API capabilities
+to your `Phoenix` server!
+It now serves a Todo application
+**and** also serves an API for people to use it.
+
+## Try It!
+
+> update this section once the API is deployed to Fly.io ...
diff --git a/config/dev.exs b/config/dev.exs
index fabc4d9f..6c5b9b27 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -75,6 +75,3 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
-
-# Disable swoosh api client as it is only required for production adapters.
-config :swoosh, :api_client, false
diff --git a/config/prod.exs b/config/prod.exs
index 4eb9e8ee..608ef137 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -11,9 +11,6 @@ import Config
# before starting your production server.
config :app, AppWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
-# Configures Swoosh API Client
-config :swoosh, :api_client, App.Finch
-
# Do not print debug messages in production
config :logger, level: :info
diff --git a/config/test.exs b/config/test.exs
index 2da5d30e..093d2d02 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -20,12 +20,6 @@ config :app, AppWeb.Endpoint,
secret_key_base: "2SvfXDONqim1uZkFDFIDe9r0Aw9vvI4uJ5M8JRP5guuW6vs/RtSsu0rw58cWpacP",
server: false
-# In test we don't send emails.
-config :app, App.Mailer, adapter: Swoosh.Adapters.Test
-
-# Disable swoosh api client as it is only required for production adapters.
-config :swoosh, :api_client, false
-
# Print only warnings and errors during test
config :logger, level: :warning
diff --git a/lib/app/todo/item.ex b/lib/app/todo/item.ex
index af917734..b18d3a96 100644
--- a/lib/app/todo/item.ex
+++ b/lib/app/todo/item.ex
@@ -2,6 +2,7 @@ defmodule App.Todo.Item do
use Ecto.Schema
import Ecto.Changeset
+ @derive {Jason.Encoder, only: [:id, :person_id, :status, :text]}
schema "items" do
field :person_id, :integer, default: 0
field :status, :integer, default: 0
@@ -15,5 +16,7 @@ defmodule App.Todo.Item do
item
|> cast(attrs, [:text, :person_id, :status])
|> validate_required([:text])
+ |> validate_number(:status, greater_than_or_equal_to: 0, less_than_or_equal_to: 2)
+ |> validate_length(:text, min: 0)
end
end
diff --git a/lib/app_web/controllers/api_controller.ex b/lib/app_web/controllers/api_controller.ex
new file mode 100644
index 00000000..bc6908c5
--- /dev/null
+++ b/lib/app_web/controllers/api_controller.ex
@@ -0,0 +1,79 @@
+defmodule AppWeb.ApiController do
+ use AppWeb, :controller
+ alias App.Todo
+ import Ecto.Changeset
+
+ def index(conn, _params) do
+ items = Todo.list_items()
+ json(conn, items)
+ end
+
+ def create(conn, params) do
+ case Todo.create_item(params) do
+ # Successfully creates item
+ {:ok, item} ->
+ json(conn, item)
+
+ # Error creating item
+ {:error, %Ecto.Changeset{} = changeset} ->
+ errors = make_errors_readable(changeset)
+
+ json(
+ conn |> put_status(400),
+ errors
+ )
+ end
+ end
+
+ def update(conn, params) do
+ id = Map.get(params, "id")
+ text = Map.get(params, "text", "")
+
+ item = Todo.get_item!(id)
+
+ case Todo.update_item(item, %{text: text}) do
+ # Successfully updates item
+ {:ok, item} ->
+ json(conn, item)
+
+ # Error creating item
+ {:error, %Ecto.Changeset{} = changeset} ->
+ errors = make_errors_readable(changeset)
+
+ json(
+ conn |> put_status(400),
+ errors
+ )
+ end
+ end
+
+ def update_status(conn, params) do
+ id = Map.get(params, "id")
+ status = Map.get(params, "status", "")
+
+ item = Todo.get_item!(id)
+
+ case Todo.update_item(item, %{status: status}) do
+ # Successfully updates item
+ {:ok, item} ->
+ json(conn, item)
+
+ # Error creating item
+ {:error, %Ecto.Changeset{} = changeset} ->
+ errors = make_errors_readable(changeset)
+
+ json(
+ conn |> put_status(400),
+ errors
+ )
+ end
+ end
+
+ defp make_errors_readable(changeset) do
+ traverse_errors(changeset, fn {msg, opts} ->
+ Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
+ opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
+ end)
+ end)
+ end
+end
diff --git a/lib/app_web/router.ex b/lib/app_web/router.ex
index eaa3c0ba..c2d7bbbf 100644
--- a/lib/app_web/router.ex
+++ b/lib/app_web/router.ex
@@ -10,10 +10,6 @@ defmodule AppWeb.Router do
plug :put_secure_browser_headers
end
- # pipeline :api do
- # plug :accepts, ["json"]
- # end
-
pipeline :authOptional, do: plug(AuthPlugOptional)
scope "/", AppWeb do
@@ -28,8 +24,14 @@ defmodule AppWeb.Router do
resources "/items", ItemController, except: [:show]
end
- # Other scopes may use custom stacks.
- # scope "/api", AppWeb do
- # pipe_through :api
- # end
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/api", AppWeb do
+ pipe_through :api
+
+ put "/items/:id/status", ApiController, :update_status
+ resources "/items", ApiController, only: [:create, :update, :index]
+ end
end
diff --git a/mix.exs b/mix.exs
index a1e021e2..c9333a11 100644
--- a/mix.exs
+++ b/mix.exs
@@ -52,13 +52,13 @@ defmodule App.MixProject do
{:phoenix_live_dashboard, "~> 0.7.2"},
{:esbuild, "~> 0.5", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},
- {:swoosh, "~> 1.3"},
{:finch, "~> 0.13"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},
{:gettext, "~> 0.20"},
{:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"},
+
# github.com/dwyl/auth_plug
{:auth_plug, "~> 1.5"},
# github.com/dwyl/useful
diff --git a/mix.lock b/mix.lock
index fef74fbc..01a13ebd 100644
--- a/mix.lock
+++ b/mix.lock
@@ -9,7 +9,7 @@
"db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"},
- "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
+ "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
"envar": {:hex, :envar, "1.1.0", "105bcac5a03800a1eb21e2c7e229edc687359b0cc184150ec1380db5928c115c", [:mix], [], "hexpm", "97028ab4a040a5c19e613fdf46a41cf51c6e848d99077e525b338e21d2993320"},
"esbuild": {:hex, :esbuild, "0.6.0", "9ba6ead054abd43cb3d7b14946a0cdd1493698ccd8e054e0e5d6286d7f0f509c", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "30f9a05d4a5bab0d3e37398f312f80864e1ee1a081ca09149d06d474318fd040"},
"excoveralls": {:hex, :excoveralls, "0.15.1", "83c8cf7973dd9d1d853dce37a2fb98aaf29b564bf7d01866e409abf59dac2c0e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f8416bd90c0082d56a2178cf46c837595a06575f70a5624f164a1ffe37de07e7"},
@@ -21,10 +21,10 @@
"heroicons": {:hex, :heroicons, "0.5.2", "a7ae72460ecc4b74a4ba9e72f0b5ac3c6897ad08968258597da11c2b0b210683", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "7ef96f455c1c136c335f1da0f1d7b12c34002c80a224ad96fc0ebf841a6ffef5"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
- "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
+ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"joken": {:hex, :joken, "2.5.0", "09be497d804b8115eb6f07615cef2e60c2a1008fb89dc0aef0d4c4b4609b99aa", [:mix], [{:jose, "~> 1.11.2", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "22b25c89617c5ed8ca7b31026340a25ea0f9ca7160f9706b79be9ed81fdf74e7"},
- "jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"},
+ "jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
@@ -46,7 +46,6 @@
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
- "swoosh": {:hex, :swoosh, "1.9.1", "0a5d7bf9954eb41d7e55525bc0940379982b090abbaef67cd8e1fd2ed7f8ca1a", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76dffff3ffcab80f249d5937a592eaef7cc49ac6f4cdd27e622868326ed6371e"},
"tailwind": {:hex, :tailwind, "0.1.9", "25ba09d42f7bfabe170eb67683a76d6ec2061952dc9bd263a52a99ba3d24bd4d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "9213f87709c458aaec313bb5f2df2b4d2cedc2b630e4ae821bf3c54c47a56d0b"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
diff --git a/test/app/todo_test.exs b/test/app/todo_test.exs
index 82e93de9..2eb5bf58 100644
--- a/test/app/todo_test.exs
+++ b/test/app/todo_test.exs
@@ -9,6 +9,7 @@ defmodule App.TodoTest do
import App.TodoFixtures
@invalid_attrs %{person_id: nil, status: nil, text: nil}
+ @valid_attrs %{person_id: 42, status: 0, text: "some text"}
test "list_items/0 returns all items" do
item = item_fixture()
@@ -21,10 +22,8 @@ defmodule App.TodoTest do
end
test "create_item/1 with valid data creates a item" do
- valid_attrs = %{person_id: 0, status: 0, text: "some text"}
-
- assert {:ok, %Item{} = item} = Todo.create_item(valid_attrs)
- assert item.person_id == 0
+ assert {:ok, %Item{} = item} = Todo.create_item(@valid_attrs)
+ assert item.person_id == 42
assert item.status == 0
assert item.text == "some text"
end
@@ -35,11 +34,11 @@ defmodule App.TodoTest do
test "update_item/2 with valid data updates the item" do
item = item_fixture()
- update_attrs = %{person_id: 1, status: 1, text: "some updated text"}
+ update_attrs = %{person_id: 43, status: 2, text: "some updated text"}
assert {:ok, %Item{} = item} = Todo.update_item(item, update_attrs)
- assert item.person_id == 1
- assert item.status == 1
+ assert item.person_id == 43
+ assert item.status == 2
assert item.text == "some updated text"
end
diff --git a/test/app_web/controllers/api_controller_test.exs b/test/app_web/controllers/api_controller_test.exs
new file mode 100644
index 00000000..278a17cb
--- /dev/null
+++ b/test/app_web/controllers/api_controller_test.exs
@@ -0,0 +1,87 @@
+defmodule AppWeb.ApiControllerTest do
+ use AppWeb.ConnCase
+ alias App.Todo
+
+ @create_attrs %{person_id: 42, status: 0, text: "some text"}
+ @update_attrs %{person_id: 43, status: 0, text: "some updated text"}
+ @update_status_attrs %{status: 1}
+ @invalid_attrs %{person_id: nil, status: nil, text: nil}
+ @invalid_status_attrs %{status: 6}
+
+ describe "list" do
+ test "all items", %{conn: conn} do
+ {:ok, _item} = Todo.create_item(@create_attrs)
+
+ conn = get(conn, ~p"/api/items")
+
+ assert conn.status == 200
+ assert length(Jason.decode!(response(conn, 200))) == 0
+ end
+ end
+
+ describe "create" do
+ test "a valid item", %{conn: conn} do
+ conn = post(conn, ~p"/api/items", @create_attrs)
+
+ assert conn.status == 200
+ assert Map.get(Jason.decode!(response(conn, 200)), :text) == Map.get(@create_attrs, "text")
+
+ assert Map.get(Jason.decode!(response(conn, 200)), :status) ==
+ Map.get(@create_attrs, "status")
+
+ assert Map.get(Jason.decode!(response(conn, 200)), :person_id) ==
+ Map.get(@create_attrs, "person_id")
+ end
+
+ test "an invalid item", %{conn: conn} do
+ conn = post(conn, ~p"/api/items", @invalid_attrs)
+
+ assert conn.status == 400
+
+ error_text = response(conn, 400) |> Jason.decode!() |> Map.get("text")
+ assert error_text == ["can't be blank"]
+ end
+ end
+
+ describe "update" do
+ test "item with valid attributes", %{conn: conn} do
+ {:ok, item} = Todo.create_item(@create_attrs)
+
+ conn = put(conn, ~p"/api/items/#{item.id}", @update_attrs)
+ assert conn.status == 200
+ assert Map.get(Jason.decode!(response(conn, 200)), :text) == Map.get(@update_attrs, "text")
+ end
+
+ test "item with invalid attributes", %{conn: conn} do
+ {:ok, item} = Todo.create_item(@create_attrs)
+
+ conn = put(conn, ~p"/api/items/#{item.id}", @invalid_attrs)
+
+ assert conn.status == 400
+ error_text = response(conn, 400) |> Jason.decode!() |> Map.get("text")
+ assert error_text == ["can't be blank"]
+ end
+ end
+
+ describe "update item status" do
+ test "with valid attributes", %{conn: conn} do
+ {:ok, item} = Todo.create_item(@create_attrs)
+
+ conn = put(conn, ~p"/api/items/#{item.id}/status", @update_status_attrs)
+ assert conn.status == 200
+
+ assert Map.get(Jason.decode!(response(conn, 200)), :status) ==
+ Map.get(@update_status_attrs, "status")
+ end
+
+ test "with invalid attributes", %{conn: conn} do
+ {:ok, item} = Todo.create_item(@create_attrs)
+
+ conn = put(conn, ~p"/api/items/#{item.id}/status", @invalid_status_attrs)
+
+ assert conn.status == 400
+ error_text = response(conn, 400) |> Jason.decode!() |> Map.get("status")
+ assert error_text == ["must be less than or equal to 2"]
+ end
+ end
+end