diff --git a/config/test.exs b/config/test.exs
index f6b7ffb..e489422 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -23,6 +23,9 @@ config :basket, BasketWeb.Endpoint,
# In test we don't send emails.
config :basket, Basket.Mailer, adapter: Swoosh.Adapters.Test
+# config :basket, :alpaca_http_client, Basket.Http.MockAlpaca
+# config :basket, :alpaca_websocket_client, Basket.Websocket.MockAlpaca
+
# Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false
diff --git a/lib/basket/http/alpaca.ex b/lib/basket/http/alpaca.ex
new file mode 100644
index 0000000..505ecfc
--- /dev/null
+++ b/lib/basket/http/alpaca.ex
@@ -0,0 +1,13 @@
+defmodule Basket.Http.Alpaca do
+ @moduledoc """
+ Interface for the Alpaca REST API.
+ """
+ @callback latest_quote(ticker :: String.t()) :: {:ok, map} | {:error, String.t()}
+ @callback list_assets() :: {:ok, map} | {:error, String.t()}
+
+ def latest_quote(ticker), do: impl().latest_quote(ticker)
+ def list_assets, do: impl().list_assets()
+
+ defp impl,
+ do: Application.get_env(:basket, :alpaca_http_client, Basket.Http.Alpaca.Impl)
+end
diff --git a/lib/basket/alpaca/http/client.ex b/lib/basket/http/alpaca/impl.ex
similarity index 72%
rename from lib/basket/alpaca/http/client.ex
rename to lib/basket/http/alpaca/impl.ex
index 40a31c7..41a3ef7 100644
--- a/lib/basket/alpaca/http/client.ex
+++ b/lib/basket/http/alpaca/impl.ex
@@ -1,22 +1,19 @@
-defmodule Basket.Alpaca.HttpClient do
+defmodule Basket.Http.Alpaca.Impl do
@moduledoc """
- HTTP client for Alpaca API.
+ Implmentation of the Alpaca API HTTP client.
"""
-
use HTTPoison.Base
- require Logger
+ @behaviour Basket.Http.Alpaca
@assets_resource "/v2/assets"
@latest_quotes_resource "/v2/stocks/bars/latest"
- def process_request_headers(headers) do
- headers ++ [{"APCA-API-KEY-ID", api_key()}, {"APCA-API-SECRET-KEY", api_secret()}]
- end
-
- @spec latest_quote(String.t()) :: {:error, any()} | {:ok, map()}
+ @impl Basket.Http.Alpaca
def latest_quote(ticker) do
- case get("#{data_url()}#{@latest_quotes_resource}", [],
+ case get(
+ "#{data_url()}#{@latest_quotes_resource}",
+ [],
params: %{feed: "iex", symbols: ticker}
) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
@@ -27,9 +24,11 @@ defmodule Basket.Alpaca.HttpClient do
end
end
- @spec list_assets() :: {:error, any()} | {:ok, list(map())}
+ @impl Basket.Http.Alpaca
def list_assets do
- case get("#{market_url()}#{@assets_resource}", [],
+ case get(
+ "#{market_url()}#{@assets_resource}",
+ [],
params: %{status: "active", asset_class: "us_equity"}
) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
@@ -40,10 +39,14 @@ defmodule Basket.Alpaca.HttpClient do
end
end
- def process_response_body(body) do
- Jason.decode!(body)
+ @impl true
+ def process_request_headers(headers) do
+ headers ++ [{"APCA-API-KEY-ID", api_key()}, {"APCA-API-SECRET-KEY", api_secret()}]
end
+ @impl true
+ def process_response_body(body), do: Jason.decode!(body)
+
defp data_url, do: Application.fetch_env!(:basket, :alpaca)[:data_http_url]
defp market_url, do: Application.fetch_env!(:basket, :alpaca)[:market_http_url]
diff --git a/lib/basket/websocket/alpaca/impl.ex b/lib/basket/websocket/alpaca/impl.ex
index 36d5c70..c1c98c9 100644
--- a/lib/basket/websocket/alpaca/impl.ex
+++ b/lib/basket/websocket/alpaca/impl.ex
@@ -5,6 +5,8 @@ defmodule Basket.Websocket.Alpaca.Impl do
require Logger
+ @behaviour Basket.Websocket.Alpaca
+
@subscribe_message %{
action: :subscribe
}
@@ -12,12 +14,14 @@ defmodule Basket.Websocket.Alpaca.Impl do
action: :unsubscribe
}
+ @impl true
def start_link(state) do
Logger.info("Starting Alpaca websocket client.")
WebSockex.start_link(iex_feed(), Basket.Websocket.Alpaca, state, extra_headers: auth_headers())
end
+ @impl true
def subscribe(tickers) do
decoded_message = Jason.encode!(build_message(@subscribe_message, tickers))
@@ -27,6 +31,7 @@ defmodule Basket.Websocket.Alpaca.Impl do
end
end
+ @impl true
def unsubscribe(tickers) do
decoded_message = Jason.encode!(build_message(@unsubscribe_message, tickers))
diff --git a/lib/basket_web/components/search_input.ex b/lib/basket_web/components/search_input.ex
index ac37a83..bb38dd2 100644
--- a/lib/basket_web/components/search_input.ex
+++ b/lib/basket_web/components/search_input.ex
@@ -42,8 +42,8 @@ defmodule BasketWeb.Components.SearchInput do
{/for}
<:actions>
- <.button class="whitespace-nowrap">
- Add
+ <.button class="bg-green-600 whitespace-nowrap w-12">
+ +
diff --git a/lib/basket_web/components/ticker_bar_table.ex b/lib/basket_web/components/ticker_bar_table.ex
new file mode 100644
index 0000000..6607180
--- /dev/null
+++ b/lib/basket_web/components/ticker_bar_table.ex
@@ -0,0 +1,48 @@
+defmodule BasketWeb.Components.TickerBarTable do
+ @moduledoc """
+ Allows the user to search for and add a ticker to the table. Will make an HTTP call
+ if the ticker list is not populated, otherwise it will pull the list from the cache.
+ """
+
+ use Surface.LiveComponent
+
+ import BasketWeb.CoreComponents
+
+ prop rows, :list, default: []
+
+ attr :id, :string, required: true
+ attr :class, :string, default: nil
+
+ def mount(socket) do
+ socket = assign(socket, basket: [])
+
+ {:ok, socket}
+ end
+
+ def render(assigns) do
+ ~F"""
+
+ <.table id="ticker-list" rows={@rows}>
+ <:col :let={ticker} key="S" label="ticker">{value_from_ticker_bar(ticker["S"])}
+ <:col :let={ticker} key="o" label="open">{value_from_ticker_bar(ticker["o"])}
+ <:col :let={ticker} key="h" label="high">{value_from_ticker_bar(ticker["h"])}
+ <:col :let={ticker} key="l" label="low">{value_from_ticker_bar(ticker["l"])}
+ <:col :let={ticker} key="c" label="close">{value_from_ticker_bar(ticker["c"])}
+ <:col :let={ticker} key="v" label="volume">{value_from_ticker_bar(ticker["v"])}
+ <:col :let={ticker} key="t" label="timestamp">{value_from_ticker_bar(ticker["t"])}
+ <:col :let={ticker} label="remove">
+ <.button
+ phx-click="ticker-remove"
+ phx-value-ticker={value_from_ticker_bar(ticker["S"])}
+ class="bg-red-600"
+ >
+ X
+
+ "
+
+
+ """
+ end
+
+ defp value_from_ticker_bar(ticker_bar), do: elem(ticker_bar, 0)
+end
diff --git a/lib/basket_web/live/overview.ex b/lib/basket_web/live/overview.ex
index 79796d7..c84b098 100644
--- a/lib/basket_web/live/overview.ex
+++ b/lib/basket_web/live/overview.ex
@@ -4,18 +4,15 @@ defmodule BasketWeb.Overview do
"""
use Surface.LiveView
- import BasketWeb.CoreComponents
-
require Logger
- alias Basket.Alpaca.HttpClient
- alias Basket.Websocket.Alpaca
- alias BasketWeb.Components.{NavRow, SearchInput}
+ alias Basket.{Http, Websocket}
+ alias BasketWeb.Components.{NavRow, SearchInput, TickerBarTable}
prop tickers, :list, default: []
def mount(_, _, socket) do
- BasketWeb.Endpoint.subscribe(Alpaca.bars_topic())
+ BasketWeb.Endpoint.subscribe(Websocket.Alpaca.bars_topic())
socket = assign(socket, tickers: [])
socket = assign(socket, basket: [])
@@ -34,38 +31,50 @@ defmodule BasketWeb.Overview do
end
def handle_event("ticker-add", %{"selected-ticker" => ticker}, socket) do
- :ok = Alpaca.subscribe(%{bars: [ticker], quotes: [], trades: []})
+ basket_tickers = tickers(socket)
- socket =
- case HttpClient.latest_quote(ticker) do
- {:ok, response} ->
- %{"bars" => %{^ticker => bars}} = response
+ if ticker in basket_tickers or String.trim(ticker) == "" do
+ {:noreply, socket}
+ else
+ :ok = Websocket.Alpaca.subscribe(%{bars: [ticker], quotes: [], trades: []})
- initial_bars = for {k, v} <- bars, into: %{}, do: {k, {v, ""}}
+ socket =
+ case Http.Alpaca.latest_quote(ticker) do
+ {:ok, response} ->
+ %{"bars" => %{^ticker => bars}} = response
- assign(
- socket,
- :basket,
- socket.assigns.basket ++ [Map.merge(initial_bars, %{"S" => {ticker, ""}})]
- )
+ initial_bars = for {k, v} <- bars, into: %{}, do: {k, {v, ""}}
- {:error, error} ->
- Logger.error("Could not subscribe to ticker: #{error}")
- socket
- end
+ assign(
+ socket,
+ :basket,
+ socket.assigns.basket ++ [Map.merge(initial_bars, %{"S" => {ticker, ""}})]
+ )
- {:reply, %{}, socket}
+ {:error, error} ->
+ Logger.error("Could not subscribe to ticker: #{error}")
+ socket
+ end
+
+ {:reply, %{}, socket}
+ end
end
def handle_event("ticker-remove", %{"ticker" => ticker}, socket) do
- :ok = Alpaca.unsubscribe(%{bars: [ticker], quotes: [], trades: []})
+ basket_tickers = tickers(socket)
- {:reply, %{},
- assign(
- socket,
- :basket,
- Enum.filter(socket.assigns.basket, fn t -> elem(t["S"], 0) != ticker end)
- )}
+ if ticker not in basket_tickers or String.trim(ticker) == "" do
+ {:noreply, socket}
+ else
+ :ok = Websocket.Alpaca.unsubscribe(%{bars: [ticker], quotes: [], trades: []})
+
+ {:reply, %{},
+ assign(
+ socket,
+ :basket,
+ Enum.filter(socket.assigns.basket, fn t -> elem(t["S"], 0) != ticker end)
+ )}
+ end
end
def handle_info(
@@ -106,20 +115,7 @@ defmodule BasketWeb.Overview do
<.live_component module={SearchInput} id="stock-search-input" tickers={@tickers} />
- <.table id="ticker-list" rows={@basket}>
- <:col :let={ticker} key="S" label="ticker">{elem(ticker["S"], 0)}
- <:col :let={ticker} key="o" label="open">{elem(ticker["o"], 0)}
- <:col :let={ticker} key="h" label="high">{elem(ticker["h"], 0)}
- <:col :let={ticker} key="l" label="low">{elem(ticker["l"], 0)}
- <:col :let={ticker} key="c" label="close">{elem(ticker["c"], 0)}
- <:col :let={ticker} key="v" label="volume">{elem(ticker["v"], 0)}
- <:col :let={ticker} key="t" label="timestamp">{elem(ticker["t"], 0)}
- <:col :let={ticker} label="remove">
- <.button phx-click="ticker-remove" phx-value-ticker={elem(ticker["S"], 0)}>
- Remove
-
- "
-
+ <.live_component module={TickerBarTable} id="ticker-bar-table" rows={@basket} />
"""
end
@@ -134,7 +130,7 @@ defmodule BasketWeb.Overview do
end
defp load_tickers do
- case HttpClient.list_assets() do
+ case Http.Alpaca.list_assets() do
{:ok, result} ->
tickers =
Enum.map(result, fn asset ->
@@ -149,4 +145,9 @@ defmodule BasketWeb.Overview do
{:ignore, []}
end
end
+
+ defp tickers(socket) do
+ Enum.map(socket.assigns.basket, &Map.get(&1, "S"))
+ |> Enum.map(fn x -> if is_tuple(x), do: elem(x, 0), else: x end)
+ end
end
diff --git a/lib/basket_web/live/overview/bars.ex b/lib/basket_web/live/overview/bars.ex
new file mode 100644
index 0000000..090afab
--- /dev/null
+++ b/lib/basket_web/live/overview/bars.ex
@@ -0,0 +1,38 @@
+defmodule BasketWeb.Overview.TickerBar do
+ @moduledoc """
+ Stateful representation of a cell on the ticker bar table.
+ """
+ alias __MODULE__
+
+ defstruct value: nil, prev_value: nil
+
+ @typedoc """
+ This module is responsible for taking the data from an external call and updating the state of the cell.
+ """
+ @type t(value, prev_value) :: %TickerBar{
+ value: value,
+ prev_value: prev_value
+ }
+
+ @spec set_value(TickerBar.t(any(), any()), any()) :: TickerBar.t(any(), any())
+ def set_value(ticker_bar, value) do
+ %TickerBar{ticker_bar | value: value, prev_value: ticker_bar.value}
+ end
+
+ def change_direction(ticker_bar) do
+ case change_value(ticker_bar) do
+ x when x > 0 -> 1
+ x when x < 0 -> -1
+ _ -> 0
+ end
+ end
+
+ @spec change_value(%{:prev_value => any() | nil, :value => any()}) :: integer()
+ def change_value(%{value: value, prev_value: prev_value}) do
+ if prev_value == nil or not (is_number(value) and is_number(prev_value)) do
+ 0
+ else
+ value - prev_value
+ end
+ end
+end
diff --git a/test/basket_web/live/overview_test.exs b/test/basket_web/live/overview_test.exs
index 4907d20..f86753b 100644
--- a/test/basket_web/live/overview_test.exs
+++ b/test/basket_web/live/overview_test.exs
@@ -1,5 +1,5 @@
defmodule BasketWeb.OverviewTest do
- use BasketWeb.ConnCase
+ use BasketWeb.ConnCase, async: false
require Phoenix.LiveViewTest
@@ -8,16 +8,45 @@ defmodule BasketWeb.OverviewTest do
alias BasketWeb.Overview
@assigns_map %{__changed__: %{__context__: true}}
- @socket %{
- assigns: @assigns_map,
- __context__: %{},
- flash: %{},
- live_action: nil
- }
+
+ setup do
+ # Shares the mock with the Cachex fallback function.
+ Mox.set_mox_global()
+
+ {:ok,
+ %{
+ bars: %{
+ "XYZ" => %{
+ "S" => "XYZ",
+ "c" => 187.15,
+ "h" => 187.15,
+ "l" => 187.05,
+ "n" => 357,
+ "o" => 187.11,
+ "t" => "2023-11-15T20:59:00Z",
+ "v" => 43_025,
+ "vw" => 187.117416
+ }
+ },
+ basket_with_row: [
+ %{
+ "S" => {"XYZ", ""},
+ "c" => {188.15, ""},
+ "h" => {188.15, ""},
+ "l" => {188.05, ""},
+ "n" => {358, ""},
+ "o" => {188.11, ""},
+ "t" => {"2023-11-15T20:59:00Z", ""},
+ "v" => {43_031, ""},
+ "vw" => {188.117416, ""}
+ }
+ ]
+ }}
+ end
describe "mount/3" do
test "assigns empty lists to keys" do
- Basket.Websocket.MockAlpaca |> expect(:start_link, fn state -> {:ok, 1} end)
+ Basket.Websocket.MockAlpaca |> expect(:start_link, fn _state -> {:ok, 1} end)
assert({:ok, socket} = Overview.mount([], %{}, @assigns_map))
@@ -32,9 +61,9 @@ defmodule BasketWeb.OverviewTest do
end
end
- describe "handle_event/3" do
+ describe "handle_event/3 search" do
test "ticker search does nothing without search criteria" do
- assert {:noreply, socket} =
+ assert {:noreply, _socket} =
Overview.handle_event(
"ticker-search",
%{"selected-ticker" => "ABC"},
@@ -42,19 +71,221 @@ defmodule BasketWeb.OverviewTest do
)
end
- # test "ticker search updates the ticker list" do
- # assert {:reply, %{}, socket} =
- # Overview.handle_event(
- # "ticker-search",
- # %{"selected-ticker" => "XYZ"},
- # Map.merge(@assigns_map, %{assigns: %{tickers: ["ABC", "XYZ"]}})
- # )
+ test "ticker search does nothing if the ticker list is already populated" do
+ assert {:noreply,
+ %{
+ __changed__: %{__context__: true},
+ assigns: %{tickers: ["ABC", "XYZ"]}
+ }} =
+ Overview.handle_event(
+ "ticker-search",
+ %{"selected-ticker" => "XYZ"},
+ Map.merge(@assigns_map, %{assigns: %{tickers: ["ABC", "XYZ"]}})
+ )
+ end
+
+ test "populates the ticker list with a web call if the cache is empty" do
+ Basket.Http.MockAlpaca
+ |> expect(:list_assets, fn ->
+ {:ok,
+ [
+ %{
+ "attributes" => [],
+ "class" => "us_equity",
+ "easy_to_borrow" => false,
+ "exchange" => "OTC",
+ "fractionable" => false,
+ "id" => "0634e31f-2a61-4990-b713-a4be6d9eee49",
+ "maintenance_margin_requirement" => 100,
+ "marginable" => false,
+ "name" => "METACRINE INC Common Stock",
+ "shortable" => false,
+ "status" => "active",
+ "symbol" => "MTCR",
+ "tradable" => false
+ },
+ %{
+ "attributes" => [],
+ "class" => "us_equity",
+ "easy_to_borrow" => false,
+ "exchange" => "OTC",
+ "fractionable" => false,
+ "id" => "ae2ab9f2-d2aa-4e7b-9ef8-2ffdf78ec0ff",
+ "maintenance_margin_requirement" => 100,
+ "marginable" => false,
+ "name" => "MTN Group, Ltd. Sponsored American Depositary Receipt",
+ "shortable" => false,
+ "status" => "active",
+ "symbol" => "MTNOY",
+ "tradable" => false
+ }
+ ]}
+ end)
- # assert socket == %{
- # __changed__: %{__context__: true, tickers: true},
- # __context__: %{},
- # tickers: ["XYZ"]
- # }
- # end
+ assert {:reply, %{},
+ %{
+ __changed__: %{__context__: true, tickers: true},
+ assigns: %{tickers: []}
+ }} =
+ Overview.handle_event(
+ "ticker-search",
+ %{"selected-ticker" => "XYZ"},
+ Map.merge(@assigns_map, %{assigns: %{tickers: []}})
+ )
+ end
+ end
+
+ describe "handle_event/3 add_ticker" do
+ test "adds bars data to the liveview for the selected ticker", %{bars: bars} do
+ expect(Basket.Http.MockAlpaca, :latest_quote, fn _ -> {:ok, %{"bars" => bars}} end)
+ expect(Basket.Websocket.MockAlpaca, :subscribe, fn _ticker_subs -> :ok end)
+
+ assert {:reply, %{},
+ %{
+ __changed__: %{__context__: true, basket: true},
+ assigns: %{tickers: [], basket: []},
+ basket: [_bars]
+ }} =
+ Overview.handle_event(
+ "ticker-add",
+ %{"selected-ticker" => "XYZ"},
+ Map.merge(@assigns_map, %{assigns: %{tickers: [], basket: []}})
+ )
+ end
+
+ test "does nothing if no ticker is provided" do
+ assert {:noreply,
+ %{
+ __changed__: %{__context__: true},
+ assigns: %{tickers: [], basket: []}
+ }} =
+ Overview.handle_event(
+ "ticker-add",
+ %{"selected-ticker" => ""},
+ Map.merge(@assigns_map, %{assigns: %{tickers: [], basket: []}})
+ )
+ end
+
+ test "does nothing if the ticker is already present", %{basket_with_row: basket_with_row} do
+ assert {:noreply,
+ %{
+ __changed__: %{__context__: true},
+ assigns: %{tickers: [], basket: [_bars]}
+ }} =
+ Overview.handle_event(
+ "ticker-add",
+ %{"selected-ticker" => "XYZ"},
+ Map.merge(@assigns_map, %{
+ assigns: %{
+ tickers: [],
+ basket: basket_with_row
+ }
+ })
+ )
+ end
+ end
+
+ describe "handle_event/3 remove_ticker" do
+ test "removes a ticker if it is present in the liveview", %{
+ bars: bars,
+ basket_with_row: basket_with_row
+ } do
+ expect(Basket.Http.MockAlpaca, :latest_quote, fn _ -> {:ok, %{"bars" => bars}} end)
+ expect(Basket.Websocket.MockAlpaca, :unsubscribe, fn _ticker_subs -> :ok end)
+
+ assert {:reply, %{},
+ %{
+ __changed__: %{__context__: true, basket: true},
+ assigns: %{tickers: [], basket: ^basket_with_row},
+ basket: []
+ }} =
+ Overview.handle_event(
+ "ticker-remove",
+ %{"ticker" => "XYZ"},
+ Map.merge(@assigns_map, %{assigns: %{tickers: [], basket: basket_with_row}})
+ )
+ end
+
+ test "does nothing if no ticker is provided" do
+ assert {:noreply,
+ %{
+ __changed__: %{__context__: true},
+ assigns: %{tickers: [], basket: []}
+ }} =
+ Overview.handle_event(
+ "ticker-remove",
+ %{"ticker" => ""},
+ Map.merge(@assigns_map, %{assigns: %{tickers: [], basket: []}})
+ )
+ end
+
+ test "does nothing if the ticker is not in the liveview" do
+ assert {:noreply,
+ %{
+ __changed__: %{__context__: true},
+ assigns: %{tickers: [], basket: []}
+ }} =
+ Overview.handle_event(
+ "ticker-remove",
+ %{"ticker" => "XYZ"},
+ Map.merge(@assigns_map, %{
+ assigns: %{
+ tickers: [],
+ basket: []
+ }
+ })
+ )
+ end
+ end
+
+ describe "handle_info/3 ticker update" do
+ test "processes a message with new bars", %{
+ basket_with_row: basket_with_row
+ } do
+ assert {
+ :noreply,
+ %{
+ __changed__: %{__context__: true, basket: true},
+ assigns: %{
+ basket: ^basket_with_row,
+ tickers: []
+ },
+ basket: [
+ %{
+ "S" => {"XYZ", ""},
+ "c" => {189.15, "down"},
+ "h" => {189.15, "down"},
+ "l" => {189.05, "down"},
+ "n" => {359, ""},
+ "o" => {189.11, "down"},
+ "t" => {"2023-11-15T21:59:00Z", ""},
+ "v" => {43_032, "down"},
+ "vw" => {189.117416, "down"}
+ }
+ ]
+ }
+ } =
+ Overview.handle_info(
+ %Phoenix.Socket.Broadcast{
+ topic: "bars",
+ event: "ticker-update",
+ payload: %{
+ "S" => "XYZ",
+ "c" => 189.15,
+ "h" => 189.15,
+ "l" => 189.05,
+ "n" => 359,
+ "o" => 189.11,
+ "t" => "2023-11-15T21:59:00Z",
+ "v" => 43_032,
+ "vw" => 189.117416
+ }
+ },
+ Map.merge(@assigns_map, %{
+ assigns: %{tickers: [], basket: basket_with_row},
+ basket: basket_with_row
+ })
+ )
+ end
end
end
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index 42d4753..9a99983 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -33,6 +33,7 @@ defmodule BasketWeb.ConnCase do
setup tags do
Basket.DataCase.setup_sandbox(tags)
+
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index 096952c..14e9932 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -39,6 +39,14 @@ defmodule Basket.DataCase do
"""
def setup_sandbox(tags) do
pid = Sandbox.start_owner!(Basket.Repo, shared: not tags[:async])
+
+ # Set up a default set of mocks for use in asynchronous tests
+ # Mox.defmock(Basket.Websocket.MockAlpaca, for: Basket.Websocket.Alpaca)
+ # Application.put_env(:basket, :alpaca_ws_client, Basket.Websocket.MockAlpaca)
+
+ # Mox.defmock(Basket.Http.MockAlpaca, for: Basket.Http.Alpaca)
+ # Application.put_env(:basket, :alpaca_http_client, Basket.Http.MockAlpaca)
+
on_exit(fn -> Sandbox.stop_owner(pid) end)
end
@@ -57,4 +65,10 @@ defmodule Basket.DataCase do
end)
end)
end
+
+ # Mox.defmock(Basket.Websocket.MockAlpaca, for: Basket.Websocket.Alpaca)
+ # Application.put_env(:basket, :alpaca_ws_client, Basket.Websocket.MockAlpaca)
+
+ # Mox.defmock(Basket.Http.MockAlpaca, for: Basket.Http.Alpaca)
+ # Application.put_env(:basket, :alpaca_http_client, Basket.Http.MockAlpaca)
end
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 6b4588f..7e71fcd 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -1,5 +1,8 @@
+ExUnit.start()
+Ecto.Adapters.SQL.Sandbox.mode(Basket.Repo, :manual)
+
Mox.defmock(Basket.Websocket.MockAlpaca, for: Basket.Websocket.Alpaca)
Application.put_env(:basket, :alpaca_ws_client, Basket.Websocket.MockAlpaca)
-ExUnit.start()
-Ecto.Adapters.SQL.Sandbox.mode(Basket.Repo, :manual)
+Mox.defmock(Basket.Http.MockAlpaca, for: Basket.Http.Alpaca)
+Application.put_env(:basket, :alpaca_http_client, Basket.Http.MockAlpaca)