| defmodule Plausible.Annotations.AnnotationTest do |
| use ExUnit.Case, async: true |
| alias Plausible.Annotations.Annotation |
|
|
| describe "changeset/3 for date granularity" do |
| for {dt, expected_errors} <- [ |
| {"2026-06-30T00:00:00", [date: {"must be supplied for chosen granularity", []}]}, |
| {"2026-06-30T00:00:00Z", [date: {"must be supplied for chosen granularity", []}]}, |
| {~U[2026-06-30 14:30:00Z], [date: {"must be supplied for chosen granularity", []}]}, |
| {"2026-07-05", [datetime: {"is invalid", [type: :utc_datetime, validation: :cast]}]}, |
| {~D[2026-07-06], [datetime: {"is invalid", [type: :utc_datetime, validation: :cast]}]} |
| ] do |
| test "rejects datetime #{dt} with appropriate error" do |
| changeset = |
| Annotation.changeset( |
| %Annotation{}, |
| %{ |
| note: "feature released", |
| type: "personal", |
| granularity: "date", |
| datetime: unquote(Macro.escape(dt)) |
| }, |
| "Etc/UTC" |
| ) |
|
|
| assert changeset.errors == unquote(Macro.escape(expected_errors)) |
| end |
| end |
|
|
| test "rejects invalid date value" do |
| changeset = |
| Annotation.changeset( |
| %Annotation{}, |
| %{ |
| note: "feature released", |
| type: "personal", |
| granularity: "date", |
| date: "2026-13-45" |
| }, |
| "Etc/UTC" |
| ) |
|
|
| assert [date: {"is invalid", _}] = changeset.errors |
| end |
|
|
| for {d, expected} <- [ |
| {"2026-06-30", ~U[2026-06-30 00:00:00Z]}, |
| {~D[2026-07-01], ~U[2026-07-01 00:00:00Z]} |
| ] do |
| test "accepts date #{d}, parsing it to that date at UTC midnight (#{expected})" do |
| changeset = |
| Annotation.changeset( |
| %Annotation{}, |
| %{ |
| note: "feature released", |
| type: "personal", |
| granularity: "date", |
| date: unquote(Macro.escape(d)) |
| }, |
| Enum.random(Plausible.Timezones.zone_list()) |
| ) |
|
|
| assert changeset.valid? |
| assert changeset.changes.datetime == unquote(Macro.escape(expected)) |
| end |
| end |
| end |
|
|
| describe "changeset/3 for minute granularity" do |
| for d <- ["2026-07-05", ~D[2026-07-06]] do |
| test "requires :datetime to be present, given date: #{d}" do |
| changeset = |
| Annotation.changeset( |
| %Annotation{}, |
| %{ |
| note: "feature released", |
| type: "personal", |
| granularity: "minute", |
| date: unquote(Macro.escape(d)) |
| }, |
| "Etc/UTC" |
| ) |
|
|
| assert changeset.errors == [datetime: {"must be supplied for chosen granularity", []}] |
| end |
| end |
|
|
| for {dt, tz, message} <- [ |
| {"2026-06-30", "Etc/UTC", "is invalid"}, |
| {~D[2026-07-01], "Etc/UTC", "is invalid"}, |
| {"invalid", "Etc/UTC", "is invalid"}, |
| {"2026-06-30T14:30:00", "Invalid/Timezone", |
| "cannot be parsed for the site timezone \"Invalid/Timezone\""} |
| ] do |
| test "rejects invalid :datetime, given #{dt} for timezone #{tz}" do |
| changeset = |
| Annotation.changeset( |
| %Annotation{}, |
| %{ |
| note: "feature released", |
| type: "personal", |
| granularity: "minute", |
| datetime: unquote(Macro.escape(dt)) |
| }, |
| unquote(tz) |
| ) |
|
|
| assert [datetime: {unquote(message), _}] = changeset.errors |
| end |
| end |
|
|
| for {dt, expected, tz} <- [ |
| {"2026-06-30T14:30:00Z", ~U[2026-06-30 14:30:00Z], |
| Enum.random(Plausible.Timezones.zone_list())}, |
| {"2026-06-30T10:00:00-02:00", ~U[2026-06-30 12:00:00Z], |
| Enum.random(Plausible.Timezones.zone_list())}, |
| {~U[2026-06-30 14:30:00Z], ~U[2026-06-30 14:30:00Z], |
| Enum.random(Plausible.Timezones.zone_list())}, |
| {"2026-06-30T14:30:00", ~U[2026-06-30 14:30:00Z], "Etc/UTC"}, |
| {"2026-06-30T14:30:00", ~U[2026-06-30 11:30:00Z], "Europe/Tallinn"} |
| ] do |
| test "accepts :datetime #{dt} and parses it to #{expected} UTC point in time for the site with timezone #{tz}" do |
| changeset = |
| Annotation.changeset( |
| %Annotation{}, |
| %{ |
| note: "feature released", |
| type: "personal", |
| granularity: "minute", |
| datetime: unquote(Macro.escape(dt)) |
| }, |
| unquote(tz) |
| ) |
|
|
| assert changeset.valid? |
| assert changeset.changes.datetime == unquote(Macro.escape(expected)) |
| end |
| end |
| end |
|
|
| describe "changeset/3 when both date and datetime are provided" do |
| for granularity <- ["date", "minute"] do |
| test "rejects both being set for #{granularity} granularity" do |
| changeset = |
| Annotation.changeset( |
| %Annotation{}, |
| %{ |
| note: "feature released", |
| type: "personal", |
| granularity: unquote(granularity), |
| date: "2026-06-30", |
| datetime: "2026-06-30T14:30:00Z" |
| }, |
| "Etc/UTC" |
| ) |
|
|
| assert changeset.errors == [ |
| granularity: {"expects either date or datetime to be set", []} |
| ] |
| end |
| end |
| end |
| end |
|
|