mirror of
https://github.com/teslamate-org/teslamate.git
synced 2026-01-24 21:06:08 +08:00
Store API tokens encrypted in the database. During the database migration a randomly generated key will be used encrypt the tokens if no ENCRYPTION_KEY environment variable was provided. If the application is started without the presence of an ENCRYPTION_KEY (or if the key failed to decrypt the existing tokens), the UI will display a warning with further instructions.
396 lines
12 KiB
Elixir
396 lines
12 KiB
Elixir
defmodule TeslaMate.ApiTest do
|
|
use TeslaMate.DataCase
|
|
|
|
alias TeslaMate.Api
|
|
alias TeslaMate.Auth.Tokens
|
|
|
|
import Mock
|
|
|
|
def start_api(name, opts \\ []) do
|
|
auth_name = :"auth_#{name}"
|
|
vehicles_name = :"vehicles_#{name}"
|
|
|
|
start_auth? = !!Keyword.get(opts, :start_auth, true)
|
|
|
|
if start_auth? do
|
|
tokens = Keyword.get(opts, :tokens)
|
|
{:ok, _pid} = start_supervised({AuthMock, name: auth_name, tokens: tokens, pid: self()})
|
|
end
|
|
|
|
{:ok, _pid} = start_supervised({VehiclesMock, name: vehicles_name, pid: self()})
|
|
|
|
opts = [
|
|
{:name, name},
|
|
{:vehicles, {VehiclesMock, vehicles_name}}
|
|
| if(start_auth?, do: [auth: {AuthMock, auth_name}], else: [])
|
|
]
|
|
|
|
with {:ok, _} <- start_supervised({Api, opts}) do
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp vehicle_mock(pid) do
|
|
{TeslaApi.Vehicle, [],
|
|
[
|
|
list: fn auth ->
|
|
send(pid, {TeslaApi.Vehicle, {:list, auth}})
|
|
{:ok, [%TeslaApi.Vehicle{}]}
|
|
end,
|
|
get: fn auth, id ->
|
|
send(pid, {TeslaApi.Vehicle, {:get, auth, id}})
|
|
{:ok, %TeslaApi.Vehicle{id: id}}
|
|
end,
|
|
get_with_state: fn auth, id ->
|
|
send(pid, {TeslaApi.Vehicle, {:get_with_state, auth, id}})
|
|
{:ok, %TeslaApi.Vehicle{id: id}}
|
|
end
|
|
]}
|
|
end
|
|
|
|
defp auth_mock(pid) do
|
|
{TeslaApi.Auth, [],
|
|
[
|
|
refresh: fn
|
|
%{token: "cannot_be_refreshed", refresh_token: "cannot_be_refreshed"} = auth ->
|
|
send(pid, {TeslaApi.Auth, {:refresh, auth}})
|
|
{:error, %TeslaApi.Error{reason: :induced_error, message: "foo"}}
|
|
|
|
auth ->
|
|
send(pid, {TeslaApi.Auth, {:refresh, auth}})
|
|
{:ok, %TeslaApi.Auth{token: "$token", refresh_token: "$token", expires_in: 10_000_000}}
|
|
end
|
|
]}
|
|
end
|
|
|
|
@valid_tokens %Tokens{access: "$access", refresh: "$refresh"}
|
|
|
|
setup do
|
|
start_supervised!(TeslaMate.Vault)
|
|
:ok
|
|
end
|
|
|
|
describe "sign in" do
|
|
test "starts without tokens", %{test: name} do
|
|
with_mocks [auth_mock(self()), vehicle_mock(self())] do
|
|
:ok = start_api(name, tokens: nil)
|
|
|
|
assert false == Api.signed_in?(name)
|
|
assert {:error, :not_signed_in} = Api.list_vehicles(name)
|
|
assert {:error, :not_signed_in} = Api.get_vehicle(name, 0)
|
|
assert {:error, :not_signed_in} = Api.get_vehicle_with_state(name, 0)
|
|
|
|
refute_receive _
|
|
end
|
|
end
|
|
|
|
test "starts if tokens are valid", %{test: name} do
|
|
with_mocks [auth_mock(self()), vehicle_mock(self())] do
|
|
:ok = start_api(name, tokens: @valid_tokens)
|
|
|
|
assert_receive {TeslaApi.Auth,
|
|
{:refresh, %TeslaApi.Auth{refresh_token: "$refresh", token: "$access"}}}
|
|
|
|
assert_receive {AuthMock, {:save, %TeslaApi.Auth{}}}
|
|
|
|
assert true == Api.signed_in?(name)
|
|
|
|
refute_receive _
|
|
end
|
|
end
|
|
|
|
@tag :capture_log
|
|
test "uses the tokens from the database if the refresh fails", %{test: name} do
|
|
with_mocks [auth_mock(self()), vehicle_mock(self())] do
|
|
:ok =
|
|
start_api(name,
|
|
tokens: %Tokens{access: "cannot_be_refreshed", refresh: "cannot_be_refreshed"}
|
|
)
|
|
|
|
assert_receive {TeslaApi.Auth,
|
|
{:refresh,
|
|
%TeslaApi.Auth{
|
|
refresh_token: "cannot_be_refreshed",
|
|
token: "cannot_be_refreshed"
|
|
}}}
|
|
|
|
assert true == Api.signed_in?(name)
|
|
|
|
refute_receive _
|
|
end
|
|
end
|
|
|
|
test "allows sign in with API tokens", %{test: name} do
|
|
with_mocks [auth_mock(self()), vehicle_mock(self())] do
|
|
:ok = start_api(name, tokens: nil)
|
|
|
|
assert false == Api.signed_in?(name)
|
|
|
|
assert :ok = Api.sign_in(name, @valid_tokens)
|
|
|
|
assert_receive {TeslaApi.Auth,
|
|
{:refresh, %TeslaApi.Auth{refresh_token: "$refresh", token: "$access"}}}
|
|
|
|
assert_receive {AuthMock, {:save, %TeslaApi.Auth{}}}
|
|
assert_receive {VehiclesMock, :restart}
|
|
assert true == Api.signed_in?(name)
|
|
|
|
refute_receive _
|
|
end
|
|
end
|
|
|
|
test "fails if already signed in", %{test: name} do
|
|
with_mocks [auth_mock(self()), vehicle_mock(self())] do
|
|
:ok = start_api(name, tokens: @valid_tokens)
|
|
|
|
assert_receive {TeslaApi.Auth, {:refresh, %TeslaApi.Auth{}}}
|
|
assert_receive {AuthMock, {:save, %TeslaApi.Auth{}}}
|
|
assert true == Api.signed_in?(name)
|
|
|
|
assert {:error, :already_signed_in} = Api.sign_in(name, @valid_tokens)
|
|
|
|
refute_receive _
|
|
end
|
|
end
|
|
|
|
test "fails if api returns error", %{test: name} do
|
|
with_mock TeslaApi.Auth,
|
|
refresh: fn _tokens ->
|
|
{:error, %TeslaApi.Error{reason: :unauthorized, env: %Finch.Response{}}}
|
|
end do
|
|
:ok = start_api(name, start_auth: false)
|
|
|
|
assert {:error, %TeslaApi.Error{reason: :unauthorized}} = Api.sign_in(name, @valid_tokens)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "refresh" do
|
|
test "refreshes tokens", %{test: name} do
|
|
with_mocks [auth_mock(self()), vehicle_mock(self())] do
|
|
:ok = start_api(name, tokens: @valid_tokens)
|
|
|
|
assert_receive {TeslaApi.Auth, {:refresh, %TeslaApi.Auth{}}}
|
|
assert_receive {AuthMock, {:save, %TeslaApi.Auth{}}}
|
|
assert true == Api.signed_in?(name)
|
|
|
|
send(name, :refresh_auth)
|
|
|
|
assert_receive {TeslaApi.Auth, {:refresh, %TeslaApi.Auth{}}}
|
|
assert_receive {AuthMock, {:save, %TeslaApi.Auth{}}}
|
|
|
|
refute_receive _
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Vehicle API" do
|
|
test "get_vehicle/1", %{test: name} do
|
|
with_mocks [auth_mock(self()), vehicle_mock(self())] do
|
|
:ok = start_api(name, tokens: @valid_tokens)
|
|
assert_receive {TeslaApi.Auth, {:refresh, _}}
|
|
assert_receive {AuthMock, {:save, %TeslaApi.Auth{}}}
|
|
|
|
assert {:ok, %TeslaApi.Vehicle{id: 0}} = Api.get_vehicle(name, 0)
|
|
assert_receive {TeslaApi.Vehicle, {:get, %TeslaApi.Auth{}, 0}}
|
|
|
|
refute_receive _
|
|
end
|
|
end
|
|
|
|
test "get_vehicle_with_state/1", %{test: name} do
|
|
with_mocks [auth_mock(self()), vehicle_mock(self())] do
|
|
:ok = start_api(name, tokens: @valid_tokens)
|
|
assert_receive {TeslaApi.Auth, {:refresh, _}}
|
|
assert_receive {AuthMock, {:save, %TeslaApi.Auth{}}}
|
|
|
|
assert {:ok, %TeslaApi.Vehicle{id: 0}} = Api.get_vehicle_with_state(name, 0)
|
|
assert_receive {TeslaApi.Vehicle, {:get_with_state, %TeslaApi.Auth{}, 0}}
|
|
|
|
refute_receive _
|
|
end
|
|
end
|
|
|
|
test "list_vehicles/0", %{test: name} do
|
|
with_mocks [auth_mock(self()), vehicle_mock(self())] do
|
|
:ok = start_api(name, tokens: @valid_tokens)
|
|
assert_receive {TeslaApi.Auth, {:refresh, _}}
|
|
assert_receive {AuthMock, {:save, %TeslaApi.Auth{}}}
|
|
|
|
assert {:ok, [%TeslaApi.Vehicle{}]} = Api.list_vehicles(name)
|
|
assert_receive {TeslaApi.Vehicle, {:list, %TeslaApi.Auth{}}}
|
|
|
|
refute_receive _
|
|
end
|
|
end
|
|
|
|
@tag :capture_log
|
|
test "signs out if the API repeatedly returns a 401 response", %{test: name} do
|
|
parent_pid = self()
|
|
|
|
vehicle_mock =
|
|
{TeslaApi.Vehicle, [],
|
|
[
|
|
list: fn _ ->
|
|
{:error, %TeslaApi.Error{reason: :unauthorized, env: %Finch.Response{}}}
|
|
end,
|
|
get: fn _, _ ->
|
|
{:error, %TeslaApi.Error{reason: :unauthorized, env: %Finch.Response{}}}
|
|
end,
|
|
get_with_state: fn _, _ ->
|
|
{:error, %TeslaApi.Error{reason: :unauthorized, env: %Finch.Response{}}}
|
|
end
|
|
]}
|
|
|
|
auth_mock =
|
|
{TeslaApi.Auth, [],
|
|
[
|
|
refresh: fn
|
|
%TeslaApi.Auth{token: "continue_token"} ->
|
|
auth = %TeslaApi.Auth{
|
|
token: "$token",
|
|
refresh_token: "$token",
|
|
expires_in: 10_000_000
|
|
}
|
|
|
|
{:ok, auth}
|
|
|
|
auth ->
|
|
send(parent_pid, {TeslaApi.Auth, {:refresh, auth}})
|
|
{:error, %TeslaApi.Error{reason: :induced_error, message: "foo"}}
|
|
end
|
|
]}
|
|
|
|
with_mocks [auth_mock, vehicle_mock] do
|
|
:ok = start_api(name, start_auth: false)
|
|
|
|
refute_receive _
|
|
|
|
for api_fn <- [
|
|
fn -> Api.list_vehicles(name) end,
|
|
fn -> Api.get_vehicle(name, 0) end,
|
|
fn -> Api.get_vehicle_with_state(name, 0) end
|
|
] do
|
|
# Sign in …
|
|
assert :ok = Api.sign_in(name, %Tokens{access: "continue_token"})
|
|
|
|
# retry until the fuse metls and we're signed out
|
|
assert :ok ==
|
|
Enum.reduce_while(1..10, nil, fn _, _ ->
|
|
case api_fn.() do
|
|
{:error, :unauthorized} ->
|
|
assert_receive {TeslaApi.Auth, {:refresh, _}}
|
|
{:cont, nil}
|
|
|
|
{:error, :not_signed_in} ->
|
|
{:halt, :ok}
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
|
|
test "returns :not_signed_in if Api GenServer is not found", %{test: name} do
|
|
assert {:error, :not_signed_in} = Api.list_vehicles(name)
|
|
end
|
|
|
|
test "handles unknown messages gracefully", %{test: name} do
|
|
vehicle_mock =
|
|
{TeslaApi.Vehicle, [],
|
|
[
|
|
list: fn _ ->
|
|
{:error, %TeslaApi.Error{reason: :unauthorized, env: %Finch.Response{}}}
|
|
end,
|
|
get: fn _, _ ->
|
|
{:error, %TeslaApi.Error{reason: :unauthorized, env: %Finch.Response{}}}
|
|
end,
|
|
get_with_state: fn _, _ ->
|
|
{:error, %TeslaApi.Error{reason: :unauthorized, env: %Finch.Response{}}}
|
|
end
|
|
]}
|
|
|
|
with_mocks [auth_mock(self()), vehicle_mock] do
|
|
:ok = start_api(name, start_auth: false)
|
|
|
|
true =
|
|
name
|
|
|> Process.whereis()
|
|
|> Process.link()
|
|
|
|
send(name, :boom)
|
|
|
|
refute_receive _
|
|
end
|
|
end
|
|
|
|
@tag :capture_log
|
|
test ":vehicle_not_found", %{test: name} do
|
|
api_error = %TeslaApi.Error{reason: :vehicle_not_found, env: %Finch.Response{}}
|
|
|
|
vehicle_mock =
|
|
{TeslaApi.Vehicle, [],
|
|
[
|
|
get: fn _auth, _id -> {:error, api_error} end,
|
|
get_with_state: fn _auth, _id -> {:error, api_error} end
|
|
]}
|
|
|
|
with_mocks [auth_mock(self()), vehicle_mock] do
|
|
:ok = start_api(name, start_auth: false)
|
|
|
|
assert :ok = Api.sign_in(name, @valid_tokens)
|
|
assert {:error, :vehicle_not_found} = Api.get_vehicle(name, 0)
|
|
assert {:error, :vehicle_not_found} = Api.get_vehicle_with_state(name, 0)
|
|
end
|
|
end
|
|
|
|
@tag :capture_log
|
|
test "other error witn Env", %{test: name} do
|
|
api_error = %TeslaApi.Error{
|
|
reason: :unknown,
|
|
message: "",
|
|
env: %Finch.Response{status: 503, body: ""}
|
|
}
|
|
|
|
vehicle_mock =
|
|
{TeslaApi.Vehicle, [],
|
|
[
|
|
list: fn _auth -> {:error, api_error} end,
|
|
get: fn _auth, _id -> {:error, api_error} end,
|
|
get_with_state: fn _auth, _id -> {:error, api_error} end
|
|
]}
|
|
|
|
with_mocks [auth_mock(self()), vehicle_mock] do
|
|
:ok = start_api(name, start_auth: false)
|
|
|
|
assert :ok = Api.sign_in(name, @valid_tokens)
|
|
|
|
assert {:error, :unknown} = Api.list_vehicles(name)
|
|
assert {:error, :unknown} = Api.get_vehicle(name, 0)
|
|
assert {:error, :unknown} = Api.get_vehicle_with_state(name, 0)
|
|
end
|
|
end
|
|
|
|
@tag :capture_log
|
|
test "other error witnout Env", %{test: name} do
|
|
api_error = %TeslaApi.Error{reason: :closed, message: "foo"}
|
|
|
|
vehicle_mock =
|
|
{TeslaApi.Vehicle, [],
|
|
[
|
|
list: fn _auth -> {:error, api_error} end,
|
|
get: fn _auth, _id -> {:error, api_error} end,
|
|
get_with_state: fn _auth, _id -> {:error, api_error} end
|
|
]}
|
|
|
|
with_mocks [auth_mock(self()), vehicle_mock] do
|
|
:ok = start_api(name, start_auth: false)
|
|
|
|
assert :ok = Api.sign_in(name, @valid_tokens)
|
|
assert {:error, :closed} = Api.list_vehicles(name)
|
|
assert {:error, :closed} = Api.get_vehicle(name, 0)
|
|
assert {:error, :closed} = Api.get_vehicle_with_state(name, 0)
|
|
end
|
|
end
|
|
end
|
|
end
|