diff --git a/config/config.exs b/config/config.exs index 9ce93272..3543eb3c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -123,8 +123,8 @@ config :logger, :console, config :phoenix, :json_library, Jason # Filter FCM tokens from logs -config :phoenix, :filter_parameters, ["fcm_token"] -config :logster, :filter_parameters, ["fcm_token"] +config :phoenix, :filter_parameters, ["fcm_token", "fcm_installation_id"] +config :logster, :filter_parameters, ["fcm_token", "fcm_installation_id"] # Use Req for making HTTP requests config :mobile_app_backend, MobileAppBackend.HTTP, Req diff --git a/lib/mix/tasks/generate_alert_summary_table.ex b/lib/mix/tasks/generate_alert_summary_table.ex index 1ec83da9..1c479613 100644 --- a/lib/mix/tasks/generate_alert_summary_table.ex +++ b/lib/mix/tasks/generate_alert_summary_table.ex @@ -733,7 +733,7 @@ if Mix.env() == :test do if is_nil(Repo.get(MobileAppBackend.User, user_id)) do Repo.insert!(%MobileAppBackend.User{ id: user_id, - fcm_token: "not-a-real-token-#{user_id}", + fcm_installation_id: "not-a-real-installation-id-#{user_id}", fcm_last_verified: ~U[2000-01-01 00:00:00Z] }) end diff --git a/lib/mobile_app_backend/notifications/deliverer.ex b/lib/mobile_app_backend/notifications/deliverer.ex index f842c599..38b644d8 100644 --- a/lib/mobile_app_backend/notifications/deliverer.ex +++ b/lib/mobile_app_backend/notifications/deliverer.ex @@ -43,6 +43,15 @@ defmodule MobileAppBackend.Notifications.Deliverer do _ -> alert_id end + {installation_id, token} = + case user do + %User{fcm_installation_id: installation_id} when not is_nil(installation_id) -> + {installation_id, nil} + + %User{fcm_installation_id: nil, fcm_token: token} -> + {nil, token} + end + request_body = %{ message: %FCM.Message{ notification: %FCM.Notification{ @@ -63,7 +72,8 @@ defmodule MobileAppBackend.Notifications.Deliverer do fcm_options: %FCM.FcmOptions{ analytics_label: analytics_label }, - token: user.fcm_token + fid: installation_id, + token: token } } diff --git a/lib/mobile_app_backend/notifications/write_payload.ex b/lib/mobile_app_backend/notifications/write_payload.ex index 269e52f9..0f911de0 100644 --- a/lib/mobile_app_backend/notifications/write_payload.ex +++ b/lib/mobile_app_backend/notifications/write_payload.ex @@ -2,6 +2,7 @@ defmodule MobileAppBackend.Notifications.WritePayload do alias Ecto.Changeset alias MobileAppBackend.Notifications alias MobileAppBackend.User + alias Util.FCMTarget defmodule Window do @type t :: %__MODULE__{ @@ -100,11 +101,11 @@ defmodule MobileAppBackend.Notifications.WritePayload do end @type t :: %__MODULE__{ - fcm_token: String.t(), + fcm_target: FCMTarget.t(), subscriptions: MapSet.t(Subscription.t()), locale: Gettext.locale() | nil } - defstruct [:fcm_token, :subscriptions, :locale] + defstruct [:fcm_target, :subscriptions, :locale] def parse(payload) do {:ok, parse!(payload)} @@ -112,9 +113,9 @@ defmodule MobileAppBackend.Notifications.WritePayload do _ -> :error end - def parse!(%{"fcm_token" => fcm_token, "subscriptions" => subscriptions} = payload) do + def parse!(%{"subscriptions" => subscriptions} = payload) do %__MODULE__{ - fcm_token: fcm_token, + fcm_target: FCMTarget.parse!(payload), subscriptions: MapSet.new(subscriptions, &Subscription.parse!/1), locale: payload["locale"] } diff --git a/lib/mobile_app_backend/user.ex b/lib/mobile_app_backend/user.ex index e5d4650e..b08240c8 100644 --- a/lib/mobile_app_backend/user.ex +++ b/lib/mobile_app_backend/user.ex @@ -10,7 +10,8 @@ defmodule MobileAppBackend.User do """ typed_schema "users" do - field(:fcm_token, :string, null: false, redact: true) + field(:fcm_token, :string, null: true, redact: true) + field(:fcm_installation_id, :string, null: true, redact: true) field(:fcm_last_verified, :utc_datetime, null: false) # should be a BCP 47 language tag like iOS’s field(:locale, :string, null: true) :: Gettext.locale() | nil diff --git a/lib/mobile_app_backend_web/controllers/notification_subscriptions_controller.ex b/lib/mobile_app_backend_web/controllers/notification_subscriptions_controller.ex index a66d18ed..80bd47d7 100644 --- a/lib/mobile_app_backend_web/controllers/notification_subscriptions_controller.ex +++ b/lib/mobile_app_backend_web/controllers/notification_subscriptions_controller.ex @@ -7,25 +7,31 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsController do alias MobileAppBackend.Notifications.WritePayload alias MobileAppBackend.Repo alias MobileAppBackend.User + alias Util.FCMTarget def set_include_accessibility(conn, params) do status = - with {:ok, fcm_token} <- Map.fetch(params, "fcm_token"), + with {:ok, fcm_target} <- FCMTarget.parse(params), {:ok, include_accessibility} <- Map.fetch(params, "include_accessibility") do locale = params["locale"] now = Map.get_lazy(conn.private, :mobile_app_backend_now, &DateTime.utc_now/0) + user_where = FCMTarget.user_where(fcm_target) + Repo.update_all( from(u in User, - where: u.fcm_token == ^fcm_token, + where: ^user_where, update: [set: [fcm_last_verified: ^now, locale: coalesce(^locale, u.locale)]] ), [] ) Repo.update_all( - from(ns in Subscription, join: u in assoc(ns, :user), where: u.fcm_token == ^fcm_token), + from(ns in Subscription, + join: u in subquery(from u in User, where: ^user_where), + on: ns.user_id == u.id + ), set: [include_accessibility: include_accessibility] ) @@ -67,17 +73,17 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsController do @spec perform_write(WritePayload.t(), DateTime.t()) :: {:ok, :ok} | {:error, term()} defp perform_write(payload, now) do - fcm_token = payload.fcm_token + user_where = FCMTarget.user_where(payload.fcm_target) Repo.transact(fn -> user = Repo.one( from u in User, - where: u.fcm_token == ^fcm_token, + where: ^user_where, preload: [notification_subscriptions: :windows] ) |> case do - nil -> %User{fcm_token: fcm_token} + nil -> FCMTarget.new_user(payload.fcm_target) user -> user end diff --git a/lib/util/fcm_target.ex b/lib/util/fcm_target.ex new file mode 100644 index 00000000..ecdd474d --- /dev/null +++ b/lib/util/fcm_target.ex @@ -0,0 +1,37 @@ +defmodule Util.FCMTarget do + alias MobileAppBackend.User + + @type t :: {:installation_id, String.t()} | {:token, String.t()} + + @spec parse!(%{String.t() => String.t()}) :: t() + def parse!(payload) do + {:ok, target} = parse(payload) + target + end + + @spec parse(%{String.t() => String.t()}) :: {:ok, t()} | :error + def parse(payload) do + case payload do + %{"fcm_installation_id" => installation_id} when not is_nil(installation_id) -> + {:ok, {:installation_id, installation_id}} + + %{"fcm_token" => token} when not is_nil(token) -> + {:ok, {:token, token}} + + _ -> + :error + end + end + + def user_where(target) + def user_where({:installation_id, installation_id}), do: [fcm_installation_id: installation_id] + def user_where({:token, token}), do: [fcm_token: token] + + def new_user(target) + + def new_user({:installation_id, installation_id}) do + %User{fcm_installation_id: installation_id} + end + + def new_user({:token, token}), do: %User{fcm_token: token} +end diff --git a/lib/util/gcp/fcm.ex b/lib/util/gcp/fcm.ex index 465175c4..b1a61c40 100644 --- a/lib/util/gcp/fcm.ex +++ b/lib/util/gcp/fcm.ex @@ -65,10 +65,11 @@ defmodule Util.GCP.FCM do android: AndroidConfig.t() | nil, apns: ApnsConfig.t() | nil, fcm_options: FcmOptions.t() | nil, - token: String.t() | nil + token: String.t() | nil, + fid: String.t() | nil } @derive Jason.Encoder - defstruct [:data, :notification, :android, :apns, :fcm_options, :token] + defstruct [:data, :notification, :android, :apns, :fcm_options, :token, :fid] end @doc "https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send" diff --git a/priv/repo/migrations/20260731173645_add_users_fcm_installation_id.exs b/priv/repo/migrations/20260731173645_add_users_fcm_installation_id.exs new file mode 100644 index 00000000..46f0f3e1 --- /dev/null +++ b/priv/repo/migrations/20260731173645_add_users_fcm_installation_id.exs @@ -0,0 +1,16 @@ +defmodule MobileAppBackend.Repo.Migrations.AddUsersFcmInstallationId do + use Ecto.Migration + + def change do + alter table(:users) do + modify :fcm_token, :string, null: true, from: {:string, null: false} + add :fcm_installation_id, :string, null: true + end + + create unique_index(:users, [:fcm_installation_id]) + + create constraint(:users, :some_fcm_target, + check: "COALESCE(fcm_installation_id, fcm_token) IS NOT NULL" + ) + end +end diff --git a/test/mobile_app_backend/notifications/deliverer_test.exs b/test/mobile_app_backend/notifications/deliverer_test.exs index 103b0ccc..b00dfb0e 100644 --- a/test/mobile_app_backend/notifications/deliverer_test.exs +++ b/test/mobile_app_backend/notifications/deliverer_test.exs @@ -16,10 +16,92 @@ defmodule MobileAppBackend.Notifications.DelivererTest do setup :verify_on_exit! setup {Req.Test, :verify_on_exit!} - test "delivers notification via FCM and marks notification as delivered" do + test "delivers notification via FCM with new installation ID and marks notification as delivered" do start_link_supervised!(Alerts) user = NotificationsFactory.insert(:user) user_id = user.id + fcm_installation_id = user.fcm_installation_id + alert = Factory.build(:alert) + Alerts.process_reset([alert], []) + alert_id = alert.id + upstream_timestamp = DateTime.utc_now(:second) + type = :notification + + title = "Notification title" + body = "Notification body" + deep_link_path = "/a/#{alert_id}/r/1/s/1" + analytics_label = "route=1;effect=delay;type=notification" + + reassign_persistent_term(GCPToken.default_key(), %GCPToken.StoredToken{ + token: "gcp_token", + expires: ~U[9999-12-31 23:59:59Z] + }) + + Req.Test.expect(Util.GCP, fn conn -> + assert conn.method == "POST" + + assert Plug.Conn.request_url(conn) == + "https://fcm.googleapis.com/v1/projects/mbta-app-c574d/messages:send" + + assert [ + {"accept", "application/json"}, + {"authorization", "Bearer gcp_token"}, + {"content-type", "application/json"}, + {"user-agent", "req/" <> _} + ] = Enum.sort(conn.req_headers) + + assert conn.body_params == %{ + "message" => %{ + "token" => nil, + "fid" => fcm_installation_id, + "notification" => %{"title" => title, "body" => body}, + "data" => %{ + "deep_link_path" => deep_link_path, + "analytics_label" => analytics_label + }, + "android" => %{ + "notification" => %{ + "tag" => alert_id, + "sound" => "default", + "visibility" => "public" + } + }, + "apns" => %{"payload" => %{"aps" => %{"sound" => "default"}}}, + "fcm_options" => %{"analytics_label" => analytics_label} + } + } + + Req.Test.json(conn, %{}) + end) + + :ok = + perform_job(Notifications.Deliverer, %{ + user_id: user_id, + alert_id: alert_id, + title: title, + body: body, + deep_link_path: deep_link_path, + upstream_timestamp: upstream_timestamp, + type: type, + analytics_label: analytics_label + }) + + assert [ + %DeliveredNotification{ + user_id: ^user_id, + alert_id: ^alert_id, + upstream_timestamp: ^upstream_timestamp, + type: ^type + } + ] = Repo.all(DeliveredNotification) + + assert DateTime.before?(user.fcm_last_verified, Repo.reload!(user).fcm_last_verified) + end + + test "delivers notification via FCM with old token and marks notification as delivered" do + start_link_supervised!(Alerts) + user = NotificationsFactory.insert(:user_with_old_token) + user_id = user.id fcm_token = user.fcm_token alert = Factory.build(:alert) Alerts.process_reset([alert], []) @@ -53,6 +135,7 @@ defmodule MobileAppBackend.Notifications.DelivererTest do assert conn.body_params == %{ "message" => %{ "token" => fcm_token, + "fid" => nil, "notification" => %{"title" => title, "body" => body}, "data" => %{ "deep_link_path" => deep_link_path, diff --git a/test/mobile_app_backend/notifications/subscription_test.exs b/test/mobile_app_backend/notifications/subscription_test.exs index 2b95685f..a9260262 100644 --- a/test/mobile_app_backend/notifications/subscription_test.exs +++ b/test/mobile_app_backend/notifications/subscription_test.exs @@ -6,7 +6,7 @@ defmodule MobileAppBackend.Notifications.SubscriptionTest do test "can insert subscription for user" do %{id: user_id} = MobileAppBackend.Repo.insert!(%User{ - fcm_token: "fake", + fcm_installation_id: "fake", fcm_last_verified: ~U[2025-09-10 00:00:00Z] }) diff --git a/test/mobile_app_backend/notifications/window_test.exs b/test/mobile_app_backend/notifications/window_test.exs index 488c48d8..0e7ba162 100644 --- a/test/mobile_app_backend/notifications/window_test.exs +++ b/test/mobile_app_backend/notifications/window_test.exs @@ -11,7 +11,7 @@ defmodule MobileAppBackend.Notifications.WindowTest do test "can insert windows for subscription" do %{id: user_id} = MobileAppBackend.Repo.insert!(%User{ - fcm_token: "fake", + fcm_installation_id: "fake", fcm_last_verified: ~U[2025-09-10 00:00:00Z] }) diff --git a/test/mobile_app_backend/user_test.exs b/test/mobile_app_backend/user_test.exs index 5ed68bc6..0c2399b8 100644 --- a/test/mobile_app_backend/user_test.exs +++ b/test/mobile_app_backend/user_test.exs @@ -2,10 +2,25 @@ defmodule MobileAppBackend.UserTest do use MobileAppBackend.DataCase alias MobileAppBackend.User - test "can insert record for user" do + test "can insert record for user with new installation id" do + MobileAppBackend.Repo.insert!(%User{ + fcm_installation_id: "fake_installation_id", + fcm_last_verified: ~U[2025-09-10 00:00:00Z] + }) + end + + test "can insert record for user with old token" do MobileAppBackend.Repo.insert!(%User{ fcm_token: "fake_token", fcm_last_verified: ~U[2025-09-10 00:00:00Z] }) end + + test "cannot insert record for user with neither token nor installation id" do + assert_raise Ecto.ConstraintError, fn -> + MobileAppBackend.Repo.insert!(%User{ + fcm_last_verified: ~U[2025-09-10 00:00:00Z] + }) + end + end end diff --git a/test/mobile_app_backend_web/controllers/notification_subscriptions_controller_test.exs b/test/mobile_app_backend_web/controllers/notification_subscriptions_controller_test.exs index 004496ce..3bbf3868 100644 --- a/test/mobile_app_backend_web/controllers/notification_subscriptions_controller_test.exs +++ b/test/mobile_app_backend_web/controllers/notification_subscriptions_controller_test.exs @@ -15,7 +15,7 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsControllerTest do %{conn: conn, now: now} end - describe "/accessibility" do + describe "/accessibility with new FCM installation ID" do test "sets values of only the correct user", %{conn: conn} do [user1, user2] = insert_pair(:user, @@ -27,6 +27,94 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsControllerTest do assert Enum.all?(user1.notification_subscriptions, & &1.include_accessibility) assert Enum.all?(user2.notification_subscriptions, & &1.include_accessibility) + conn = + post(conn, "/api/notifications/subscriptions/accessibility", %{ + fcm_installation_id: user1.fcm_installation_id, + include_accessibility: false + }) + + assert json_response(conn, :ok) == nil + + user1 = Repo.preload(user1, :notification_subscriptions, force: true) + user2 = Repo.preload(user2, :notification_subscriptions, force: true) + + assert Enum.all?(user1.notification_subscriptions, &(not &1.include_accessibility)) + assert Enum.all?(user2.notification_subscriptions, & &1.include_accessibility) + end + + test "correctly ignores unknown FCM installation ID", %{conn: conn} do + conn = + post(conn, "/api/notifications/subscriptions/accessibility", %{ + fcm_installation_id: "fake_installation_id", + include_accessibility: true + }) + + assert json_response(conn, :ok) == nil + end + + test "sets locale", %{conn: conn} do + user = insert(:user, locale: "en") + assert user.locale == "en" + + conn = + post(conn, "/api/notifications/subscriptions/accessibility", %{ + fcm_installation_id: user.fcm_installation_id, + include_accessibility: false, + locale: "es" + }) + + assert json_response(conn, :ok) == nil + assert %User{locale: "es"} = Repo.reload(user) + end + + test "does not clear locale", %{conn: conn} do + user = insert(:user, locale: "en") + assert user.locale == "en" + + conn = + post(conn, "/api/notifications/subscriptions/accessibility", %{ + fcm_installation_id: user.fcm_installation_id, + include_accessibility: false + }) + + assert json_response(conn, :ok) == nil + assert %User{locale: "en"} = Repo.reload(user) + end + + test "refreshes FCM verification", %{conn: conn, now: now} do + user = insert(:user) + + conn = + post(conn, "/api/notifications/subscriptions/accessibility", %{ + fcm_installation_id: user.fcm_installation_id, + include_accessibility: true + }) + + assert json_response(conn, :ok) == nil + assert %User{fcm_last_verified: ^now} = Repo.reload(user) + end + + @tag :capture_log + test "sends 400 on bad request", %{conn: conn} do + conn = + post(conn, "/api/notifications/subscriptions/accessibility", %{is_reasonable: false}) + + assert json_response(conn, :bad_request) == nil + end + end + + describe "/accessibility with old FCM token" do + test "sets values of only the correct user", %{conn: conn} do + [user1, user2] = + insert_pair(:user_with_old_token, + notification_subscriptions: fn -> + build_pair(:notification_subscription, include_accessibility: true) + end + ) + + assert Enum.all?(user1.notification_subscriptions, & &1.include_accessibility) + assert Enum.all?(user2.notification_subscriptions, & &1.include_accessibility) + conn = post(conn, "/api/notifications/subscriptions/accessibility", %{ fcm_token: user1.fcm_token, @@ -53,7 +141,7 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsControllerTest do end test "sets locale", %{conn: conn} do - user = insert(:user, locale: "en") + user = insert(:user_with_old_token, locale: "en") assert user.locale == "en" conn = @@ -68,7 +156,7 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsControllerTest do end test "does not clear locale", %{conn: conn} do - user = insert(:user, locale: "en") + user = insert(:user_with_old_token, locale: "en") assert user.locale == "en" conn = @@ -82,7 +170,7 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsControllerTest do end test "refreshes FCM verification", %{conn: conn, now: now} do - user = insert(:user) + user = insert(:user_with_old_token) conn = post(conn, "/api/notifications/subscriptions/accessibility", %{ @@ -103,7 +191,199 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsControllerTest do end end - describe "/write" do + describe "/write with new FCM installation ID" do + test "creates new user with subscription", %{conn: conn, now: now} do + conn = + post(conn, "/api/notifications/subscriptions/write", %{ + fcm_installation_id: "fake_installation_id", + subscriptions: [ + %{ + route_id: "Red", + stop_id: "place-sstat", + direction_id: 0, + include_accessibility: true, + windows: [ + %{start_time: "08:00", end_time: "09:00", days_of_week: [1, 2, 3, 4, 5]}, + %{start_time: "10:00", end_time: "17:00", days_of_week: [0, 7]} + ] + } + ], + locale: "en" + }) + + assert json_response(conn, :ok) == nil + + assert [ + %User{ + id: user_id, + fcm_installation_id: "fake_installation_id", + fcm_last_verified: ^now, + locale: "en" + } + ] = + Repo.all(User) + + assert [ + %Subscription{ + id: subscription_id, + user_id: ^user_id, + route_id: "Red", + stop_id: "place-sstat", + direction_id: 0, + include_accessibility: true + } + ] = Repo.all(Subscription) + + assert [ + %Window{ + subscription_id: ^subscription_id, + start_time: ~T[08:00:00], + end_time: ~T[09:00:00], + days_of_week: [1, 2, 3, 4, 5] + }, + %Window{ + subscription_id: ^subscription_id, + start_time: ~T[10:00:00], + end_time: ~T[17:00:00], + days_of_week: [0, 7] + } + ] = Repo.all(from(w in Window, order_by: w.start_time)) + end + + test "removes subscription", %{conn: conn, now: now} do + user = insert(:user, notification_subscriptions: [build(:notification_subscription)]) + + assert Repo.aggregate(Subscription, :count) == 1 + assert Repo.aggregate(Window, :count) > 0 + + conn = + post(conn, "/api/notifications/subscriptions/write", %{ + fcm_installation_id: user.fcm_installation_id, + subscriptions: [] + }) + + assert json_response(conn, :ok) == nil + assert [%User{fcm_last_verified: ^now}] = Repo.all(User) + assert Repo.aggregate(Subscription, :count) == 0 + assert Repo.aggregate(Window, :count) == 0 + end + + test "modifies subscription including windows", %{conn: conn} do + user = insert(:user) + + old_subscription = + insert(:notification_subscription, user_id: user.id, windows: build_list(3, :window)) + + new_windows = + old_subscription.windows + |> Enum.drop(-1) + |> Enum.map(fn %Window{ + start_time: start_time, + end_time: end_time, + days_of_week: days_of_week + } -> + %{ + start_time: start_time |> Time.add(15, :second) |> Time.to_iso8601(), + end_time: end_time |> Time.add(-15, :second) |> Time.to_iso8601(), + days_of_week: + case days_of_week do + [single_day] -> Enum.sort([single_day, Integer.mod(single_day, 7) + 1]) + [_first_day | days] -> days + end + } + end) + + conn = + post(conn, "/api/notifications/subscriptions/write", %{ + fcm_installation_id: user.fcm_installation_id, + subscriptions: [ + %{ + route_id: old_subscription.route_id, + stop_id: old_subscription.stop_id, + direction_id: old_subscription.direction_id, + include_accessibility: not old_subscription.include_accessibility, + windows: new_windows + } + ] + }) + + assert json_response(conn, :ok) == nil + assert [new_subscription] = Repo.all(from(Subscription, preload: :windows)) + + assert new_subscription.user_id == old_subscription.user_id + assert new_subscription.id == old_subscription.id + assert new_subscription.route_id == old_subscription.route_id + assert new_subscription.stop_id == old_subscription.stop_id + assert new_subscription.direction_id == old_subscription.direction_id + + assert new_subscription.include_accessibility == not old_subscription.include_accessibility + assert length(new_subscription.windows) == length(old_subscription.windows) - 1 + + Enum.each(new_subscription.windows, fn actual_window -> + matching_expected_window = + Enum.find( + new_windows, + &(Time.from_iso8601!(&1.start_time) == actual_window.start_time && + Time.from_iso8601!(&1.end_time) == actual_window.end_time) + ) + + assert matching_expected_window != nil + + assert Enum.sort(matching_expected_window.days_of_week) == + Enum.sort(actual_window.days_of_week) + end) + end + + test "sets locale", %{conn: conn} do + user = insert(:user, locale: "en") + + conn = + post(conn, "/api/notifications/subscriptions/write", %{ + fcm_installation_id: user.fcm_installation_id, + subscriptions: [], + locale: "es" + }) + + assert json_response(conn, :ok) == nil + assert %User{locale: "es"} = Repo.reload(user) + end + + test "does not clear locale", %{conn: conn} do + user = insert(:user, locale: "en") + + conn = + post(conn, "/api/notifications/subscriptions/write", %{ + fcm_installation_id: user.fcm_installation_id, + subscriptions: [] + }) + + assert json_response(conn, :ok) == nil + assert %User{locale: "en"} = Repo.reload(user) + end + + @tag :capture_log + test "sends 400 on bad request", %{conn: conn} do + conn = + post(conn, "/api/notifications/subscriptions/write", %{ + fcm_installation_id: "fake_installation_id", + subscriptions: [ + %{ + route_id: "Red", + stop_id: "place-sstat", + direction_id: 0, + include_accessibility: true, + windows: [ + %{start_time: "not-a-time", end_time: "09:00", days_of_week: [1, 2, 3, 4, 5]} + ] + } + ] + }) + + assert json_response(conn, :bad_request) == nil + end + end + + describe "/write with old FCM token" do test "creates new user with subscription", %{conn: conn, now: now} do conn = post(conn, "/api/notifications/subscriptions/write", %{ @@ -156,7 +436,10 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsControllerTest do end test "removes subscription", %{conn: conn, now: now} do - user = insert(:user, notification_subscriptions: [build(:notification_subscription)]) + user = + insert(:user_with_old_token, + notification_subscriptions: [build(:notification_subscription)] + ) assert Repo.aggregate(Subscription, :count) == 1 assert Repo.aggregate(Window, :count) > 0 @@ -174,7 +457,7 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsControllerTest do end test "modifies subscription including windows", %{conn: conn} do - user = insert(:user) + user = insert(:user_with_old_token) old_subscription = insert(:notification_subscription, user_id: user.id, windows: build_list(3, :window)) @@ -240,7 +523,7 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsControllerTest do end test "sets locale", %{conn: conn} do - user = insert(:user, locale: "en") + user = insert(:user_with_old_token, locale: "en") conn = post(conn, "/api/notifications/subscriptions/write", %{ @@ -254,7 +537,7 @@ defmodule MobileAppBackendWeb.NotificationSubscriptionsControllerTest do end test "does not clear locale", %{conn: conn} do - user = insert(:user, locale: "en") + user = insert(:user_with_old_token, locale: "en") conn = post(conn, "/api/notifications/subscriptions/write", %{ diff --git a/test/support/notifications_factory.ex b/test/support/notifications_factory.ex index a71d77cc..b74d9136 100644 --- a/test/support/notifications_factory.ex +++ b/test/support/notifications_factory.ex @@ -13,6 +13,13 @@ defmodule MobileAppBackend.NotificationsFactory do end def user_factory do + %MobileAppBackend.User{ + fcm_installation_id: :rand.bytes(64) |> Base.url_encode64(padding: false), + fcm_last_verified: DateTime.from_unix!(0) + } + end + + def user_with_old_token_factory do %MobileAppBackend.User{ fcm_token: :rand.bytes(64) |> Base.url_encode64(padding: false), fcm_last_verified: DateTime.from_unix!(0)