Skip to content

Commit

Permalink
feat(ex/skate-web/detours-controller): add tests for new `activated_a…
Browse files Browse the repository at this point in the history
…t` field

Also update factory to add new field
  • Loading branch information
firestack committed Dec 21, 2024
1 parent bd21b33 commit ca99c97
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 7 deletions.
18 changes: 16 additions & 2 deletions lib/skate/detours/snapshot_serde.ex
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,22 @@ defmodule Skate.Detours.SnapshotSerde do

defp selectedreason_from_detour(_), do: nil

defp activated_at_from_detour(%Detour{activated_at: %DateTime{} = activated_at}),
do: DateTime.to_iso8601(activated_at)
defp activated_at_from_detour(%Detour{activated_at: %DateTime{} = activated_at}) do
activated_at
# For the time being, the frontend is responsible for generating the
# `activated_at` snapshot. Because browsers are limited to millisecond
# resolution and Ecto doesn't preserve the `milliseconds` field of a
# `DateTime`, we need to truncate the date if we want it to match what's in
# the stored snapshot.
#
# Once we're not trying to be equivalent with the stored snapshot, we could
# probably remove this.
#
# See `Skate.DetourFactory.browser_date/1` and `Skate.DetourFactory.db_date`
# for more context.
|> DateTime.truncate(:millisecond)
|> DateTime.to_iso8601()
end

defp activated_at_from_detour(%Detour{activated_at: nil}), do: nil

Expand Down
53 changes: 52 additions & 1 deletion test/skate_web/controllers/detours_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ defmodule SkateWeb.DetoursControllerTest do
assert Skate.Repo.aggregate(Notifications.Db.Detour, :count) == 1
end

@tag :authenticated
test "adds `activated_at` field when provided", %{conn: conn} do
%Skate.Detours.Db.Detour{id: id, state: snapshot, activated_at: nil} = insert(:detour)

activated_at_time =
DateTime.utc_now() |> Skate.DetourFactory.browser_date() |> Skate.DetourFactory.db_date()

put(conn, ~p"/api/detours/update_snapshot", %{
"snapshot" => snapshot |> activated(activated_at_time) |> with_id(id)
})

Process.sleep(10)

assert Skate.Detours.Detours.get_detour!(id).activated_at ==
activated_at_time
end

@tag :authenticated
test "does not create a new notification if detour was already activated", %{conn: conn} do
setup_notification_server()
Expand Down Expand Up @@ -238,11 +255,45 @@ defmodule SkateWeb.DetoursControllerTest do

put(conn, "/api/detours/update_snapshot", %{"snapshot" => detour_snapshot})

conn = get(conn, "/api/detours/#{detour_id}")
{conn, log} =
CaptureLog.with_log(fn ->
get(conn, "/api/detours/#{detour_id}")
end)

refute log =~
"Serialized detour doesn't match saved snapshot. Falling back to snapshot for detour_id=#{detour_id}"

assert detour_snapshot == json_response(conn, 200)["data"]["state"]
end

@tag :authenticated
test "serialized snapshot `activatedAt` value is formatted as iso-8601", %{conn: conn} do
activated_at = Skate.DetourFactory.browser_date()

%{id: id} =
detour =
:detour
|> build()
|> activated(activated_at)
|> insert()

# Make ID match snapshot
detour
|> Skate.Detours.Detours.change_detour(detour |> update_id() |> Map.from_struct())
|> Skate.Repo.update!()

{conn, log} =
CaptureLog.with_log(fn ->
get(conn, "/api/detours/#{id}")
end)

refute log =~
"Serialized detour doesn't match saved snapshot. Falling back to snapshot for detour_id=#{id}"

assert DateTime.to_iso8601(activated_at) ==
json_response(conn, 200)["data"]["state"]["context"]["activatedAt"]
end

@tag :authenticated
test "log an error if the serialized detour does not match db state", %{conn: conn} do
detour_id = 4
Expand Down
51 changes: 47 additions & 4 deletions test/support/factories/detour_factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,27 @@ defmodule Skate.DetourFactory do
put_in(snapshot["context"]["uuid"], id)
end

def activated(%Skate.Detours.Db.Detour{} = detour) do
%{detour | state: activated(detour.state)}
def update_id(%Skate.Detours.Db.Detour{id: id} = detour) do
with_id(detour, id)
end

def activated(%{"value" => %{}} = state) do
put_in(state["value"], %{"Detour Drawing" => %{"Active" => "Reviewing"}})
def activated(update_arg, activated_at \\ DateTime.utc_now())

def activated(%Skate.Detours.Db.Detour{} = detour, activated_at) do
activated_at = Skate.DetourFactory.browser_date(activated_at)
%{detour | state: activated(detour.state, activated_at), activated_at: activated_at}
end

def activated(%{"value" => %{}, "context" => %{}} = state, activated_at) do
state =
put_in(state["value"], %{"Detour Drawing" => %{"Active" => "Reviewing"}})

put_in(
state["context"]["activatedAt"],
activated_at
|> Skate.DetourFactory.browser_date()
|> DateTime.to_iso8601()
)
end

def deactivated(%Skate.Detours.Db.Detour{} = detour) do
Expand Down Expand Up @@ -95,4 +110,32 @@ defmodule Skate.DetourFactory do
end
end
end

@doc """
Browsers cannot generate javascript `Date` objects with more precision than a
`millisecond` for security reasons.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now#reduced_time_precision
This function truncates a `DateTime` to milliseconds to create `DateTime` objects
that are similar to that of one made in a Browser JS context.
"""
def browser_date(%DateTime{} = date \\ DateTime.utc_now()) do
DateTime.truncate(date, :millisecond)
end

@doc """
While a Browser may generate a date truncated to milliseconds
(see `browser_date` for more context) a `DateTime` stored into Postgres with
the `:utc_datetime_usec` type does not store the extra information about the
non-presence of nanoseconds that a `DateTime` object does.
This means a `DateTime` object that's been truncated by `browser_date` cannot
be compared to a `DateTime` object reconstructed by Ecto after a Database query.
This function adds 0 nanoseconds to a `DateTime` object to make the `DateTime`
object match what Ecto would return to make testing easier when comparing
values.
"""
def db_date(%DateTime{} = date) do
DateTime.add(date, 0, :nanosecond)
end
end

0 comments on commit ca99c97

Please sign in to comment.