diff --git a/lib/radiator/accounts.ex b/lib/radiator/accounts.ex index b9fe323b..4a054613 100644 --- a/lib/radiator/accounts.ex +++ b/lib/radiator/accounts.ex @@ -6,7 +6,7 @@ defmodule Radiator.Accounts do import Ecto.Query, warn: false alias Radiator.Repo - alias Radiator.Accounts.{User, UserNotifier, UserToken} + alias Radiator.Accounts.{User, UserNotifier, UserToken, WebService} ## Database getters @@ -247,6 +247,27 @@ defmodule Radiator.Accounts do end end + @doc """ + Get the user's Raindrop tokens if they exist. + + ## Examples + + iex> get_raindrop_tokens(23) + %WebService{} + + iex> get_raindrop_tokens(42) + nil + + """ + def get_raindrop_tokens(user_id) do + service_name = WebService.raindrop_service_name() + + WebService + |> where([w], w.user_id == ^user_id) + |> where([w], w.service_name == ^service_name) + |> Repo.one() + end + @doc """ Sets a users optional Raindrop tokens and expiration time. Given a user id, access token, refresh token, and expiration time, @@ -266,16 +287,47 @@ defmodule Radiator.Accounts do raindrop_refresh_token, raindrop_expires_at ) do - User - |> Radiator.Repo.get!(user_id) - |> User.set_raindrop_token_changeset( - raindrop_access_token, - raindrop_refresh_token, - raindrop_expires_at + %WebService{} + |> WebService.changeset(%{ + service_name: WebService.raindrop_service_name(), + user_id: user_id, + data: %{ + access_token: raindrop_access_token, + refresh_token: raindrop_refresh_token, + expires_at: raindrop_expires_at + } + }) + |> Repo.insert( + on_conflict: {:replace_all_except, [:id, :created_at]}, + conflict_target: [:user_id, :service_name], + set: [updated_at: DateTime.utc_now()] ) - |> Repo.update() end + @doc """ + Radiator.Accounts.connect_show_with_raindrop(1, 23, 42) + """ + def connect_show_with_raindrop(user_id, show_id, collection_id) do + case get_raindrop_tokens(user_id) do + nil -> + {:error, "No Raindrop tokens found"} + + %{data: data} = service -> + data = + Map.update!(data, :collection_mappings, fn mappings -> + Map.put(mappings, show_id_to_collection_id(show_id), collection_id) + end) + |> Map.from_struct() + + service + |> WebService.changeset(%{data: data}) + |> Repo.update() + end + end + + defp show_id_to_collection_id(show_id) when is_integer(show_id), do: Integer.to_string(show_id) + defp show_id_to_collection_id(show_id), do: show_id + ## Session @doc """ diff --git a/lib/radiator/accounts/raindrop_client.ex b/lib/radiator/accounts/raindrop_client.ex new file mode 100644 index 00000000..1a352274 --- /dev/null +++ b/lib/radiator/accounts/raindrop_client.ex @@ -0,0 +1,149 @@ +defmodule Radiator.Accounts.RaindropClient do + @moduledoc """ + Client for Raindrop API + """ + require Logger + + alias Radiator.Accounts + + def config, do: Application.fetch_env!(:radiator, :raindrop) + + def redirect_uri_encoded(user_id) do + user_id + |> redirect_uri() + |> URI.encode() + end + + def redirect_uri(user_id) do + config()[:redirect_url] + |> URI.parse() + |> URI.append_query("user_id=#{user_id}") + |> URI.to_string() + end + + def redirect_uri do + config()[:redirect_url] + |> URI.parse() + |> URI.to_string() + end + + @doc """ + Check if the user has access to Raindrop API + """ + def access_enabled?(user_id) do + not_enabled = + user_id + |> Accounts.get_raindrop_tokens() + |> is_nil() + + !not_enabled + end + + @doc """ + Get all collections for a user + """ + def get_collections(user_id) do + service = + user_id + |> Accounts.get_raindrop_tokens() + |> refresh_token_if() + + if is_nil(service) do + {:error, :unauthorized} + else + [ + method: :get, + url: "https://api.raindrop.io/rest/v1/collections", + headers: [ + {"Authorization", "Bearer #{service.data.access_token}"} + ] + ] + |> Req.request() + |> parse_collection_response() + end + end + + @doc """ + first time fetching access token and storing it as webservice entry + """ + def init_and_store_access_token(user_id, code) do + {:ok, response} = + [ + method: :post, + url: "https://raindrop.io/oauth/access_token", + json: %{ + client_id: config()[:client_id], + client_secret: config()[:client_secret], + grant_type: "authorization_code", + code: code, + redirect_uri: redirect_uri() + } + ] + |> Keyword.merge(config()[:options]) + |> Req.request() + + parse_access_token_response(response, user_id) + end + + defp refresh_token_if(service) do + if DateTime.before?(service.data.expires_at, DateTime.utc_now()) do + {:ok, response} = + [ + method: :post, + url: "https://raindrop.io/oauth/access_token", + headers: [ + {"Content-Type", "application/json"} + ], + json: %{ + client_id: config()[:client_id], + client_secret: config()[:client_secret], + grant_type: "refresh_token", + refresh_token: service.data.refresh_token + } + ] + |> Req.request() + + parse_access_token_response(response, service.user_id) + else + service + end + end + + defp parse_access_token_response( + %Req.Response{ + body: %{ + "access_token" => access_token, + "refresh_token" => refresh_token, + "expires_in" => expires_in + } + }, + user_id + ) do + expires_at = + DateTime.now!("Etc/UTC") + |> DateTime.shift(second: expires_in) + |> DateTime.truncate(:second) + + Accounts.update_raindrop_tokens( + user_id, + access_token, + refresh_token, + expires_at + ) + end + + defp parse_access_token_response(response, _user_id) do + Logger.error("Error fetching access token: #{inspect(response)}") + {:error, :unauthorized} + end + + defp parse_collection_response({:ok, %Req.Response{status: 401}}) do + {:error, :unauthorized} + end + + defp parse_collection_response({:ok, %Req.Response{body: body}}) do + body + |> Map.get("items") + |> Enum.map(&Map.take(&1, ["_id", "title"])) + end +end diff --git a/lib/radiator/accounts/user.ex b/lib/radiator/accounts/user.ex index f8fcc2fd..cdf5b241 100644 --- a/lib/radiator/accounts/user.ex +++ b/lib/radiator/accounts/user.ex @@ -4,6 +4,7 @@ defmodule Radiator.Accounts.User do """ use Ecto.Schema import Ecto.Changeset + alias Radiator.Accounts.WebService alias Radiator.Podcast.Show schema "users" do @@ -12,10 +13,8 @@ defmodule Radiator.Accounts.User do field :hashed_password, :string, redact: true field :current_password, :string, virtual: true, redact: true field :confirmed_at, :utc_datetime - field :raindrop_access_token, :string, redact: true - field :raindrop_refresh_token, :string, redact: true - field :raindrop_expires_at, :utc_datetime + has_many :services, WebService many_to_many :hosting_shows, Show, join_through: "show_hosts" timestamps(type: :utc_datetime) @@ -137,17 +136,6 @@ defmodule Radiator.Accounts.User do change(user, confirmed_at: now) end - @doc """ - Sets the raindrop tokens and expiration time. - """ - def set_raindrop_token_changeset(user, access_token, refresh_token, expires_at) do - change(user, - raindrop_access_token: access_token, - raindrop_refresh_token: refresh_token, - raindrop_expires_at: expires_at - ) - end - @doc """ Verifies the password. diff --git a/lib/radiator/accounts/web_service.ex b/lib/radiator/accounts/web_service.ex new file mode 100644 index 00000000..cb79d731 --- /dev/null +++ b/lib/radiator/accounts/web_service.ex @@ -0,0 +1,45 @@ +defmodule Radiator.Accounts.WebService do + @moduledoc """ + Model for storing all kinds of information about a user's service. + First implementation is for Raindrop.io + In the future we may have support for other services and https://hexdocs.pm/polymorphic_embed/ might be a solution + """ + use Ecto.Schema + import Ecto.Changeset + + alias Radiator.Accounts.User + + @raindrop_service_name "raindrop" + + schema "web_services" do + field :service_name, :string + + embeds_one :data, RaindropService, on_replace: :delete, primary_key: false do + field :access_token, :string, redact: true + field :refresh_token, :string, redact: true + field :expires_at, :utc_datetime + # Show ID => Raindrop Collection ID + field :collection_mappings, :map, default: %{} + end + + belongs_to :user, User + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(service, attrs) do + service + |> cast(attrs, [:service_name, :user_id]) + |> cast_embed(:data, required: true, with: &raindrop_changeset/2) + |> validate_required([:service_name, :data]) + end + + def raindrop_changeset(service, attrs \\ %{}) do + service + |> cast(attrs, [:access_token, :refresh_token, :expires_at, :collection_mappings]) + |> validate_required([:access_token, :refresh_token, :expires_at]) + end + + def raindrop_service_name, do: @raindrop_service_name +end diff --git a/lib/radiator/raindrop_client.ex b/lib/radiator/raindrop_client.ex deleted file mode 100644 index a0f7e99d..00000000 --- a/lib/radiator/raindrop_client.ex +++ /dev/null @@ -1,67 +0,0 @@ -defmodule Radiator.RaindropClient do - @moduledoc """ - Client for Raindrop API - """ - alias Radiator.Accounts - - def config, do: Application.fetch_env!(:radiator, :raindrop) - - def redirect_uri_encoded(user_id) do - user_id - |> redirect_uri() - |> URI.encode() - end - - def redirect_uri(user_id) do - config()[:redirect_url] - |> URI.parse() - |> URI.append_query("user_id=#{user_id}") - |> URI.to_string() - end - - def redirect_uri do - config()[:redirect_url] - |> URI.parse() - |> URI.to_string() - end - - @doc """ - Check if the user has access to Raindrop API - """ - def access_enabled?(user_id) do - not_enabled = - user_id - |> Accounts.get_user!() - |> Map.get(:raindrop_access_token) - |> is_nil() - - !not_enabled - end - - @doc """ - Get all collections for a user - """ - def get_collections(user_id) do - user = Accounts.get_user!(user_id) - - [ - method: :get, - url: "https://api.raindrop.io/rest/v1/collections", - headers: [ - {"Authorization", "Bearer #{user.raindrop_access_token}"} - ] - ] - |> Req.request() - |> parse_collection_response() - end - - defp parse_collection_response({:ok, %Req.Response{status: 401}}) do - {:error, :unauthorized} - end - - defp parse_collection_response({:ok, %Req.Response{body: body}}) do - body - |> Map.get("items") - |> Enum.map(&Map.take(&1, ["_id", "title"])) - end -end diff --git a/lib/radiator_web/controllers/api/raindrop_controller.ex b/lib/radiator_web/controllers/api/raindrop_controller.ex index cca81eed..26499549 100644 --- a/lib/radiator_web/controllers/api/raindrop_controller.ex +++ b/lib/radiator_web/controllers/api/raindrop_controller.ex @@ -1,8 +1,7 @@ defmodule RadiatorWeb.Api.RaindropController do use RadiatorWeb, :controller - alias Radiator.Accounts - alias Radiator.RaindropClient + alias Radiator.Accounts.RaindropClient require Logger def auth_redirect(conn, %{"user_id" => user_id, "code" => code}) do @@ -10,36 +9,7 @@ defmodule RadiatorWeb.Api.RaindropController do "Raindrop auth redirect code: #{code}, redirect_uri: #{RaindropClient.redirect_uri()}" ) - {:ok, response} = - [ - method: :post, - url: "https://raindrop.io/oauth/access_token", - json: %{ - client_id: RaindropClient.config()[:client_id], - client_secret: RaindropClient.config()[:client_secret], - grant_type: "authorization_code", - code: code, - redirect_uri: RaindropClient.redirect_uri() - } - ] - |> Keyword.merge(RaindropClient.config()[:options]) - |> Req.request() - - Logger.error("Response from raindrop: #{inspect(response)}") - - if response.body != "Unauthorized" && !is_nil(response.body["access_token"]) do - expires_at = - DateTime.now!("Etc/UTC") - |> DateTime.shift(second: response.body["expires_in"]) - |> DateTime.truncate(:second) - - Accounts.update_raindrop_tokens( - user_id, - response.body["access_token"], - response.body["refresh_token"], - expires_at - ) - end + RaindropClient.init_and_store_access_token(user_id, code) conn |> put_resp_content_type("application/json") diff --git a/lib/radiator_web/live/admin_live/index.ex b/lib/radiator_web/live/admin_live/index.ex index 796ce314..e8a526af 100644 --- a/lib/radiator_web/live/admin_live/index.ex +++ b/lib/radiator_web/live/admin_live/index.ex @@ -2,8 +2,8 @@ defmodule RadiatorWeb.AdminLive.Index do use RadiatorWeb, :live_view alias Radiator.Accounts + alias Radiator.Accounts.RaindropClient alias Radiator.Podcast - alias Radiator.RaindropClient alias RadiatorWeb.Endpoint @impl true @@ -14,16 +14,29 @@ defmodule RadiatorWeb.AdminLive.Index do |> assign(:page_description, "Tools to create and manage your prodcasts") |> assign(:networks, Podcast.list_networks(preload: :shows)) |> assign(:bookmarklet, get_bookmarklet(Endpoint.url() <> "/api/v1/outline", socket)) - |> assign( - :raindrop_access, - RaindropClient.access_enabled?(socket.assigns.current_user.id) - ) + |> assign_raindrop(RaindropClient.access_enabled?(socket.assigns.current_user.id)) + |> reply(:ok) + end + + defp assign_raindrop(socket, true) do + items = + socket.assigns.current_user.id + |> RaindropClient.get_collections() + |> Enum.map(fn item -> {item["title"], item["_id"]} end) + + socket + |> assign(:raindrop_access, true) + |> assign(:raindrop_collections, items) + end + + defp assign_raindrop(socket, false) do + socket |> assign( :raindrop_url, "https://raindrop.io/oauth/authorize?client_id=#{RaindropClient.config()[:client_id]}&redirect_uri=#{RaindropClient.redirect_uri_encoded(socket.assigns.current_user.id)}" ) + |> assign(:raindrop_access, false) |> assign(:raindrop_collections, []) - |> reply(:ok) end @impl true @@ -166,21 +179,7 @@ defmodule RadiatorWeb.AdminLive.Index do def handle_event("connect_raindrop", _params, socket) do socket - |> assign( - :raindrop_access, - RaindropClient.access_enabled?(socket.assigns.current_user.id) - ) - |> reply(:noreply) - end - - def handle_event("show_raindrop_collections", _params, socket) do - items = - socket.assigns.current_user.id - |> RaindropClient.get_collections() - |> Enum.map(fn item -> {item["title"], item["_id"]} end) - - socket - |> assign(:raindrop_collections, items) + |> assign_raindrop(RaindropClient.access_enabled?(socket.assigns.current_user.id)) |> reply(:noreply) end @@ -199,7 +198,9 @@ defmodule RadiatorWeb.AdminLive.Index do defp save_show(socket, :new_show, params) do case Podcast.create_show(params, socket.assigns.selected_hosts) do - {:ok, _show} -> + {:ok, show} -> + save_raindrop(socket, show.id, params) + socket |> assign(:action, nil) |> assign(:networks, Podcast.list_networks(preload: :shows)) @@ -219,7 +220,9 @@ defmodule RadiatorWeb.AdminLive.Index do defp save_show(socket, :edit_show, params) do case Podcast.update_show(socket.assigns.show, params, socket.assigns.selected_hosts) do - {:ok, _show} -> + {:ok, show} -> + save_raindrop(socket, show.id, params) + socket |> assign(:action, nil) |> assign(:networks, Podcast.list_networks(preload: :shows)) @@ -237,6 +240,13 @@ defmodule RadiatorWeb.AdminLive.Index do end end + # defp save_raindrop(socket, show_id, %{"show" => %{"raindrop_collection" => collection_id}}) do + defp save_raindrop(socket, show_id, %{"raindrop_collection" => collection_id}) do + Accounts.connect_show_with_raindrop(socket.assigns.current_user.id, show_id, collection_id) + end + + defp save_raindrop(_socket, _show_id, _params), do: nil + defp get_bookmarklet(api_uri, socket) do token = socket.assigns.current_user diff --git a/lib/radiator_web/live/admin_live/index.html.heex b/lib/radiator_web/live/admin_live/index.html.heex index 42b63ae8..f266762d 100644 --- a/lib/radiator_web/live/admin_live/index.html.heex +++ b/lib/radiator_web/live/admin_live/index.html.heex @@ -138,19 +138,12 @@

Raindrop Integration

<%= if @raindrop_access do %>

- + <.input + type="select" + field={f[:raindrop_collection]} + options={@raindrop_collections} + prompt="Please select a collection for the show" + />

<% else %> diff --git a/priv/repo/migrations/20241105184254_create_web_services.exs b/priv/repo/migrations/20241105184254_create_web_services.exs new file mode 100644 index 00000000..8f30eba8 --- /dev/null +++ b/priv/repo/migrations/20241105184254_create_web_services.exs @@ -0,0 +1,16 @@ +defmodule Radiator.Repo.Migrations.CreateWebServices do + use Ecto.Migration + + def change do + create table(:web_services) do + add :service_name, :string + add :data, :map + add :user_id, references(:users, on_delete: :nothing) + + timestamps(type: :utc_datetime) + end + + create index(:web_services, [:user_id]) + create unique_index(:web_services, [:user_id, :service_name]) + end +end diff --git a/priv/repo/migrations/20241106201557_remove_rainbow_tokens_from_user.exs b/priv/repo/migrations/20241106201557_remove_rainbow_tokens_from_user.exs new file mode 100644 index 00000000..4284182a --- /dev/null +++ b/priv/repo/migrations/20241106201557_remove_rainbow_tokens_from_user.exs @@ -0,0 +1,11 @@ +defmodule Radiator.Repo.Migrations.RemoveRainbowTokensFromUser do + use Ecto.Migration + + def change do + alter table(:users) do + remove :raindrop_access_token, :string + remove :raindrop_refresh_token, :string + remove :raindrop_expires_at, :utc_datetime + end + end +end diff --git a/test/radiator/accounts/raindrop_client_test.exs b/test/radiator/accounts/raindrop_client_test.exs new file mode 100644 index 00000000..b2149257 --- /dev/null +++ b/test/radiator/accounts/raindrop_client_test.exs @@ -0,0 +1,26 @@ +defmodule Radiator.Accounts.RaindropClientTest do + use Radiator.DataCase + + import Radiator.AccountsFixtures + + alias Radiator.Accounts.RaindropClient + + describe "access_enabled?" do + test "true when a webservice entry for this user exists" do + webservice = raindrop_service_fixture() + assert RaindropClient.access_enabled?(webservice.user_id) + end + + test "false when no webservice entry exists" do + user = user_fixture() + refute RaindropClient.access_enabled?(user.id) + end + + test "false when no webservice entry for this user exists" do + other_user_id = raindrop_service_fixture().user_id + user = user_fixture() + assert other_user_id != user.id + refute RaindropClient.access_enabled?(user.id) + end + end +end diff --git a/test/radiator/accounts_test.exs b/test/radiator/accounts_test.exs index ec28b918..4e1ee632 100644 --- a/test/radiator/accounts_test.exs +++ b/test/radiator/accounts_test.exs @@ -4,7 +4,8 @@ defmodule Radiator.AccountsTest do alias Radiator.Accounts import Radiator.AccountsFixtures - alias Radiator.Accounts.{User, UserToken} + alias Radiator.Accounts.{User, UserToken, WebService} + alias Radiator.PodcastFixtures describe "list_users/0" do test "returns all users" do @@ -623,25 +624,125 @@ defmodule Radiator.AccountsTest do end end + describe "get_raindrop_tokens/1" do + setup do + %{web_service: raindrop_service_fixture()} + end + + test "returns the raindrop tokens", %{web_service: web_service} do + fetched_web_service = Accounts.get_raindrop_tokens(web_service.user_id) + assert fetched_web_service.data == web_service.data + end + end + describe "update_raindrop_tokens/4" do setup do - %{user: user_fixture()} + user = user_fixture() + + web_service = %{ + user_id: user.id, + access_token: "ae261404-11r4-47c0-bce3-e18a423da828", + refresh_token: "c8080368-fad2-4a3f-b2c9-71d3z85011vb", + expires_at: + DateTime.utc_now() |> DateTime.shift(second: 1_209_599) |> DateTime.truncate(:second) + } + + %{user: user, web_service: web_service} end - test "updates the raindrop tokens", %{user: user} do - access_token = "ae261404-11r4-47c0-bce3-e18a423da828" - refresh_token = "c8080368-fad2-4a3f-b2c9-71d3z85011vb" + test "creates a new entry if none exists", %{user: user, web_service: web_service} do + count_before = Repo.aggregate(WebService, :count) - expires_at = - DateTime.utc_now() |> DateTime.shift(second: 1_209_599) |> DateTime.truncate(:second) + Accounts.update_raindrop_tokens( + user.id, + web_service.access_token, + web_service.refresh_token, + web_service.expires_at + ) - {:ok, user} = - Accounts.update_raindrop_tokens(user.id, access_token, refresh_token, expires_at) + count_after = Repo.aggregate(WebService, :count) + assert count_after == count_before + 1 + + %WebService{} = service = Accounts.get_raindrop_tokens(user.id) + + assert service.data.access_token == web_service.access_token + assert service.data.refresh_token == web_service.refresh_token + assert service.data.expires_at == web_service.expires_at + end + + test "updates the raindrop tokens", %{user: user, web_service: web_service} do + # Create a new entry + Accounts.update_raindrop_tokens( + user.id, + web_service.access_token, + web_service.refresh_token, + web_service.expires_at + ) + + count_before = Repo.aggregate(WebService, :count) + # Update the entry, must not create a new one + Accounts.update_raindrop_tokens( + user.id, + "new-access-token", + "new-refresh-token", + web_service.expires_at + ) + + count_after = Repo.aggregate(WebService, :count) + assert count_after == count_before + + %WebService{} = service = Accounts.get_raindrop_tokens(user.id) + assert service.data.access_token == "new-access-token" + assert service.data.refresh_token == "new-refresh-token" + end + end + + describe "connect_show_with_rainbow/3" do + setup do + %{web_service: raindrop_service_fixture(), show: PodcastFixtures.show_fixture()} + end + + test "saves a show - collection connection in collection_mappings", %{ + web_service: web_service, + show: show + } do + Accounts.connect_show_with_raindrop(web_service.user_id, show.id, 42) + + service = Accounts.get_raindrop_tokens(web_service.user_id) + assert service.data.collection_mappings == %{"#{show.id}" => 42} + end + + test "can add multiple shows", %{ + web_service: web_service, + show: show + } do + Accounts.connect_show_with_raindrop(web_service.user_id, show.id, 42) + + second_show = PodcastFixtures.show_fixture() + third_show = PodcastFixtures.show_fixture() + + Accounts.connect_show_with_raindrop(web_service.user_id, second_show.id, 23) + Accounts.connect_show_with_raindrop(web_service.user_id, third_show.id, 666) + + service = Accounts.get_raindrop_tokens(web_service.user_id) + + assert service.data.collection_mappings == %{ + "#{show.id}" => 42, + "#{second_show.id}" => 23, + "#{third_show.id}" => 666 + } + end + + test "can override show", %{ + web_service: web_service, + show: show + } do + Accounts.connect_show_with_raindrop(web_service.user_id, show.id, 42) + + Accounts.connect_show_with_raindrop(web_service.user_id, show.id, 23) + service = Accounts.get_raindrop_tokens(web_service.user_id) - user = Repo.reload(user) - assert user.raindrop_access_token == access_token - assert user.raindrop_refresh_token == refresh_token - assert user.raindrop_expires_at == expires_at + assert service.data.collection_mappings == %{"#{show.id}" => 23} end end end diff --git a/test/radiator_web/controllers/api/raindrop_controller_test.exs b/test/radiator_web/controllers/api/raindrop_controller_test.exs index f6b4ff66..88ae79f5 100644 --- a/test/radiator_web/controllers/api/raindrop_controller_test.exs +++ b/test/radiator_web/controllers/api/raindrop_controller_test.exs @@ -1,11 +1,12 @@ defmodule RadiatorWeb.Api.RaindropControllerTest do use RadiatorWeb.ConnCase, async: true + alias Radiator.Accounts.RaindropClient alias Radiator.AccountsFixtures describe "GET /raindrop/auth/redirect/:user_id" do setup %{conn: conn} do - raindrop = Radiator.RaindropClient.config() + raindrop = RaindropClient.config() %{host: host, path: path} = URI.parse(raindrop.url) %{ diff --git a/test/support/fixtures/accounts_fixtures.ex b/test/support/fixtures/accounts_fixtures.ex index 68cd8904..052f0ba6 100644 --- a/test/support/fixtures/accounts_fixtures.ex +++ b/test/support/fixtures/accounts_fixtures.ex @@ -28,4 +28,16 @@ defmodule Radiator.AccountsFixtures do [_, token | _] = String.split(captured_email.text_body, "[TOKEN]") token end + + def raindrop_service_fixture(user \\ user_fixture()) do + {:ok, service} = + Radiator.Accounts.update_raindrop_tokens( + user.id, + "ae261404-11r4-47c0-bce3-e18a423da828", + "c8080368-fad2-4a3f-b2c9-71d3z85011vb", + DateTime.utc_now() |> DateTime.shift(second: 1_209_599) |> DateTime.truncate(:second) + ) + + service + end end