diff --git a/apps/bankingAPI/lib/bankingAPI/accounts/schemas/account.ex b/apps/bankingAPI/lib/bankingAPI/accounts/schemas/account.ex index 79b9627..c8aee10 100644 --- a/apps/bankingAPI/lib/bankingAPI/accounts/schemas/account.ex +++ b/apps/bankingAPI/lib/bankingAPI/accounts/schemas/account.ex @@ -1,19 +1,32 @@ defmodule BankingAPI.Accounts.Schemas.Account do @moduledoc """ - The entity of Account. + Account entity. - 1 user (id) - N accounts (FK user_id) + 1 account (FK user_id) -> 1 user (id) """ use Ecto.Schema + import Ecto.Changeset + alias BankingAPI.Users.Schemas.User + @derive {Jason.Encoder, except: [:__meta__]} + @required [:amount] + @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "accounts" do belongs_to(:user, User) + field(:account_number, :integer, read_after_writes: true) field(:amount, :integer) timestamps() end + + def changeset(model \\ %__MODULE__{}, params) do + model + |> cast(params, @required) + |> validate_required(@required) + |> validate_number(:amount, greater_than_or_equal_to: 0) + end end diff --git a/apps/bankingAPI/lib/bankingAPI/users/inputs/create_input.ex b/apps/bankingAPI/lib/bankingAPI/users/inputs/create_input.ex index 89924de..24cb7f3 100644 --- a/apps/bankingAPI/lib/bankingAPI/users/inputs/create_input.ex +++ b/apps/bankingAPI/lib/bankingAPI/users/inputs/create_input.ex @@ -1,6 +1,6 @@ defmodule BankingAPI.Users.Inputs.Create do @moduledoc """ - Input data for calling insert_new_user/1. + Input data for calling create/1. """ use Ecto.Schema diff --git a/apps/bankingAPI/lib/bankingAPI/users/schemas/user.ex b/apps/bankingAPI/lib/bankingAPI/users/schemas/user.ex index 23e4058..8e4c6c1 100644 --- a/apps/bankingAPI/lib/bankingAPI/users/schemas/user.ex +++ b/apps/bankingAPI/lib/bankingAPI/users/schemas/user.ex @@ -1,20 +1,22 @@ defmodule BankingAPI.Users.Schemas.User do @moduledoc """ - The user of an account + The User """ use Ecto.Schema import Ecto.Changeset - @required [:name, :email] + alias BankingAPI.Accounts.Schemas.Account @derive {Jason.Encoder, except: [:__meta__]} + @required [:name, :email] @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "users" do field(:name, :string) field(:email, :string) + has_one(:account, Account) timestamps() end @@ -22,6 +24,7 @@ defmodule BankingAPI.Users.Schemas.User do def changeset(model \\ %__MODULE__{}, params) do model |> cast(params, @required) + |> cast_assoc(:account, with: &Account.changeset/2) |> validate_required(@required) |> validate_length(:name, min: 3) |> validate_format(:email, ~r/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/) diff --git a/apps/bankingAPI/lib/bankingAPI/users/users.ex b/apps/bankingAPI/lib/bankingAPI/users/users.ex index 1d9f4f9..db97dc4 100644 --- a/apps/bankingAPI/lib/bankingAPI/users/users.ex +++ b/apps/bankingAPI/lib/bankingAPI/users/users.ex @@ -2,24 +2,24 @@ defmodule BankingAPI.Users do @moduledoc """ Domain public functions for users context """ + require Logger alias BankingAPI.Repo alias BankingAPI.Users.Inputs alias BankingAPI.Users.Schemas.User - require Logger - @doc """ Given a VALID changeset it attempts to insert a new user. It might fail due to email unique index and we transform that return into an error tuple. """ @spec create(Inputs.Create.t()) :: - {:ok, User.t()} | {:error, Ecto.Changeset.t() | :email_conflict} + {:ok, User.t()} | {:error, Ecto.Changeset.t()} def create(%Inputs.Create{} = input) do Logger.info("Inserting new user") - params = %{name: input.name, email: input.email} + new_account = %{amount: 1000} + params = %{name: input.name, email: input.email, account: new_account} params |> User.changeset() @@ -29,10 +29,6 @@ defmodule BankingAPI.Users do Logger.info("User successfully inserted", email: inspect(user.email)) {:ok, user} - {:error, %{errors: [email: {"has already been taken", _}]}} -> - Logger.info("Email already taken") - {:error, :email_conflict} - {:error, changeset} -> Logger.error("Error while inserting new user", error: inspect(changeset)) {:error, changeset} diff --git a/apps/bankingAPI/priv/repo/migrations/20210430202024_create_users.exs b/apps/bankingAPI/priv/repo/migrations/20210430202024_create_users.exs index 6a44924..1311968 100644 --- a/apps/bankingAPI/priv/repo/migrations/20210430202024_create_users.exs +++ b/apps/bankingAPI/priv/repo/migrations/20210430202024_create_users.exs @@ -4,8 +4,8 @@ defmodule BankingAPI.Repo.Migrations.CreateUser do def change do create table(:users, primary_key: false) do add(:id, :uuid, primary_key: true) - add(:name, :string) - add(:email, :string) + add(:name, :string, null: false) + add(:email, :string, null: false) timestamps() end diff --git a/apps/bankingAPI/priv/repo/migrations/20210430203629_create_accounts.exs b/apps/bankingAPI/priv/repo/migrations/20210430203629_create_accounts.exs index fe152e2..ff68981 100644 --- a/apps/bankingAPI/priv/repo/migrations/20210430203629_create_accounts.exs +++ b/apps/bankingAPI/priv/repo/migrations/20210430203629_create_accounts.exs @@ -4,12 +4,25 @@ defmodule BankingAPI.Repo.Migrations.CreateAccount do def change do create table(:accounts, primary_key: false) do add(:id, :uuid, primary_key: true) - add(:user_id, references(:users, type: :uuid)) - add(:amount, :integer) + add(:user_id, references(:users, type: :uuid), null: false) + add(:account_number, :serial, null: false) + add(:amount, :integer, null: false) timestamps() end + create( + constraint(:accounts, "account_number_must_be_between_10000_and_99999", + check: "account_number >= 10000 and account_number <= 99999" + ) + ) + + create(unique_index(:accounts, [:account_number])) + + execute "ALTER SEQUENCE accounts_account_number_seq START with 10000 RESTART" + + create(constraint(:accounts, "ammount_must_be_0_or_positive", check: "amount >= 0")) + create(index(:accounts, [:user_id])) end end diff --git a/apps/bankingAPI/test/users_test.exs b/apps/bankingAPI/test/users_test.exs index 9092b59..2e4f7fb 100644 --- a/apps/bankingAPI/test/users_test.exs +++ b/apps/bankingAPI/test/users_test.exs @@ -6,25 +6,129 @@ defmodule BankingAPI.UsersTest do alias BankingAPI.Users.Schemas.User describe "create/1" do + test "successfully create an user and account (with amount 1000) using valid input" do + email = "#{Ecto.UUID.generate()}@email.com" + name = "abc" + + create_input = %Inputs.Create{ + name: name, + email: email + } + + assert {:ok, + %User{ + id: user_id, + name: ^name, + email: ^email, + inserted_at: %NaiveDateTime{}, + updated_at: %NaiveDateTime{}, + account: %{amount: 1000} + } = expected_user} = Users.create(create_input) + + assert expected_user == Repo.get(User, user_id) |> Repo.preload(:account) + + assert [expected_user] == Repo.all(User) |> Repo.preload(:account) + end + + @tag capture_log: true + test "fail if try to create user with empty name" do + email = "#{Ecto.UUID.generate()}@email.com" + name = "" + + create_input = %Inputs.Create{ + name: name, + email: email + } + + assert {:error, + %{valid?: false, errors: [name: {"can't be blank", [validation: :required]}]}} = + Users.create(create_input) + + assert [] == Repo.all(User) + end + + @tag capture_log: true + test "fail if try to create user with empty email" do + email = "" + name = "abc" + + create_input = %Inputs.Create{ + name: name, + email: email + } + + assert {:error, + %{valid?: false, errors: [email: {"can't be blank", [validation: :required]}]}} = + Users.create(create_input) + + assert [] == Repo.all(User) + end + + @tag capture_log: true + test "fail if try to create user with name having two characters" do + email = "#{Ecto.UUID.generate()}@email.com" + name = "ab" + + create_input = %Inputs.Create{ + name: name, + email: email + } + + assert {:error, + %{ + valid?: false, + errors: [ + name: + {"should be at least %{count} character(s)", + [{:count, 3}, {:validation, :length}, {:kind, :min}, {:type, :string}]} + ] + }} = Users.create(create_input) + + assert [] == Repo.all(User) + end + @tag capture_log: true - test "fail if email is already taken" do - email = "taken@email.com" - Repo.insert!(%User{email: email}) + test "fail if try to create user with invalid email" do + email = "abs.email.com" + name = "abc" + + create_input = %Inputs.Create{ + name: name, + email: email + } - assert {:error, :email_conflict} == - Users.create(%Inputs.Create{name: "abc", email: email}) + assert {:error, + %{ + valid?: false, + errors: [email: {"has invalid format", [validation: :format]}] + }} = Users.create(create_input) + + assert [] == Repo.all(User) end - test "successfully create an user with valid input" do + @tag capture_log: true + test "fail if try to create 2 users with same email" do email = "#{Ecto.UUID.generate()}@email.com" + name = "abc" + + create_input = %Inputs.Create{ + name: name, + email: email + } + + assert {:ok, first_user} = Users.create(create_input) - assert {:ok, %User{name: "random name", email: ^email, inserted_at: %NaiveDateTime{}, updated_at: %NaiveDateTime{}} = user} = - Users.create(%Inputs.Create{ - name: "random name", - email: email - }) + assert {:error, + %{ + valid?: false, + errors: [ + email: + {"has already been taken", + [constraint: :unique, constraint_name: "users_email_index"]} + ] + }} = Users.create(create_input) - assert user == Repo.get_by(User, email: email) + assert [first_user] == Repo.all(User) |> Repo.preload(:account) end end end diff --git a/apps/bankingAPI_web/lib/bankingAPI_web/controllers/user_controller.ex b/apps/bankingAPI_web/lib/bankingAPI_web/controllers/user_controller.ex index 39bd05e..90d2a29 100644 --- a/apps/bankingAPI_web/lib/bankingAPI_web/controllers/user_controller.ex +++ b/apps/bankingAPI_web/lib/bankingAPI_web/controllers/user_controller.ex @@ -8,19 +8,67 @@ defmodule BankingAPIWeb.UserController do alias BankingAPI.Users.Inputs alias BankingAPIWeb.InputValidation + alias BankingAPIWeb.UserView def create(conn, params) do with {:ok, input} <- InputValidation.cast_and_apply(params, Inputs.Create), {:ok, user} <- Users.create(input) do - send_json(conn, 200, user) + # send_json(conn, 200, user) + conn + |> put_status(200) + |> put_view(UserView) + |> render("show.json", %{ + user: user + }) else - {:error, %Ecto.Changeset{}} -> - msg = %{type: "bad_input", description: "Invalid input"} - send_json(conn, 400, msg) - - {:error, :email_conflict} -> - msg = %{type: "conflict", description: "Email already taken"} + {:error, + %Ecto.Changeset{ + errors: [ + email: + {"has already been taken", + [constraint: :unique, constraint_name: "users_email_index"]} + ] + }} -> + msg = %{type: "unprocessable_entity", description: "Email already taken"} send_json(conn, 422, msg) + + {:error, + %Ecto.Changeset{ + errors: [ + name: + {"should be at least %{count} character(s)", + [count: 3, validation: :length, kind: :min, type: :string]} + ] + }} -> + msg = %{type: "length_required", description: "Name must have 3 digits or more"} + send_json(conn, 411, msg) + + {:error, + %Ecto.Changeset{ + errors: [ + email_confirmation: {"does not match confirmation", [validation: :confirmation]} + ] + }} -> + msg = %{type: "precondition_failed", description: "E-mail confirmations does not match"} + send_json(conn, 412, msg) + + {:error, %Ecto.Changeset{errors: [email: {"has invalid format", [validation: :format]}]}} -> + msg = %{type: "precondition_failed", description: "E-mail has invalid format"} + send_json(conn, 412, msg) + + {:error, + %Ecto.Changeset{ + errors: [ + name: {"can't be blank", [validation: :required]}, + email: {"can't be blank", [validation: :required]} + ] + }} -> + msg = %{type: "precondition_required", description: "Name or Email can not be blank"} + send_json(conn, 428, msg) + + {:error, %Ecto.Changeset{}} -> + msg = %{type: "internal_server_error", description: "Internal Server Error"} + send_json(conn, 500, msg) end end diff --git a/apps/bankingAPI_web/lib/bankingAPI_web/views/user_view.ex b/apps/bankingAPI_web/lib/bankingAPI_web/views/user_view.ex new file mode 100644 index 0000000..95e6634 --- /dev/null +++ b/apps/bankingAPI_web/lib/bankingAPI_web/views/user_view.ex @@ -0,0 +1,14 @@ +defmodule BankingAPIWeb.UserView do + @moduledoc """ + User view + """ + use BankingAPIWeb, :view + + def render("show.json", %{user: user}) do + %{ + name: user.name, + email: user.email, + account: %{account_number: user.account.account_number, amount: user.account.amount} + } + end +end diff --git a/apps/bankingAPI_web/test/bankingAPI_web/controllers/user_controller_test.exs b/apps/bankingAPI_web/test/bankingAPI_web/controllers/user_controller_test.exs index ea6f510..b63d540 100644 --- a/apps/bankingAPI_web/test/bankingAPI_web/controllers/user_controller_test.exs +++ b/apps/bankingAPI_web/test/bankingAPI_web/controllers/user_controller_test.exs @@ -6,13 +6,13 @@ defmodule BankingAPIWeb.UserControllerTest do describe "POST /api/users" do test "sucess with 200 and user created", ctx do - name = "Renan infinity PR" - email = "infinityPR@mail.com" + input_name = "Renan infinity PR" + input_email = "infinityPR@mail.com" input = %{ - "name" => name, - "email" => email, - "email_confirmation" => email + "name" => input_name, + "email" => input_email, + "email_confirmation" => input_email } response = @@ -20,73 +20,118 @@ defmodule BankingAPIWeb.UserControllerTest do |> post("/api/users", input) |> json_response(200) - assert [%{id: created_id, name: created_name, email: created_email}] = Repo.all(User) + assert response["name"] == input_name + assert response["email"] == input_email - assert created_id == response["id"] - assert created_name == name - assert created_email == email + assert [%{name: created_name, email: created_email, account: created_account}] = + Repo.all(User) |> Repo.preload(:account) + + assert response["name"] == created_name + assert response["email"] == created_email + assert response["account"]["account_number"] == created_account.account_number + assert response["account"]["amount"] == created_account.amount end - test "fail with 400 when email_confirmation does NOT match email", ctx do - input = %{"name" => "abc", "email" => "a@a.com", "email_confirmation" => "b@a.com"} + test "fail with 400 when name length is lower than 3", ctx do + input_name = "aa" + input_email = "aa@mail.com" - assert ctx.conn - |> post("/api/users", input) - |> json_response(400) == %{ - "description" => "Invalid input", - "type" => "bad_input" + input = %{ + "name" => input_name, + "email" => input_email, + "email_confirmation" => input_email + } + + reponse = + ctx.conn + |> post("/api/users", input) + |> json_response(411) + + assert reponse == %{ + "description" => "Name must have 3 digits or more", + "type" => "length_required" } assert [] == Repo.all(User) end - test "fail with 400 when name is too small", ctx do - input = %{"name" => "A", "email" => "a@a.com", "email_confirmation" => "a@a.com"} + test "fail with 412 when email_confirmation does NOT match email", ctx do + input_name = "aabbcc" + input_email = "aabbcc@mail.com" - assert ctx.conn - |> post("/api/users", input) - |> json_response(400) == %{ - "description" => "Invalid input", - "type" => "bad_input" + input = %{ + "name" => input_name, + "email" => input_email, + "email_confirmation" => "aabb@mail.com" + } + + response = + ctx.conn + |> post("/api/users", input) + |> json_response(412) + + assert response == %{ + "description" => "E-mail confirmations does not match", + "type" => "precondition_failed" } assert [] == Repo.all(User) end - test "fail with 400 when email format is invalid", ctx do - input = %{"name" => "Abc de D", "email" => "a@@a.com", "email_confirmation" => "a@a.com"} + test "fail with 412 when email format is invalid", ctx do + input_name = "aabbcc" + input_email = "aabbcc.mail.com" - assert ctx.conn - |> post("/api/users", input) - |> json_response(400) == %{ - "description" => "Invalid input", - "type" => "bad_input" + input = %{ + "name" => input_name, + "email" => input_email, + "email_confirmation" => input_email + } + + response = + ctx.conn + |> post("/api/users", input) + |> json_response(412) + + assert response == %{ + "description" => "E-mail has invalid format", + "type" => "precondition_failed" } assert [] == Repo.all(User) end - test "fail with 400 when email_confirmation format is invalid", ctx do - input = %{"name" => "Abc de D", "email" => "a@a.com", "email_confirmation" => "a@@a.com"} + test "fail with 412 when email_confirmation format is invalid", ctx do + input_name = "aabbcc" + input_email = "aabbcc@mail.com" - assert ctx.conn - |> post("/api/users", input) - |> json_response(400) == %{ - "description" => "Invalid input", - "type" => "bad_input" + input = %{ + "name" => input_name, + "email" => input_email, + "email_confirmation" => "aabbcc.mail.com" + } + + response = + ctx.conn + |> post("/api/users", input) + |> json_response(412) + + assert response == %{ + "description" => "E-mail confirmations does not match", + "type" => "precondition_failed" } assert [] == Repo.all(User) end - test "fail with 400 when required fields are missing", ctx do + test "fail with 428 when required fields are missing", ctx do input = %{} assert ctx.conn |> post("/api/users", input) - |> json_response(400) == %{ - "description" => "Invalid input", - "type" => "bad_input" + |> json_response(428) == %{ + "description" => "Name or Email can not be blank", + "type" => "precondition_required" } assert [] == Repo.all(User) @@ -94,23 +139,27 @@ defmodule BankingAPIWeb.UserControllerTest do @tag capture_log: true test "fail with 422 when email is already taken", ctx do - email = "#{Ecto.UUID.generate()}@email.com" + input_name = "aabbcc" + input_email = "aabbcc@mail.com" - Repo.insert!(%User{email: email}) + Repo.insert!(%User{name: input_name, email: input_email}) assert [initial_user] = Repo.all(User) input = %{ - "name" => "Um Dois TrĂªs de Oliveira Quatro", - "email" => email, - "email_confirmation" => email + "name" => input_name, + "email" => input_email, + "email_confirmation" => input_email } - assert ctx.conn - |> post("/api/users", input) - |> json_response(422) == %{ + response = + ctx.conn + |> post("/api/users", input) + |> json_response(422) + + assert response == %{ "description" => "Email already taken", - "type" => "conflict" + "type" => "unprocessable_entity" } assert [initial_user] == Repo.all(User)