diff --git a/.gitignore b/.gitignore index 403bbea2..cae306cf 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,12 @@ erl_crash.dump xref_graph.dot /graphs +# Visual Code +.elixir_ls/ + +# Elixir +.iex.exs + **GPATH **GRTAGS **GTAGS diff --git a/lib/account/websocket/channel/account.ex b/lib/account/websocket/channel/account.ex index 088dfd78..800721dc 100644 --- a/lib/account/websocket/channel/account.ex +++ b/lib/account/websocket/channel/account.ex @@ -14,6 +14,8 @@ channel Helix.Account.Websocket.Channel.Account do alias Helix.Network.Websocket.Requests.Bounce.Remove, as: BounceRemoveRequest alias Helix.Software.Websocket.Requests.Virus.Collect, as: VirusCollectRequest alias Helix.Story.Websocket.Requests.Email.Reply, as: EmailReplyRequest + alias Helix.Universe.Bank.Websocket.Requests.CreateAccount, + as: BankAccountCreateRequest @doc """ Joins the Account channel. @@ -27,7 +29,7 @@ channel Helix.Account.Websocket.Channel.Account do authenticated on the socket. + base errors """ - join _, AccountJoin + join "account:" <> _, AccountJoin @doc """ Forces a bootstrap to happen. It is the exact same operation ran during join. @@ -233,6 +235,24 @@ channel Helix.Account.Websocket.Channel.Account do """ topic "virus.collect", VirusCollectRequest + @doc """ + Creates a BankAccount + + Params: + *atm_id: ATM.id related to bank that player wants to create account in + + Returns: :ok + + Errors: + + Henforcer: + - atm_not_a_bank: Given `atm_id` is not a bank + + Input: + + base errors + """ + topic "bank.createacc", BankAccountCreateRequest + @doc """ Intercepts and handles outgoing events. """ diff --git a/lib/core/validator/validator.ex b/lib/core/validator/validator.ex index 9a788c5a..f59f2918 100644 --- a/lib/core/validator/validator.ex +++ b/lib/core/validator/validator.ex @@ -5,9 +5,12 @@ defmodule Helix.Core.Validator do | :hostname | :bounce_name | :reply_id + | :token @regex_hostname ~r/^[a-zA-Z0-9-_.@#]{1,20}$/ + @uuid ~r/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + @spec validate_input(input :: String.t, input_type, opts :: term) :: {:ok, validated_input :: String.t} | :error @@ -15,7 +18,7 @@ defmodule Helix.Core.Validator do This is a generic function meant to validate external input that does not conform to a specific shape or format (like internal IDs or IP addresses). - The `element` argument identifies what the input is supposed to represent, and + The `type` argument identifies what the input is supposed to represent, and we leverage this information to customize the validation for different kinds of input. """ @@ -24,6 +27,9 @@ defmodule Helix.Core.Validator do def validate_input(input, :password, _), do: validate_password(input) + def validate_input(input, :money, _), + do: validate_money(input) + def validate_input(input, :hostname, _), do: validate_hostname(input) @@ -33,6 +39,9 @@ defmodule Helix.Core.Validator do def validate_input(input, :reply_id, _), do: validate_reply_id(input) + def validate_input(input, :token, _), + do: validate_token(input) + defp validate_hostname(v) when not is_binary(v), do: :error defp validate_hostname(v) do @@ -43,6 +52,16 @@ defmodule Helix.Core.Validator do end end + defp validate_token(v) when not is_binary(v), + do: :error + defp validate_token(v) do + if Regex.match?(@uuid, v) do + {:ok, v} + else + :error + end + end + defp validate_password(input), do: validate_hostname(input) # TODO @@ -51,4 +70,8 @@ defmodule Helix.Core.Validator do defp validate_reply_id(v), do: validate_hostname(v) # TODO + + defp validate_money(v), + do: validate_hostname(v) + end diff --git a/lib/event/dispatcher.ex b/lib/event/dispatcher.ex index b9dc7a2d..0d24198f 100644 --- a/lib/event/dispatcher.ex +++ b/lib/event/dispatcher.ex @@ -272,16 +272,21 @@ defmodule Helix.Event.Dispatcher do # All event BankEvent.Bank.Account.Login event BankEvent.Bank.Account.Updated + event BankEvent.Bank.Account.Removed event BankEvent.Bank.Account.Password.Revealed + event BankEvent.Bank.Account.Password.Changed event BankEvent.Bank.Account.Token.Acquired event BankEvent.Bank.Transfer.Processed event BankEvent.Bank.Transfer.Aborted + event BankEvent.AccountCreate.Processed event BankEvent.RevealPassword.Processed + event BankEvent.ChangePassword.Processed # Custom handlers event BankEvent.Bank.Transfer.Processed, BankHandler.Bank.Transfer, :transfer_processed + event BankEvent.Bank.Transfer.Processed, NetworkHandler.Connection, :bank_transfer_processed @@ -289,6 +294,7 @@ defmodule Helix.Event.Dispatcher do event BankEvent.Bank.Transfer.Aborted, BankHandler.Bank.Transfer, :transfer_aborted + event BankEvent.Bank.Transfer.Aborted, SoftwareHandler.Cracker, :bank_transfer_aborted @@ -301,11 +307,28 @@ defmodule Helix.Event.Dispatcher do BankHandler.Bank.Account, :password_reveal_processed + event BankEvent.ChangePassword.Processed, + BankHandler.Bank.Account, + :password_change_processed + + event BankEvent.AccountCreate.Processed, + BankHandler.Bank.Account, + :account_create_processed + + event BankEvent.AccountClose.Processed, + BankHandler.Bank.Account, + :account_close_processed + event BankEvent.Bank.Account.Password.Revealed, EntityHandler.Database, :bank_password_revealed + event BankEvent.Bank.Account.Password.Changed, + BankHandler.Bank.Account, + :password_changed + event BankEvent.Bank.Account.Login, EntityHandler.Database, :bank_account_login + end diff --git a/lib/event/notificable/notificable.ex b/lib/event/notificable/notificable.ex index aa3424b4..4254cbd4 100644 --- a/lib/event/notificable/notificable.ex +++ b/lib/event/notificable/notificable.ex @@ -63,7 +63,7 @@ defprotocol Helix.Event.Notificable do action, also receive special treatment. Hence: - third AT attack_target - Third party on victim's server - - third AT attack_source - Third party on attacker's server. + - third AT attack_source - Third party on attacker's server. - third AT random_server - Third party on unrelated (random) server. Last but not least, in some cases we do not have an attacker/victim @@ -98,11 +98,14 @@ defprotocol Helix.Event.Notificable do alias Helix.Account.Model.Account alias Helix.Server.Model.Server + alias Helix.Universe.Bank.Model.ATM + alias Helix.Universe.Bank.Model.BankAccount @type whom_to_notify :: %{ optional(:server) => [Server.id], - optional(:account) => [Account.id] + optional(:account) => [Account.id], + optional(:bank_acc) => [term] } @spec generate_payload(event :: struct, Socket.t) :: diff --git a/lib/event/notification_handler.ex b/lib/event/notification_handler.ex index 38cf2b0b..119ff9e3 100644 --- a/lib/event/notification_handler.ex +++ b/lib/event/notification_handler.ex @@ -8,8 +8,11 @@ defmodule Helix.Event.NotificationHandler do alias Helix.Entity.Model.Entity alias Helix.Server.Model.Server alias Helix.Server.State.Websocket.Channel, as: ServerWebsocketChannelState + alias Helix.Universe.Bank.Model.ATM + alias Helix.Universe.Bank.Model.BankAccount @type channel_account_id :: Account.id | Entity.id + @type channel_bank_id :: {ATM.id, BankAccount.account} @doc """ Handler responsible for guiding the event through the Notificable flow. It @@ -34,6 +37,18 @@ defmodule Helix.Event.NotificationHandler do @spec channel_mapper(Notificable.whom_to_notify) :: channels :: [String.t] defp channel_mapper(whom_to_notify, acc \\ []) + defp channel_mapper(notify = %{bank_acc: bank_accs}, acc) do + acc = + bank_accs + |> Utils.ensure_list() + |> Enum.uniq() + |> get_bank_channels() + |> List.flatten() + |> Kernel.++(acc) + + channel_mapper(Map.delete(notify, :bank_acc), acc) + end + defp channel_mapper(notify = %{server: servers}, acc) do acc = servers @@ -94,6 +109,13 @@ defmodule Helix.Event.NotificationHandler do defp get_account_channels(account_id), do: ["account:" <> to_string(account_id)] + @spec get_bank_channels([channel_bank_id] | channel_bank_id) :: + channels :: [String.t] + defp get_bank_channels(bank_accs) when is_list(bank_accs), + do: Enum.map(bank_accs, &get_bank_channels/1) + defp get_bank_channels({atm_id, account_number}), + do: ["bank:" <> to_string(account_number) <> "@" <> to_string(atm_id)] + defp concat(a, b), do: a <> to_string(b) end diff --git a/lib/process/executable.ex b/lib/process/executable.ex index 6f0c274b..1d460997 100644 --- a/lib/process/executable.ex +++ b/lib/process/executable.ex @@ -318,8 +318,12 @@ defmodule Helix.Process.Executable do src_acc_number: BankAccount.account | nil } @doc false - defp get_source_bank_account(_, _, _, _), - do: %{src_atm_id: nil, src_acc_number: nil} + defp get_source_bank_account(_, _, _, meta) do + %{ + src_atm_id: Map.get(meta, :src_atm_id, nil), + src_acc_number: Map.get(meta, :src_acc_number, nil) + } + end @spec get_target_bank_account(Server.t, Server.t, params, meta) :: %{ @@ -327,9 +331,12 @@ defmodule Helix.Process.Executable do tgt_acc_number: BankAccount.account | nil } @doc false - defp get_target_bank_account(_, _, _, _), - do: %{tgt_atm_id: nil, tgt_acc_number: nil} - + defp get_target_bank_account(_, _, _, meta) do + %{ + tgt_atm_id: Map.get(meta, :tgt_atm_id, nil), + tgt_acc_number: Map.get(meta, :tgt_acc_number, nil) + } + end @spec get_target_process(Server.t, Server.t, params, meta) :: %{tgt_process_id: Process.t | nil} @doc false diff --git a/lib/process/model/process.ex b/lib/process/model/process.ex index a9938606..b64e1b00 100644 --- a/lib/process/model/process.ex +++ b/lib/process/model/process.ex @@ -77,6 +77,9 @@ defmodule Helix.Process.Model.Process do | :cracker_bruteforce | :cracker_overflow | :install_virus + | :bank_change_password + | :bank_reveal_password + | :bank_account_create @typedoc """ List of signals a process may receive during its lifetime. diff --git a/lib/server/websocket/channel/server.ex b/lib/server/websocket/channel/server.ex index 039c2d99..47e996e4 100644 --- a/lib/server/websocket/channel/server.ex +++ b/lib/server/websocket/channel/server.ex @@ -293,7 +293,7 @@ channel Helix.Server.Websocket.Channel.Server do Disables the PublicFTP server of the player. Params: none - + Returns: %{} Errors: diff --git a/lib/software/model/file.ex b/lib/software/model/file.ex index 16af7df7..176f0878 100644 --- a/lib/software/model/file.ex +++ b/lib/software/model/file.ex @@ -214,8 +214,8 @@ defmodule Helix.Software.Model.File do path_size = (byte_size(path) - 1) * 8 case path do - <> <> "/" -> - <> + <> <> "/" -> + <> path -> path end diff --git a/lib/universe/bank/action/bank.ex b/lib/universe/bank/action/bank.ex index 831b7a6d..11f0c06a 100644 --- a/lib/universe/bank/action/bank.ex +++ b/lib/universe/bank/action/bank.ex @@ -10,6 +10,7 @@ defmodule Helix.Universe.Bank.Action.Bank do alias Helix.Universe.Bank.Internal.BankAccount, as: BankAccountInternal alias Helix.Universe.Bank.Internal.BankToken, as: BankTokenInternal alias Helix.Universe.Bank.Internal.BankTransfer, as: BankTransferInternal + alias Helix.Universe.Bank.Henforcer.Bank, as: BankHenforcer alias Helix.Universe.Bank.Model.ATM alias Helix.Universe.Bank.Model.BankAccount alias Helix.Universe.Bank.Model.BankToken @@ -22,13 +23,21 @@ defmodule Helix.Universe.Bank.Action.Bank do as: BankAccountUpdatedEvent alias Helix.Universe.Bank.Event.Bank.Account.Password.Revealed, as: BankAccountPasswordRevealedEvent + alias Helix.Universe.Bank.Event.Bank.Account.Password.Changed, + as: BankAccountPasswordChangedEvent alias Helix.Universe.Bank.Event.Bank.Account.Token.Acquired, as: BankAccountTokenAcquiredEvent + alias Helix.Universe.Bank.Event.Bank.Account.Removed, + as: BankAccountRemovedEvent + alias Helix.Universe.Bank.Event.Bank.Transfer.Successful, + as: BankTransferSuccessfulEvent + alias Helix.Universe.Bank.Event.Bank.Transfer.Failed, + as: BankTransferFailedEvent @spec start_transfer(BankAccount.t, BankAccount.t, pos_integer, Account.idt) :: {:ok, BankTransfer.t} | {:error, {:funds, :insufficient}} - | {:error, {:account, :notfound}} + | {:error, {:bank_account, :not_found}} | {:error, Ecto.Changeset.t} @doc """ Starts a bank transfer. @@ -48,9 +57,8 @@ defmodule Helix.Universe.Bank.Action.Bank do as: :start @spec complete_transfer(BankTransfer.t) :: - :ok - | {:error, {:transfer, :notfound}} - | {:error, :internal} + {:ok, BankTransfer.t, [BankTransferSuccessfulEvent.t]} + | {:error, term, [BankTransferFailedEvent.t]} @doc """ Completes the transfer. @@ -63,9 +71,19 @@ defmodule Helix.Universe.Bank.Action.Bank do This function should not be called directly by Public. Instead, it must be triggered by the BankTransferCompletedEvent. """ - defdelegate complete_transfer(transfer), - to: BankTransferInternal, - as: :complete + def complete_transfer(transfer) do + case BankTransferInternal.complete(transfer) do + :ok -> + {:ok, transfer, [BankTransferSuccessfulEvent.new(transfer)]} + + {:error, reason} -> + { + :error, + reason, + [BankTransferFailedEvent.new(transfer, reason)] + } + end + end @spec abort_transfer(BankTransfer.t) :: :ok @@ -87,36 +105,48 @@ defmodule Helix.Universe.Bank.Action.Bank do to: BankTransferInternal, as: :abort - @spec open_account(Account.idt, ATM.id) :: - {:ok, BankAccount.t} - | {:error, Ecto.Changeset.t} + @spec open_account(Account.id, ATM.id) :: + {:ok, BankAccount.t, [BankAccountUpdatedEvent.t]} + | {:error, :internal} @doc """ Opens a bank account. """ - def open_account(owner, atm) do - bank = - atm + def open_account(owner, atm_id) do + bank_id = + atm_id |> EntityQuery.fetch_by_server() |> Map.get(:entity_id) |> NPCQuery.fetch() + |> Map.get(:npc_id) + + case BankAccountInternal.create(owner, atm_id, bank_id) do + {:ok, bank_acc} -> + {:ok, bank_acc, [BankAccountUpdatedEvent.new(bank_acc, :created)]} - %{owner_id: owner, atm_id: atm, bank_id: bank} - |> BankAccountInternal.create() + {:error, _} -> + {:error, :internal} + end end @spec close_account(BankAccount.t) :: :ok - | {:error, {:account, :notfound}} - | {:error, {:account, :notempty}} + | {:error, {:bank_account, :not_found}} + | {:error, {:bank_account, :not_empty}} @doc """ Closes a bank account. May fail if the account is invalid or not empty. In order to close an account, its balance must be empty. """ - defdelegate close_account(account), - to: BankAccountInternal, - as: :close + def close_account(account) do + case BankAccountInternal.close(account) do + :ok -> + {:ok, [BankAccountRemovedEvent.new(account)]} + + {:error, reason} -> + {:error, reason} + end + end @spec direct_deposit(BankAccount.t, BankAccount.amount) :: {:ok, BankAccount.t, [BankAccountUpdatedEvent.t]} @@ -185,9 +215,7 @@ defmodule Helix.Universe.Bank.Action.Bank do """ def reveal_password(account, token_id, revealed_by) do with \ - token = %{} <- BankQuery.fetch_token(token_id), - true <- account.account_number == token.account_number, - true <- account.atm_id == token.atm_id + {true, relay} <- BankHenforcer.token_valid?(account, token_id) do event = BankAccountPasswordRevealedEvent.new(account, revealed_by) @@ -198,6 +226,27 @@ defmodule Helix.Universe.Bank.Action.Bank do end end + @spec change_password(BankAccount.t) :: + {:ok, BankAccount.t, [BankAccountPasswordChangedEvent.t]} + | {:error, :internal} + def change_password(account) do + with {:ok, account} <- update_password(account) do + event = BankAccountPasswordChangedEvent.new(account) + + {:ok, account, [event]} + else + _ -> + {:error, :internal} + end + end + + @spec update_password(BankAccount.t) :: + {:ok, BankAccount.t} + | {:error, :internal} + defdelegate update_password(account), + to: BankAccountInternal, + as: :change_password + @spec login_password(BankAccount.t, String.t, Entity.idt) :: {:ok, BankAccount.t, [BankAccountLoginEvent.t]} | term @@ -226,12 +275,14 @@ defmodule Helix.Universe.Bank.Action.Bank do | term def login_token(account, token_id, login_by) do with \ - token = %{} <- BankQuery.fetch_token(token_id), - true <- token.account_number == account.account_number + {true, relay} <- BankHenforcer.token_valid?(account, token_id) do event = BankAccountLoginEvent.new(account, login_by, token_id) {:ok, account, [event]} + else + {false, reason, _} -> + {:error, reason} end end diff --git a/lib/universe/bank/action/flow/bank_account.ex b/lib/universe/bank/action/flow/bank_account.ex index 3c7e3548..01b3947c 100644 --- a/lib/universe/bank/action/flow/bank_account.ex +++ b/lib/universe/bank/action/flow/bank_account.ex @@ -3,6 +3,7 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccount do import HELF.Flow alias Helix.Event + alias Helix.Entity.Model.Entity alias Helix.Entity.Query.Entity, as: EntityQuery alias Helix.Network.Action.Flow.Tunnel, as: TunnelFlow alias Helix.Network.Query.Network, as: NetworkQuery @@ -13,8 +14,14 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccount do alias Helix.Universe.Bank.Model.BankToken alias Helix.Universe.Bank.Query.Bank, as: BankQuery + alias Helix.Universe.Bank.Process.Bank.Account.AccountCreate, + as: BankAccountCreateProcess + alias Helix.Universe.Bank.Process.Bank.Account.AccountClose, + as: BankAccountCloseProcess alias Helix.Universe.Bank.Process.Bank.Account.RevealPassword, as: BankAccountRevealPasswordProcess + alias Helix.Universe.Bank.Process.Bank.Account.ChangePassword, + as: BankAccountChangePasswordProcess @typep relay :: Event.relay @@ -29,7 +36,12 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccount do Emits: ProcessCreatedEvent """ - @spec reveal_password(BankAccount.t, BankToken.id, Server.t, Server.t, relay) :: + @spec reveal_password( + BankAccount.t, + BankToken.id, + Server.t, + Server.t, + relay) :: {:ok, Process.t} | BankAccountRevealPasswordProcess.executable_error def reveal_password(account, token_id, gateway, atm, relay) do @@ -46,6 +58,69 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccount do BankAccountRevealPasswordProcess.execute(gateway, atm, params, meta, relay) end + @spec open(Server.t, Account.id, Server.t, relay) :: + {:ok, Process.t} + | BankAccountChangeProcess.executable_error + @doc """ + Starts the `bank_account_create` process. + """ + def open(gateway, account_id, atm, relay) do + entity_id = Entity.ID.cast!(to_string(account_id)) + atm_id = atm.server_id + + meta = %{ + network_id: NetworkQuery.internet().network_id, + bounce: nil, + source_entity_id: entity_id + } + + BankAccountCreateProcess.execute(gateway, atm, %{atm_id: atm_id}, meta, relay) + end + + @spec close(Server.t, BankAccount.t, Server.t, relay) :: + {:ok, Process.t} + | BankAccountCloseProcess.executable_error + @doc """ + Starts the `bank_account_close` process. + """ + def close(gateway, bank_account, atm, relay) do + meta = %{ + network_id: NetworkQuery.internet().network_id, + bounce: nil + } + + params = %{ + atm_id: bank_account.atm_id, + account_number: bank_account.account_number + } + + BankAccountCloseProcess.execute(gateway, atm, params, meta, relay) + end + + @spec change_password(BankAccount.t, Server.t, Server.t, relay) :: + {:ok, Process.t} + | BankAccountChangePasswordProcess.executable_error + @doc """ + Starts the `bank_change_password` process. + + This game mechanic happens on the BankAccount control panel where the + BankAccount's owner can change the BankAccount's Password + + It is a process managed by TOP. in the sense that the password change does not + happen instantly. Completion is handled by `BankAccountEvent`. + + Emits: ProcessCreatedEvent + """ + def change_password(account, gateway, atm, relay) do + meta = %{ + network_id: NetworkQuery.internet().network_id, + src_atm_id: atm.server_id, + src_acc_number: account.account_number, + bounce: nil + } + + BankAccountChangePasswordProcess.execute(gateway, atm, %{}, meta, relay) + end @doc """ Logs into a bank account using a password. If the given password matches the current account password, the login is successful, in which case a BankLogin @@ -53,7 +128,7 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccount do Emits: BankAccountLoginEvent, (ConnectionStartedEvent) """ - def login_password(atm_id, account_number, gateway_id, bounce_id, password) do + def login_password(atm_id, account_number, gateway_id, bounce_id, password, relay) do acc = BankQuery.fetch_account(atm_id, account_number) entity = EntityQuery.fetch_by_server(gateway_id) @@ -72,11 +147,11 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccount do with \ true <- not is_nil(acc), {:ok, _, events} <- BankAction.login_password(acc, password, entity), - on_success(fn -> Event.emit(events) end), + on_success(fn -> Event.emit(events, from: relay) end), - {:ok, _, connection} <- start_connection.() + {:ok, tunnel, connection} <- start_connection.() do - {:ok, connection} + {:ok, tunnel, connection} end end end @@ -88,7 +163,7 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccount do Emits: BankAccountLoginEvent, (ConnectionStartedEvent) """ - def login_token(atm_id, account_number, gateway_id, bounce_id, token) do + def login_token(atm_id, account_number, gateway_id, bounce_id, token, relay) do acc = BankQuery.fetch_account(atm_id, account_number) entity = EntityQuery.fetch_by_server(gateway_id) @@ -107,11 +182,11 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccount do with \ true <- not is_nil(acc), {:ok, _, events} <- BankAction.login_token(acc, token, entity), - on_success(fn -> Event.emit(events) end), + on_success(fn -> Event.emit(events, from: relay) end), - {:ok, _, connection} <- start_connection.() + {:ok, tunnel, connection} <- start_connection.() do - {:ok, connection} + {:ok, tunnel, connection} end end end diff --git a/lib/universe/bank/action/flow/bank_transfer.ex b/lib/universe/bank/action/flow/bank_transfer.ex index 939ec8f8..dc03d008 100644 --- a/lib/universe/bank/action/flow/bank_transfer.ex +++ b/lib/universe/bank/action/flow/bank_transfer.ex @@ -63,7 +63,11 @@ defmodule Helix.Universe.Bank.Action.Flow.BankTransfer do meta = %{ network_id: tunnel.network_id, - bounce: tunnel.bounce_id + bounce: tunnel.bounce_id, + src_atm_id: from_account.atm_id, + src_acc_number: from_account.account_number, + tgt_atm_id: to_account.atm_id, + tgt_acc_number: to_account.account_number } BankTransferProcess.execute(gateway, target_atm, params, meta, relay) diff --git a/lib/universe/bank/event/account_close.ex b/lib/universe/bank/event/account_close.ex new file mode 100644 index 00000000..403c1566 --- /dev/null +++ b/lib/universe/bank/event/account_close.ex @@ -0,0 +1,37 @@ +defmodule Helix.Universe.Bank.Event.AccountClose do + + import Helix.Event + + event Processed do + + alias Helix.Process.Model.Process + alias Helix.Universe.Bank.Model.ATM + alias Helix.Universe.Bank.Model.BankAccount + alias Helix.Universe.Bank.Process.Bank.Account.AccountClose, + as: AccountCloseProcess + + @type t :: %__MODULE__{ + atm_id: ATM.id, + account_number: BankAccount.account + } + + event_struct [:atm_id, :account_number] + + @spec new(Process.t, AccountCloseProcess.t) :: t + def new(process = %Process{}, data = %AccountCloseProcess{}) do + %__MODULE__{ + atm_id: data.atm_id, + account_number: data.account_number + } + end + + @spec new(ATM.id, BankAccount.account):: t + + def new(atm_id, account_number) do + %__MODULE__{ + atm_id: atm_id, + account_number: account_number + } + end + end +end diff --git a/lib/universe/bank/event/account_create.ex b/lib/universe/bank/event/account_create.ex new file mode 100644 index 00000000..1c929173 --- /dev/null +++ b/lib/universe/bank/event/account_create.ex @@ -0,0 +1,40 @@ +defmodule Helix.Universe.Bank.Event.AccountCreate do + + import Helix.Event + + event Processed do + + alias Helix.Entity.Model.Entity + alias Helix.Process.Model.Process + alias Helix.Universe.Bank.Model.ATM + alias Helix.Universe.Bank.Process.Bank.Account.AccountCreate, + as: AccountCreateProcess + + @type t :: %__MODULE__{ + atm_id: ATM.id, + requester: Entity.id + } + + event_struct [:atm_id, :requester] + + @spec new(ATM.id, Entity.idt) :: t + + def new(atm_id, entity = %Entity{}) do + %__MODULE__{ + atm_id: atm_id, + requester: entity.id + } + end + + def new(atm_id, entity_id = %Entity.ID{}), + do: %__MODULE__{atm_id: atm_id, requester: entity_id} + + @spec new(Process.t, AccountCreateProcess.t) :: t + def new(process = %Process{}, data = %AccountCreateProcess{}) do + %__MODULE__{ + atm_id: process.data.atm_id, + requester: process.source_entity_id + } + end + end +end diff --git a/lib/universe/bank/event/bank/account.ex b/lib/universe/bank/event/bank/account.ex index 4d1c3ef6..911f7bc0 100644 --- a/lib/universe/bank/event/bank/account.ex +++ b/lib/universe/bank/event/bank/account.ex @@ -1,7 +1,48 @@ -defmodule Helix.Universe.Bank.Event.Bank.Account do + defmodule Helix.Universe.Bank.Event.Bank.Account do import Helix.Event + event Removed do + + alias Helix.Universe.Bank.Model.BankAccount + + event_struct [:account] + + @type t :: + %__MODULE__{ + account: BankAccount.t + } + + @spec new(BankAccount.t) :: + t + + def new(account = %BankAccount{}) do + %__MODULE__{ + account: account + } + end + + notify do + + @event :bank_account_removed + + @doc false + def generate_payload(event, _socket) do + data = + %{ + atm_id: to_string(event.account.atm_id), + account_number: event.account.account_number + } + + {:ok, data} + end + + @doc false + def whom_to_notify(event), + do: %{account: event.account.owner_id} + end + end + event Updated do @moduledoc """ `BankAccountUpdatedEvent` is fired when the underlying bank account has @@ -19,7 +60,7 @@ defmodule Helix.Universe.Bank.Event.Bank.Account do reason: reason } - @type reason :: :balance | :password + @type reason :: :balance | :password | :created @spec new(BankAccount.t, reason) :: t @@ -60,11 +101,13 @@ defmodule Helix.Universe.Bank.Event.Bank.Account do event Login do + alias Helix.Account.Model.Account alias Helix.Entity.Model.Entity + alias Helix.Entity.Query.Entity, as: EntityQuery alias Helix.Universe.Bank.Model.BankAccount alias Helix.Universe.Bank.Model.BankToken - event_struct [:entity_id, :account, :token_id] + event_struct [:entity_id, :notify_owner, :account, :token_id] @type t :: %__MODULE__{ entity_id: Entity.id, @@ -72,14 +115,120 @@ defmodule Helix.Universe.Bank.Event.Bank.Account do token_id: BankToken.id | nil } - @spec new(BankAccount.t, Entity.id) :: + @spec new(BankAccount.t, Entity.idt) :: t - def new(account = %BankAccount{}, entity_id, token_id \\ nil) do + def new(account, entity_or_entity_id, token_id \\ nil) + def new(account = %BankAccount{}, entity = %Entity{}, token_id) do + is_account? = entity.entity_type == :account + + %__MODULE__{ + entity_id: entity.entity_id, + account: account, + token_id: token_id, + notify_owner: is_account? + } + end + def new(account = %BankAccount{}, entity_id = %Entity.ID{}, token_id) do + is_account? = EntityQuery.fetch(entity_id).entity_type == :account + entity_id = + if is_account? do + %Account.ID{id: entity_id.id, root: Account} + else + entity_id + end + %__MODULE__{ entity_id: entity_id, account: account, - token_id: token_id + token_id: token_id, + notify_owner: is_account? } end + + notify do + @moduledoc """ + Notifies the client of bank account login, so it can properly update + the local data. + """ + + @event :bank_login + + @doc false + def generate_payload(event, _socket) do + data = + %{ + atm_id: to_string(event.account.atm_id), + account_number: event.account.account_number, + balance: event.account.balance, + password: event.account.password + } + + {:ok, data} + end + + def whom_to_notify(event) do + atm_id = event.account.atm_id + bank_account = event.account.account_number + + entity_id = + case event.entity_id do + entity = %Entity{} -> + %Account.ID{id: entity.entity_id.id, root: Account} + entity_id = %Entity.ID{} -> + %Account.ID{id: entity_id.id, root: Account} + end + + base_map = + %{ + bank_acc: [{atm_id, bank_account}] + } + + if event.notify_owner do + Map.merge(%{account: entity_id}, base_map) + else + base_map + end + end + end + end + + event Logout do + + alias Helix.Entity.Model.Entity + alias Helix.Universe.Bank.Model.BankAccount + + event_struct [:account, :entity_id] + + @type t :: %__MODULE__{ + account: BankAccount.t, + entity_id: Entity.id + } + + @spec new(BankAccount.t, Entity.id) :: + t + def new(account = %BankAccount{}, entity_id = %Entity.ID{}) do + %__MODULE__{ + account: account, + entity_id: entity_id + } + end + + notify do + + @event :bank_logout + + def generate_payload(event, _socket) do + data = + %{ + atm_id: to_string(event.account.atm_id), + account_number: event.account.account_number + } + + {:ok, data} + end + + def whom_to_notify(event), + do: %{account: event.entity_id} + end end end diff --git a/lib/universe/bank/event/bank/account/password.ex b/lib/universe/bank/event/bank/account/password.ex index 4cf0d8e6..60c22edf 100644 --- a/lib/universe/bank/event/bank/account/password.ex +++ b/lib/universe/bank/event/bank/account/password.ex @@ -22,5 +22,61 @@ defmodule Helix.Universe.Bank.Event.Bank.Account.Password do account: account } end + + notify do + + @event :bank_password_revealed + + @doc false + def generate_payload(event, _socket) do + data = + %{ + atm_id: event.account.atm_id, + account_number: event.account.account_number, + password: event.account.password + } + end + + def whom_to_notify(event), + do: %{account: event.entity_id} + end + end + + event Changed do + + alias Helix.Entity.Model.Entity + alias Helix.Universe.Bank.Model.BankAccount + + @type t :: %__MODULE__{ + account: BankAccount.t + } + + event_struct [:account] + + @spec new(BankAccount.t) :: + t + def new(account = %BankAccount{}) do + %__MODULE__{ + account: account + } + end + + notify do + + @event :bank_password_changed + + @doc false + def generate_payload(event, _socket) do + data = + %{ + atm_id: event.account.atm_id, + account_number: event.account.account_number, + password: event.account.password + } + end + + def whom_to_notify(event), + do: %{account: event.account.owner_id} + end end end diff --git a/lib/universe/bank/event/bank/account/token.ex b/lib/universe/bank/event/bank/account/token.ex index 98580e2a..3a8ba57e 100644 --- a/lib/universe/bank/event/bank/account/token.ex +++ b/lib/universe/bank/event/bank/account/token.ex @@ -29,5 +29,24 @@ defmodule Helix.Universe.Bank.Event.Bank.Account.Token do account: account } end + + notify do + + @event :bank_token_acquired + + @doc false + def generate_payload(event, _socket) do + data = + %{ + atm_id: to_string(event.account.atm_id), + account_number: to_string(event.account.account_number), + token_id: event.token.token_id + } + end + + @doc false + def whom_to_notify(event), + do: %{account: event.entity_id} + end end end diff --git a/lib/universe/bank/event/bank/transfer.ex b/lib/universe/bank/event/bank/transfer.ex old mode 100644 new mode 100755 index 7fa74bc3..c34b6491 --- a/lib/universe/bank/event/bank/transfer.ex +++ b/lib/universe/bank/event/bank/transfer.ex @@ -49,15 +49,146 @@ defmodule Helix.Universe.Bank.Event.Bank.Transfer do connection_id: Connection.id } - event_struct [:transfer_id, :connection_id] + event_struct [:transfer_id, :started_by, :connection_id] @spec new(Process.t, BankTransferProcess.t) :: t def new(process = %Process{}, data = %BankTransferProcess{}) do %__MODULE__{ transfer_id: data.transfer_id, + started_by: data.started_by, connection_id: process.src_connection_id } end + + notify do + + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + + @event :bank_transfer_aborted + + def generate_payload(event, _socket) do + transfer = BankQuery.fetch_transfer(event.transfer_id) + + data = + %{ + atm_id: to_string(transfer.atm_to), + account_number: transfer.account_to, + transfer_id: event.transfer_id + } + + {:ok, data} + end + + def whom_to_notify(event), + do: %{account: event.started_by} + end + end + + event Successful do + + alias Helix.Universe.Bank.Model.BankAccount + alias Helix.Universe.Bank.Model.BankTransfer + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + + @type t :: + %__MODULE__{ + transfer: BankTransfer.t, + account: BankAccount.t + } + + event_struct [:transfer, :account] + + @spec new(BankTransfer.t) :: + t + def new(transfer = %BankTransfer{}) do + account = + BankQuery.fetch_account(transfer.atm_from, transfer.account_from) + + %__MODULE__{ + transfer: transfer, + account: account + } + end + + notify do + + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + alias Helix.Universe.Bank.Public.Index, as: BankIndex + + @event :bank_transfer_successful + + def generate_payload(event, _socket) do + transfer = BankIndex.render_transfer(event.transfer) + + data = + %{ + transfer: transfer + } + + {:ok, data} + end + + def whom_to_notify(event) do + transfer = BankQuery.fetch_transfer(event.transfer_id) + %{ + account: transfer.started_by + } + end + end + end + + event Failed do + + alias Helix.Universe.Bank.Model.BankAccount + alias Helix.Universe.Bank.Model.BankTransfer + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + + @type reason :: term + + @type t :: + %__MODULE__{ + transfer: BankTransfer.t, + account: BankAccount.t, + reason: reason + } + + event_struct [:transfer, :account, :reason] + + @spec new(BankTransfer.t, reason) :: + t + def new(transfer = %BankTransfer{}, reason) do + account = + BankQuery.fetch_account(transfer.atm_from, transfer.account_from) + + %__MODULE__{ + transfer: transfer, + account: account, + reason: reason + } + end + + notify do + + alias Helix.Universe.Bank.Public.Index, as: BankIndex + + @event :bank_transfer_failed + + def generate_payload(event, _socket) do + data = + %{ + transfer: BankIndex.render_transfer(event.transfer), + reason: to_string(event.reason) + } + + {:ok, data} + end + + def whom_to_notify(event) do + %{ + account: event.transfer.started_by + } + end + end end end diff --git a/lib/universe/bank/event/change_password.ex b/lib/universe/bank/event/change_password.ex new file mode 100644 index 00000000..207f136e --- /dev/null +++ b/lib/universe/bank/event/change_password.ex @@ -0,0 +1,43 @@ +defmodule Helix.Universe.Bank.Event.ChangePassword do + + import Helix.Event + + event Processed do + + alias Helix.Process.Model.Process + alias Helix.Server.Model.Server + alias Helix.Universe.Bank.Model.BankAccount + alias Helix.Universe.Bank.Process.Bank.Account.ChangePassword, + as: ChangePasswordProcess + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + + @type t :: %__MODULE__{ + gateway_id: Server.id, + account: BankAccount.t + } + + event_struct [:gateway_id, :account] + + @spec new(BankAccount.t, Server.id) :: + t + def new(account = %BankAccount{}, gateway_id) do + %__MODULE__{ + gateway_id: gateway_id, + account: account + } + end + + @spec new(Process.t, ChangePasswordProcess.t) :: + t + def new(process = %Process{}, _data = %ChangePasswordProcess{}) do + atm_id = process.src_atm_id + account_number = process.src_acc_number + account = BankQuery.fetch_account(atm_id, account_number) + + %__MODULE__{ + gateway_id: process.gateway_id, + account: account + } + end + end +end diff --git a/lib/universe/bank/event/handler/bank/account.ex b/lib/universe/bank/event/handler/bank/account.ex index 3589125d..eda72847 100644 --- a/lib/universe/bank/event/handler/bank/account.ex +++ b/lib/universe/bank/event/handler/bank/account.ex @@ -5,10 +5,48 @@ defmodule Helix.Universe.Bank.Event.Handler.Bank.Account do alias Helix.Event alias Helix.Entity.Query.Entity, as: EntityQuery alias Helix.Universe.Bank.Action.Bank, as: BankAction + alias Helix.Universe.Bank.Query.Bank, as: BankQuery alias Helix.Software.Event.Virus.Collected, as: VirusCollectedEvent + alias Helix.Universe.Bank.Event.Bank.Account.Removed, + as: BankAccountRemovedEvent + alias Helix.Universe.Bank.Event.Bank.Account.Updated, + as: BankAccountUpdatedEvent + alias Helix.Universe.Bank.Event.AccountCreate.Processed, + as: AccountCreateProcessedEvent + alias Helix.Universe.Bank.Event.AccountClose.Processed, + as: AccountCloseProcessedEvent alias Helix.Universe.Bank.Event.RevealPassword.Processed, as: RevealPasswordProcessedEvent + alias Helix.Universe.Bank.Event.ChangePassword.Processed, + as: ChangePasswordProcessedEvent + alias Helix.Universe.Bank.Event.Bank.Account.Password.Changed, + as: BankPasswordChangedEvent + + def account_create_processed(event = %AccountCreateProcessedEvent{}) do + flowing do + with \ + {:ok, _bank_account, events} <- + BankAction.open_account(event.requester, event.atm_id), + on_success(fn -> Event.emit(events, from: event) end) + do + :ok + end + end + end + + def account_close_processed(event = %AccountCloseProcessedEvent{}) do + flowing do + with \ + bank_acc = BankQuery.fetch_account(event.atm_id, event.account_number), + true <- not is_nil(bank_acc), + {:ok, events} <- BankAction.close_account(bank_acc), + on_success(fn -> Event.emit(events, from: event) end) + do + :ok + end + end + end @doc """ Handles the conclusion of a `PasswordRevealProcess`, described at @@ -34,6 +72,36 @@ defmodule Helix.Universe.Bank.Event.Handler.Bank.Account do end end + @doc """ + Handles the conclusion of a `PasswordChangeProcess`, described at + `BankAccountFlow` + + Emits: `BankAccountPasswordChangedEvent` + """ + def password_change_processed(event = %ChangePasswordProcessedEvent{}) do + flowing do + with \ + changed_by = %{} <- EntityQuery.fetch_by_server(event.gateway_id), + {:ok, _bank_account, events} <- + BankAction.change_password(event.account), + on_success(fn -> Event.emit(events, from: event) end) + do + :ok + end + end + end + + @doc """ + Emits BankAccountUpdatedEvent with reason :password to client update local + information + + Emits: `BankAccountUpdatedEvent` + """ + def password_changed(event = %BankPasswordChangedEvent{}) do + account = event.account + Event.emit(BankAccountUpdatedEvent.new(account, :password), from: event) + end + @doc """ When the rewards of a virus have been successfully collected, it's time to update their bank account. That's what we do here. diff --git a/lib/universe/bank/event/handler/bank/transfer.ex b/lib/universe/bank/event/handler/bank/transfer.ex index 03974b60..7b3a8e3b 100644 --- a/lib/universe/bank/event/handler/bank/transfer.ex +++ b/lib/universe/bank/event/handler/bank/transfer.ex @@ -1,5 +1,8 @@ defmodule Helix.Universe.Bank.Event.Handler.Bank.Transfer do + import HELF.Flow + + alias Helix.Event alias Helix.Universe.Bank.Action.Bank, as: BankAction alias Helix.Universe.Bank.Query.Bank, as: BankQuery alias Helix.Universe.Bank.Event.Bank.Transfer.Aborted, @@ -9,7 +12,14 @@ defmodule Helix.Universe.Bank.Event.Handler.Bank.Transfer do def transfer_processed(event = %BankTransferProcessedEvent{}) do transfer = BankQuery.fetch_transfer(event.transfer_id) - BankAction.complete_transfer(transfer) + flowing do + with \ + {:ok, _transfer, events} <- BankAction.complete_transfer(transfer), + on_success(fn -> Event.emit(events, from: event) end) + do + :ok + end + end end def transfer_aborted(event = %BankTransferAbortedEvent{}) do diff --git a/lib/universe/bank/henforcer/bank.ex b/lib/universe/bank/henforcer/bank.ex index 925abca8..e30758f9 100644 --- a/lib/universe/bank/henforcer/bank.ex +++ b/lib/universe/bank/henforcer/bank.ex @@ -2,9 +2,15 @@ defmodule Helix.Universe.Bank.Henforcer.Bank do import Helix.Henforcer + alias Helix.Account.Model.Account + alias Helix.Entity.Model.Entity alias Helix.Server.Model.Server + alias Helix.Universe.Bank.Model.Bank alias Helix.Universe.Bank.Model.ATM alias Helix.Universe.Bank.Model.BankAccount + alias Helix.Universe.Bank.Model.BankToken + alias Helix.Entity.Henforcer.Entity, as: EntityHenforcer + alias Helix.Network.Henforcer.Network, as: NetworkHenforcer alias Helix.Universe.Bank.Query.Bank, as: BankQuery @type account_exists_relay :: %{bank_account: BankAccount.t} @@ -12,18 +18,332 @@ defmodule Helix.Universe.Bank.Henforcer.Bank do @type account_exists_error :: {false, {:bank_account, :not_found}, account_exists_relay_partial} - @spec account_exists?(ATM.id, BankAccount.account) :: + @spec account_exists?(ATM.idt, BankAccount.account) :: {true, account_exists_relay} | account_exists_error @doc """ Henforces that the given bank account ({atm_id, account_number}) exists. """ def account_exists?(atm_id = %Server.ID{}, account_number) do - with bank_acc = %{} <- BankQuery.fetch_account(atm_id, account_number) do - reply_ok(%{bank_account: bank_acc}) - else + case BankQuery.fetch_account(atm_id, account_number) do + bank_acc = %BankAccount{} -> + reply_ok(%{bank_account: bank_acc}) _ -> reply_error({:bank_account, :not_found}) end end + + + @type password_valid_relay :: %{} + @type password_valid_relay_partial :: %{} + @type password_valid_error :: + {false, {:password, :invalid}, password_valid_relay_partial} + + @spec password_valid?(BankAccount.t, BankAccount.password) :: + {true, password_valid_relay} + | password_valid_error + @doc """ + Henforces that the given password is the same as given account's password + """ + def password_valid?(%BankAccount{password: password}, password), + do: reply_ok(%{password: password}) + def password_valid?(_, _), + do: reply_error({:password, :invalid}) + + @type can_join_password_relay :: %{} + @type can_join_password_error :: + password_valid_error + | account_exists_error + @type can_join_password_partial :: %{} + + @spec can_join_password?(ATM.id, BankAccount.number, String.t, Entity.id) :: + {true, can_join_password_relay} + | {false, can_join_password_error, can_join_password_partial} + @doc """ + Henforces that the player can join with given password + """ + def can_join_password?(atm_id, account_number, password, entity_id) do + with \ + {true, r1} <- account_exists?(atm_id, account_number), + bank_account = r1.bank_account, + {true, r2} <- password_valid?(bank_account, password), + {true, r3} <- EntityHenforcer.entity_exists?(entity_id) + do + reply_ok(relay([r1, r2, r3])) + end + end + + @type can_join_token_relay :: %{} + @type can_join_token_error :: + token_exists_error + | token_valid_error + | account_exists_error + @type can_join_token_partial :: %{} + + @spec can_join_token?(ATM.id, BankAccount.number, BankToken.id, Entity.id) :: + {true, can_join_token_relay} + | {false, can_join_token_error, can_join_token_partial} + @doc """ + Henforces that the player can join with the given token + """ + def can_join_token?(atm_id, account_number, token, entity_id) do + with \ + {true, r1} <- account_exists?(atm_id, account_number), + bank_account = r1.bank_account, + {true, r2} <- token_valid?(bank_account, token), + {true, r3} <- EntityHenforcer.entity_exists?(entity_id) + do + reply_ok(relay([r1, r2, r3])) + end + end + + @type token_valid_relay :: %{token: BankToken.t} + @type token_valid_relay_partial :: %{} + @type invalid_error :: + {false, {:token, :not_belongs}, token_valid_relay_partial} + @type token_valid_error :: + invalid_error + | token_exists_error + | token_not_expired_error + + @spec token_valid?(BankAccount.t, BankToken.id) :: + {true, token_valid_relay} + | token_valid_error + @doc """ + Henforces that given token exists, is not expired and matches the bank account + data + """ + def token_valid?(bank_account, token) do + with \ + {true, r1} <- token_exists?(token), + token = r1.token, + true <- token.atm_id == bank_account.atm_id, + true <- token.account_number == bank_account.account_number, + {true, r2} <- token_not_expired?(token) + do + reply_ok(r2) + else + {false, reason, _} -> + reply_error(reason) + false -> + reply_error({:token, :not_belongs}) + end + end + + @type token_exists_relay :: %{} + @type token_exists_error :: + {false, {:token, :not_found}, token_exists_relay_partial} + @type token_exists_relay_partial :: %{} + + @spec token_exists?(BankToken.id) :: + {true, token_exists_relay} + | token_exists_error + @doc """ + Henforces that given token exists on the database + """ + def token_exists?(nil), + do: reply_error{:token, :not_found} + def token_exists?(token) do + case BankQuery.fetch_token(token) do + token = %BankToken{} -> + reply_ok(%{token: token}) + _ -> + reply_error({:token, :not_found}) + end + end + + @type token_not_expired_relay :: %{} + @type token_not_expired_error :: + {false, {:token, :expired}, token_not_expired_partial_relay} + @type token_not_expired_partial_relay :: %{} + + @spec token_exists?(BankToken.t) :: + {true, token_not_expired_relay} + | token_not_expired_error + @doc """ + Henforces that the given token is not expired + """ + def token_not_expired?(token) do + time_now = + DateTime.utc_now() + + token_exp = + token.expiration_date + + case DateTime.compare(time_now, token_exp) do + :gt -> + reply_error{:token, :expired} + _ -> + reply_ok(%{token: token}) + end + end + + @type transfer_error :: + account_exists_error + | transfer_no_funds_error + + @type can_transfer_relay :: + %{ + amount: BankAccount.amount, + to_account: BankAccount.t + } + @type transfer_no_funds_error :: + {:bank_account, :no_funds} + @spec can_transfer?( + {Network.ID, Network.ip}, + BankAccount.account, + {ATM.idt, BankAccount.account}, + non_neg_integer + ) :: + {true, can_transfer_relay} + | {false, transfer_error, %{}} + @doc """ + Henforces that given account can transfer to another given account + """ + def can_transfer?({net_id, ip}, to_acc_num, {atm_id, acc_num}, amount) + do + with \ + {true, r1} <- NetworkHenforcer.nip_exists?(net_id, ip), + {true, r2} <- is_a_bank?(r1.server.server_id), + to_atm_id = r2.atm.atm_id, + {true, r3} <- account_exists?(to_atm_id, to_acc_num), + {_r3, to_account} <- get_and_drop(r3, :bank_account), + {true, r4} <- account_exists?(atm_id, acc_num), + {true, r5} <- has_enough_funds?(r4.bank_account, amount) + do + relay = + %{ + to_account: to_account + } + reply_ok(relay([relay, r5])) + end + end + + @type has_enough_funds_relay :: %{amount: BankAccount.amount} + @type has_enough_funds_partial_relay :: %{} + + @spec has_enough_funds?(BankAccount.t, BankAccount.amount) :: + {true, has_enough_funds_relay} + | {false, transfer_no_funds_error, has_enough_funds_partial_relay} + @doc """ + Henforces that given account has given amount in the balance + """ + def has_enough_funds?(account, amount) do + if account.balance >= amount do + reply_ok() + else + reply_error({:bank_account, :no_funds}) + end + |> wrap_relay(%{amount: amount}) + end + + @type owns_account_relay :: %{bank_account: BankAccount.t} + @type owns_account_partial_relay :: %{} + @type owns_account_error :: + {false, {:bank_account, :not_belongs}, owns_account_partial_relay} + def owns_account?(entity = %Entity{}, account = %BankAccount{}) do + with \ + entity_id = to_string(entity.entity_id), + true <- account.owner_id == Account.ID.cast!(entity_id) + do + reply_ok(%{bank_account: account}) + else + _ -> + reply_error({:bank_account, :not_belongs}) + end + end + + @type is_empty_relay :: %{bank_account: BankAccount.t} + @type is_empty_partial_relay :: %{} + @type is_empty_error :: + {false, {:bank_account, :not_empty}, is_empty_partial_relay} + + @spec is_empty?(BankAccount.t) :: + {true, is_empty_relay} + | is_empty_error + @doc """ + Henforces that the given account is empty. + """ + def is_empty?(account = %BankAccount{}) do + if account.balance == 0 do + reply_ok() + else + reply_error({:bank_account, :not_empty}) + end + |> wrap_relay(%{bank_account: account}) + end + + @type can_close_relay :: %{} + @type can_close_partial_relay :: %{} + @type can_close_error :: + account_exists_error + | is_empty_error + + @spec can_close?(Server.id, BankAccount.account) :: + {true, can_close_relay} + | can_close_error + @doc """ + Henforces that player can close account + """ + def can_close?(atm_id = %Server.ID{}, account_number) do + with \ + {true, r1} <- account_exists?(atm_id, account_number), + {true, _r2} <- is_empty?(r1.bank_account) + do + reply_ok() + end + end + + @type is_a_bank_relay :: %{} + @type is_a_bank_partial_relay :: %{} + @type is_a_bank_error :: {false, {:atm, :not_a_bank}, %{}} + + @spec is_a_bank?(Server.id) :: + is_a_bank_relay + | is_a_bank_error + @doc """ + Henforces that given server is a bank + """ + def is_a_bank?(server_id = %Server.ID{}) do + case BankQuery.fetch_atm(server_id) do + atm = %ATM{} -> + reply_ok(%{atm: atm}) + _ -> + reply_error({:atm, :not_a_bank}) + end + end + + @type can_create_account_relay :: + %{ + bank_id: Bank.id, + atm_id: ATM.id + } + @type can_create_account_partial_relay :: %{} + @type can_create_account_error :: + is_a_bank_error + + @spec can_create_account?(Server.t) :: + {true, can_create_account_relay} + | can_create_account_error + @doc """ + Henforces that player can create a account + """ + def can_create_account?(atm = %Server{}) do + with \ + {true, r1} <- is_a_bank?(atm.server_id), + bank_id = r1.atm.bank_id, + atm_id = r1.atm.atm_id + do + relay = + %{ + bank_id: bank_id, + atm_id: atm_id + } + + reply_ok(relay) + else + {false, reason, _} -> + reply_error(reason) + end + end end diff --git a/lib/universe/bank/internal/bank_account.ex b/lib/universe/bank/internal/bank_account.ex index f32d3e56..0004e0bd 100644 --- a/lib/universe/bank/internal/bank_account.ex +++ b/lib/universe/bank/internal/bank_account.ex @@ -2,6 +2,7 @@ defmodule Helix.Universe.Bank.Internal.BankAccount do alias Helix.Account.Model.Account alias Helix.Universe.Bank.Model.ATM + alias Helix.Universe.Bank.Model.Bank alias Helix.Universe.Bank.Model.BankAccount alias Helix.Universe.Repo @@ -63,11 +64,11 @@ defmodule Helix.Universe.Bank.Internal.BankAccount do total || 0 end - @spec create(BankAccount.creation_params) :: + @spec create(Account.id, ATM.id, Bank.id) :: {:ok, BankAccount.t} | {:error, Ecto.Changeset.t} - def create(params) do - params + def create(owner, atm, bank_id) do + %{owner_id: owner, atm_id: atm, bank_id: bank_id} |> BankAccount.create_changeset() |> Repo.insert() end @@ -128,8 +129,8 @@ defmodule Helix.Universe.Bank.Internal.BankAccount do @spec close(BankAccount.t) :: :ok - | {:error, {:account, :notfound}} - | {:error, {:account, :notempty}} + | {:error, {:bank_account, :not_found}} + | {:error, {:bank_account, :not_empty}} def close(account) do trans = Repo.transaction(fn -> @@ -138,8 +139,8 @@ defmodule Helix.Universe.Bank.Internal.BankAccount do account = fetch_for_update(account.atm_id, account.account_number) with \ - true <- not is_nil(account) || {:account, :notfound}, - true <- account.balance == 0 || {:account, :notempty} + true <- not is_nil(account) || {:bank_account, :not_found}, + true <- account.balance == 0 || {:bank_account, :not_empty} do delete(account) else diff --git a/lib/universe/bank/internal/bank_token.ex b/lib/universe/bank/internal/bank_token.ex index 6f3d856f..17c48703 100644 --- a/lib/universe/bank/internal/bank_token.ex +++ b/lib/universe/bank/internal/bank_token.ex @@ -11,7 +11,6 @@ defmodule Helix.Universe.Bank.Internal.BankToken do def fetch(token) do token |> BankToken.Query.by_token() - |> BankToken.Query.filter_expired() |> Repo.one() end @@ -21,7 +20,6 @@ defmodule Helix.Universe.Bank.Internal.BankToken do def fetch_by_connection(connection) do connection |> BankToken.Query.by_connection() - |> BankToken.Query.filter_expired() |> Repo.one() end diff --git a/lib/universe/bank/internal/bank_transfer.ex b/lib/universe/bank/internal/bank_transfer.ex index 230e007e..0df979e0 100644 --- a/lib/universe/bank/internal/bank_transfer.ex +++ b/lib/universe/bank/internal/bank_transfer.ex @@ -34,7 +34,7 @@ defmodule Helix.Universe.Bank.Internal.BankTransfer do @spec start(BankAccount.t, BankAccount.t, pos_integer, Account.idt) :: {:ok, BankTransfer.t} | {:error, {:funds, :insufficient}} - | {:error, {:account, :notfound}} + | {:error, {:bank_account, :not_found}} | {:error, Ecto.Changeset.t} def start(from_acc, to_acc, amount, started_by) do Repo.transaction(fn -> diff --git a/lib/universe/bank/model/bank_account.ex b/lib/universe/bank/model/bank_account.ex old mode 100644 new mode 100755 index de2c723c..bae34db8 --- a/lib/universe/bank/model/bank_account.ex +++ b/lib/universe/bank/model/bank_account.ex @@ -19,13 +19,14 @@ defmodule Helix.Universe.Bank.Model.BankAccount do account_number: account, bank_id: NPC.id, atm_id: ATM.id, - password: String.t, + password: password, balance: balance, owner_id: Account.id } @type balance :: non_neg_integer @type amount :: pos_integer + @type password :: String.t @type creation_params :: %{ bank_id: NPC.idtb, @@ -136,7 +137,7 @@ defmodule Helix.Universe.Bank.Model.BankAccount do do: Enum.random(@account_range) @spec generate_account_password :: - String.t + BankAccount.password defp generate_account_password, do: Password.generate(:bank_account) diff --git a/lib/universe/bank/process/bank/account/close_account.ex b/lib/universe/bank/process/bank/account/close_account.ex new file mode 100644 index 00000000..a622a2bc --- /dev/null +++ b/lib/universe/bank/process/bank/account/close_account.ex @@ -0,0 +1,104 @@ +import Helix.Process + +process Helix.Universe.Bank.Process.Bank.Account.AccountClose do + + alias Helix.Universe.Bank.Model.ATM + alias Helix.Universe.Bank.Model.BankAccount + + process_struct [:atm_id, :account_number] + + @process_type :bank_close_account + + @type t :: %__MODULE__{ + atm_id: ATM.id, + account_number: BankAccount.account + } + + @type creation_params :: %{ + atm_id: ATM.id, + account_number: BankAccount.account + } + + @type objective :: %{cpu: resource_usage} + + @type resources :: %{ + objective: objective, + static: map, + l_dynamic: [:cpu], + r_dynamic: [] + } + + @type resources_params :: %{} + + @spec new(creation_params) :: t + def new(params) do + %__MODULE__{ + atm_id: params.atm_id, + account_number: params.account_number + } + end + + @spec resources(resources_params) :: resources + def resources(params = %{}), + do: get_resources params + + processable do + + alias Helix.Universe.Bank.Event.AccountClose.Processed, + as: AccountCloseProcessedEvent + + on_completion(process, data) do + event = AccountCloseProcessedEvent.new(process, data) + + {:delete, [event]} + end + end + + resourceable do + + alias Helix.Universe.Bank.Process.Bank.Account.AccountClose, + as: AccountCloseProcess + + @type params :: AccountCloseProcess.resources_params + @type factors :: term + + # TODO proper balance + get_factors(%{}) do end + + # TODO: Use Time, not CPU #364 + cpu(_) do + 1 + end + + dynamic do + [:cpu] + end + end + + executable do + + alias Helix.Universe.Bank.Process.Bank.Account.AccountClose, + as: AccountCloseProcess + + @type params :: AccountCloseProcess.creation_params + + @type meta :: + %{ + optional(atom) => term + } + + resources(_, _, %{}, _) do + %{} + end + + source_connection(_gateway, _target, _params, _meta) do + {:create, :bank_login} + end + end + process_viewable do + + @type data :: %{} + + render_empty_data() + end +end diff --git a/lib/universe/bank/process/bank/account/create_account.ex b/lib/universe/bank/process/bank/account/create_account.ex new file mode 100644 index 00000000..c2608d06 --- /dev/null +++ b/lib/universe/bank/process/bank/account/create_account.ex @@ -0,0 +1,95 @@ +import Helix.Process + +process Helix.Universe.Bank.Process.Bank.Account.AccountCreate do + + alias Helix.Universe.Bank.Model.ATM + alias Helix.Universe.Bank.Model.BankAccount + + process_struct [:atm_id] + + @process_type :bank_create_account + + @type t :: %__MODULE__{atm_id: ATM.id} + + @type creation_params :: %{atm_id: ATM.id} + + @type objective :: %{cpu: resource_usage} + + @type resources :: %{ + objective: objective, + static: map, + l_dynamic: [:cpu], + r_dynamic: [] + } + + @type resources_params :: %{} + + @spec new(creation_params) :: t + def new(%{atm_id: atm_id}) do + %__MODULE__{atm_id: atm_id} + end + + @spec resources(resources_params) :: resources + def resources(params = %{}), + do: get_resources params + + processable do + + alias Helix.Universe.Bank.Event.AccountCreate.Processed, + as: AccountCreateProcess + + on_completion(process, data) do + event = AccountCreateProcess.new(process, data) + + {:delete, [event]} + end + end + + resourceable do + + alias Helix.Universe.Bank.Process.Bank.Account.AccountCreate, + as: AccountCreateProcess + + @type params :: AccountCreateProcess.resources_params + @type factors :: term + + # TODO proper balance + get_factors(%{}) do end + + # TODO: Use Time, not CPU #364 + cpu(_) do + 1 + end + + dynamic do + [:cpu] + end + end + + executable do + + alias Helix.Universe.Bank.Process.Bank.Account.AccountCreate, + as: AccountCreateProcess + + @type params :: AccountCreateProcess.creation_params + + @type meta :: + %{ + optional(atom) => term + } + + resources(_, _, %{}, _) do + %{} + end + + source_connection(_gateway, _target, _params, _meta) do + {:create, :bank_login} + end + end + process_viewable do + + @type data :: %{} + + render_empty_data() + end +end diff --git a/lib/universe/bank/process/bank/account/password_change.ex b/lib/universe/bank/process/bank/account/password_change.ex new file mode 100644 index 00000000..d5defa97 --- /dev/null +++ b/lib/universe/bank/process/bank/account/password_change.ex @@ -0,0 +1,98 @@ +import Helix.Process + +process Helix.Universe.Bank.Process.Bank.Account.ChangePassword do + + process_struct [] + + @process_type :bank_change_password + + @type t :: + %__MODULE__{} + + @type creation_params :: + %{} + + @type objective :: %{cpu: resource_usage} + + @type resources :: + %{ + objective: objective, + static: map, + l_dynamic: [:cpu], + r_dynamic: [] + } + + @type resources_params :: + %{} + + @spec new(creation_params) :: + t + def new(%{}) do + %__MODULE__{} + end + + @spec resources(resources_params) :: + resources + def resources(params = %{}), + do: get_resources params + + processable do + + alias Helix.Universe.Bank.Event.ChangePassword.Processed, + as: ChangePasswordProcessedEvent + + on_completion(process, data) do + event = ChangePasswordProcessedEvent.new(process, data) + + {:delete, [event]} + end + end + + resourceable do + + alias Helix.Universe.Bank.Process.Bank.Account.ChangePassword, + as: ChangePasswordProcess + + @type params :: ChangePasswordProcess.resources_params + @type factors :: term + + # TODO proper balance + get_factors(%{}) do end + + # TODO: Use Time, not CPU #364 + cpu(_) do + 1 + end + + dynamic do + [:cpu] + end + end + + executable do + + alias Helix.Universe.Bank.Process.Bank.Account.ChangePassword, + as: ChangePasswordProcess + + @type params :: ChangePasswordProcess.creation_params + + @type meta :: + %{ + optional(atom) => term + } + + resources(_gateway, _atm, %{}, _meta) do + %{} + end + + source_connection(_gateway, _target, _params, _meta) do + {:create, :bank_login} + end + end + process_viewable do + + @type data :: %{} + + render_empty_data() + end +end diff --git a/lib/universe/bank/process/bank/account/password_reveal.ex b/lib/universe/bank/process/bank/account/password_reveal.ex index dbb9a2f7..1135b028 100644 --- a/lib/universe/bank/process/bank/account/password_reveal.ex +++ b/lib/universe/bank/process/bank/account/password_reveal.ex @@ -100,4 +100,10 @@ process Helix.Universe.Bank.Process.Bank.Account.RevealPassword do %{account: account} end end + process_viewable do + + @type data :: %{} + + render_empty_data() + end end diff --git a/lib/universe/bank/process/bank/transfer.ex b/lib/universe/bank/process/bank/transfer.ex index 166b125e..e03b9cb1 100644 --- a/lib/universe/bank/process/bank/transfer.ex +++ b/lib/universe/bank/process/bank/transfer.ex @@ -2,15 +2,17 @@ import Helix.Process process Helix.Universe.Bank.Process.Bank.Transfer do + alias Helix.Account.Model.Account alias Helix.Universe.Bank.Model.BankTransfer - process_struct [:transfer_id, :amount] + process_struct [:transfer_id, :started_by, :amount] @process_type :wire_transfer @type t :: %__MODULE__{ transfer_id: BankTransfer.id, + started_by: Account.id, amount: BankTransfer.amount } @@ -38,6 +40,7 @@ process Helix.Universe.Bank.Process.Bank.Transfer do def new(%{transfer: transfer = %BankTransfer{}}) do %__MODULE__{ transfer_id: transfer.transfer_id, + started_by: transfer.started_by, amount: transfer.amount } end @@ -112,4 +115,10 @@ process Helix.Universe.Bank.Process.Bank.Transfer do {:create, :wire_transfer} end end + process_viewable do + + @type data :: %{} + + render_empty_data() + end end diff --git a/lib/universe/bank/public/bank.ex b/lib/universe/bank/public/bank.ex new file mode 100755 index 00000000..27d06850 --- /dev/null +++ b/lib/universe/bank/public/bank.ex @@ -0,0 +1,145 @@ +defmodule Helix.Universe.Bank.Public.Bank do + + alias Helix.Event + alias Helix.Account.Model.Account + alias Helix.Server.Model.Server + alias Helix.Network.Model.Tunnel + alias Helix.Universe.Bank.Action.Flow.BankAccount, + as: BankAccountFlow + alias Helix.Universe.Bank.Action.Flow.BankTransfer, + as: BankTransferFlow + alias Helix.Universe.Bank.Public.Index, as: BankIndex + alias Helix.Universe.Bank.Model.BankAccount + alias Helix.Universe.Bank.Model.BankToken + alias Helix.Universe.Bank.Model.ATM + + #TODO: Add Transfer History + @type bootstrap :: BankIndex.index + + @type rendered_bootstrap :: BankIndex.rendered_index + + @spec bootstrap({ATM.id, BankAccount.account}) :: + bootstrap + @doc """ + Gets the BankAccount information and puts into a map. + """ + def bootstrap({atm_id, account_number}) do + BankIndex.index(atm_id, account_number) + end + + @spec render_bootstrap(bootstrap) :: + rendered_bootstrap + + @doc """ + Gets Bootstrap information and turns to client friendly format. + """ + def render_bootstrap(bootstrap) do + BankIndex.render_index(bootstrap) + end + + @spec change_password( + BankAccount.t, + Server.t, + Server.t, + Event.relay + ) :: + {:ok, Process.t} + | {:error, :internal} + @doc """ + Starts a ChangePasswordProcess. + """ + def change_password(account, gateway, atm, relay) do + password_change = + BankAccountFlow.change_password(account, gateway, atm, relay) + case password_change do + {:ok, process} -> + {:ok, process} + _ -> + {:error, :internal} + end + end + + @spec reveal_password( + BankAccount.t, + BankToken.token_id, + Server.t, + Server.t, + Event.relay + ) :: + {:ok, Process.t} + | {:error, :internal} + @doc """ + Starts a RevealPasswordProcess. + """ + def reveal_password(bank_account, token, gateway, atm, relay) do + BankAccountFlow.reveal_password( + bank_account, + token, + gateway, + atm, + relay + ) + end + + @spec open_account(Server.t, Account.id, Server.t, Event.relay) :: + {:ok, BankAccount.t} + | {:error, :internal} + @doc """ + Opens a BankAccount on given atm to given account_id. + """ + def open_account(gateway, account_id, atm, relay), + do: BankAccountFlow.open(gateway, account_id, atm, relay) + + @spec close_account(Server.t, BankAccount.t, Server.t, Event.relay) :: + :ok + | {:error, {:bank_account, :not_found}} + | {:error, {:bank_account, :not_empty}} + @doc """ + Closes given account. + """ + def close_account(gateway, account, atm, relay), + do: BankAccountFlow.close(gateway, account, atm, relay) + + @spec transfer( + BankAccount.t, + BankAccount.t, + BankAccount.amount, + Account.t, + Server.t, + Tunnel.t, + Event.relay + ) :: + {:ok, Process.t} + | {:error, :internal} + @doc """ + Starts a BankTransferProcess. + """ + def transfer( + from_account, + to_account, + amount, + started_by, + gateway, + tunnel, + relay) + do + transfer = + BankTransferFlow.start( + from_account, + to_account, + amount, + started_by, + gateway, + tunnel, + relay + ) + + case transfer do + {:ok, process} -> + {:ok, process} + + {:error, _} -> + {:error, :internal} + end + end +end diff --git a/lib/universe/bank/public/index.ex b/lib/universe/bank/public/index.ex new file mode 100755 index 00000000..33da29c3 --- /dev/null +++ b/lib/universe/bank/public/index.ex @@ -0,0 +1,69 @@ +defmodule Helix.Universe.Bank.Public.Index do + + alias HELL.ClientUtils + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + alias Helix.Universe.Bank.Model.BankAccount + + @type index :: + %{ + balance: BankAccount.balance + } + + @type rendered_index :: + %{ + balance: BankAccount.balance, + } + + @type rendered_transfer :: + %{ + transfer_id: String.t, + account_from: String.t, + account_to: String.t, + atm_from: String.t, + atm_to: String.t, + amount: BankTransfer.amount, + started_by: String.t, + started_time: Float.t + } + + @spec index(ATM.id, BankAccount.account) :: + index + @doc """ + Creates index for given BankAccount{atm_id, account_number}. + """ + def index(atm_id, account_number) do + bank_account = BankQuery.fetch_account(atm_id, account_number) + + %{ + balance: bank_account.balance, + } + end + + @spec render_index(index) :: + rendered_index + @doc """ + Renders index to client-friendly format. + """ + def render_index(index) do + %{ + balance: index.balance, + } + end + + @spec render_transfer(BankTransfer.t) :: rendered_transfer + @doc """ + Renders a BankTransfer to client-friendly format. + """ + def render_transfer(transfer) do + %{ + transfer_id: to_string(transfer.transfer_id), + account_from: to_string(transfer.account_from), + account_to: to_string(transfer.account_to), + atm_from: to_string(transfer.atm_from), + atm_to: to_string(transfer.atm_to), + amount: transfer.amount, + started_by: to_string(transfer.started_by), + started_time: ClientUtils.to_timestamp(transfer.started_time) + } + end +end diff --git a/lib/universe/bank/query/bank.ex b/lib/universe/bank/query/bank.ex index e221c271..f6ae5b2c 100644 --- a/lib/universe/bank/query/bank.ex +++ b/lib/universe/bank/query/bank.ex @@ -5,6 +5,7 @@ defmodule Helix.Universe.Bank.Query.Bank do alias Helix.Universe.Bank.Internal.BankAccount, as: BankAccountInternal alias Helix.Universe.Bank.Internal.BankToken, as: BankTokenInternal alias Helix.Universe.Bank.Internal.BankTransfer, as: BankTransferInternal + alias Helix.Universe.Bank.Internal.ATM, as: ATMInternal alias Helix.Universe.Bank.Model.ATM alias Helix.Universe.Bank.Model.BankAccount alias Helix.Universe.Bank.Model.BankTransfer @@ -39,6 +40,10 @@ defmodule Helix.Universe.Bank.Query.Bank do fetch_account(atm_id, account_number) end + defdelegate fetch_atm(atm_id), + to: ATMInternal, + as: :fetch + @spec get_account_balance(BankAccount.t) :: non_neg_integer @doc """ diff --git a/lib/universe/bank/websocket/channel/bank.ex b/lib/universe/bank/websocket/channel/bank.ex new file mode 100644 index 00000000..35269559 --- /dev/null +++ b/lib/universe/bank/websocket/channel/bank.ex @@ -0,0 +1,142 @@ +import Helix.Websocket.Channel + +channel Helix.Universe.Bank.Websocket.Channel.Bank do + @moduledoc """ + `BankChannel` handles incoming and outgoing messages between players and ] + bank servers + + Base errors (applicable to all requests expected to replay something): + + - "bad_request" - One or more request params are invalid + - "internal" - Something unexpected happened + """ + alias Helix.Universe.Bank.Websocket.Channel.Bank.Join, as: BankJoin + alias Helix.Universe.Bank.Websocket.Requests.Bootstrap, as: BootstrapRequest + alias Helix.Universe.Bank.Websocket.Requests.Transfer, as: BankTransferRequest + alias Helix.Universe.Bank.Websocket.Requests.CloseAccount, + as: BankCloseAccountRequest + alias Helix.Universe.Bank.Websocket.Requests.ChangePassword, + as: BankChangePasswordRequest + alias Helix.Universe.Bank.Websocket.Requests.RevealPassword, + as: BankRevealPasswordRequest + alias Helix.Universe.Bank.Websocket.Requests.Logout, as: LogoutRequest + + @doc """ + Join the Bank Channel + + Topic: "bank:@" + + Params: + *password: Target account password. + + Returns: BankAccountBootstrap + + Errors: + + Henforcer: + - "password_invalid": Password is invalid. + - "bank_account_not_found" + + Input: + + base errors + """ + + join "bank:" <> _, BankJoin + + @doc """ + Starts a financial transaction. + + Params: + *to_bank_ip: Receiving bank ip. + *to_bank_net: Receiving bank network. + *to_acc: Account number that receives money. + *password: Sending Bank Account password. + *value: Amount of money that is being sent. + + Returns: :ok + + Errors: + + Henforcer: + - "atm_not_a_bank" + - "bank_account_no_funds" + - "bank_account_not_found" + """ + topic "bank.transfer", BankTransferRequest + + @doc """ + Starts a password changing request + + Params: none + + Returns: :ok + + Errors: + - internal + + Henforcer: + - "bank_account_not_belongs" + """ + topic "bank.changepass", BankChangePasswordRequest + + @doc """ + Closes a Logged in BankAccount. + + Params: none + + Returns: :ok + + Errors: + - internal + + Henforcer: + - "bank_account_not_belongs" + """ + topic "bank.closeacc", BankCloseAccountRequest + + @doc """ + Starts a RevealPasswordProcess. + + Params: + *token: token.id for logged in account + + Henforcer: + - "token_not_belongs" + - "token_expired" + - "token_not_found" + """ + topic "bank.reveal", BankRevealPasswordRequest + + @doc """ + Forces a bootstrap to happen. It is the exact same operation ran during join. + Useful if the client wants to force a resynchronization of the local data. + + Params: none + + Returns: BankBootstrap + + Errors: + + base errors + """ + topic "bootstrap", BootstrapRequest + + @doc """ + Logs out from the channel + + Params: nil + + Returns: :ok + + **Channel will be closed** + + Errors: + - internal + """ + topic "bank.logout", LogoutRequest + + event_handler "event" + + def terminate(_reason, _socket) do + :ok + end +end diff --git a/lib/universe/bank/websocket/channel/bank/join.ex b/lib/universe/bank/websocket/channel/bank/join.ex new file mode 100644 index 00000000..7780d1ee --- /dev/null +++ b/lib/universe/bank/websocket/channel/bank/join.ex @@ -0,0 +1,367 @@ +import Helix.Websocket.Join + +join Helix.Universe.Bank.Websocket.Channel.Bank.Join do + @moduledoc """ + Joinable implementation for the Bank channel. + + There are two main methods to joining the Bank channel: + - using password + - using token + + On Logging with password the player has access for unlimited time on the + BankAccount that he logged in. + + On Logging with token the player has access for the time that the token is not + expired, when logged with a token the player can convert the token to the + password. + + Params: + - password: Password for given account. + - token: Token for the given account. + - *entity_id: Entity.id for the logging player. + - *gateway_id: Server.id for the logging server. + - bounce_id: Bounce id, if is nil logs with no bounce. + + The given password or token must match the given BankAccount, otherwise the + user is denied access to the BankAccount. + + It Returns Bootstrap for the logged BankAccount. + """ + + use Helix.Logger + + import HELL.Macros + + alias Helix.Websocket.Utils, as: WebsocketUtils + alias Helix.Account.Model.Account + alias Helix.Entity.Henforcer.Entity, as: EntityHenforcer + alias Helix.Entity.Query.Entity, as: EntityQuery + alias Helix.Server.Model.Server + alias Helix.Server.Henforcer.Server, as: ServerHenforcer + alias Helix.Network.Henforcer.Bounce, as: BounceHenforcer + alias Helix.Universe.Bank.Action.Flow.BankAccount, as: BankAccountFlow + alias Helix.Universe.Bank.Model.BankAccount + alias Helix.Universe.Bank.Henforcer.Bank, as: BankHenforcer + alias Helix.Universe.Bank.Public.Bank, as: BankPublic + + def check_params(request, _socket) do + {_, password} = + if request.unsafe["password"] do + validate_input(request.unsafe["password"], :password) + else + {:ok, nil} + end + + {_, token} = + if request.unsafe["token"] do + Ecto.UUID.cast(request.unsafe["token"]) + else + {:ok, nil} + end + with \ + "bank:" <> bank_account_id <- request.topic, + + # Get ATM.id and account number from topic + {account_number, atm_id} <- get_bank_account_id(bank_account_id), + + # Verify ATM.id parameter + {:ok, atm_id} <- Server.ID.cast(atm_id), + + # Verify account number parameter + account_number <- String.to_integer(account_number), + {:ok, account_number} <- BankAccount.cast(account_number), + + # Validate Account.id + {:ok, account_id} <- Account.ID.cast(request.unsafe["entity_id"]), + + # Validate Gateway.id + {:ok, gateway_id} <- Server.ID.cast(request.unsafe["gateway_id"]), + + # Validate Bounce.id + {:ok, bounce_id} <- validate_bounce(request.unsafe["bounce_id"]), + + # Check if has password or token + true <- not is_nil(password) or not is_nil(token) + do + params = %{ + atm_id: atm_id, + account_number: account_number, + account_id: account_id, + gateway_id: gateway_id, + bounce_id: bounce_id + } + + params = + cond do + not is_nil(password) -> + Map.put(params, :password, password) + not is_nil(token) -> + Map.put(params, :token, token) + true -> + params + end + + update_params(request, params, reply: true) + else + _ -> + bad_request(request) + end + end + + def check_permissions(request = %{params: %{token: token}}, _socket) do + atm_id = request.params.atm_id + account_number = request.params.account_number + account_id = request.params.account_id + entity_id = EntityQuery.get_entity_id(account_id) + gateway_id = request.params.gateway_id + bounce_id = request.params.bounce_id + + with \ + {true, r1} <- + BankHenforcer.can_join_token?(atm_id, account_number, token, entity_id), + bank_account = r1.bank_account, + entity_id = r1.entity.entity_id, + token = r1.token, + + # Checking if gateway exists + {true, r2} <- ServerHenforcer.server_exists?(gateway_id), + gateway = r2.server, + + # Checking if gateway belongs to entity + {true, r3} <- EntityHenforcer.owns_server?(entity_id, gateway), + gateway = r3.server, + + # Checking if bounce exists + {true, r4} <- BounceHenforcer.can_use_bounce?(entity_id, bounce_id), + bounce = r4.bounce + do + meta = + %{ + gateway: gateway, + account_id: account_id, + entity_id: entity_id, + bank_account: bank_account, + token: token, + bounce: bounce + } + update_meta(request, meta, reply: true) + else + {false, reason, _} -> + reply_error(request, reason) + _ -> + bad_request(request) + end + end + + def check_permissions(request = %{params: %{password: password}}, _socket) do + atm_id = request.params.atm_id + account_number = request.params.account_number + account_id = request.params.account_id + entity_id = EntityQuery.get_entity_id(account_id) + gateway_id = request.params.gateway_id + bounce_id = request.params.bounce_id + + with \ + {true, r1} <- + BankHenforcer.can_join_password?( + atm_id, + account_number, + password, + entity_id + ), + bank_account = r1.bank_account, + password = r1.password, + entity_id = r1.entity.entity_id, + + # Checking if gateway exists + {true, r2} <- ServerHenforcer.server_exists?(gateway_id), + gateway = r2.server, + + # Checking if gateway belongs to entity + {true, r3} <- EntityHenforcer.owns_server?(entity_id, gateway), + gateway = r3.server, + + # Checking if bounce exists + {true, r4} <- BounceHenforcer.can_use_bounce?(entity_id, bounce_id), + bounce = r4.bounce + do + meta = + %{ + gateway: gateway, + account_id: account_id, + entity_id: entity_id, + bank_account: bank_account, + password: password, + bounce: bounce + } + update_meta(request, meta, reply: true) + else + {false, reason, _} -> + reply_error(request, reason) + _ -> + bad_request(request) + end + end + + @doc """ + Joins a BankAccount. + """ + def join(request = %{meta: %{password: password}}, socket, assign) do + account_id = request.meta.account_id + gateway = request.meta.gateway + bounce = request.meta.bounce + entity_id = request.meta.entity_id + atm_id = request.meta.bank_account.atm_id + account_number = request.meta.bank_account.account_number + bank_account_id = {atm_id, account_number} + + bounce_id = + if bounce do + bounce.bounce_id + else + nil + end + + with \ + {:ok, tunnel, connection} <- + BankAccountFlow.login_password( + atm_id, + account_number, + gateway.server_id, + bounce_id, + password, + request.relay + ) + do + gateway_data = + %{ + server_id: gateway.server_id, + entity_id: entity_id + } + + bootstrap = + bank_account_id + |> BankPublic.bootstrap() + |> BankPublic.render_bootstrap() + |> WebsocketUtils.wrap_data() + + socket = + socket + |> assign.(:atm_id, atm_id) + |> assign.(:account_number, account_number) + |> assign.(:account_id, account_id) + |> assign.(:gateway, gateway_data) + |> assign.(:tunnel, tunnel) + |> assign.(:bank_login, connection) + + "bank:" <> topic_name = request.topic + + log :join, topic_name, + relay: request.relay, + data: %{ + channel: :bank, + bank_account: topic_name, + status: :ok + } + + {:ok, bootstrap, socket} + end + end + + def join(request = %{meta: %{token: token}}, socket, assign) do + account_id = request.meta.account_id + gateway = request.meta.gateway + bounce = request.meta.bounce + entity_id = request.meta.entity_id + atm_id = request.meta.bank_account.atm_id + account_number = request.meta.bank_account.account_number + bank_account_id = {atm_id, account_number} + + bounce_id = + if bounce do + bounce.bounce_id + else + nil + end + + with \ + {:ok, tunnel, connection} <- + BankAccountFlow.login_token( + atm_id, + account_number, + gateway.server_id, + bounce_id, + token.token_id, + request.relay + ) + do + gateway_data = + %{ + server_id: gateway.server_id, + entity_id: entity_id + } + + bootstrap = + bank_account_id + |> BankPublic.bootstrap() + |> BankPublic.render_bootstrap() + |> WebsocketUtils.wrap_data() + + socket = + socket + |> assign.(:atm_id, atm_id) + |> assign.(:account_number, account_number) + |> assign.(:account_id, account_id) + |> assign.(:gateway, gateway_data) + |> assign.(:tunnel, tunnel) + |> assign.(:bank_login, connection) + + "bank:" <> topic_name = request.topic + + log :join, topic_name, + relay: request.relay, + data: %{ + channel: :bank, + bank_account: topic_name, + status: :ok + } + + {:ok, bootstrap, socket} + end + end + + docp """ + Gets the atm_id and account_number united by an `@` separate then and return + as a Tuple + """ + @spec get_bank_account_id(String.t) :: + {String.t, String.t} + + defp get_bank_account_id(bank_account_id) do + bank_account_id + |> String.split("@") + |> List.to_tuple + end + + def log_error(request, _socket, reason) do + "bank:" <> topic_name = request.topic + + id = + if Enum.empty?(request.params) do + nil + else + topic_name + end + + log :join, id, + relay: request.relay, + data: + %{ + channel: :bank, + type: request.type, + status: :error, + reason: reason + } + end + +end diff --git a/lib/universe/bank/websocket/channel/requests/bootstrap.ex b/lib/universe/bank/websocket/channel/requests/bootstrap.ex new file mode 100644 index 00000000..d287c3bf --- /dev/null +++ b/lib/universe/bank/websocket/channel/requests/bootstrap.ex @@ -0,0 +1,37 @@ +import Helix.Websocket.Request + +request Helix.Universe.Bank.Websocket.Requests.Bootstrap do + @moduledoc """ + BankBootstrapRequest is used to allow the client to resync its local data + with the Helix server. + + It returns the BankBootstrap, which is the exact same struct returned after + joining a local or remote bank Channel. + """ + alias Helix.Universe.Bank.Public.Bank, as: BankPublic + + def check_params(request, _socket) do + reply_ok(request) + end + + def check_permissions(request, _socket) do + reply_ok(request) + end + + def handle_request(request, socket) do + atm_id = socket.assigns.atm_id + account_number = socket.assigns.account_number + bank_account = {atm_id, account_number} + + bootstrap = + BankPublic.bootstrap(bank_account) + + update_meta(request, %{bootstrap: bootstrap}, reply: true) + end + + render(request, _socket) do + data = BankPublic.render_bootstrap(request.meta.bootstrap) + + {:ok, data} + end +end diff --git a/lib/universe/bank/websocket/channel/requests/change_password.ex b/lib/universe/bank/websocket/channel/requests/change_password.ex new file mode 100644 index 00000000..0c3236eb --- /dev/null +++ b/lib/universe/bank/websocket/channel/requests/change_password.ex @@ -0,0 +1,58 @@ +import Helix.Websocket.Request + +request Helix.Universe.Bank.Websocket.Requests.ChangePassword do + @moduledoc """ + BankChangePasswordRequest is used when the player wants to change + he's BankAccount's password. + + It Returns :ok or :error + """ + alias Helix.Entity.Query.Entity, as: EntityQuery + alias Helix.Server.Query.Server, as: ServerQuery + alias Helix.Universe.Bank.Public.Bank, as: BankPublic + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + alias Helix.Universe.Bank.Henforcer.Bank, as: BankHenforcer + + def check_params(request, _socket) do + reply_ok(request) + end + + @doc """ + Checks if Logged player owns the BankAccount, most of logic is delegated to + BankHenforcer.owns_account?/2 + """ + def check_permissions(request, socket) do + entity = EntityQuery.fetch(socket.assigns.gateway.entity_id) + atm_id = socket.assigns.atm_id + account_number = socket.assigns.account_number + bank_account = BankQuery.fetch_account(atm_id, account_number) + with \ + {true, relay} <- BankHenforcer.owns_account?(entity, bank_account), + bank_account = relay.bank_account + do + update_meta(request, %{bank_account: bank_account}, reply: true) + else + {false, reason, _} -> + reply_error(request, reason) + end + end + + def handle_request(request, socket) do + atm_id = socket.assigns.atm_id + account = request.meta.bank_account + atm = ServerQuery.fetch(atm_id) + gateway = ServerQuery.fetch(socket.assigns.gateway.server_id) + + password_change = + BankPublic.change_password(account, gateway, atm, request.relay) + + case password_change do + {:ok, process} -> + update_meta(request, %{process: process}, reply: true) + {:error, reason} -> + reply_error(request, reason) + end + end + + render_empty() +end diff --git a/lib/universe/bank/websocket/channel/requests/close_account.ex b/lib/universe/bank/websocket/channel/requests/close_account.ex new file mode 100644 index 00000000..9af909b8 --- /dev/null +++ b/lib/universe/bank/websocket/channel/requests/close_account.ex @@ -0,0 +1,53 @@ +import Helix.Websocket.Request + +request Helix.Universe.Bank.Websocket.Requests.CloseAccount do + @moduledoc """ + BankCloseAccountRequest is used when player wants to close the + BankAccount. + + It Returs :ok or :error + """ + alias Helix.Server.Query.Server, as: ServerQuery + alias Helix.Universe.Bank.Henforcer.Bank, as: BankHenforcer + alias Helix.Universe.Bank.Public.Bank, as: BankPublic + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + + def check_params(request, _socket) do + reply_ok(request) + end + + @doc """ + Checks if player can close BankAccount, most of logic is delegated to + BankHenforcer.can_close?/2 + """ + def check_permissions(request, socket) do + atm_id = socket.assigns.atm_id + account_number = socket.assigns.account_number + with \ + {true, _relay} <- BankHenforcer.can_close?(atm_id, account_number) + do + reply_ok(request) + else + {false, reason, _} -> + reply_error(request, reason) + end + end + + def handle_request(request, socket) do + atm_id = socket.assigns.atm_id + account_number = socket.assigns.account_number + bank_account = BankQuery.fetch_account(atm_id, account_number) + atm = ServerQuery.fetch(atm_id) + relay = request.relay + gateway = ServerQuery.fetch(socket.assigns.gateway.server_id) + process = BankPublic.close_account(gateway, bank_account, atm, relay) + + case process do + {:ok, process} -> + update_meta(request, %{process: process}, reply: true) + {:error, reason} -> + reply_error(request, reason) + end + end + render_empty() +end diff --git a/lib/universe/bank/websocket/channel/requests/create_account.ex b/lib/universe/bank/websocket/channel/requests/create_account.ex new file mode 100644 index 00000000..8b7e3702 --- /dev/null +++ b/lib/universe/bank/websocket/channel/requests/create_account.ex @@ -0,0 +1,76 @@ +import Helix.Websocket.Request + +request Helix.Universe.Bank.Websocket.Requests.CreateAccount do + @moduledoc """ + BankCreateAccountRequest is used when the player want to create + a BankAccount on given bank. + + It Returns :ok or :error + """ + alias Helix.Server.Model.Server + alias Helix.Server.Henforcer.Server, as: ServerHenforcer + alias Helix.Server.Query.Server, as: ServerQuery + alias Helix.Universe.Bank.Public.Bank, as: BankPublic + alias Helix.Universe.Bank.Henforcer.Bank, as: BankHenforcer + + def check_params(request, _socket) do + with \ + {:ok, atm_id} <- Server.ID.cast(request.unsafe["atm_id"]), + {:ok, gateway_id} <- Server.ID.cast(request.unsafe["gateway"]) + do + params = + %{ + atm_id: atm_id, + gateway_id: gateway_id + } + + update_params(request, params, reply: true) + else + _ -> + bad_request(request) + end + end + + @doc """ + Checks if player can open BankAccount, most of logic is delegated to + BankHenforcer.can_create_account?/1 + """ + def check_permissions(request, _socket) do + atm = ServerQuery.fetch(request.params.atm_id) + with \ + {true, r1} <- BankHenforcer.can_create_account?(atm), + atm_id = r1.atm_id, + {true, r2} <- ServerHenforcer.server_exists?(request.params.gateway_id) + do + meta = + %{ + atm_id: atm_id, + gateway_id: r2.server.server_id + } + + update_meta(request, meta, reply: true) + else + {false, reason, _} -> + reply_error(request, reason) + _ -> + bad_request(request) + end + end + + def handle_request(request, socket) do + atm_id = request.meta.atm_id + account_id = socket.assigns.account_id + gateway = ServerQuery.fetch(request.meta.gateway_id) + atm = ServerQuery.fetch(atm_id) + relay = request.relay + + process = BankPublic.open_account(gateway, account_id, atm, relay) + case process do + {:ok, process} -> + update_meta(request, %{process: process}, reply: true) + {:error, reason} -> + reply_error(request, reason) + end + end + render_empty() +end diff --git a/lib/universe/bank/websocket/channel/requests/logout.ex b/lib/universe/bank/websocket/channel/requests/logout.ex new file mode 100644 index 00000000..8339eafc --- /dev/null +++ b/lib/universe/bank/websocket/channel/requests/logout.ex @@ -0,0 +1,39 @@ +import Helix.Websocket.Request + +request Helix.Universe.Bank.Websocket.Requests.Logout do + + alias Helix.Event + alias Helix.Websocket + + alias Helix.Universe.Bank.Event.Bank.Account.Logout, + as: BankAccountLogoutEvent + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + + def check_params(request, _socket), + do: reply_ok(request) + + def check_permissions(request, _socket), + do: reply_ok(request) + + def handle_request(request, socket) do + atm_id = socket.assigns.atm_id + account_number = socket.assigns.account_number + account = BankQuery.fetch_account(atm_id, account_number) + + entity_id = socket.assigns.gateway.entity_id + + events = + [BankAccountLogoutEvent.new(account, entity_id)] + + socket + |> Websocket.id() + |> Helix.Endpoint.broadcast("disconnect", %{}) + + Event.emit(events) + + reply_ok(request) + end + + def reply(_request, _socket), + do: {:stop, :shutdown} +end diff --git a/lib/universe/bank/websocket/channel/requests/reveal_password.ex b/lib/universe/bank/websocket/channel/requests/reveal_password.ex new file mode 100644 index 00000000..9a7a5346 --- /dev/null +++ b/lib/universe/bank/websocket/channel/requests/reveal_password.ex @@ -0,0 +1,76 @@ +import Helix.Websocket.Request + +request Helix.Universe.Bank.Websocket.Requests.RevealPassword do + @moduledoc """ + This request is called when the player wants to get the password for + a bank account which he's logged in by using a token + """ + + alias Helix.Server.Query.Server, as: ServerQuery + alias Helix.Universe.Bank.Henforcer.Bank, as: BankHenforcer + alias Helix.Universe.Bank.Public.Bank, as: BankPublic + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + + def check_params(request, _socket) do + # Check if token value has the correct format + with \ + {:ok, token} <- validate_input(request.unsafe["token"], :token) + do + update_params(request, %{token: token}, reply: true) + else + _ -> + bad_request(request) + end + end + + @doc """ + Verifies the permission for the password reveal. Most of the perimssion + logic has been delegated to `BankHenforcer.token_valid?/2` + + This is where we the token.account_number, token.atm_id is the same + as the logged in bank account and if expiration date is not expired + """ + def check_permissions(request, socket) do + token = request.params.token + atm_id = socket.assigns.atm_id + account_number = socket.assigns.account_number + bank_account = BankQuery.fetch_account(atm_id, account_number) + case BankHenforcer.token_valid?(bank_account, token) do + {true, relay} -> + token = relay.token + update_meta(request, %{token: token}, reply: true) + {false, reason, _} -> + reply_error(request, reason) + end + end + + def handle_request(request, socket) do + atm_id = socket.assigns.atm_id + account_number = socket.assigns.account_number + bank_account = BankQuery.fetch_account(atm_id, account_number) + token = request.meta.token.token_id + gateway_id = socket.assigns.gateway.server_id + gateway = ServerQuery.fetch(gateway_id) + atm = ServerQuery.fetch(atm_id) + relay = request.relay + + reveal_password = + BankPublic.reveal_password( + bank_account, + token, + gateway, + atm, + relay + ) + + case reveal_password do + {:ok, process} -> + update_meta(request, %{process: process}, reply: true) + {:error, reason} -> + reply_error(request, reason) + end + end + + render_empty() + +end diff --git a/lib/universe/bank/websocket/channel/requests/transfer.ex b/lib/universe/bank/websocket/channel/requests/transfer.ex new file mode 100644 index 00000000..228fa0d2 --- /dev/null +++ b/lib/universe/bank/websocket/channel/requests/transfer.ex @@ -0,0 +1,112 @@ +import Helix.Websocket.Request + +request Helix.Universe.Bank.Websocket.Requests.Transfer do + @moduledoc """ + BankTransferRequest is used when the player transfer money from an account to other. + + It Returns :ok or :error + """ + + alias HELL.IPv4 + alias Helix.Account.Query.Account, as: AccountQuery + alias Helix.Network.Model.Network + alias Helix.Server.Query.Server, as: ServerQuery + alias Helix.Universe.Bank.Henforcer.Bank, as: BankHenforcer + alias Helix.Universe.Bank.Model.BankAccount + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + alias Helix.Universe.Bank.Public.Bank, as: BankPublic + + def check_params(request, _socket) do + with \ + receiving_bank_acc <- request.unsafe["to_acc"], + {:ok, receiving_bank_acc} <- BankAccount.cast(receiving_bank_acc), + {:ok, network_id} <- Network.ID.cast(request.unsafe["to_bank_net"]), + {:ok, ip} <- IPv4.cast(request.unsafe["to_bank_ip"]), + amount <- request.unsafe["amount"], + {true, amount} <- (fn amount -> {amount > 0, amount} end).(amount) + do + params = + %{ + bank_account: receiving_bank_acc, + bank_ip: ip, + bank_net: network_id, + amount: amount + } + update_params(request, params, reply: true) + else + _ -> + bad_request(request) + end + end + + def check_permissions(request, socket) do + nip = {request.params.bank_net, request.params.bank_ip} + atm_id = socket.assigns.atm_id + account_number = socket.assigns.account_number + sending_acc = {socket.assigns.atm_id, socket.assigns.account_number} + receiving_acc = request.params.bank_account + amount = request.params.amount + gateway_id = socket.assigns.gateway.server_id + account_id = socket.assigns.account_id + + with \ + {true, relay} <- + BankHenforcer.can_transfer?( + nip, + receiving_acc, + sending_acc, + amount + ), + amount = relay.amount, + to_account = relay.to_account + do + meta = + %{ + to_account: to_account, + from_account: BankQuery.fetch_account(atm_id, account_number), + amount: amount, + started_by: AccountQuery.fetch(account_id), + gateway: ServerQuery.fetch(gateway_id) + } + + update_meta(request, meta, reply: true) + else + {false, reason, _} -> + reply_error(request, reason) + _ -> + bad_request(request) + end + end + + def handle_request(request, socket) do + to_account = request.meta.to_account + from_account = request.meta.from_account + amount = request.meta.amount + started_by = request.meta.started_by + relay = request.relay + gateway = request.meta.gateway + tunnel = socket.assigns.tunnel + + transfer = + BankPublic.transfer( + from_account, + to_account, + amount, + started_by, + gateway, + tunnel, + relay + ) + + case transfer do + {:ok, process} -> + update_meta(request, %{process: process}, reply: true) + + {:error, reason} -> + reply_error(request, reason) + end + end + + render_empty() + +end diff --git a/lib/universe/npc/internal/web.ex b/lib/universe/npc/internal/web.ex index c63254fb..5b586893 100644 --- a/lib/universe/npc/internal/web.ex +++ b/lib/universe/npc/internal/web.ex @@ -1,6 +1,7 @@ defmodule Helix.Universe.NPC.Internal.Web do alias HELL.IPv4 + alias Helix.Cache.Query.Cache, as: CacheQuery alias Helix.Network.Model.Network alias Helix.Universe.NPC.Model.NPC @@ -18,9 +19,14 @@ defmodule Helix.Universe.NPC.Internal.Web do Map.merge(common, custom) end - def generate_content(%NPC{npc_type: :bank}, _net, _ip) do + def generate_content(%NPC{npc_type: :bank}, net, ip) do common = common("Nubank") - custom = %{} + {_, atm_id} = + CacheQuery.from_nip_get_server(net, ip) + + custom = %{ + atm_id: atm_id + } Map.merge(common, custom) end diff --git a/lib/websocket/websocket.ex b/lib/websocket/websocket.ex index be74082c..2218d273 100644 --- a/lib/websocket/websocket.ex +++ b/lib/websocket/websocket.ex @@ -35,6 +35,7 @@ defmodule Helix.Websocket do channel "account:*", Helix.Account.Websocket.Channel.Account channel "server:*", Helix.Server.Websocket.Channel.Server + channel "bank:*", Helix.Universe.Bank.Websocket.Channel.Bank unless Mix.env == :prod do channel "logflix", HELL.Logflix diff --git a/test/support/channel/helper.ex b/test/support/channel/helper.ex index 91d7ac5a..4c914e5b 100644 --- a/test/support/channel/helper.ex +++ b/test/support/channel/helper.ex @@ -11,4 +11,7 @@ defmodule Helix.Test.Channel.Helper do "server:" <> network_id <> "@" <> ip <> "#" <> counter end + + def bank_topic_name(atm_id = %Server.ID{}, account_number), + do: "bank:" <> to_string(account_number) <> "@" <> to_string(atm_id) end diff --git a/test/support/channel/setup.ex b/test/support/channel/setup.ex index 65caf198..bac0e82a 100644 --- a/test/support/channel/setup.ex +++ b/test/support/channel/setup.ex @@ -20,8 +20,11 @@ defmodule Helix.Test.Channel.Setup do alias Helix.Test.Cache.Helper, as: CacheHelper alias Helix.Test.Entity.Setup, as: EntitySetup alias Helix.Test.Network.Helper, as: NetworkHelper + alias Helix.Test.Network.Setup, as: NetworkSetup + alias Helix.Test.Network.Setup.Connection, as: ConnectionSetup alias Helix.Test.Server.Setup, as: ServerSetup alias Helix.Test.Software.Setup, as: SoftwareSetup + alias Helix.Test.Universe.Bank.Setup, as: BankSetup alias Helix.Test.Channel.Helper, as: ChannelHelper @endpoint Helix.Endpoint @@ -386,6 +389,50 @@ defmodule Helix.Test.Channel.Setup do } end + def mock_bank_socket(opts \\ []) do + gateway_id = Keyword.get(opts, :gateway_id, ServerSetup.id) + gateway_entity_id = Keyword.get(opts, :gateway_entity_id, EntitySetup.id()) + + account = BankSetup.account!() + atm_id = Keyword.get(opts, :atm_id, account.atm_id) + account_number = Keyword.get(opts, :account_number, account.account_number) + + account = AccountSetup.account!() + account_id = account.account_id + tunnel = NetworkSetup.tunnel!(gateway_id: gateway_id, target_id: atm_id) + + connection = + ConnectionSetup.connection( + entity_id: gateway_entity_id + ) + + gateway_data = + %{ + server_id: gateway_id, + entity_id: gateway_entity_id + } + + assigns = + %{ + atm_id: atm_id, + account_number: account_number, + account_id: account_id, + gateway: gateway_data, + tunnel: tunnel, + bank_login: connection + } + + assigns = + (opts[:connect_opts] || []) + |> fake_connection_socket_assigns() + |> Map.merge(assigns) + + %{ + assigns: assigns, + joined: true + } + end + @doc """ Opts: @@ -395,7 +442,9 @@ defmodule Helix.Test.Channel.Setup do """ def fake_connection_socket_assigns(opts \\ []) do gen_account_id = fn entity_id -> - entity_id |> to_string() |> Account.ID.cast!() + entity_id + |> to_string() + |> Account.ID.cast!() end entity_id = Keyword.get(opts, :entity_id, Entity.ID.generate()) diff --git a/test/support/event/setup/bank.ex b/test/support/event/setup/bank.ex index b3d6a636..46503eb8 100644 --- a/test/support/event/setup/bank.ex +++ b/test/support/event/setup/bank.ex @@ -5,8 +5,20 @@ defmodule Helix.Test.Event.Setup.Bank do alias Helix.Universe.Bank.Event.Bank.Account.Login, as: BankAccountLoginEvent + alias Helix.Universe.Bank.Event.Bank.Account.Removed, + as: BankAccountRemovedEvent + alias Helix.Universe.Bank.Event.Bank.Account.Updated, + as: BankAccountUpdatedEvent + alias Helix.Universe.Bank.Event.Bank.Account.Logout, + as: BankAccountLogoutEvent alias Helix.Universe.Bank.Event.Bank.Account.Password.Revealed, as: BankAccountPasswordRevealedEvent + alias Helix.Universe.Bank.Event.Bank.Account.Password.Changed, + as: BankAccountPasswordChangedEvent + alias Helix.Universe.Bank.Event.ChangePassword.Processed, + as: ChangePasswordProcessedEvent + alias Helix.Universe.Bank.Event.RevealPassword.Processed, + as: RevealPasswordProcessedEvent alias Helix.Universe.Bank.Event.Bank.Account.Token.Acquired, as: BankAccountTokenAcquiredEvent @@ -46,9 +58,45 @@ defmodule Helix.Test.Event.Setup.Bank do %{event| account: account} end + @doc """ + Accepts (BankAccount.t, Server.id) + """ + def password_change_processed(account, gateway_id), + do: ChangePasswordProcessedEvent.new(account, gateway_id) + + @doc """ + Accepts (BankAccount.t, Server.id) + """ + def password_reveal_processed(account, gateway_id, token_id), + do: RevealPasswordProcessedEvent.new(account, gateway_id, token_id) + + @doc """ + Accepts (BankAccount.t) + """ + def password_changed(account), + do: BankAccountPasswordChangedEvent.new(account) + @doc """ Accepts: (BankAccount.t, Entity.id) """ def login(account, entity_id, token_id \\ nil), do: BankAccountLoginEvent.new(account, entity_id, token_id) + + @doc """ + Accepts: (BankAccount.t, Entity.id) + """ + def logout(account, entity_id), + do: BankAccountLogoutEvent.new(account, entity_id) + + @doc """ + Accepts: (BankAccount.t, term) + """ + def updated(account, reason), + do: BankAccountUpdatedEvent.new(account, reason) + + @doc """ + Accepts: (BankAccount.t) + """ + def removed(account), + do: BankAccountRemovedEvent.new(account) end diff --git a/test/support/event/setup/bank_transfer.ex b/test/support/event/setup/bank_transfer.ex new file mode 100644 index 00000000..ad86cfba --- /dev/null +++ b/test/support/event/setup/bank_transfer.ex @@ -0,0 +1,24 @@ +defmodule Helix.Test.Event.Setup.BankTransfer do + + alias Helix.Universe.Bank.Event.Bank.Transfer.Processed, + as: BankTransferProcessedEvent + alias Helix.Universe.Bank.Event.Bank.Transfer.Aborted, + as: BankTransferAbortedEvent + alias Helix.Universe.Bank.Event.Bank.Transfer.Successful, + as: BankTransferSuccessfulEvent + alias Helix.Universe.Bank.Event.Bank.Transfer.Failed, + as: BankTransferFailedEvent + + def processed(process, transfer_proc), + do: BankTransferProcessedEvent.new(process, transfer_proc) + + def aborted(process, transfer_proc), + do: BankTransferAbortedEvent.new(process, transfer_proc) + + def successful(transfer_id), + do: BankTransferSuccessfulEvent.new(transfer_id) + + def failed(transfer_id, reason), + do: BankTransferFailedEvent.new(transfer_id, reason) + +end diff --git a/test/support/process/setup/data.ex b/test/support/process/setup/data.ex index fff5eb6e..d133a49d 100644 --- a/test/support/process/setup/data.ex +++ b/test/support/process/setup/data.ex @@ -26,11 +26,12 @@ defmodule Helix.Test.Process.Data.Setup do alias Helix.Software.Model.SoftwareType.LogForge alias Helix.Software.Process.File.Transfer, as: FileTransferProcess alias Helix.Software.Process.File.Install, as: FileInstallProcess + alias Helix.Universe.Bank.Process.Bank.Account.ChangePassword, + as: BankChangePassword alias HELL.TestHelper.Random alias Helix.Test.Log.Helper, as: LogHelper alias Helix.Test.Process.Helper.TOP, as: TOPHelper - @doc """ Chooses a random implementation and uses it. Beware that `data_opts`, used by `custom/3`, is always an empty list when called from `random/1`. @@ -166,6 +167,20 @@ defmodule Helix.Test.Process.Data.Setup do {:cracker_bruteforce, data, meta, resources} end + def custom(:bank_change_password, _data_opts, meta) do + data = BankChangePassword.new(%{}) + + resources = + %{ + l_dynamic: [:cpu], + r_dynamic: [], + static: TOPHelper.Resources.random_static(), + objective: TOPHelper.Resources.objective(cpu: 500) + } + + {:bank_login, data, meta, resources} + end + @doc """ Probably does not work """ @@ -196,7 +211,7 @@ defmodule Helix.Test.Process.Data.Setup do - operation: :edit | :create. Defaults to :edit. - target_log_id: Which log to edit. Won't generate a real one. - message: Revision message. - + All others are automatically derived from process meta data. """ def custom(:forge, data_opts, meta) do diff --git a/test/support/software/helper.ex b/test/support/software/helper.ex index b2eda377..9a71df60 100644 --- a/test/support/software/helper.ex +++ b/test/support/software/helper.ex @@ -137,8 +137,8 @@ defmodule Helix.Test.Software.Helper do path_size = (byte_size(path) - 1) * 8 case path do - <> <> "/" -> - <> + <> <> "/" -> + <> path -> path end diff --git a/test/support/universe/bank/helper.ex b/test/support/universe/bank/helper.ex index c5a65240..1085f667 100644 --- a/test/support/universe/bank/helper.ex +++ b/test/support/universe/bank/helper.ex @@ -20,4 +20,10 @@ defmodule Helix.Test.Universe.Bank.Helper do """ def amount, do: Random.number(min: 1, max: 5000) + + @doc """ + Generates a random password + """ + def password, + do: Random.string(min: 8, max: 20) end diff --git a/test/support/universe/bank/setup.ex b/test/support/universe/bank/setup.ex old mode 100644 new mode 100755 index d393d874..93ab994f --- a/test/support/universe/bank/setup.ex +++ b/test/support/universe/bank/setup.ex @@ -80,13 +80,15 @@ defmodule Helix.Test.Universe.Bank.Setup do 0 end + password = Keyword.get(opts, :password, BankHelper.password()) + acc = %BankAccount{ account_number: number, balance: balance, bank_id: bank.id, atm_id: atm_id, - password: "secret", + password: password, owner_id: owner_id, creation_date: DateTime.utc_now() } @@ -114,6 +116,12 @@ defmodule Helix.Test.Universe.Bank.Setup do {inserted, related} end + def transfer!(opts \\ []) do + {transfer, related} = transfer(opts) + + transfer + end + @doc """ - transfer_id: Force the transfer to have this id. - amount: Specify transfer amount. Defaults to a random number. @@ -175,6 +183,11 @@ defmodule Helix.Test.Universe.Bank.Setup do {inserted, related} end + def token!(opts \\ []) do + {inserted, _related} = token(opts) + + inserted + end @doc """ - acc: Associate that account to the token (BankAccount.t) - connection_id: Specify the connection ID. @@ -206,7 +219,10 @@ defmodule Helix.Test.Universe.Bank.Setup do |> Kernel.+(-1) |> DateTime.from_unix!(:second) else - nil + DateTime.utc_now() + |> DateTime.to_unix(:second) + |> Kernel.+(60 * 5) + |> DateTime.from_unix!(:second) end token = @@ -262,9 +278,9 @@ defmodule Helix.Test.Universe.Bank.Setup do {server, %{entity: entity}} = ServerSetup.server() # Login with the right password - {:ok, connection} = + {:ok, _, connection} = BankAccountFlow.login_password( - acc.atm_id, acc.account_number, server.server_id, nil, acc.password + acc.atm_id, acc.account_number, server.server_id, nil, acc.password, nil ) {connection, %{acc: acc, server: server, entity: entity}} diff --git a/test/universe/bank/action/bank_test.exs b/test/universe/bank/action/bank_test.exs index ba1021d4..743967a4 100644 --- a/test/universe/bank/action/bank_test.exs +++ b/test/universe/bank/action/bank_test.exs @@ -56,9 +56,9 @@ defmodule Helix.Universe.Bank.Action.BankTest do {transfer, %{acc1: account_from, acc2: account_to}} = BankSetup.transfer([amount: amount]) - assert :ok == BankAction.complete_transfer(transfer) + assert {:ok, transfer, _events} = BankAction.complete_transfer(transfer) - refute BankTransferInternal.fetch(transfer) + refute BankTransferInternal.fetch(transfer.transfer_id) assert BankAccountInternal.get_balance(account_from) == 0 assert BankAccountInternal.get_balance(account_to) == amount end @@ -66,7 +66,9 @@ defmodule Helix.Universe.Bank.Action.BankTest do test "with invalid data" do {fake_transfer, _} = BankSetup.fake_transfer() - assert {:error, reason} = BankAction.complete_transfer(fake_transfer) + assert {:error, reason, _events} = + BankAction.complete_transfer(fake_transfer) + assert reason == {:transfer, :notfound} end end @@ -77,7 +79,7 @@ defmodule Helix.Universe.Bank.Action.BankTest do {transfer, %{acc1: account_from, acc2: account_to}} = BankSetup.transfer([amount: amount]) - assert :ok == BankAction.abort_transfer(transfer) + assert :ok = BankAction.abort_transfer(transfer) refute BankTransferInternal.fetch(transfer) assert BankAccountInternal.get_balance(account_from) == amount @@ -103,7 +105,7 @@ defmodule Helix.Universe.Bank.Action.BankTest do |> Map.get(:id) |> ServerQuery.fetch() - assert {:ok, acc} = BankAction.open_account(player, atm) + assert {:ok, acc, _events} = BankAction.open_account(player, atm) assert acc.account_number assert acc.owner_id == player.account_id @@ -117,8 +119,7 @@ defmodule Helix.Universe.Bank.Action.BankTest do test "closes the account" do {acc, _} = BankSetup.account() - assert BankAccountInternal.fetch(acc.atm_id, acc.account_number) - assert :ok == BankAction.close_account(acc) + assert {:ok, _events} = BankAction.close_account(acc) refute BankAccountInternal.fetch(acc.atm_id, acc.account_number) end @@ -127,14 +128,14 @@ defmodule Helix.Universe.Bank.Action.BankTest do assert BankAccountInternal.fetch(acc.atm_id, acc.account_number) assert {:error, reason} = BankAction.close_account(acc) - assert reason == {:account, :notempty} + assert reason == {:bank_account, :not_empty} assert BankAccountInternal.fetch(acc.atm_id, acc.account_number) end test "with invalid data" do {fake_acc, _} = BankSetup.fake_account() assert {:error, reason} = BankAction.close_account(fake_acc) - assert reason == {:account, :notfound} + assert reason == {:bank_account, :not_found} end end @@ -254,14 +255,16 @@ defmodule Helix.Universe.Bank.Action.BankTest do {token, _} = BankSetup.token() entity_id = Entity.ID.generate() - refute BankAction.login_token(acc, token.token_id, entity_id) + assert {:error, {:token, :not_belongs}} = + BankAction.login_token(acc, token.token_id, entity_id) end test "login fails when given token is expired" do {token, %{acc: acc}} = BankSetup.token([expired: true]) entity_id = Entity.ID.generate() - refute BankAction.login_token(acc, token.token_id, entity_id) + assert {:error, {:token, :expired}} = + BankAction.login_token(acc, token.token_id, entity_id) end end diff --git a/test/universe/bank/action/flow/bank_account_test.exs b/test/universe/bank/action/flow/bank_account_test.exs index 0e633414..7828d038 100644 --- a/test/universe/bank/action/flow/bank_account_test.exs +++ b/test/universe/bank/action/flow/bank_account_test.exs @@ -3,12 +3,15 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccountTest do use Helix.Test.Case.Integration alias HELL.Utils + alias Helix.Entity.Model.Entity alias Helix.Entity.Query.Database, as: DatabaseQuery alias Helix.Network.Query.Tunnel, as: TunnelQuery alias Helix.Process.Query.Process, as: ProcessQuery alias Helix.Server.Query.Server, as: ServerQuery alias Helix.Universe.Bank.Action.Flow.BankAccount, as: BankAccountFlow + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + alias Helix.Test.Account.Setup, as: AccountSetup alias Helix.Test.Universe.Bank.Setup, as: BankSetup alias Helix.Test.Entity.Database.Setup, as: DatabaseSetup alias Helix.Test.Process.TOPHelper @@ -58,6 +61,39 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccountTest do end end + describe "change_password/4" do + test "default life cycle" do + bank_account = BankSetup.account!() + {gateway, _} = ServerSetup.server() + + atm = ServerQuery.fetch(bank_account.atm_id) + old_password = bank_account.password + + # Create process to change password + {:ok, process} = + BankAccountFlow.change_password(bank_account, gateway, atm, @relay) + + # Ensure process is valid + assert process.gateway_id == gateway.server_id + assert process.target_id == bank_account.atm_id + + assert process.src_atm_id == bank_account.atm_id + assert process.src_acc_number == bank_account.account_number + + TOPHelper.force_completion(process) + + # Process no longer exists + refute ProcessQuery.fetch(process.process_id) + + # Ensure it changed the `BankAccount`'s password + atm_id = bank_account.atm_id + account_number = bank_account.account_number + bank_account = BankQuery.fetch_account(atm_id, account_number) + + refute bank_account.password == old_password + end + end + describe "login_password/5" do test "login with valid password on third-party account" do time_before_event = Utils.date_before(1) @@ -65,7 +101,7 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccountTest do {server, %{entity: entity}} = ServerSetup.server() # Login with the right password - assert {:ok, connection} = + assert {:ok, _tunnel, connection} = BankAccountFlow.login_password( acc.atm_id, acc.account_number, server.server_id, nil, acc.password ) @@ -96,7 +132,7 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccountTest do assert to_string(acc.owner_id) == to_string(entity.entity_id) # Login with the right password - assert {:ok, connection} = + assert {:ok, _tunnel, connection} = BankAccountFlow.login_password( acc.atm_id, acc.account_number, server.server_id, nil, acc.password ) @@ -146,7 +182,7 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccountTest do {server, %{entity: entity}} = ServerSetup.server() # Login with the right credentials - assert {:ok, connection} = + assert {:ok, _tunnel, connection} = BankAccountFlow.login_token( acc.atm_id, acc.account_number, server.server_id, nil, token.token_id ) @@ -179,7 +215,7 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccountTest do assert to_string(acc.owner_id) == to_string(entity.entity_id) # Login with the right token - assert {:ok, connection} = + assert {:ok, _tunnel, connection} = BankAccountFlow.login_token( acc.atm_id, acc.account_number, server.server_id, nil, token.token_id ) @@ -236,4 +272,53 @@ defmodule Helix.Universe.Bank.Action.Flow.BankAccountTest do refute DatabaseQuery.fetch_bank_account(entity, acc) end end + + describe "open/4" do + test "creates a process to opens account when everything is OK" do + {account, %{server: gateway}} = AccountSetup.account(with_server: true) + + account_id = account.account_id + + bank_acc = BankSetup.account!() + atm_id = bank_acc.atm_id + atm = ServerQuery.fetch(atm_id) + + assert {:ok, process} = + BankAccountFlow.open(gateway, account_id, atm, @relay) + + assert process.data.atm_id == atm_id + assert process.source_entity_id == %Entity.ID{id: account_id.id} + + TOPHelper.force_completion(process) + + refute ProcessQuery.fetch(process.process_id) + end + end + + describe "close/4" do + test "creates a process to close the given bank account" do + {account, %{server: gateway}} = AccountSetup.account(with_server: true) + account_id = account.account_id + + entity_id = %Entity.ID{id: account_id.id} + + bank_acc = BankSetup.account!(owner_id: entity_id) + + atm_id = bank_acc.atm_id + atm = ServerQuery.fetch(atm_id) + account_number = bank_acc.account_number + + assert {:ok, process} = + BankAccountFlow.close(gateway, bank_acc, atm, @relay) + + assert process.data.atm_id == atm_id + assert process.data.account_number == account_number + + TOPHelper.force_completion(process) + + refute ProcessQuery.fetch(process.process_id) + + refute BankQuery.fetch_account(atm_id, account_number) + end + end end diff --git a/test/universe/bank/event/bank/account/password_test.exs b/test/universe/bank/event/bank/account/password_test.exs new file mode 100644 index 00000000..84f60ea7 --- /dev/null +++ b/test/universe/bank/event/bank/account/password_test.exs @@ -0,0 +1,33 @@ +defmodule Helix.Universe.Bank.Event.PasswordTest do + + use Helix.Test.Case.Integration + + alias Helix.Event + + alias Helix.Entity.Query.Database, as: DatabaseQuery + + alias Helix.Test.Event.Setup, as: EventSetup + alias Helix.Test.Entity.Setup, as: EntitySetup + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + describe "event reactions" do + test "accepts if password is added to database when event is fired" do + # Setups a Bank Account + bank_acc = BankSetup.account!() + + # Setups a new Entity and gets it's id + entity_id = EntitySetup.entity!().entity_id + + # Setups a BankAccountPasswordRevealedEvent + event = EventSetup.Bank.password_revealed(bank_acc, entity_id) + + # Emits BankAccountPasswordRevealedEvent + Event.emit(event) + + assert password = + DatabaseQuery.fetch_bank_account(entity_id, bank_acc).password + + assert password == bank_acc.password + end + end +end diff --git a/test/universe/bank/event/bank/account/token_test.exs b/test/universe/bank/event/bank/account/token_test.exs new file mode 100644 index 00000000..3929ade5 --- /dev/null +++ b/test/universe/bank/event/bank/account/token_test.exs @@ -0,0 +1,40 @@ +defmodule Helix.Universe.Bank.Event.TokenTest do + + use Helix.Test.Case.Integration + + alias Helix.Event + + alias Helix.Entity.Query.Database, as: DatabaseQuery + + alias Helix.Test.Event.Setup, as: EventSetup + alias Helix.Test.Entity.Setup, as: EntitySetup + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + describe "event reactions" do + test "accepts when token is created on database after event been fired" do + # Setups a Bank Account + bank_acc = BankSetup.account!() + + # Setups a new Entity and gets it's id + entity_id = EntitySetup.entity!().entity_id + + # Setups a new Token for earlier created Bank Account + token_id = BankSetup.token!(acc: bank_acc).token_id + + # Setups a BankAccountTokenAcquiredEvent + event = EventSetup.Bank.token_acquired(token_id, bank_acc, entity_id) + + # Refutes if the Bank Account already exists on Entity's Database + refute DatabaseQuery.fetch_bank_account(entity_id, bank_acc) + + # Emits BankAccountTokenAcquiredEvent + Event.emit(event) + + # Fetchs the BankAccount from Entity's Database + db_bank_acc = DatabaseQuery.fetch_bank_account(entity_id, bank_acc) + + # Asserts that token exists on Entity's Database + assert db_bank_acc.token == token_id + end + end +end diff --git a/test/universe/bank/event/bank/account_test.exs b/test/universe/bank/event/bank/account_test.exs new file mode 100755 index 00000000..86b0e200 --- /dev/null +++ b/test/universe/bank/event/bank/account_test.exs @@ -0,0 +1,36 @@ +defmodule Helix.Universe.Bank.Event.AccountTest do + + use Helix.Test.Case.Integration + + alias Helix.Event + + alias Helix.Entity.Query.Database, as: DatabaseQuery + + alias Helix.Test.Event.Setup, as: EventSetup + alias Helix.Test.Entity.Setup, as: EntitySetup + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + describe "event reactions" do + test "login reactions for logging from non-owner entity" do + # Setups a Bank Account + bank_acc = BankSetup.account!() + + # Setups a new Entity and gets it's id + entity_id = EntitySetup.entity!().entity_id + + # Setups a BankAccountLoginEvent for earlier created Bank Account + event = EventSetup.Bank.login(bank_acc, entity_id) + + # Emit login event + Event.emit(event) + + # Asserts that the Bank Account has been created on created entity's + # database and that Bank Account's information are the same + assert db_acc = DatabaseQuery.fetch_bank_account(entity_id, bank_acc) + assert bank_acc.account_number == db_acc.account_number + assert bank_acc.atm_id == db_acc.atm_id + assert bank_acc.password == db_acc.password + assert bank_acc.balance == db_acc.known_balance + end + end +end diff --git a/test/universe/bank/event/change_password_test.exs b/test/universe/bank/event/change_password_test.exs new file mode 100644 index 00000000..4843392a --- /dev/null +++ b/test/universe/bank/event/change_password_test.exs @@ -0,0 +1,41 @@ +defmodule Helix.Universe.Bank.Event.ChangePasswordTest do + + use Helix.Test.Case.Integration + + alias Helix.Event + + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + + alias Helix.Test.Event.Setup, as: EventSetup + alias Helix.Test.Server.Setup, as: ServerSetup + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + describe "event reactions" do + test "accepts if account's password changes after fired" do + # Setups a Gateway + {gateway, %{entity: entity}} = ServerSetup.server() + + # Gets gateway's server_id + gateway_id = gateway.server_id + + # Setups a BankAccount + bank_acc = BankSetup.account!(owner_id: entity.entity_id) + + # Stores BankAccount's password for checking later + old_password = bank_acc.password + + # Setups a ChangePasswordProcessedEvent + event = EventSetup.Bank.password_change_processed(bank_acc, gateway_id) + + # Emits ChangePasswordProcessedEvent + Event.emit(event) + + # Fetchs the new BankAccount + assert bank_acc = + BankQuery.fetch_account(bank_acc.atm_id, bank_acc.account_number) + + # Refutes if the BankAccount's password still the same as before + refute bank_acc.password == old_password + end + end +end diff --git a/test/universe/bank/event/reveal_password_test.exs b/test/universe/bank/event/reveal_password_test.exs new file mode 100644 index 00000000..8c0bea13 --- /dev/null +++ b/test/universe/bank/event/reveal_password_test.exs @@ -0,0 +1,48 @@ +defmodule Helix.Universe.Bank.Event.RevealPasswordTest do + + use Helix.Test.Case.Integration + + alias Helix.Event + + alias Helix.Entity.Query.Database, as: DatabaseQuery + + alias Helix.Test.Event.Setup, as: EventSetup + alias Helix.Test.Server.Setup, as: ServerSetup + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + describe "event reactions" do + test "accepts if account's password is revealed" do + # Setups a Gateway + {gateway, %{entity: entity}} = ServerSetup.server() + + # Gets gateway's server_id + gateway_id = gateway.server_id + + # Setups a BankAccount + bank_acc = BankSetup.account!() + + # Setups a BankAccount Token + token_id = BankSetup.token!(acc: bank_acc).token_id + + # Setups a RevealPasswordProcessedEvent + event = + EventSetup.Bank.password_reveal_processed( + bank_acc, + gateway_id, + token_id + ) + + # Refutes that earlier created bank account exists. + refute DatabaseQuery.fetch_bank_account(entity, bank_acc) + + # Emits RevealPasswordProcessedEvent + Event.emit(event) + + # Asserts that account exists on Entity's database + assert db_bank_acc = DatabaseQuery.fetch_bank_account(entity, bank_acc) + + # Asserts that password is revealed on Entity's database + assert db_bank_acc.password == bank_acc.password + end + end +end diff --git a/test/universe/bank/henforcer/bank_test.exs b/test/universe/bank/henforcer/bank_test.exs index 102a9f04..a42ecfe8 100644 --- a/test/universe/bank/henforcer/bank_test.exs +++ b/test/universe/bank/henforcer/bank_test.exs @@ -4,30 +4,464 @@ defmodule Helix.Universe.Bank.Henforcer.BankTest do import Helix.Test.Henforcer.Macros + alias Helix.Server.Query.Server, as: ServerQuery alias Helix.Universe.Bank.Henforcer.Bank, as: BankHenforcer + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + alias Helix.Test.Network.Helper, as: NetworkHelper + alias Helix.Test.Server.Helper, as: ServerHelper + alias Helix.Test.Server.Setup, as: ServerSetup alias Helix.Test.Universe.Bank.Helper, as: BankHelper alias Helix.Test.Universe.Bank.Setup, as: BankSetup + alias Helix.Test.Entity.Setup, as: EntitySetup + + @internet_id NetworkHelper.internet_id() describe "account_exists?/1" do test "accepts when account exists" do + # Setups a BankAccount for testing bank_acc = BankSetup.account!() + # Asserts that henforcer accepts when account exists assert {true, relay} = BankHenforcer.account_exists?(bank_acc.atm_id, bank_acc.account_number) + # Asserts that bank account on relay is the same + # as created before assert relay.bank_account == bank_acc + # Assert that relay only contains :bank_account key assert_relay relay, [:bank_account] end test "rejects when account does not exist" do + # Asserts that henforcer returns error when account does not exist assert {false, reason, _} = BankHenforcer.account_exists?( BankHelper.atm_id(), BankHelper.account_number() ) + # Asserts that the reason for failing is `{:bank_account, :not_found}` + assert reason == {:bank_account, :not_found} + end + end + + describe "password_valid?/2" do + test "accepts when password is the same as the bank account's password" do + # Setups a BankAccount for testing + bank_acc = BankSetup.account!() + + # Asserts that henforcer accepts when passwords match + assert {true, relay} = + BankHenforcer.password_valid?(bank_acc, bank_acc.password) + + # Asserts that relay's password is the same as BankAccounts's Password + assert relay.password == bank_acc.password + + # Asserts that relay only contains the :password key + assert_relay relay, [:password] + end + + test "rejects when password is not the same as the bank account's password" do + # Setups a BankAccount for testing + bank_acc = BankSetup.account!() + + # Asserts that henforcer returns error when password does not match + assert {false, reason, _} = + BankHenforcer.password_valid?(bank_acc, BankHelper.password()) + + # Asserts that the reason for failing is `{:password, :invalid}` + assert reason == {:password, :invalid} + end + end + + describe "can_join_password?/4" do + test "accepts when account and entity exists and the password is valid" do + # Setups an Entity for testing + {entity, _} = EntitySetup.entity() + entity_id = entity.entity_id + + # Setups a BankAccount for testing + bank_acc = BankSetup.account!() + atm_id = bank_acc.atm_id + account_number = bank_acc.account_number + password = bank_acc.password + + # Asserts that henforcer accepts when account and entity exists and + # the passwords match + assert {true, relay} = + BankHenforcer.can_join_password?( + atm_id, + account_number, + password, + entity_id + ) + + # Asserts that relay information is correct + assert relay.bank_account == bank_acc + assert relay.password == password + assert relay.entity == entity + + # Asserts that relay only contains desired keys + assert_relay relay, [:bank_account, :password, :entity] + end + + test "rejects when account does not exists" do + # Setups an Entity for testing + {entity, _} = EntitySetup.entity() + + # Setups fake BankAccount Values + atm_id = BankHelper.atm_id() + account_number = BankHelper.account_number + password = BankHelper.password + entity_id = entity.entity_id + + # Asserts that henforcer rejects trying to join a account that does not + # exist + assert {false, reason, _} = + BankHenforcer.can_join_password?( + atm_id, + account_number, + password, + entity_id + ) + + # Asserts that reason to fail is `{:bank_account, :not_found}` + # Because the account does't exists assert reason == {:bank_account, :not_found} end + + test "rejects when entity does not exists" do + # Setups BankAccount for testing + bank_acc = BankSetup.account!() + atm_id = bank_acc.atm_id + account_number = bank_acc.account_number + password = bank_acc.password + + # Generates an entity id that not exists on database + entity_id = EntitySetup.id() + + # Asserts that henforcer rejects when trying to join with a entity that + # does not exist + assert {false, reason, _} = + BankHenforcer.can_join_password?( + atm_id, + account_number, + password, + entity_id + ) + + # Asserts that reason for fail is `{:entity, :not_found}` + assert reason == {:entity, :not_found} + end + + test "rejects when password does not match" do + # Setups an Entity and a BankAccount for testing + {entity, _} = EntitySetup.entity() + bank_acc = BankSetup.account!() + entity_id = entity.entity_id + atm_id = bank_acc.atm_id + account_number = bank_acc.account_number + + # Generates a password that not match BankAccount's password + password = BankHelper.password() + + # Asserts the henforcer rejects when passwords does not match + assert {false, reason, _} = + BankHenforcer.can_join_password?( + atm_id, + account_number, + password, + entity_id + ) + + # Asserts that reason for fail is `{:password, :invalid}` + assert reason == {:password, :invalid} + end + end + + describe "can_join_token?/4" do + test "accepts when account and entity exists and the token is valid" do + # Setups an Entity for testing + {entity, _} = EntitySetup.entity() + entity_id = entity.entity_id + + # Setups a BankAccount for testing + bank_acc = BankSetup.account!() + atm_id = bank_acc.atm_id + account_number = bank_acc.account_number + + # Setups a Token for testing + token = BankSetup.token!(acc: bank_acc) + + # Asserts that accepts when both Entity, Account and Token are valid. + assert {true, relay} = + BankHenforcer.can_join_token?( + atm_id, + account_number, + token.token_id, + entity_id + ) + + # Asserts that token is the same as the token created before. + assert relay.token.token_id == token.token_id + assert relay.token.atm_id == atm_id + assert relay.token.account_number == account_number + + # Asserts that relay's account still the same as when created. + assert relay.bank_account.atm_id == atm_id + assert relay.bank_account.account_number == account_number + + # Asserts that relay's entity still the same as when created. + assert relay.entity == entity + + # Asserts that relay only contains the :token key. + assert_relay relay, [:token, :bank_account, :entity] + end + + test "rejects when token is not related to given account" do + # Setups an Entity for testing + {entity, _} = EntitySetup.entity() + entity_id = entity.entity_id + + # Setups a BankAccount for trying to join + bank_acc = BankSetup.account!() + + # Setups a BankAccount for generate token + bank_acc_token = BankSetup.account!() + + # Gets the trying to join BankAccount information + atm_id = bank_acc.atm_id + account_number = bank_acc.account_number + + # Setup a Token for the `bank_acc_token` BankAccount + token = BankSetup.token!(acc: bank_acc_token) + + # Asserts that henforcer rejects because the given token + # does not belongs for the BankAccount used to try the join + assert {false, reason, _} = + BankHenforcer.can_join_token?( + atm_id, + account_number, + token.token_id, + entity_id + ) + + # Asserts that the reason is `{:token, :not_belongs}` + assert reason == {:token, :not_belongs} + end + + test "rejects when token is expired" do + # Setups an Entity for testing + {entity, _} = EntitySetup.entity() + entity_id = entity.entity_id + + # Setups a BankAccount for testing + bank_acc = BankSetup.account!() + atm_id = bank_acc.atm_id + account_number = bank_acc.account_number + + # Setups an expired Token + token = BankSetup.token!(acc: bank_acc, expired: true) + token_id = token.token_id + + # Asserts that henforcer rejects because the token is expired + assert {false, reason, _} = + BankHenforcer.can_join_token?( + atm_id, + account_number, + token_id, + entity_id + ) + + # Asserts that the reason for failing is `{:token, :expired}` + assert reason == {:token, :expired} + end + + test "rejects when token does not exists" do + # Setups an Entity for testing + {entity, _} = EntitySetup.entity() + entity_id = entity.entity_id + + # Setups a BankAccount for testing + bank_acc = BankSetup.account!() + atm_id = bank_acc.atm_id + account_number = bank_acc.account_number + + # Setups an token id that does not exist + token_id = Ecto.UUID.generate() + + # Asserts that henforcer rejects because token does not exist + assert {false, reason, _} = + BankHenforcer.can_join_token?( + atm_id, + account_number, + token_id, + entity_id + ) + + # Asserts that the failing reason is `{:token, :not_found}` + assert reason == {:token, :not_found} + end + end + + describe "owns_account?/2" do + test "accepts when the entity owns the account" do + # Setups an Entity and a BankAccount that belongs to that Entity + {entity, _} = EntitySetup.entity() + bank_acc = BankSetup.account!(owner_id: entity.entity_id) + + # Asserts that henforcer accepts because `bank_acc` belongs to `entity` + assert {true, relay} = + BankHenforcer.owns_account?(entity, bank_acc) + + # Asserts that relay's BankAccount is the same as created BankAccount + assert relay.bank_account == bank_acc + + # Asserts that relay only contains :bank_account key + assert_relay relay, [:bank_account] + end + + test "rejects when the entity not owns the account" do + # Setups an Entity for testing + {entity, _} = EntitySetup.entity() + + # Setups a BankAccount which not belongs to that entity + bank_acc = BankSetup.account!() + + # Asserts that henforcer rejects because entity does not own the + # BankAccount + assert{false, reason, _} = + BankHenforcer.owns_account?(entity, bank_acc) + + # Asserts that reason for failing is `{:bank_account, :not_found}` + assert reason == {:bank_account, :not_belongs} + end + end + + describe "is_empty?/1" do + test "accepts when the account has no funds" do + # Setups a BankAccount for testing + bank_acc = BankSetup.account!() + + # Asserts that henforcer accepts because the BankAccount has no funds + assert {true, relay} = + BankHenforcer.is_empty?(bank_acc) + + # Asserts that the relay's BankAccount is the same as `bank_acc` + assert relay.bank_account == bank_acc + + # Asserts that relay only contains :bank_account key + assert_relay relay, [:bank_account] + end + + test "rejects when account has funds" do + # Setups an BankAccount that has funds + bank_acc = BankSetup.account!(balance: 500) + + # Asserts that henforcer rejects because the BankAccount has funds + assert {false, reason, _} = + BankHenforcer.is_empty?(bank_acc) + + # Asserts that reason for failing is `{:bank_account, :not_empty}` + assert reason == {:bank_account, :not_empty} + end + end + + describe "has_enough_funds?/2" do + test "accepts when account has enough funds" do + # Setups a BankAccount that has enough funds for tranfer + bank_acc = BankSetup.account!(balance: 500) + + # Asserts that henforcer accepts because the BankAccount has the required + # funds to do the transfer + assert {true, relay} = + BankHenforcer.has_enough_funds?(bank_acc, 500) + + # Asserts that relay's amount is the same as given + assert relay.amount == 500 + + # Asserts that relay only contains :amount key + assert_relay relay, [:amount] + end + + test "rejects when account has not enough funds" do + # Setups a BankAccount that does not have enough funds + bank_acc = BankSetup.account!(balance: 500) + + # Asserts that henforcer reject because the BankAccount + # has no enough funds + assert {false, reason, _} = + BankHenforcer.has_enough_funds?(bank_acc, 501) + + # Asserts that reason for failing is `{:bank_account, :no_funds}` + assert reason == {:bank_account, :no_funds} + end + end + + describe "can_transfer?/5" do + test "accepts when everything is ok" do + # Setups a BankAccount to send money + snd_acc = BankSetup.account!(balance: 500) + snd_acc_id = {snd_acc.atm_id, snd_acc.account_number} + + # Setups a BankAccount to receive money + rcv_acc = BankSetup.account!() + + # Gets the receiving BankAccount's ATM server information + atm = ServerQuery.fetch(rcv_acc.atm_id) + atm_ip = ServerHelper.get_ip(atm) + atm_nip = {@internet_id, atm_ip} + + # Asserts that henforcer accepts because both + # BankAccount exists, the sending BankAccount has enough money, + # the ATM exists and is a bank + assert {true, relay} = + BankHenforcer.can_transfer?( + atm_nip, + rcv_acc.account_number, + snd_acc_id, + 500, + snd_acc.password + ) + + # Asserts that relay value are all correct + assert relay.amount == 500 + assert relay.password == snd_acc.password + assert relay.to_account == rcv_acc + + # Asserts that relay only contains the neccessary keys + assert_relay relay, [:amount, :password, :to_account] + end + end + + describe "is_a_bank?" do + test "accepts when the given server is a bank" do + # Setups a BankAccount for getting an atm_id that + # really comes from a bank + bank_acc = BankSetup.account!() + + # Asserts that henforcer accepts because the atm_id belongs to a bank + assert {true, relay} = + BankHenforcer.is_a_bank?(bank_acc.atm_id) + + # Asserts that relay values are correct + assert relay.atm == BankQuery.fetch_atm(bank_acc.atm_id) + + # Asserts that relay only contains needed values + assert relay, [:atm] + end + + test "rejects when the given server is not a bank" do + # Setups a Server that is not a bank + server = ServerSetup.server!() + + # Asserts the henforcer rejects because the server is not a bank + assert {false, reason, _} = + BankHenforcer.is_a_bank?(server.server_id) + + # Asserts that reason for failing is `{:atm, :not_a_bank}` + assert reason == {:atm, :not_a_bank} + end end end diff --git a/test/universe/bank/internal/bank_account_test.exs b/test/universe/bank/internal/bank_account_test.exs index 182fca75..84bf409e 100644 --- a/test/universe/bank/internal/bank_account_test.exs +++ b/test/universe/bank/internal/bank_account_test.exs @@ -85,13 +85,8 @@ defmodule Helix.Universe.Bank.Internal.BankAccountTest do atm_id = Enum.random(bank.servers).id owner_id = Random.pk() - params = %{ - bank_id: bank.id, - atm_id: atm_id, - owner_id: owner_id - } - - assert {:ok, acc} = BankAccountInternal.create(params) + assert {:ok, acc} = + BankAccountInternal.create(owner_id, atm_id, bank.id) assert_id acc.atm_id, atm_id assert_id acc.bank_id, bank.id assert_id acc.owner_id, owner_id @@ -170,7 +165,7 @@ defmodule Helix.Universe.Bank.Internal.BankAccountTest do assert BankAccountInternal.fetch(acc.atm_id, acc.account_number) assert {:error, reason} = BankAccountInternal.close(acc) - assert reason == {:account, :notempty} + assert reason == {:bank_account, :not_empty} assert BankAccountInternal.fetch(acc.atm_id, acc.account_number) end @@ -178,7 +173,7 @@ defmodule Helix.Universe.Bank.Internal.BankAccountTest do {fake_acc, _} = BankSetup.fake_account() assert {:error, reason} = BankAccountInternal.close(fake_acc) - assert reason == {:account, :notfound} + assert reason == {:bank_account, :not_found} end end end diff --git a/test/universe/bank/internal/bank_token_test.exs b/test/universe/bank/internal/bank_token_test.exs index e0ec699f..c3006b9c 100644 --- a/test/universe/bank/internal/bank_token_test.exs +++ b/test/universe/bank/internal/bank_token_test.exs @@ -21,14 +21,6 @@ defmodule Helix.Universe.Bank.Internal.BankTokenTest do assert token2.token_id == token.token_id end - test "wont fetch expired token" do - {expired, _} = BankSetup.token([expired: true]) - - token2 = BankTokenInternal.fetch(expired.token_id) - - refute token2 - end - test "with non-existing token" do refute BankTokenInternal.fetch(UUID.generate()) end @@ -44,14 +36,6 @@ defmodule Helix.Universe.Bank.Internal.BankTokenTest do assert token2.token_id == token.token_id end - test "wont fetch expired token" do - {token, _} = BankSetup.token([expired: true]) - - token2 = BankTokenInternal.fetch_by_connection(token.connection_id) - - refute token2 - end - test "with non-existing connection" do refute BankTokenInternal.fetch_by_connection(Connection.ID.generate()) end @@ -74,9 +58,6 @@ defmodule Helix.Universe.Bank.Internal.BankTokenTest do test "updates the token expiration" do {token, _} = BankSetup.token() - # No expiration_date set - refute token.expiration_date - assert {:ok, token2} = BankTokenInternal.set_expiration(token) # Expiration date set diff --git a/test/universe/bank/process/change_password_test.exs b/test/universe/bank/process/change_password_test.exs new file mode 100644 index 00000000..4c1753ef --- /dev/null +++ b/test/universe/bank/process/change_password_test.exs @@ -0,0 +1,59 @@ +defmodule Helix.Universe.Bank.Process.Bank.Account.ChangePasswordTest do + + use Helix.Test.Case.Integration + + alias Helix.Network.Query.Tunnel, as: TunnelQuery + alias Helix.Server.Query.Server, as: ServerQuery + alias Helix.Universe.Bank.Process.Bank.Account.ChangePassword, + as: BankChangePasswordProcess + + alias Helix.Test.Server.Helper, as: ServerHelper + alias Helix.Test.Server.Setup, as: ServerSetup + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + @relay nil + + describe "Process.Executable" do + test "starts a change password process when everything is OK" do + {gateway, %{entity: entity}} = ServerSetup.server() + bank_account = BankSetup.account!(owner_id: entity.entity_id) + atm = ServerQuery.fetch(bank_account.atm_id) + atm_nip = ServerHelper.get_nip(atm) + + params = + %{ + account: bank_account, + gateway: gateway + } + + meta = + %{ + network_id: atm_nip.network_id, + bounce: nil + } + + assert {:ok, process} = + BankChangePasswordProcess.execute( + gateway, atm, params, meta, @relay + ) + assert process.src_connection_id + assert process.type == :bank_change_password + assert process.gateway_id == gateway.server_id + assert process.source_entity_id == entity.entity_id + assert process.target_id == atm.server_id + assert process.network_id == atm_nip.network_id + + refute process.src_file_id + + connection = TunnelQuery.fetch_connection process.src_connection_id + + assert connection.connection_type == :bank_login + + tunnel = TunnelQuery.fetch(connection.tunnel_id) + + assert tunnel.gateway_id == gateway.server_id + assert tunnel.target_id == atm.server_id + assert tunnel.network_id == atm_nip.network_id + end + end +end diff --git a/test/universe/bank/public/bank_test.exs b/test/universe/bank/public/bank_test.exs new file mode 100644 index 00000000..dfcdb7de --- /dev/null +++ b/test/universe/bank/public/bank_test.exs @@ -0,0 +1,190 @@ +defmodule Helix.Test.Universe.Bank.Public.BankTest do + + use Helix.Test.Case.Integration + + alias Helix.Process.Query.Process, as: ProcessQuery + alias Helix.Server.Query.Server, as: ServerQuery + alias Helix.Universe.Bank.Public.Bank, as: BankPublic + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + + alias Helix.Test.Account.Setup, as: AccountSetup + alias Helix.Test.Network.Setup, as: NetworkSetup + alias Helix.Test.Process.TOPHelper + alias Helix.Test.Server.Setup, as: ServerSetup + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + alias Helix.Test.Universe.Bank.Helper, as: BankHelper + alias Helix.Test.Universe.NPC.Helper, as: NPCHelper + + @relay nil + + describe "bootstrap/2" do + test "bootstrap is created" do + # Setups a BankAccount. + bank_account = BankSetup.account!(balance: BankHelper.amount) + atm_id = bank_account.atm_id + account_number = bank_account.account_number + account_id = {atm_id, account_number} + + # Gets Bootstrap. + bootstrap = BankPublic.bootstrap(account_id) + + # Asserts that the bootstrap's balance is equals to BankAccount's balance. + assert bootstrap.balance == bank_account.balance + end + end + + describe "render_bootstrap/1" do + test "bootstrap is being properly rendered" do + # Setups a BankAccount. + bank_account = BankSetup.account!(balance: BankHelper.amount) + atm_id = bank_account.atm_id + account_number = bank_account.account_number + account_id = {atm_id, account_number} + + # Gets Bootstrap. + bootstrap = BankPublic.bootstrap(account_id) + + # Renders Bootstrap. + rendered_bootstrap = BankPublic.render_bootstrap(bootstrap) + + # Asserts that Bootstrap information is correct. + assert rendered_bootstrap.balance == bank_account.balance + end + end + + describe "change_password/4" do + test "create the process" do + # Setups a BankAccount. + bank_account = BankSetup.account!() + + # Stores the password for comparing later. + old_password = bank_account.password + + # Setups a gateway. + {gateway, _} = ServerSetup.server() + + # Fetchs ATM server. + atm = ServerQuery.fetch(bank_account.atm_id) + + # Asserts that process has been created. + assert {:ok, process} = + BankPublic.change_password(bank_account, gateway, atm, @relay) + + # Asserts that process properties are correct. + assert process.gateway_id == gateway.server_id + assert process.target_id == atm.server_id + assert process.type == :bank_change_password + assert process.src_atm_id == bank_account.atm_id + assert process.src_acc_number == bank_account.account_number + refute process.src_file_id + refute process.tgt_connection_id + + # Gets process' id + process_id = process.process_id + + # Forces process completion + TOPHelper.force_completion(process_id) + + # Process no longer exists + refute ProcessQuery.fetch(process_id) + + bank_account = + BankQuery.fetch_account(atm.server_id, bank_account.account_number) + + # Refutes if password is equals to old password + refute bank_account.password == old_password + end + end + + describe "transfer/6" do + test "create the process" do + # Setups a gateway and gets it's entity. + {gateway, _} = ServerSetup.server() + + # Setups sending bank account. + bank_acc_snd = BankSetup.account!(balance: 500) + + # Setups eceiving bank account. + bank_acc_rec = BankSetup.account!() + + # Connection Related Stuff. + atm = ServerQuery.fetch(bank_acc_snd.atm_id) + + # Setups a Tunnel. + tunnel = NetworkSetup.tunnel!( + gateway_id: gateway.server_id, + destination_id: atm.server_id + ) + + # Setups an Account. + account = AccountSetup.account!() + + # Asserts the transfer starts correctly. + assert {:ok, process} = + BankPublic.transfer( + bank_acc_snd, + bank_acc_rec, + 300, + account, + gateway, + tunnel, + @relay + ) + + # Asserts that process properties are correct. + assert process.gateway_id == gateway.server_id + assert process.target_id == bank_acc_rec.atm_id + assert process.type == :wire_transfer + assert process.src_connection_id + refute process.src_file_id + refute process.tgt_connection_id + + TOPHelper.top_stop(gateway.server_id) + end + end + + describe "reveal_password/5" do + test "creates the process" do + # Setups a Gateway. + {gateway, _} = ServerSetup.server() + + # Setups a BankAccount. + bank_account = BankSetup.account!() + + # Setups a Token. + token = BankSetup.token!(acc: bank_account) + token_id = token.token_id + + # Fetches ATM Server. + atm = ServerQuery.fetch(bank_account.atm_id) + + # Asserts the reveal password process is being created. + assert {:ok, process} = + BankPublic.reveal_password( + bank_account, + token_id, + gateway, + atm, + @relay + ) + + # Asserts that process properties are correct. + assert process.gateway_id == gateway.server_id + assert process.target_id == atm.server_id + assert process.type == :bank_reveal_password + assert process.data.token_id == token.token_id + assert process.data.atm_id == atm.server_id + assert process.data.account_number == bank_account.account_number + refute process.src_file_id + refute process.tgt_connection_id + end + end + + describe "open_account/4" do + # Tested on BankAccountFlowTest + end + + describe "close_account/4" do + # Tested on BankAccountFlowTest + end +end diff --git a/test/universe/bank/public/index_test.exs b/test/universe/bank/public/index_test.exs new file mode 100755 index 00000000..bc9cd24d --- /dev/null +++ b/test/universe/bank/public/index_test.exs @@ -0,0 +1,56 @@ +defmodule Helix.Universe.Bank.Public.IndexTest do + + use Helix.Test.Case.Integration + + alias Helix.Universe.Bank.Public.Index, as: BankIndex + + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + describe "index/2" do + test "returns expected data" do + # Setups a BankAccount. + bank_account = BankSetup.account!() + atm_id = bank_account.atm_id + account_num = bank_account.account_number + + # Gets BankAccount's Index. + index = BankIndex.index(atm_id, account_num) + + assert index.balance == bank_account.balance + end + end + + describe "render_index/1" do + test "returns JSON-friendly index" do + # Setups a BankAccount. + bank_account = BankSetup.account!() + atm_id = bank_account.atm_id + account_num = bank_account.account_number + + # Renders BankAccount's Index. + rendered_index = + BankIndex.index(atm_id, account_num) + |> BankIndex.render_index() + + assert rendered_index.balance == bank_account.balance + end + end + + describe "render_transfer/1" do + # Setups a BankTransfer. + transfer = BankSetup.transfer!() + + # Renders BankTransfer. + rendered_transfer = + BankIndex.render_transfer(transfer) + + assert is_binary(rendered_transfer.transfer_id) + assert is_binary(rendered_transfer.account_from) + assert is_binary(rendered_transfer.account_to) + assert is_binary(rendered_transfer.atm_from) + assert is_binary(rendered_transfer.atm_to) + assert rendered_transfer.amount == transfer.amount + assert is_binary(rendered_transfer.started_by) + assert is_float(rendered_transfer.started_time) + end +end diff --git a/test/universe/bank/websocket/channel/bank/join_test.exs b/test/universe/bank/websocket/channel/bank/join_test.exs new file mode 100755 index 00000000..157207bf --- /dev/null +++ b/test/universe/bank/websocket/channel/bank/join_test.exs @@ -0,0 +1,289 @@ +defmodule Helix.Universe.Bank.Websocket.Channel.Bank.JoinTest do + + use Helix.Test.Case.Integration + + import Phoenix.ChannelTest + + alias Helix.Test.Channel.Helper, as: ChannelHelper + alias Helix.Test.Channel.Setup, as: ChannelSetup + alias Helix.Test.Network.Setup.Bounce, as: BounceSetup + alias Helix.Test.Server.Setup, as: ServerSetup + alias Helix.Test.Universe.Bank.Helper, as: BankHelper + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + @moduletag :driver + + describe "BankJoin" do + test "can join a bank account with password" do + {socket, %{account: player, server: gateway}} = + ChannelSetup.create_socket() + + entity_id = socket.assigns.entity_id + bounce = BounceSetup.bounce!(entity_id: entity_id) + + bank_account = BankSetup.account!() + + payload = + %{ + "entity_id" => to_string(entity_id), + "password" => bank_account.password, + "gateway_id" => to_string(gateway.server_id), + "bounce_id" => to_string(bounce.bounce_id) + } + + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Joins the channel + assert {:ok, bootstrap, new_socket} = join(socket, topic, payload) + + # `atm_id` and `account_number` are valid + assert new_socket.assigns.atm_id == atm_id + assert new_socket.assigns.account_number == account_number + + # `entity` data is valid + assert new_socket.assigns.entity_id == entity_id + + # `account` data is valid + assert new_socket.assigns.account_id == + player.account_id + + # `gateway` data is valid + assert new_socket.assigns.gateway.server_id == gateway.server_id + + # connection is being created + assert Map.has_key?(new_socket.assigns, :tunnel) + assert Map.has_key?(new_socket.assigns, :bank_login) + + # Socket related stuff + assert new_socket.joined + assert new_socket.topic == topic + + # It returned the bank account bootstrap + assert bootstrap.data.balance + end + + test "can join a bank account with token" do + {socket, %{account: player, server: gateway}} = + ChannelSetup.create_socket() + + entity_id = socket.assigns.entity_id + bounce = BounceSetup.bounce!(entity_id: entity_id) + + bank_account = BankSetup.account!() + token = BankSetup.token!(acc: bank_account) + token_id = token.token_id + + payload = + %{ + "entity_id" => to_string(entity_id), + "token" => token_id, + "gateway_id" => to_string(gateway.server_id), + "bounce_id" => to_string(bounce.bounce_id) + } + + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Joins the channel + assert {:ok, bootstrap, new_socket} = join(socket, topic, payload) + + # `atm_id` and `account_number` are valid + assert new_socket.assigns.atm_id == atm_id + assert new_socket.assigns.account_number == account_number + + # `entity` data is valid + assert new_socket.assigns.entity_id == entity_id + + # `account` data is valid + assert new_socket.assigns.account_id == + player.account_id + + # `gateway` data is valid + assert new_socket.assigns.gateway.server_id == gateway.server_id + + # connection is being created + assert Map.has_key?(new_socket.assigns, :tunnel) + assert Map.has_key?(new_socket.assigns, :bank_login) + + # Socket related stuff + assert new_socket.joined + assert new_socket.topic == topic + + # It returned the bank account bootstrap + assert bootstrap.data.balance + end + + test "can't join bank account with expired token" do + {socket, %{server: gateway}} = + ChannelSetup.create_socket() + + entity_id = socket.assigns.entity_id + bounce = BounceSetup.bounce!(entity_id: entity_id) + + bank_account = BankSetup.account!() + token = BankSetup.token!(acc: bank_account, expired: true) + token_id = token.token_id + + payload = + %{ + "entity_id" => to_string(entity_id), + "token" => token_id, + "gateway_id" => to_string(gateway.server_id), + "bounce_id" => to_string(bounce.bounce_id) + } + + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Joins the channel + assert {:error, reason} = join(socket, topic, payload) + + assert reason.data == "token_expired" + end + + test "can't join bank account with not matching token" do + {socket, %{server: gateway}} = + ChannelSetup.create_socket() + + entity_id = socket.assigns.entity_id + bounce = BounceSetup.bounce!(entity_id: entity_id) + + bank_account = BankSetup.account!() + token = BankSetup.token!(expired: true) + token_id = token.token_id + + payload = + %{ + "entity_id" => to_string(entity_id), + "token" => token_id, + "gateway_id" => to_string(gateway.server_id), + "bounce_id" => to_string(bounce.bounce_id) + } + + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Joins the channel + assert {:error, reason} = join(socket, topic, payload) + + assert reason.data == "token_not_belongs" + end + + test "can not join with the wrong password" do + {socket, %{server: gateway}} = ChannelSetup.create_socket() + + entity_id = socket.assigns.entity_id + bounce = BounceSetup.bounce!(entity_id: entity_id) + + bank_account = BankSetup.account!() + + payload = + %{ + "entity_id" => to_string(entity_id), + "password" => BankHelper.password(), + "gateway_id" => to_string(gateway.server_id), + "bounce_id" => to_string(bounce.bounce_id) + } + + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Try to join the channel + assert {:error, reason} = join(socket, topic, payload) + + assert reason.data == "password_invalid" + end + + test "can not join with an account that does not exist" do + {socket, %{server: gateway}} = ChannelSetup.create_socket() + + entity_id = socket.assigns.entity_id + bounce = BounceSetup.bounce!(entity_id: entity_id) + + payload = + %{ + "entity_id" => to_string(entity_id), + "password" => BankHelper.password(), + "gateway_id" => to_string(gateway.server_id), + "bounce_id" => to_string(bounce.bounce_id) + } + + atm_id = BankHelper.atm_id() + account_number = BankHelper.account_number() + + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Try to join the channel + assert {:error, reason} = + join(socket, topic, payload) + + assert reason.data == "bank_account_not_found" + end + test "can not join with a not owned bounce" do + {socket, %{server: gateway}} = ChannelSetup.create_socket() + + entity_id = socket.assigns.entity_id + bounce = BounceSetup.bounce!() + + bank_account = BankSetup.account!() + + payload = + %{ + "entity_id" => to_string(entity_id), + "password" => bank_account.password, + "gateway_id" => to_string(gateway.server_id), + "bounce_id" => to_string(bounce.bounce_id) + } + + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Try to join the channel + assert {:error, reason} = join(socket, topic, payload) + + assert reason.data == "bounce_not_belongs" + + end + test "can not join with a not owned gateway" do + {socket, _} = ChannelSetup.create_socket() + + random_server = ServerSetup.server!() + entity_id = socket.assigns.entity_id + bounce = BounceSetup.bounce!(entity_id: entity_id) + + bank_account = BankSetup.account!() + + payload = + %{ + "entity_id" => to_string(entity_id), + "password" => bank_account.password, + "gateway_id" => to_string(random_server.server_id), + "bounce_id" => to_string(bounce.bounce_id) + } + + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Try to join the channel + assert {:error, reason} = join(socket, topic, payload) + + assert reason.data == "server_not_belongs" + end + end + +end diff --git a/test/universe/bank/websocket/channel/requests/bootstrap_test.exs b/test/universe/bank/websocket/channel/requests/bootstrap_test.exs new file mode 100755 index 00000000..04b63af3 --- /dev/null +++ b/test/universe/bank/websocket/channel/requests/bootstrap_test.exs @@ -0,0 +1,65 @@ +defmodule Helix.Test.Universe.Bank.Websocket.Requests.BootstrapTest do + + use Helix.Test.Case.Integration + + import Phoenix.ChannelTest + + alias Helix.Universe.Bank.Websocket.Requests.Bootstrap, + as: BankBootstrapRequest + alias Helix.Websocket.Requestable + + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + alias Helix.Test.Universe.Bank.Helper, as: BankHelper + alias Helix.Test.Channel.Helper, as: ChannelHelper + alias Helix.Test.Channel.Setup, as: ChannelSetup + + describe "BankBootstrapRequest.handle_request/2" do + test "bootstrap" do + # Setups an Account socket. + {socket, %{entity: entity, server: gateway}} = + ChannelSetup.create_socket() + + # Setups a BankAccount. + bank_acc = BankSetup.account!(balance: BankHelper.amount) + atm_id = bank_acc.atm_id + account_number = bank_acc.account_number + + # Creates topic to log in the bank channel. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Create the payload which will be used to join the bank channel. + payload = + %{ + "entity_id" => to_string(entity.entity_id), + "password" => bank_acc.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins the bank channel. + {:ok, _bootstrap, bnk_socket} = + join(socket, topic, payload) + + # Mocks a BankBootstrapRequest. + request = + BankBootstrapRequest.new(%{}) + + # Request Checking Parameters (which does not exists in this case). + {:ok, request} = + Requestable.check_params(request, bnk_socket) + + # Request Checking Permissions (which does nothing in this case + # because receives no parameters and is already logged in the + # Bank channel). + {:ok, request} = + Requestable.check_permissions(request, bnk_socket) + + # Asserts that request has been handled successfully. + assert {:ok, request} = + Requestable.handle_request(request, bnk_socket) + + # Asserts that bootstrap information is correct. + assert request.meta.bootstrap + assert request.meta.bootstrap.balance == bank_acc.balance + end + end +end diff --git a/test/universe/bank/websocket/channel/requests/change_password_test.exs b/test/universe/bank/websocket/channel/requests/change_password_test.exs new file mode 100644 index 00000000..3ebdab4c --- /dev/null +++ b/test/universe/bank/websocket/channel/requests/change_password_test.exs @@ -0,0 +1,162 @@ +defmodule Helix.Test.Universe.Bank.Websocket.Requests.ChangePasswordTest do + + use Helix.Test.Case.Integration + + import Phoenix.ChannelTest + + alias Helix.Websocket.Requestable + alias Helix.Universe.Bank.Query.Bank, as: BankQuery + alias Helix.Universe.Bank.Websocket.Requests.ChangePassword, + as: BankChangePasswordRequest + + alias Helix.Test.Channel.Request.Helper, as: RequestHelper + alias Helix.Test.Channel.Helper, as: ChannelHelper + alias Helix.Test.Channel.Setup, as: ChannelSetup + alias Helix.Test.Process.TOPHelper + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + describe "BankChangePasswordRequest.check_permissions/2" do + test "accepts when account belongs to player" do + # Setups an Account socket and get gateway and entity + # from related information. + {socket, %{server: gateway, entity: entity}} = + ChannelSetup.create_socket() + + # Setups a BankAccount for testing. + bank_account = + BankSetup.account!(owner_id: entity.entity_id) + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + # Creates the topic to join on bank channel. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Creates the payload for joining the bank channel. + payload = + %{ + "entity_id" => to_string(entity.entity_id), + "password" => bank_account.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins the bank channel with the created BankAccount. + {:ok, _bootstrap, bnk_socket} = + join(socket, topic, payload) + + # Mocks a BankChangePasswordRequest that does not receive any parameters. + request = + RequestHelper.mock_request(BankChangePasswordRequest, %{}) + + # Asserts that get permissions to continue with the request flow. + assert {:ok, request} = + Requestable.check_permissions(request, bnk_socket) + + # Asserts that meta contains the BankAccount which is necessary for + # changing the BankAccount's password + assert request.meta.bank_account == bank_account + end + + test "rejects when account does not belongs to player" do + # Setups an Account socket and get gateway and entity + # from related information. + {socket, %{server: gateway, entity: entity}} = + ChannelSetup.create_socket() + + # Setups a BankAccount that not belongs to entity. + bank_account = + BankSetup.account!() + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + # Creates a bank channel topic based on BankAccount information. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Creates the payload for logging in the bank channel on created + # BankAccount. + payload = + %{ + "entity_id" => to_string(entity.entity_id), + "password" => bank_account.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins the bank channel. + {:ok, _bootstrap, bnk_socket} = + join(socket, topic, payload) + + # Mocks a BankChangePasswordRequest that does not receive any parameters. + request = + RequestHelper.mock_request(BankChangePasswordRequest, %{}) + + # Asserts that get no permissions to change password because the entity + # does not own the given BankAccount. + assert {:error, reason, _} = + Requestable.check_permissions(request, bnk_socket) + + # Asserts the reason for failing is "bank_account_not_belong". + assert reason.message == "bank_account_not_belongs" + end + end + + describe "BankChangePasswordRequest.handle_request/2" do + test "accepts if the password changes after process ends" do + # Setups an Account socket and gets gateway and entity from + # related information. + {socket, %{server: gateway, entity: entity}} = + ChannelSetup.create_socket() + + # Setups a BankAccount for testing. + bank_account = + BankSetup.account!(owner_id: entity.entity_id) + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + # Stores the old password on a variable to compare after the process. + old_password = bank_account.password + + # Creates the topic used for logging in the bank channel. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Creates the payload used as parameters to log in on bank channel. + payload = + %{ + "entity_id" => to_string(entity.entity_id), + "password" => bank_account.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins the bank channel. + {:ok, _bootstrap, bnk_socket} = + join(socket, topic, payload) + + # Mocks BankChangePasswordRequest that does not receive any parameters. + request = + RequestHelper.mock_request(BankChangePasswordRequest, %{}) + + # Checks the permissions for given Entity on BankAccount. + {:ok, request} = + Requestable.check_permissions(request, bnk_socket) + + # Asserts that request is handled correctly. + assert {:ok, request} = + Requestable.handle_request(request, bnk_socket) + + # Gets the process' id for forcing it's termination. + process_id = request.meta.process.process_id + + # Forces the process to complete. + TOPHelper.force_completion(process_id) + + # Fetchs the BankAccount from the database. + bank_account = + BankQuery.fetch_account( + bank_account.atm_id, + bank_account.account_number + ) + + # Refutes if the current BankAccount password is equals + # to the old_password. + refute bank_account.password == old_password + end + end +end diff --git a/test/universe/bank/websocket/channel/requests/close_account_test.exs b/test/universe/bank/websocket/channel/requests/close_account_test.exs new file mode 100644 index 00000000..7f993c9a --- /dev/null +++ b/test/universe/bank/websocket/channel/requests/close_account_test.exs @@ -0,0 +1,10 @@ +defmodule Helix.Test.Universe.Bank.Websocket.Requests.CloseAccount do + + use Helix.Test.Case.Integration + + import Phoenix.ChannelTest + + describe "BankCloseAccountRequest.handle_request/2" do + # Tested on BankAccountInternal + end +end diff --git a/test/universe/bank/websocket/channel/requests/create_account_test.exs b/test/universe/bank/websocket/channel/requests/create_account_test.exs new file mode 100644 index 00000000..60675c09 --- /dev/null +++ b/test/universe/bank/websocket/channel/requests/create_account_test.exs @@ -0,0 +1,184 @@ +defmodule Helix.Test.Universe.Bank.Websocket.Requests.CreateAccountTest do + + use Helix.Test.Case.Integration + + alias Helix.Websocket.Requestable + alias Helix.Entity.Model.Entity + alias Helix.Network.Query.Network, as: NetworkQuery + alias Helix.Universe.Bank.Websocket.Requests.CreateAccount, + as: BankCreateAccountRequest + + alias Helix.Test.Channel.Setup, as: ChannelSetup + alias Helix.Test.Server.Setup, as: ServerSetup + alias Helix.Test.Universe.NPC.Helper, as: NPCHelper + + describe "BankCreateAccountRequest.check_params/2" do + test "accepts when receives valid information" do + # Setups an Account socket. + {socket, %{account: account}} = ChannelSetup.create_socket() + + entity_id = Entity.ID.cast!(to_string(account.account_id)) + + # Setups an Server new Server to created account + {gateway, _} = ServerSetup.server(entity_id: entity_id) + + # Gets a Bank from database. + {bank, _} = NPCHelper.bank + atm_id = List.first(bank.servers).id + + # Creates the parameters for creating a account. + payload = + %{ + "atm_id" => to_string(atm_id), + "gateway" => to_string(gateway.server_id) + } + + # Creates a new BankCreateAccountRequest with the parameters. + request = + BankCreateAccountRequest.new(payload) + + # Asserts that given parameters are okay. + assert {:ok, request} = + Requestable.check_params(request, socket) + + # Assert the parameters after checking are right. + assert request.params.atm_id == atm_id + end + + test "rejects when receives invalid atm_id" do + # Setups an Account socket. + {socket, _} = ChannelSetup.create_socket() + + # Creates invalid parameters. + payload = + %{ + "atm_id" => "DROP TABLE helix_dev_accounts;", + "gateway" => "FROM *" + } + + # Create BankCreateAccountRequest with invalid parameters. + request = + BankCreateAccountRequest.new(payload) + + # Asserts that given parameters are rejected. + assert {:error, reason, _} = + Requestable.check_params(request, socket) + + # Asserts that the error message is "bad_request. + assert reason.message == "bad_request" + end + end + + describe "BankCreateAccountRequest.check_permissions/2" do + test "accepts when the ATM is a bank" do + # Setups an Account socket. + {socket, %{account: account}} = ChannelSetup.create_socket() + + entity_id = Entity.ID.cast!(to_string(account.account_id)) + + # Setups an Server new Server to created account + {gateway, _} = ServerSetup.server(entity_id: entity_id) + + # Gets a Bank and it's atm_id. + {bank, _} = NPCHelper.bank + atm_id = List.first(bank.servers).id + + # Create valid parameters. + payload = + %{ + "atm_id" => to_string(atm_id), + "gateway" => to_string(gateway.server_id) + } + + # Create BankCreateAccountRequest with valid parameters. + request = + BankCreateAccountRequest.new(payload) + + # Checks if parameters are valid. + {:ok, request} = + Requestable.check_params(request, socket) + + # Asserts that Bank is a Bank. + assert {:ok, request} = + Requestable.check_permissions(request, socket) + + # Asserts that atm_id is on meta. + assert request.meta.atm_id == atm_id + end + + test "rejects when the ATM is not a server" do + # Setups an Account socket. + {socket, %{account: account}} = ChannelSetup.create_socket() + + entity_id = Entity.ID.cast!(to_string(account.account_id)) + + # Setups an Server new Server to created account + {gateway, _} = ServerSetup.server(entity_id: entity_id) + + # Setups a ordinary server. + {not_bank, _} = ServerSetup.server() + + # Create parameters passing the ordinary server's id as + # an atm_id. + payload = + %{ + "atm_id" => to_string(not_bank.server_id), + "gateway" => to_string(gateway.server_id) + } + + # Create BankCreateAccountRequest with parameters. + request = + BankCreateAccountRequest.new(payload) + + # Asserts the parameters are in right format. + assert {:ok, request} = + Requestable.check_params(request, socket) + + # Asserts error because the ordinary server is not a bank. + assert {:error, reason, _} = + Requestable.check_permissions(request, socket) + + # Asserts the error message is "atm_not_a_bank". + assert reason.message == "atm_not_a_bank" + end + end + + describe "BankCreateAccountRequest.handle_request/2" do + test "creates BankAccount on database" do + # Setups an Account socket. + {socket, %{account: account}} = ChannelSetup.create_socket() + + entity_id = Entity.ID.cast!(to_string(account.account_id)) + + # Setups an Server new Server to created account + {gateway, _} = ServerSetup.server(entity_id: entity_id) + + # Gets a Bank and it's atm_id. + {bank, _} = NPCHelper.bank + atm_id = List.first(bank.servers).id + + # Creates valid parameters. + payload = + %{ + "atm_id" => to_string(atm_id), + "gateway" => to_string(gateway.server_id) + } + + # Creates a BankCreateAccountRequest with the parameters. + request = + BankCreateAccountRequest.new(payload) + + # Checks if parameters are in correct format. + {:ok, request} = + Requestable.check_params(request, socket) + + # Checks if the given bank is a bank. + {:ok, request} = + Requestable.check_permissions(request, socket) + + # Asserts that the request is handled correctly. + assert {:ok, request} = + Requestable.handle_request(request, socket) + end + end +end diff --git a/test/universe/bank/websocket/channel/requests/logout_test.exs b/test/universe/bank/websocket/channel/requests/logout_test.exs new file mode 100644 index 00000000..97832b5f --- /dev/null +++ b/test/universe/bank/websocket/channel/requests/logout_test.exs @@ -0,0 +1,47 @@ +defmodule Helix.Test.Universe.Bank.Websocket.Requests.LogoutTest do + + use Helix.Test.Case.Integration + + import Phoenix.ChannelTest + + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + alias Helix.Test.Channel.Helper, as: ChannelHelper + alias Helix.Test.Channel.Setup, as: ChannelSetup + + describe "logout" do + test "terminates connection with the channel" do + # Setups an Account socket. + {socket, %{entity: entity, server: gateway}} = + ChannelSetup.create_socket() + + # Setups a BankAccount. + bank_acc = BankSetup.account!() + atm_id = bank_acc.atm_id + account_number = bank_acc.account_number + + # Creates topic to log in the bank channel. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Create the payload which will be used to join the bank channel. + payload = + %{ + "entity_id" => to_string(entity.entity_id), + "password" => bank_acc.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins the bank channel. + {:ok, _bootstrap, bnk_socket} = + join(socket, topic, payload) + + # Request logout + push bnk_socket, "bank.logout", %{} + + # Wait process teardown. Required + :timer.sleep(50) + + # Channel no longer exists + refute Process.alive? bnk_socket.channel_pid + end + end +end diff --git a/test/universe/bank/websocket/channel/requests/reveal_password_test.exs b/test/universe/bank/websocket/channel/requests/reveal_password_test.exs new file mode 100644 index 00000000..177354ff --- /dev/null +++ b/test/universe/bank/websocket/channel/requests/reveal_password_test.exs @@ -0,0 +1,193 @@ +defmodule Helix.Test.Universe.Bank.Websocket.Requests.RevealPassword do + + use Helix.Test.Case.Integration + + import Phoenix.ChannelTest + + alias Helix.Websocket.Requestable + alias Helix.Universe.Bank.Websocket.Requests.RevealPassword, + as: BankRevealPasswordRequest + + alias Helix.Test.Channel.Helper, as: ChannelHelper + alias Helix.Test.Channel.Setup, as: ChannelSetup + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + describe "BankRevealPasswordRequest.check_params/2" do + test "accepts when token is in valid format" do + {socket, %{server: gateway, entity: entity}} = + ChannelSetup.create_socket() + + # Create a Expiration date for the token that will expire + # in 5 minutes. + expiration = + DateTime.utc_now() + |> DateTime.to_unix(:second) + |> Kernel.+(60 * 5) + |> DateTime.from_unix!(:second) + + # Create Bank Account and get it's information. + bank_account = BankSetup.account!() + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + # Create the topic to log in the bank channel. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Create a token with signied to previous created bank account + # and expiration date. + {token, _} = + BankSetup.token(acc: bank_account, expiration_date: expiration) + + # Create the payload which will be used to join the bank channel. + payload = + %{ + "entity_id" => to_string(entity.entity_id), + "password" => bank_account.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins the bank channel. + {:ok, _bootstrap, bnk_socket} = + join(socket, topic, payload) + + params = + %{ + "token" => token.token_id + } + + # Creates a BankRevealPasswordRequest. + request = BankRevealPasswordRequest.new(params) + + assert {:ok, request} = + Requestable.check_params(request, bnk_socket) + + # Check if token.id still the same as before after checking. + assert request.params.token == token.token_id + end + end + + describe "BankRevealPasswordRequest.check_perimssions/2" do + test "accepts when token exists and belongs to bank account" do + {socket, %{server: gateway, entity: entity}} = + ChannelSetup.create_socket() + + # Create a Expiration date for the token that will expire + # in 5 minutes. + expiration = + DateTime.utc_now() + |> DateTime.to_unix(:second) + |> Kernel.+(60 * 5) + |> DateTime.from_unix!(:second) + + # Create Bank Account and get it's information. + bank_account = BankSetup.account!() + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + # Create the topic to log in the bank channel. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Create a token with signied to previous created bank account + # and expiration date. + {token, _} = + BankSetup.token(acc: bank_account, expiration_date: expiration) + + # Create the payload which will be used to join the bank channel. + payload = + %{ + "entity_id" => to_string(entity.entity_id), + "password" => bank_account.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins the bank channel. + {:ok, _bootstrap, bnk_socket} = + join(socket, topic, payload) + + params = + %{ + "token" => token.token_id + } + + # Creates a BankRevealPasswordRequest. + request = BankRevealPasswordRequest.new(params) + + {:ok, request} = + Requestable.check_params(request, bnk_socket) + + assert {:ok, request} = + Requestable.check_permissions(request, bnk_socket) + + # Checks if token information is correct. + assert request.meta.token.atm_id == token.atm_id + assert request.meta.token.token_id == token.token_id + assert request.meta.token.account_number == token.account_number + end + end + + describe "BankRevealPasswordRequest.handle_request/2" do + test "creates reveal password process" do + {socket, %{server: gateway, entity: entity}} = + ChannelSetup.create_socket() + + # Create a Expiration date for the token that will expire + # in 5 Minutes. + expiration = + DateTime.utc_now() + |> DateTime.to_unix(:second) + |> Kernel.+(60 * 5) + |> DateTime.from_unix!(:second) + + # Create Bank Account and get it's information. + bank_account = BankSetup.account!() + atm_id = bank_account.atm_id + account_number = bank_account.account_number + + # Create the topic to log in the bank channel. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Create a token with signied to previous created bank account + # and expiration date. + {token, _} = + BankSetup.token(acc: bank_account, expiration_date: expiration) + + # Create the payload which will be used to join the bank channel. + payload = + %{ + "entity_id" => to_string(entity.entity_id), + "password" => bank_account.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins the bank channel. + {:ok, _bootstrap, bnk_socket} = + join(socket, topic, payload) + + params = + %{ + "token" => token.token_id + } + + # Creates a BankRevealPasswordRequest. + request = BankRevealPasswordRequest.new(params) + + # Checks if params isn't invalid. + {:ok, request} = + Requestable.check_params(request, bnk_socket) + + # Checks if token exists and is assigned to the correct account. + {:ok, request} = + Requestable.check_permissions(request, bnk_socket) + + assert {:ok, request} = + Requestable.handle_request(request, bnk_socket) + + # Checks if process information is correct. + assert request.meta.process + assert request.meta.process.type == :bank_reveal_password + assert request.meta.process.data.atm_id == bank_account.atm_id + assert request.meta.process.data.account_number == + bank_account.account_number + end + end +end diff --git a/test/universe/bank/websocket/channel/requests/transfer_test.exs b/test/universe/bank/websocket/channel/requests/transfer_test.exs new file mode 100644 index 00000000..fb03df18 --- /dev/null +++ b/test/universe/bank/websocket/channel/requests/transfer_test.exs @@ -0,0 +1,202 @@ +defmodule Helix.Test.Universe.Bank.Websocket.Requests.TransferTest do + + use Helix.Test.Case.Integration + + import Phoenix.ChannelTest + + alias Helix.Account.Query.Account, as: AccountQuery + alias Helix.Websocket.Requestable + alias Helix.Universe.Bank.Websocket.Requests.Transfer, as: BankTransferRequest + + alias Helix.Test.Channel.Request.Helper, as: RequestHelper + alias Helix.Test.Channel.Helper, as: ChannelHelper + alias Helix.Test.Channel.Setup, as: ChannelSetup + alias Helix.Test.Network.Helper, as: NetworkHelper + alias Helix.Test.Process.TOPHelper + alias Helix.Test.Server.Helper, as: ServerHelper + alias Helix.Test.Universe.Bank.Setup, as: BankSetup + + @internet_id NetworkHelper.internet_id() + + describe "BankTransferRequest.check_params/2" do + test "validates expected data" do + # Setups an Account socket and gets the gateway + # from the related information. + {socket, %{server: gateway}} = + ChannelSetup.create_socket() + + # Setups sending BankAccount. + sending_acc = BankSetup.account!(balance: 600) + atm_id = sending_acc.atm_id + account_number = sending_acc.account_number + + # Setups receiving BankAccount. + to_account = BankSetup.account!() + + # Gets ip for the receiving BankAccount's ATM.id. + bank_ip = ServerHelper.get_ip(to_account.atm_id) + + # Gets entity id from the socket assigns. + entity_id = socket.assigns.entity_id + + # Creates topic for connecting on bank channel. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Creates parameters for joining the bank channel. + payload = + %{ + "entity_id" => to_string(entity_id), + "password" => sending_acc.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins the bank channel. + {:ok, _bootstrap, bank_socket} = + join(socket, topic, payload) + + # Creates parameters for BankTransferRequest. + params = + %{ + "to_acc" => to_account.account_number, + "to_bank_net" => to_string(@internet_id), + "to_bank_ip" => bank_ip, + "password" => to_account.password, + "amount" => 300 + } + + # Creates BankTransferRequest with parameters. + request = BankTransferRequest.new(params) + + # Asserts that parameters are in the correct format. + assert {:ok, _request} = Requestable.check_params(request, bank_socket) + end + end + + describe "BankTransferRequest.check_permissions/2" do + test "accepts when everything is OK" do + # Setups an Account socket and gets the related gateway. + {socket, %{server: gateway}} = + ChannelSetup.create_socket() + + # Gets the Entity related to the socket. + entity_id = socket.assigns.entity_id + + # Setups the sending BankAccount. + sending_acc = BankSetup.account!(balance: 600) + atm_id = sending_acc.atm_id + account_number = sending_acc.account_number + + # Setups the receiving BankAccount. + to_account = BankSetup.account!() + + # Gets the Bank's ip and sets the amount to transfer. + bank_ip = ServerHelper.get_ip(to_account.atm_id) + amount = 300 + + # Creates topic for connecting on bank channel. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Creates parameters for joining the bank channel. + payload = + %{ + "entity_id" => to_string(entity_id), + "password" => sending_acc.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins bank channel. + {:ok, _bootstrap, bnk_socket} = + join(socket, topic, payload) + + # Gets the player's account_id from socket. + account_id = bnk_socket.assigns.account_id + + # Creates parameters for the request. + params = + %{ + bank_account: to_account.account_number, + bank_ip: bank_ip, + bank_net: @internet_id, + password: sending_acc.password, + amount: amount + } + + # Mocks the BankTransferRequest pasing parameters. + request = RequestHelper.mock_request(BankTransferRequest, params) + + # Asserts that request passes on permission test. + assert {:ok, request} = Requestable.check_permissions(request, bnk_socket) + + # Asserts that meta fields are all valid. + assert request.meta.to_account == to_account + assert request.meta.from_account == sending_acc + assert request.meta.amount == amount + assert request.meta.started_by == AccountQuery.fetch(account_id) + assert request.meta.gateway == gateway + end + end + + describe "BankTransferRequest.handle_request/2" do + test "starts the process" do + # Setups an Account socket and gets related gateway. + {socket, %{server: gateway}} = + ChannelSetup.create_socket() + + # Gets entity_id from socket. + entity_id = socket.assigns.entity_id + + # Setups sending BankAccount. + sending_acc = BankSetup.account!(balance: 600) + atm_id = sending_acc.atm_id + account_number = sending_acc.account_number + + # Setups receiving BankAccount. + to_account = BankSetup.account!() + + # Gets receiving BankAccount's ATM ip. + bank_ip = ServerHelper.get_ip(to_account.atm_id) + + # Sets the amount to be tranfered. + amount = 300 + + # Creates topic for connecting on bank channel. + topic = ChannelHelper.bank_topic_name(atm_id, account_number) + + # Creates the parameters for joining bank channel. + payload = + %{ + "entity_id" => to_string(entity_id), + "password" => sending_acc.password, + "gateway_id" => to_string(gateway.server_id) + } + + # Joins bank channel. + {:ok, _bootstrap, bnk_socket} = + join(socket, topic, payload) + + # Create parameters to request. + params = + %{ + bank_account: to_account.account_number, + bank_ip: bank_ip, + bank_net: @internet_id, + password: sending_acc.password, + amount: amount + } + + # Mocks BankTranferRequest. + request = RequestHelper.mock_request(BankTransferRequest, params) + + # Checks permissions to the player do the tranfer. + {:ok, request} = Requestable.check_permissions(request, bnk_socket) + + # Asserts that request is handled correctly. + assert {:ok, request} = Requestable.handle_request(request, bnk_socket) + + # Asserts that process has been created. + assert request.meta.process + + TOPHelper.top_stop(gateway) + end + end +end