diff --git a/lib/fun_with_flags/flag.ex b/lib/fun_with_flags/flag.ex index 535185ff..fc5de56b 100644 --- a/lib/fun_with_flags/flag.ex +++ b/lib/fun_with_flags/flag.ex @@ -7,17 +7,29 @@ defmodule FunWithFlags.Flag do alias FunWithFlags.Gate - defstruct [name: nil, gates: []] - @type t :: %FunWithFlags.Flag{name: atom, gates: [FunWithFlags.Gate.t]} - @typep options :: Keyword.t + defstruct name: nil, gates: [], last_modified_at: nil + @type t :: %FunWithFlags.Flag{ + name: atom, + gates: [FunWithFlags.Gate.t()], + last_modified_at: DateTime.t() | nil + } + @typep options :: Keyword.t() @doc false def new(name, gates \\ []) when is_atom(name) do - %__MODULE__{name: name, gates: gates} + last_modified_at = + gates + |> Enum.map(& &1.updated_at) + |> Enum.reject(&is_nil/1) + |> case do + [] -> nil + dates -> Enum.max(dates) + end + + %__MODULE__{name: name, gates: gates, last_modified_at: last_modified_at} end - @doc false @spec enabled?(t, options) :: boolean def enabled?(flag, options \\ []) diff --git a/lib/fun_with_flags/gate.ex b/lib/fun_with_flags/gate.ex index c3aea258..122ee1da 100644 --- a/lib/fun_with_flags/gate.ex +++ b/lib/fun_with_flags/gate.ex @@ -16,10 +16,17 @@ defmodule FunWithFlags.Gate do defexception [:message] end + defstruct [:type, :for, :enabled, inserted_at: nil, updated_at: nil] - defstruct [:type, :for, :enabled] - @type t :: %FunWithFlags.Gate{type: atom, for: (nil | String.t), enabled: boolean} - @typep options :: Keyword.t + @type t :: %FunWithFlags.Gate{ + type: atom, + for: nil | String.t(), + enabled: boolean, + inserted_at: DateTime.t() | nil, + updated_at: DateTime.t() | nil + } + + @typep options :: Keyword.t() @doc false @spec new(atom, boolean | float) :: t diff --git a/lib/fun_with_flags/store/persistent/ecto/record.ex b/lib/fun_with_flags/store/persistent/ecto/record.ex index ffda5af3..c0ca0cf1 100644 --- a/lib/fun_with_flags/store/persistent/ecto/record.ex +++ b/lib/fun_with_flags/store/persistent/ecto/record.ex @@ -13,6 +13,7 @@ defmodule FunWithFlags.Store.Persistent.Ecto.Record do field :gate_type, :string field :target, :string field :enabled, :boolean + timestamps(type: :utc_datetime) end @fields [:flag_name, :gate_type, :target, :enabled] diff --git a/lib/fun_with_flags/store/serializer/ecto.ex b/lib/fun_with_flags/store/serializer/ecto.ex index 4122b4dd..171d3616 100644 --- a/lib/fun_with_flags/store/serializer/ecto.ex +++ b/lib/fun_with_flags/store/serializer/ecto.ex @@ -26,24 +26,24 @@ defmodule FunWithFlags.Store.Serializer.Ecto do def deserialize_gate(_flag_name, _record), do: nil - defp do_deserialize_gate(%Record{gate_type: "boolean", enabled: enabled}) do - %Gate{type: :boolean, for: nil, enabled: enabled} + defp do_deserialize_gate(%Record{gate_type: "boolean", enabled: enabled} = record) do + %Gate{type: :boolean, for: nil, enabled: enabled, inserted_at: record.inserted_at, updated_at: record.updated_at} end - defp do_deserialize_gate(%Record{gate_type: "actor", enabled: enabled, target: target}) do - %Gate{type: :actor, for: target, enabled: enabled} + defp do_deserialize_gate(%Record{gate_type: "actor", enabled: enabled, target: target} = record) do + %Gate{type: :actor, for: target, enabled: enabled, inserted_at: record.inserted_at, updated_at: record.updated_at} end - defp do_deserialize_gate(%Record{gate_type: "group", enabled: enabled, target: target}) do - %Gate{type: :group, for: target, enabled: enabled} + defp do_deserialize_gate(%Record{gate_type: "group", enabled: enabled, target: target} = record) do + %Gate{type: :group, for: target, enabled: enabled, inserted_at: record.inserted_at, updated_at: record.updated_at} end - defp do_deserialize_gate(%Record{gate_type: "percentage", target: "time/" <> ratio_s}) do - %Gate{type: :percentage_of_time, for: parse_float(ratio_s), enabled: true} + defp do_deserialize_gate(%Record{gate_type: "percentage", target: "time/" <> ratio_s} = record) do + %Gate{type: :percentage_of_time, for: parse_float(ratio_s), enabled: true, inserted_at: record.inserted_at, updated_at: record.updated_at} end - defp do_deserialize_gate(%Record{gate_type: "percentage", target: "actors/" <> ratio_s}) do - %Gate{type: :percentage_of_actors, for: parse_float(ratio_s), enabled: true} + defp do_deserialize_gate(%Record{gate_type: "percentage", target: "actors/" <> ratio_s} = record) do + %Gate{type: :percentage_of_actors, for: parse_float(ratio_s), enabled: true, inserted_at: record.inserted_at, updated_at: record.updated_at} end def to_atom(atm) when is_atom(atm), do: atm diff --git a/priv/ecto_repo/migrations/00000000000001_add_timestamps.exs b/priv/ecto_repo/migrations/00000000000001_add_timestamps.exs new file mode 100644 index 00000000..302704bd --- /dev/null +++ b/priv/ecto_repo/migrations/00000000000001_add_timestamps.exs @@ -0,0 +1,14 @@ +defmodule FunWithFlags.Dev.EctoRepo.Migrations.CreateFeatureFlagsTable do + use Ecto.Migration + + # This migration assumes the default table name of "fun_with_flags_toggles" + # is being used. If you have overridden that via configuration, you should + # change this migration accordingly. + + def change do + alter table(:fun_with_flags_toggles) do + add :inserted_at, :utc_datetime, null: false, default: fragment("CURRENT_TIMESTAMP") + add :updated_at, :utc_datetime, null: false, default: fragment("CURRENT_TIMESTAMP") + end + end +end diff --git a/test/fun_with_flags/notifications/phoenix_pubsub_test.exs b/test/fun_with_flags/notifications/phoenix_pubsub_test.exs index 20a042f3..ebed6285 100644 --- a/test/fun_with_flags/notifications/phoenix_pubsub_test.exs +++ b/test/fun_with_flags/notifications/phoenix_pubsub_test.exs @@ -169,17 +169,21 @@ defmodule FunWithFlags.Notifications.PhoenixPubSubTest do setup do name = unique_atom() gate = %Gate{type: :boolean, enabled: true} - stored_flag = %Flag{name: name, gates: [gate]} + expected_stored_flag = %Flag{name: name, gates: [gate]} gate2 = %Gate{type: :boolean, enabled: false} cached_flag = %Flag{name: name, gates: [gate2]} - {:ok, ^stored_flag} = Config.persistence_adapter.put(name, gate) + {:ok, stored_flag} = Config.persistence_adapter.put(name, gate) + assert drop_timestamps(stored_flag) == expected_stored_flag + assert_with_retries(fn -> - {:ok, ^cached_flag} = Cache.put(cached_flag) + {:ok, put} = Cache.put(cached_flag) + assert drop_timestamps(put) == drop_timestamps(cached_flag) end) - assert {:ok, ^stored_flag} = Config.persistence_adapter.get(name) + {:ok, persisted} = Config.persistence_adapter.get(name) + assert drop_timestamps(persisted) == expected_stored_flag assert {:ok, ^cached_flag} = Cache.get(name) wait_until_pubsub_is_ready!() @@ -190,7 +194,7 @@ defmodule FunWithFlags.Notifications.PhoenixPubSubTest do # This should be in `setup` but in there it produces a compiler warning because # the two variables will never match (duh). test "verify test setup", %{cached_flag: cached_flag, stored_flag: stored_flag} do - refute match? ^stored_flag, cached_flag + refute match? ^stored_flag, drop_timestamps(cached_flag) end @@ -203,7 +207,8 @@ defmodule FunWithFlags.Notifications.PhoenixPubSubTest do Phoenix.PubSub.broadcast!(client, channel, message) assert_with_retries(fn -> - assert {:ok, ^cached_flag} = Cache.get(name) + assert {:ok, put} = Cache.get(name) + assert drop_timestamps(put) == drop_timestamps(cached_flag) end) end @@ -220,8 +225,17 @@ defmodule FunWithFlags.Notifications.PhoenixPubSubTest do Phoenix.PubSub.broadcast!(client, channel, message) assert_with_retries(fn -> - assert {:ok, ^stored_flag} = Cache.get(name) + assert {:ok, result} = Cache.get(name) + assert drop_timestamps(result) == drop_timestamps(stored_flag) end) end end + + defp drop_timestamps(%FunWithFlags.Gate{} = gate) do + %{gate | inserted_at: nil, updated_at: nil} + end + + defp drop_timestamps(%FunWithFlags.Flag{} = flag) do + %{flag | last_modified_at: nil, gates: Enum.map(flag.gates, &drop_timestamps/1)} + end end diff --git a/test/fun_with_flags/simple_store_test.exs b/test/fun_with_flags/simple_store_test.exs index 2376b111..07028fa7 100644 --- a/test/fun_with_flags/simple_store_test.exs +++ b/test/fun_with_flags/simple_store_test.exs @@ -26,16 +26,22 @@ defmodule FunWithFlags.SimpleStoreTest do assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.lookup(name) SimpleStore.put(name, gate) - assert {:ok, %Flag{name: ^name, gates: [^gate]}} = SimpleStore.lookup(name) + {:ok, result} = SimpleStore.lookup(name) + assert %Flag{name: ^name} = result + assert [persisted_gate] = result.gates + assert drop_timestamps(persisted_gate) == gate gate2 = %Gate{gate | enabled: false} SimpleStore.put(name, gate2) - assert {:ok, %Flag{name: ^name, gates: [^gate2]}} = SimpleStore.lookup(name) - refute match? {:ok, %Flag{name: ^name, gates: [^gate]}}, SimpleStore.lookup(name) + {:ok, result2} = SimpleStore.lookup(name) + assert %Flag{name: ^name} = result2 + assert [persisted_gate2] = result2.gates + assert drop_timestamps(persisted_gate2) == gate2 end test "put() returns the tuple {:ok, %Flag{}}", %{name: name, gate: gate, flag: flag} do - assert {:ok, ^flag} = SimpleStore.put(name, gate) + {:ok, result} = SimpleStore.put(name, gate) + assert drop_timestamps(result) == flag end @tag :telemetry @@ -91,27 +97,45 @@ defmodule FunWithFlags.SimpleStoreTest do SimpleStore.put(name, bool_gate) SimpleStore.put(name, group_gate) {:ok, flag} = SimpleStore.lookup(name) - assert %Flag{name: ^name, gates: [^bool_gate, ^group_gate]} = flag + assert %Flag{name: ^name} = flag + assert [persisted_bool, persisted_group] = flag.gates + assert drop_timestamps(persisted_bool) == bool_gate + assert drop_timestamps(persisted_group) == group_gate - {:ok, name: name, bool_gate: bool_gate, group_gate: group_gate} + {:ok, name: name, bool_gate: persisted_bool, group_gate: persisted_group} end test "delete(flag_name, gate) can change the value of a flag", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do - assert {:ok, %Flag{name: ^name, gates: [^bool_gate, ^group_gate]}} = SimpleStore.lookup(name) + {:ok, flag} = SimpleStore.lookup(name) + assert %Flag{name: ^name} = flag + assert length(flag.gates) == 2 SimpleStore.delete(name, bool_gate) - assert {:ok, %Flag{name: ^name, gates: [^group_gate]}} = SimpleStore.lookup(name) + {:ok, flag2} = SimpleStore.lookup(name) + assert %Flag{name: ^name} = flag2 + assert [remaining_gate] = flag2.gates + assert drop_timestamps(remaining_gate) == drop_timestamps(group_gate) + SimpleStore.delete(name, group_gate) assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.lookup(name) end test "delete(flag_name, gate) returns the tuple {:ok, %Flag{}}", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do - assert {:ok, %Flag{name: ^name, gates: [^group_gate]}} = SimpleStore.delete(name, bool_gate) + {:ok, result} = SimpleStore.delete(name, bool_gate) + assert %Flag{name: ^name} = result + assert [remaining_gate] = result.gates + assert drop_timestamps(remaining_gate) == drop_timestamps(group_gate) end test "deleting is safe and idempotent", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do - assert {:ok, %Flag{name: ^name, gates: [^group_gate]}} = SimpleStore.delete(name, bool_gate) - assert {:ok, %Flag{name: ^name, gates: [^group_gate]}} = SimpleStore.delete(name, bool_gate) + {:ok, result1} = SimpleStore.delete(name, bool_gate) + assert [g1] = result1.gates + assert drop_timestamps(g1) == drop_timestamps(group_gate) + + {:ok, result2} = SimpleStore.delete(name, bool_gate) + assert [g2] = result2.gates + assert drop_timestamps(g2) == drop_timestamps(group_gate) + assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.delete(name, group_gate) assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.delete(name, group_gate) end @@ -169,13 +193,18 @@ defmodule FunWithFlags.SimpleStoreTest do SimpleStore.put(name, bool_gate) SimpleStore.put(name, group_gate) {:ok, flag} = SimpleStore.lookup(name) - assert %Flag{name: ^name, gates: [^bool_gate, ^group_gate]} = flag + assert %Flag{name: ^name} = flag + assert [persisted_bool, persisted_group] = flag.gates + assert drop_timestamps(persisted_bool) == bool_gate + assert drop_timestamps(persisted_group) == group_gate - {:ok, name: name, bool_gate: bool_gate, group_gate: group_gate} + {:ok, name: name, bool_gate: persisted_bool, group_gate: persisted_group} end - test "delete(flag_name) will reset all the flag gates", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do - assert {:ok, %Flag{name: ^name, gates: [^bool_gate, ^group_gate]}} = SimpleStore.lookup(name) + test "delete(flag_name) will reset all the flag gates", %{name: name} do + {:ok, flag} = SimpleStore.lookup(name) + assert %Flag{name: ^name} = flag + assert length(flag.gates) == 2 SimpleStore.delete(name) assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.lookup(name) @@ -246,7 +275,10 @@ defmodule FunWithFlags.SimpleStoreTest do assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.lookup(name) SimpleStore.put(name, gate) - assert {:ok, %Flag{name: ^name, gates: [^gate]}} = SimpleStore.lookup(name) + {:ok, result} = SimpleStore.lookup(name) + assert %Flag{name: ^name} = result + assert [persisted_gate] = result.gates + assert drop_timestamps(persisted_gate) == gate end @tag :telemetry @@ -327,12 +359,14 @@ defmodule FunWithFlags.SimpleStoreTest do {:ok, result} = SimpleStore.all_flags() assert 3 = length(result) + result_without_timestamps = Enum.map(result, &drop_timestamps/1) + for flag <- [ %Flag{name: name1, gates: [g_1a, g_1b, g_1c]}, %Flag{name: name2, gates: [g_2a, g_2b]}, %Flag{name: name3, gates: [g_3a]} ] do - assert flag in result + assert flag in result_without_timestamps end end @@ -531,4 +565,12 @@ defmodule FunWithFlags.SimpleStoreTest do end end end + + defp drop_timestamps(%Gate{} = gate) do + %{gate | inserted_at: nil, updated_at: nil} + end + + defp drop_timestamps(%Flag{} = flag) do + %{flag | last_modified_at: nil, gates: Enum.map(flag.gates, &drop_timestamps/1)} + end end diff --git a/test/fun_with_flags/store/persistent/ecto_test.exs b/test/fun_with_flags/store/persistent/ecto_test.exs index 49bb8a32..e410fffb 100644 --- a/test/fun_with_flags/store/persistent/ecto_test.exs +++ b/test/fun_with_flags/store/persistent/ecto_test.exs @@ -21,84 +21,113 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do assert {:ok, %Flag{name: ^name, gates: []}} = PersiEcto.get(name) PersiEcto.put(name, first_bool_gate) - assert {:ok, %Flag{name: ^name, gates: [^first_bool_gate]}} = PersiEcto.get(name) + assert {:ok, %Flag{name: ^name, gates: [persisted_first_bool_gate]} = persisted} = PersiEcto.get(name) + assert drop_timestamps(persisted_first_bool_gate) == drop_timestamps(first_bool_gate) + assert %Gate{inserted_at: %DateTime{}, updated_at: %DateTime{}} = persisted_first_bool_gate + assert persisted.last_modified_at == persisted_first_bool_gate.updated_at other_bool_gate = %Gate{first_bool_gate | enabled: false} PersiEcto.put(name, other_bool_gate) - assert {:ok, %Flag{name: ^name, gates: [^other_bool_gate]}} = PersiEcto.get(name) + assert {:ok, %Flag{name: ^name, gates: [persisted_other_bool_gate]}} = PersiEcto.get(name) + assert drop_timestamps(persisted_other_bool_gate) == drop_timestamps(other_bool_gate) + assert %Gate{inserted_at: %DateTime{}, updated_at: %DateTime{}} = persisted_other_bool_gate + assert persisted.last_modified_at == persisted_other_bool_gate.updated_at refute match? {:ok, %Flag{name: ^name, gates: [^first_bool_gate]}}, PersiEcto.get(name) actor_gate = %Gate{type: :actor, for: "string:qwerty", enabled: true} PersiEcto.put(name, actor_gate) - assert {:ok, %Flag{name: ^name, gates: [^actor_gate, ^other_bool_gate]}} = PersiEcto.get(name) + {:ok, result} = PersiEcto.get(name) + assert %Flag{name: ^name} = result + assert length(result.gates) == 2 + assert Enum.any?(result.gates, &(drop_timestamps(&1) == actor_gate)) PersiEcto.put(name, first_bool_gate) - assert {:ok, %Flag{name: ^name, gates: [^actor_gate, ^first_bool_gate]}} = PersiEcto.get(name) + {:ok, result2} = PersiEcto.get(name) + assert %Flag{name: ^name} = result2 + assert length(result2.gates) == 2 + assert Enum.any?(result2.gates, &(drop_timestamps(&1) == first_bool_gate)) end test "put() returns the tuple {:ok, %Flag{}}", %{name: name, gate: gate, flag: flag} do - assert {:ok, %Flag{name: ^name, gates: [^gate]}} = PersiEcto.put(name, gate) - assert {:ok, ^flag} = PersiEcto.put(name, gate) + {:ok, result1} = PersiEcto.put(name, gate) + assert %Flag{name: ^name} = result1 + assert [persisted_gate] = result1.gates + assert drop_timestamps(persisted_gate) == gate + + {:ok, result2} = PersiEcto.put(name, gate) + assert drop_timestamps(result2) == flag end test "put()'ing more gates will return an increasily updated flag", %{name: name, gate: gate} do - assert {:ok, %Flag{name: ^name, gates: [^gate]}} = PersiEcto.put(name, gate) + {:ok, result1} = PersiEcto.put(name, gate) + assert %Flag{name: ^name} = result1 + assert [persisted_gate] = result1.gates + assert drop_timestamps(persisted_gate) == gate other_gate = %Gate{type: :actor, for: "string:asdf", enabled: true} - assert {:ok, %Flag{name: ^name, gates: [^other_gate, ^gate]}} = PersiEcto.put(name, other_gate) + {:ok, result2} = PersiEcto.put(name, other_gate) + assert %Flag{name: ^name} = result2 + assert length(result2.gates) == 2 end test "put() will UPSERT gates, inserting new ones and editing existing ones", %{name: name, gate: first_bool_gate} do assert {:ok, %Flag{name: ^name, gates: []}} = PersiEcto.get(name) PersiEcto.put(name, first_bool_gate) - assert {:ok, %Flag{name: ^name, gates: [^first_bool_gate]}} = PersiEcto.get(name) + {:ok, result} = PersiEcto.get(name) + assert %Flag{name: ^name} = result + assert [persisted_gate] = result.gates + assert drop_timestamps(persisted_gate) == first_bool_gate other_bool_gate = %Gate{first_bool_gate | enabled: false} PersiEcto.put(name, other_bool_gate) - assert {:ok, %Flag{name: ^name, gates: [^other_bool_gate]}} = PersiEcto.get(name) - refute match? {:ok, %Flag{name: ^name, gates: [^first_bool_gate]}}, PersiEcto.get(name) + {:ok, result2} = PersiEcto.get(name) + assert [pg2] = result2.gates + assert drop_timestamps(pg2) == other_bool_gate first_actor_gate = %Gate{type: :actor, for: "string:qwerty", enabled: true} PersiEcto.put(name, first_actor_gate) expected_flag = make_expected_flag(name, [first_actor_gate, other_bool_gate]) - assert {:ok, ^expected_flag} = sort_db_result_gates(PersiEcto.get(name)) + {:ok, result3} = sort_db_result_gates(PersiEcto.get(name)) + assert drop_timestamps(result3) == expected_flag PersiEcto.put(name, first_bool_gate) expected_flag = make_expected_flag(name, [first_actor_gate, first_bool_gate]) - assert {:ok, ^expected_flag} = sort_db_result_gates(PersiEcto.get(name)) + {:ok, result4} = sort_db_result_gates(PersiEcto.get(name)) + assert drop_timestamps(result4) == expected_flag other_actor_gate = %Gate{type: :actor, for: "string:asd", enabled: true} PersiEcto.put(name, other_actor_gate) expected_flag = make_expected_flag(name, [other_actor_gate, first_actor_gate, first_bool_gate]) - assert {:ok, ^expected_flag} = sort_db_result_gates(PersiEcto.get(name)) + {:ok, result5} = sort_db_result_gates(PersiEcto.get(name)) + assert drop_timestamps(result5) == expected_flag first_actor_gate_disabled = %Gate{first_actor_gate | enabled: false} PersiEcto.put(name, first_actor_gate_disabled) expected_flag = make_expected_flag(name, [other_actor_gate, first_actor_gate_disabled, first_bool_gate]) - assert {:ok, ^expected_flag} = sort_db_result_gates(PersiEcto.get(name)) - expected_flag = make_expected_flag(name, [other_actor_gate, first_actor_gate, first_bool_gate]) - refute match? {:ok, ^expected_flag}, sort_db_result_gates(PersiEcto.get(name)) + {:ok, result6} = sort_db_result_gates(PersiEcto.get(name)) + assert drop_timestamps(result6) == expected_flag first_group_gate = %Gate{type: :group, for: "smurfs", enabled: true} PersiEcto.put(name, first_group_gate) expected_flag = make_expected_flag(name, [other_actor_gate, first_actor_gate_disabled, first_bool_gate, first_group_gate]) - assert {:ok, ^expected_flag} = sort_db_result_gates(PersiEcto.get(name)) + {:ok, result7} = sort_db_result_gates(PersiEcto.get(name)) + assert drop_timestamps(result7) == expected_flag other_group_gate = %Gate{type: :group, for: "gnomes", enabled: true} PersiEcto.put(name, other_group_gate) expected_flag = make_expected_flag(name, [other_actor_gate, first_actor_gate_disabled, first_bool_gate, other_group_gate, first_group_gate]) - assert {:ok, ^expected_flag} = sort_db_result_gates(PersiEcto.get(name)) + {:ok, result8} = sort_db_result_gates(PersiEcto.get(name)) + assert drop_timestamps(result8) == expected_flag first_group_gate_disabled = %Gate{first_group_gate | enabled: false} PersiEcto.put(name, first_group_gate_disabled) expected_flag = make_expected_flag(name, [other_actor_gate, first_actor_gate_disabled, first_bool_gate, other_group_gate, first_group_gate_disabled]) - assert {:ok, ^expected_flag} = sort_db_result_gates(PersiEcto.get(name)) - expected_flag = make_expected_flag(name, [other_actor_gate, first_actor_gate_disabled, first_bool_gate, other_group_gate, first_group_gate]) - refute match? {:ok, ^expected_flag}, sort_db_result_gates(PersiEcto.get(name)) + {:ok, result9} = sort_db_result_gates(PersiEcto.get(name)) + assert drop_timestamps(result9) == expected_flag end end @@ -116,31 +145,43 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do assert {:ok, %Flag{name: ^name, gates: []}} = PersiEcto.get(name) PersiEcto.put(name, pot_gate) - assert {:ok, %Flag{name: ^name, gates: [^pot_gate]}} = PersiEcto.get(name) + {:ok, result1} = PersiEcto.get(name) + assert [pg1] = result1.gates + assert drop_timestamps(pg1) == pot_gate other_pot_gate = %Gate{pot_gate | for: 0.42} PersiEcto.put(name, other_pot_gate) - assert {:ok, %Flag{name: ^name, gates: [^other_pot_gate]}} = PersiEcto.get(name) - refute match? {:ok, %Flag{name: ^name, gates: [^pot_gate]}}, PersiEcto.get(name) + {:ok, result2} = PersiEcto.get(name) + assert [pg2] = result2.gates + assert drop_timestamps(pg2) == other_pot_gate actor_gate = %Gate{type: :actor, for: "string:qwerty", enabled: true} PersiEcto.put(name, actor_gate) - assert {:ok, %Flag{name: ^name, gates: [^actor_gate, ^other_pot_gate]}} = PersiEcto.get(name) + {:ok, result3} = PersiEcto.get(name) + assert length(result3.gates) == 2 PersiEcto.put(name, pot_gate) - assert {:ok, %Flag{name: ^name, gates: [^actor_gate, ^pot_gate]}} = PersiEcto.get(name) + {:ok, result4} = PersiEcto.get(name) + assert length(result4.gates) == 2 end test "put() returns the tuple {:ok, %Flag{}}", %{name: name, pot_gate: pot_gate} do - assert {:ok, %Flag{name: ^name, gates: [^pot_gate]}} = PersiEcto.put(name, pot_gate) + {:ok, result} = PersiEcto.put(name, pot_gate) + assert %Flag{name: ^name} = result + assert [pg] = result.gates + assert drop_timestamps(pg) == pot_gate end test "put()'ing more gates will return an increasily updated flag", %{name: name, pot_gate: pot_gate} do bool_gate = Gate.new(:boolean, false) - assert {:ok, %Flag{name: ^name, gates: [^bool_gate]}} = PersiEcto.put(name, bool_gate) - assert {:ok, %Flag{name: ^name, gates: [^bool_gate, ^pot_gate]}} = PersiEcto.put(name, pot_gate) + {:ok, result1} = PersiEcto.put(name, bool_gate) + assert [pg1] = result1.gates + assert drop_timestamps(pg1) == bool_gate + + {:ok, result2} = PersiEcto.put(name, pot_gate) + assert length(result2.gates) == 2 end end @@ -158,31 +199,43 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do assert {:ok, %Flag{name: ^name, gates: []}} = PersiEcto.get(name) PersiEcto.put(name, poa_gate) - assert {:ok, %Flag{name: ^name, gates: [^poa_gate]}} = PersiEcto.get(name) + {:ok, result1} = PersiEcto.get(name) + assert [pg1] = result1.gates + assert drop_timestamps(pg1) == poa_gate other_poa_gate = %Gate{poa_gate | for: 0.42} PersiEcto.put(name, other_poa_gate) - assert {:ok, %Flag{name: ^name, gates: [^other_poa_gate]}} = PersiEcto.get(name) - refute match? {:ok, %Flag{name: ^name, gates: [^poa_gate]}}, PersiEcto.get(name) + {:ok, result2} = PersiEcto.get(name) + assert [pg2] = result2.gates + assert drop_timestamps(pg2) == other_poa_gate actor_gate = %Gate{type: :actor, for: "string:qwerty", enabled: true} PersiEcto.put(name, actor_gate) - assert {:ok, %Flag{name: ^name, gates: [^actor_gate, ^other_poa_gate]}} = PersiEcto.get(name) + {:ok, result3} = PersiEcto.get(name) + assert length(result3.gates) == 2 PersiEcto.put(name, poa_gate) - assert {:ok, %Flag{name: ^name, gates: [^actor_gate, ^poa_gate]}} = PersiEcto.get(name) + {:ok, result4} = PersiEcto.get(name) + assert length(result4.gates) == 2 end test "put() returns the tuple {:ok, %Flag{}}", %{name: name, poa_gate: poa_gate} do - assert {:ok, %Flag{name: ^name, gates: [^poa_gate]}} = PersiEcto.put(name, poa_gate) + {:ok, result} = PersiEcto.put(name, poa_gate) + assert %Flag{name: ^name} = result + assert [pg] = result.gates + assert drop_timestamps(pg) == poa_gate end test "put()'ing more gates will return an increasily updated flag", %{name: name, poa_gate: poa_gate} do bool_gate = Gate.new(:boolean, false) - assert {:ok, %Flag{name: ^name, gates: [^bool_gate]}} = PersiEcto.put(name, bool_gate) - assert {:ok, %Flag{name: ^name, gates: [^bool_gate, ^poa_gate]}} = PersiEcto.put(name, poa_gate) + {:ok, result1} = PersiEcto.put(name, bool_gate) + assert [pg1] = result1.gates + assert drop_timestamps(pg1) == bool_gate + + {:ok, result2} = PersiEcto.put(name, poa_gate) + assert length(result2.gates) == 2 end end @@ -194,14 +247,16 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do bool_gate = %Gate{type: :boolean, enabled: false} group_gate = %Gate{type: :group, for: "admins", enabled: true} actor_gate = %Gate{type: :actor, for: "string_actor", enabled: true} - flag = %Flag{name: name, gates: sort_gates_by_type([bool_gate, group_gate, actor_gate])} {:ok, %Flag{name: ^name}} = PersiEcto.put(name, bool_gate) {:ok, %Flag{name: ^name}} = PersiEcto.put(name, group_gate) - {:ok, ^flag} = PersiEcto.put(name, actor_gate) + {:ok, flag} = PersiEcto.put(name, actor_gate) {:ok, ^flag} = PersiEcto.get(name) - {:ok, name: name, flag: flag, bool_gate: bool_gate, group_gate: group_gate, actor_gate: actor_gate} + # Extract persisted gates with timestamps + [persisted_actor, persisted_bool, persisted_group] = flag.gates + + {:ok, name: name, flag: flag, bool_gate: persisted_bool, group_gate: persisted_group, actor_gate: persisted_actor} end @@ -255,15 +310,16 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do actor_gate = %Gate{type: :actor, for: "string_actor", enabled: true} pot_gate = %Gate{type: :percentage_of_time, for: 0.5, enabled: true} - flag = %Flag{name: name, gates: sort_gates_by_type([bool_gate, group_gate, actor_gate, pot_gate])} - {:ok, %Flag{name: ^name}} = PersiEcto.put(name, bool_gate) {:ok, %Flag{name: ^name}} = PersiEcto.put(name, group_gate) {:ok, %Flag{name: ^name}} = PersiEcto.put(name, actor_gate) - {:ok, ^flag} = PersiEcto.put(name, pot_gate) + {:ok, flag} = PersiEcto.put(name, pot_gate) {:ok, ^flag} = PersiEcto.get(name) - {:ok, name: name, flag: flag, bool_gate: bool_gate, group_gate: group_gate, actor_gate: actor_gate, pot_gate: pot_gate} + # Extract persisted gates with timestamps + [persisted_actor, persisted_bool, persisted_group, persisted_pot] = flag.gates + + {:ok, name: name, flag: flag, bool_gate: persisted_bool, group_gate: persisted_group, actor_gate: persisted_actor, pot_gate: persisted_pot} end @@ -318,15 +374,19 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do actor_gate = %Gate{type: :actor, for: "string_actor", enabled: true} poa_gate = %Gate{type: :percentage_of_actors, for: 0.5, enabled: true} - flag = %Flag{name: name, gates: sort_gates_by_type([bool_gate, group_gate, actor_gate, poa_gate])} + expected_flag = %Flag{name: name, gates: sort_gates_by_type([bool_gate, group_gate, actor_gate, poa_gate])} {:ok, %Flag{name: ^name}} = PersiEcto.put(name, bool_gate) {:ok, %Flag{name: ^name}} = PersiEcto.put(name, group_gate) {:ok, %Flag{name: ^name}} = PersiEcto.put(name, actor_gate) - {:ok, ^flag} = PersiEcto.put(name, poa_gate) + {:ok, flag} = PersiEcto.put(name, poa_gate) + assert drop_timestamps(flag) == expected_flag {:ok, ^flag} = PersiEcto.get(name) - {:ok, name: name, flag: flag, bool_gate: bool_gate, group_gate: group_gate, actor_gate: actor_gate, poa_gate: poa_gate} + # Extract persisted gates with timestamps + [persisted_actor, persisted_bool, persisted_group, persisted_poa] = flag.gates + + {:ok, name: name, flag: flag, bool_gate: persisted_bool, group_gate: persisted_group, actor_gate: persisted_actor, poa_gate: persisted_poa} end @@ -379,14 +439,16 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do bool_gate = %Gate{type: :boolean, enabled: false} group_gate = %Gate{type: :group, for: "admins", enabled: true} actor_gate = %Gate{type: :actor, for: "string_actor", enabled: true} - flag = %Flag{name: name, gates: sort_gates_by_type([bool_gate, group_gate, actor_gate])} {:ok, %Flag{name: ^name}} = PersiEcto.put(name, bool_gate) {:ok, %Flag{name: ^name}} = PersiEcto.put(name, group_gate) - {:ok, ^flag} = PersiEcto.put(name, actor_gate) + {:ok, flag} = PersiEcto.put(name, actor_gate) {:ok, ^flag} = PersiEcto.get(name) - {:ok, name: name, flag: flag, bool_gate: bool_gate, group_gate: group_gate, actor_gate: actor_gate} + # Extract persisted gates with timestamps + [persisted_actor, persisted_bool, persisted_group] = flag.gates + + {:ok, name: name, flag: flag, bool_gate: persisted_bool, group_gate: persisted_group, actor_gate: persisted_actor} end @@ -428,7 +490,10 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do assert {:ok, %Flag{name: ^name, gates: []}} = PersiEcto.get(name) PersiEcto.put(name, gate) - assert {:ok, %Flag{name: ^name, gates: [^gate]}} = PersiEcto.get(name) + {:ok, result} = PersiEcto.get(name) + assert %Flag{name: ^name} = result + assert [persisted_gate] = result.gates + assert drop_timestamps(persisted_gate) == gate end end @@ -460,12 +525,14 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do {:ok, result} = PersiEcto.all_flags() assert 3 = length(result) + result_without_timestamps = Enum.map(result, &drop_timestamps/1) + for flag <- [ %Flag{name: name1, gates: [g_1a, g_1b, g_1c]}, %Flag{name: name2, gates: [g_2a, g_2b]}, %Flag{name: name3, gates: [g_3a]} ] do - assert flag in result + assert flag in result_without_timestamps end end end @@ -540,4 +607,12 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do defp make_expected_flag(name, gates) do sort_flag_gates(%Flag{name: name, gates: gates}) end + + defp drop_timestamps(%Gate{} = gate) do + %{gate | inserted_at: nil, updated_at: nil} + end + + defp drop_timestamps(%Flag{} = flag) do + %{flag | last_modified_at: nil, gates: Enum.map(flag.gates, &drop_timestamps/1)} + end end diff --git a/test/fun_with_flags/store_test.exs b/test/fun_with_flags/store_test.exs index 9a90d67b..fb48cc1a 100644 --- a/test/fun_with_flags/store_test.exs +++ b/test/fun_with_flags/store_test.exs @@ -34,7 +34,8 @@ defmodule FunWithFlags.StoreTest do test "looking up a defined flag returns the flag", %{name: name, gate: gate, flag: flag} do assert {:ok, %Flag{name: ^name, gates: []}} = Store.lookup(name) Store.put(name, gate) - assert {:ok, ^flag} = Store.lookup(name) + {:ok, result} = Store.lookup(name) + assert drop_timestamps(result) == flag end @tag :telemetry @@ -43,8 +44,10 @@ defmodule FunWithFlags.StoreTest do ref = :telemetry_test.attach_event_handlers(self(), [event]) # Write a flag to populate the cache, then read it. - assert {:ok, ^flag} = Store.put(name, gate) - assert {:ok, ^flag} = Store.lookup(name) + {:ok, persisted_flag} = Store.put(name, gate) + assert drop_timestamps(persisted_flag) == flag + {:ok, lookup_result} = Store.lookup(name) + assert drop_timestamps(lookup_result) == flag refute_received { ^event, @@ -63,10 +66,12 @@ defmodule FunWithFlags.StoreTest do # Note: this setup could be omitted, and we could run the test with an # empty store and an empty cache. It would be the same. - assert {:ok, ^flag} = Store.put(name, gate) + {:ok, persisted_flag} = Store.put(name, gate) + assert drop_timestamps(persisted_flag) == flag Cache.flush() - assert {:ok, ^flag} = Store.lookup(name) + {:ok, lookup_result} = Store.lookup(name) + assert drop_timestamps(lookup_result) == flag assert_received { ^event, @@ -88,7 +93,8 @@ defmodule FunWithFlags.StoreTest do # Note: this setup could be omitted, and we could run the test with an # empty store and an empty cache. It would be the same. - assert {:ok, ^flag} = Store.put(name, gate) + {:ok, persisted_flag} = Store.put(name, gate) + assert drop_timestamps(persisted_flag) == flag Cache.flush() with_mock(@persistence, [], [get: fn(^name) -> {:error, error_reason} end]) do @@ -117,16 +123,20 @@ defmodule FunWithFlags.StoreTest do assert {:ok, %Flag{name: ^name, gates: []}} = Store.lookup(name) Store.put(name, gate) - assert {:ok, ^flag} = Store.lookup(name) + {:ok, result} = Store.lookup(name) + assert drop_timestamps(result) == flag gate2 = %Gate{gate | enabled: false} Store.put(name, gate2) - assert {:ok, %Flag{name: ^name, gates: [^gate2]}} = Store.lookup(name) - refute match? ^flag, Store.lookup(name) + {:ok, result2} = Store.lookup(name) + assert %Flag{name: ^name} = result2 + assert [persisted_gate2] = result2.gates + assert drop_timestamps(persisted_gate2) == gate2 end test "put() returns the tuple {:ok, %Flag{}}", %{name: name, gate: gate, flag: flag} do - assert {:ok, ^flag} = Store.put(name, gate) + {:ok, result} = Store.put(name, gate) + assert drop_timestamps(result) == flag end @tag :telemetry @@ -180,7 +190,8 @@ defmodule FunWithFlags.StoreTest do with_mocks([ {Redix, [:passthrough], []} ]) do - assert {:ok, ^flag} = Store.put(name, gate) + {:ok, result} = Store.put(name, gate) + assert drop_timestamps(result) == flag :timer.sleep(20) assert called( @@ -201,7 +212,8 @@ defmodule FunWithFlags.StoreTest do with_mocks([ {Phoenix.PubSub, [:passthrough], []} ]) do - assert {:ok, ^flag} = Store.put(name, gate) + {:ok, result} = Store.put(name, gate) + assert drop_timestamps(result) == flag :timer.sleep(20) assert called( @@ -231,7 +243,8 @@ defmodule FunWithFlags.StoreTest do 500 -> flunk "Subscribe didn't work" end - assert {:ok, ^flag} = Store.put(name, gate) + {:ok, result} = Store.put(name, gate) + assert drop_timestamps(result) == flag payload = "#{u_id}:#{to_string(name)}" @@ -265,7 +278,8 @@ defmodule FunWithFlags.StoreTest do :ok = Phoenix.PubSub.subscribe(:fwf_test, channel) # implicit self - assert {:ok, ^flag} = Store.put(name, gate) + {:ok, result} = Store.put(name, gate) + assert drop_timestamps(result) == flag payload = {:updated, name, u_id} @@ -308,7 +322,8 @@ defmodule FunWithFlags.StoreTest do {Config, [:passthrough], [change_notifications_enabled?: fn() -> false end]}, {Phoenix.PubSub, [:passthrough], []} ]) do - assert {:ok, ^flag} = Store.put(name, gate) + {:ok, put} = Store.put(name, gate) + assert drop_timestamps(put) == drop_timestamps(flag) :timer.sleep(20) refute called( @@ -332,9 +347,12 @@ defmodule FunWithFlags.StoreTest do Store.put(name, bool_gate) Store.put(name, group_gate) {:ok, flag} = Store.lookup(name) - assert %Flag{name: ^name, gates: [^bool_gate, ^group_gate]} = flag + assert %Flag{name: ^name} = flag + [persisted_bool, persisted_group] = flag.gates + assert drop_timestamps(persisted_bool) == bool_gate + assert drop_timestamps(persisted_group) == group_gate - {:ok, bool_gate: bool_gate, group_gate: group_gate} + {:ok, bool_gate: persisted_bool, group_gate: persisted_group} end test "delete(flag_name, gate) can change the value of a flag", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do @@ -562,9 +580,12 @@ defmodule FunWithFlags.StoreTest do Store.put(name, bool_gate) Store.put(name, group_gate) {:ok, flag} = Store.lookup(name) - assert %Flag{name: ^name, gates: [^bool_gate, ^group_gate]} = flag + assert %Flag{name: ^name} = flag + [persisted_bool, persisted_group] = flag.gates + assert drop_timestamps(persisted_bool) == bool_gate + assert drop_timestamps(persisted_group) == group_gate - {:ok, bool_gate: bool_gate, group_gate: group_gate} + {:ok, bool_gate: persisted_bool, group_gate: persisted_group} end test "delete(flag_name) will reset all the flag gates", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do @@ -795,8 +816,10 @@ defmodule FunWithFlags.StoreTest do end test "if the flag is stored in the DB, it stores it in the Cache", %{name: name, gate: gate, flag: flag} do - {:ok, ^flag} = @persistence.put(name, gate) - assert {:ok, ^flag} = @persistence.get(name) + {:ok, persisted_flag} = @persistence.put(name, gate) + assert drop_timestamps(persisted_flag) == flag + {:ok, persist_result} = @persistence.get(name) + assert drop_timestamps(persist_result) == flag gate2 = %Gate{gate | enabled: false} flag2 = %Flag{name: name, gates: [gate2]} @@ -804,13 +827,13 @@ defmodule FunWithFlags.StoreTest do Cache.put(flag2) assert {:ok, ^flag2} = Cache.get(name) assert {:ok, ^flag2} = Store.lookup(name) - refute match? {:ok, ^flag}, Store.lookup(name) Store.reload(name) - assert {:ok, ^flag} = Cache.get(name) - assert {:ok, ^flag} = Store.lookup(name) - refute match? {:ok, ^flag2}, Store.lookup(name) + {:ok, cache_result} = Cache.get(name) + assert drop_timestamps(cache_result) == flag + {:ok, lookup_result} = Store.lookup(name) + assert drop_timestamps(lookup_result) == flag end @tag :telemetry @@ -887,12 +910,14 @@ defmodule FunWithFlags.StoreTest do {:ok, result} = Store.all_flags() assert 3 = length(result) + result_without_timestamps = Enum.map(result, &drop_timestamps/1) + for flag <- [ %Flag{name: name1, gates: [g_1a, g_1b, g_1c]}, %Flag{name: name2, gates: [g_2a, g_2b]}, %Flag{name: name3, gates: [g_3a]} ] do - assert flag in result + assert flag in result_without_timestamps end end @@ -1062,27 +1087,31 @@ defmodule FunWithFlags.StoreTest do assert {:ok, ^empty_flag} = @persistence.get(name) Store.put(name, gate) - assert {:ok, ^flag} = Cache.get(name) - assert {:ok, ^flag} = @persistence.get(name) + {:ok, cache_result} = Cache.get(name) + assert drop_timestamps(cache_result) == flag + {:ok, persist_result} = @persistence.get(name) + assert drop_timestamps(persist_result) == flag end test "deleting a gate will update both the cache and the persistent store", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do Store.put(name, bool_gate) - Store.put(name, group_gate) + {:ok, flag} = Store.put(name, group_gate) + # Extract persisted gates with timestamps + [persisted_bool, persisted_group] = flag.gates - assert {:ok, %Flag{name: ^name, gates: [^bool_gate, ^group_gate]}} = Cache.get(name) - assert {:ok, %Flag{name: ^name, gates: [^bool_gate, ^group_gate]}} = @persistence.get(name) + assert {:ok, %Flag{name: ^name, gates: [^persisted_bool, ^persisted_group]}} = Cache.get(name) + assert {:ok, %Flag{name: ^name, gates: [^persisted_bool, ^persisted_group]}} = @persistence.get(name) - Store.delete(name, group_gate) - assert {:ok, %Flag{name: ^name, gates: [^bool_gate]}} = Cache.get(name) - assert {:ok, %Flag{name: ^name, gates: [^bool_gate]}} = @persistence.get(name) + Store.delete(name, persisted_group) + assert {:ok, %Flag{name: ^name, gates: [^persisted_bool]}} = Cache.get(name) + assert {:ok, %Flag{name: ^name, gates: [^persisted_bool]}} = @persistence.get(name) # repeat. check it's safe and idempotent - Store.delete(name, group_gate) - assert {:ok, %Flag{name: ^name, gates: [^bool_gate]}} = Cache.get(name) - assert {:ok, %Flag{name: ^name, gates: [^bool_gate]}} = @persistence.get(name) + Store.delete(name, persisted_group) + assert {:ok, %Flag{name: ^name, gates: [^persisted_bool]}} = Cache.get(name) + assert {:ok, %Flag{name: ^name, gates: [^persisted_bool]}} = @persistence.get(name) - Store.delete(name, bool_gate) + Store.delete(name, persisted_bool) assert {:ok, %Flag{name: ^name, gates: []}} = Cache.get(name) assert {:ok, %Flag{name: ^name, gates: []}} = @persistence.get(name) end @@ -1090,10 +1119,12 @@ defmodule FunWithFlags.StoreTest do test "deleting a flag will reset both the cache and the persistent store", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do Store.put(name, bool_gate) - Store.put(name, group_gate) + {:ok, flag} = Store.put(name, group_gate) + # Extract persisted gates with timestamps + [persisted_bool, persisted_group] = flag.gates - assert {:ok, %Flag{name: ^name, gates: [^bool_gate, ^group_gate]}} = Cache.get(name) - assert {:ok, %Flag{name: ^name, gates: [^bool_gate, ^group_gate]}} = @persistence.get(name) + assert {:ok, %Flag{name: ^name, gates: [^persisted_bool, ^persisted_group]}} = Cache.get(name) + assert {:ok, %Flag{name: ^name, gates: [^persisted_bool, ^persisted_group]}} = @persistence.get(name) Store.delete(name) assert {:ok, %Flag{name: ^name, gates: []}} = Cache.get(name) @@ -1111,10 +1142,13 @@ defmodule FunWithFlags.StoreTest do @persistence.put(name, gate) assert {:miss, :not_found, nil} = Cache.get(name) - assert {:ok, ^flag} = @persistence.get(name) + {:ok, persist_result} = @persistence.get(name) + assert drop_timestamps(persist_result) == flag - assert {:ok, ^flag} = Store.lookup(name) - assert {:ok, ^flag} = Cache.get(name) + {:ok, lookup_result} = Store.lookup(name) + assert drop_timestamps(lookup_result) == flag + {:ok, cache_result} = Cache.get(name) + assert drop_timestamps(cache_result) == flag end @@ -1131,19 +1165,25 @@ defmodule FunWithFlags.StoreTest do test "put() will change both the value stored in the Cache and in Redis", %{name: name, gate: gate, flag: flag} do - {:ok, ^flag} = @persistence.put(name, gate) - {:ok, ^flag} = Cache.put(flag) + {:ok, persisted_flag} = @persistence.put(name, gate) + assert drop_timestamps(persisted_flag) == flag + {:ok, _} = Cache.put(persisted_flag) - assert {:ok, ^flag} = Cache.get(name) - assert {:ok, ^flag} = @persistence.get(name) + {:ok, cache_result} = Cache.get(name) + assert drop_timestamps(cache_result) == flag + {:ok, persist_result} = @persistence.get(name) + assert drop_timestamps(persist_result) == flag gate2 = %Gate{gate | enabled: false} flag2 = %Flag{name: name, gates: [gate2]} - {:ok, ^flag2} = Store.put(name, gate2) + {:ok, result2} = Store.put(name, gate2) + assert drop_timestamps(result2) == flag2 - assert {:ok, ^flag2} = Cache.get(name) - assert {:ok, ^flag2} = @persistence.get(name) + {:ok, cache_result2} = Cache.get(name) + assert drop_timestamps(cache_result2) == flag2 + {:ok, persist_result2} = @persistence.get(name) + assert drop_timestamps(persist_result2) == flag2 end @@ -1152,15 +1192,21 @@ defmodule FunWithFlags.StoreTest do @persistence.put(name, gate) assert {:miss, :not_found, nil} = Cache.get(name) - assert {:ok, ^flag} = @persistence.get(name) + {:ok, persist_result} = @persistence.get(name) + assert drop_timestamps(persist_result) == flag - assert {:ok, ^flag} = Store.lookup(name) - assert {:ok, ^flag} = Cache.get(name) + {:ok, lookup_result} = Store.lookup(name) + assert drop_timestamps(lookup_result) == flag + {:ok, cache_result} = Cache.get(name) + assert drop_timestamps(cache_result) == flag timetravel by: (Config.cache_ttl + 1) do - assert {:miss, :expired, ^flag} = Cache.get(name) - assert {:ok, ^flag} = Store.lookup(name) - assert {:ok, ^flag} = Cache.get(name) + {:miss, :expired, expired_flag} = Cache.get(name) + assert drop_timestamps(expired_flag) == flag + {:ok, lookup_result2} = Store.lookup(name) + assert drop_timestamps(lookup_result2) == flag + {:ok, cache_result2} = Cache.get(name) + assert drop_timestamps(cache_result2) == flag end end end @@ -1183,7 +1229,8 @@ defmodule FunWithFlags.StoreTest do test "if the Cached value is expired, it will still be used", %{name: name, gate: gate, flag: flag} do @persistence.put(name, gate) - assert {:ok, ^flag} = Store.lookup(name) + {:ok, lookup_result} = Store.lookup(name) + assert drop_timestamps(lookup_result) == flag gate2 = %Gate{gate | enabled: false} flag2 = %Flag{name: name, gates: [gate2]} @@ -1204,7 +1251,8 @@ defmodule FunWithFlags.StoreTest do test "if there is no cached value, it raises an error", %{name: name, gate: gate, flag: flag} do @persistence.put(name, gate) - assert {:ok, ^flag} = Store.lookup(name) + {:ok, lookup_result} = Store.lookup(name) + assert drop_timestamps(lookup_result) == flag Cache.flush() assert {:miss, :not_found, nil} = Cache.get(name) @@ -1219,4 +1267,11 @@ defmodule FunWithFlags.StoreTest do end end + defp drop_timestamps(%Gate{} = gate) do + %{gate | inserted_at: nil, updated_at: nil} + end + + defp drop_timestamps(%Flag{} = flag) do + %{flag | last_modified_at: nil, gates: Enum.map(flag.gates, &drop_timestamps/1)} + end end diff --git a/test/fun_with_flags_test.exs b/test/fun_with_flags_test.exs index 40073873..093b599f 100644 --- a/test/fun_with_flags_test.exs +++ b/test/fun_with_flags_test.exs @@ -6,6 +6,9 @@ defmodule FunWithFlagsTest do @moduletag :integration doctest FunWithFlags + alias FunWithFlags.Flag + alias FunWithFlags.Gate + setup_all do on_exit(__MODULE__, fn() -> clear_test_db() @@ -781,13 +784,15 @@ defmodule FunWithFlagsTest do {:ok, result} = FunWithFlags.all_flags() assert 4 = length(result) + result_without_timestamps = Enum.map(result, &drop_timestamps/1) + for flag <- [ %Flag{name: name1, gates: [Gate.new(:boolean, true)]}, %Flag{name: name2, gates: [Gate.new(:boolean, false)]}, %Flag{name: name3, gates: [Gate.new(:actor, actor, true)]}, %Flag{name: name4, gates: [Gate.new(:percentage_of_time, 0.9)]}, ] do - assert flag in result + assert flag in result_without_timestamps end FunWithFlags.clear(name1) @@ -795,12 +800,14 @@ defmodule FunWithFlagsTest do {:ok, result} = FunWithFlags.all_flags() assert 3 = length(result) + result_without_timestamps = Enum.map(result, &drop_timestamps/1) + for flag <- [ %Flag{name: name2, gates: [Gate.new(:boolean, false)]}, %Flag{name: name3, gates: [Gate.new(:actor, actor, true)]}, %Flag{name: name4, gates: [Gate.new(:percentage_of_time, 0.9)]}, ] do - assert flag in result + assert flag in result_without_timestamps end FunWithFlags.clear(name4) @@ -808,11 +815,13 @@ defmodule FunWithFlagsTest do {:ok, result} = FunWithFlags.all_flags() assert 2 = length(result) + result_without_timestamps = Enum.map(result, &drop_timestamps/1) + for flag <- [ %Flag{name: name2, gates: [Gate.new(:boolean, false)]}, %Flag{name: name3, gates: [Gate.new(:actor, actor, true)]}, ] do - assert flag in result + assert flag in result_without_timestamps end end end @@ -889,11 +898,13 @@ defmodule FunWithFlagsTest do gates: [ Gate.new(:boolean, false), Gate.new(:group, "foobar", true), - Gate.new(:percentage_of_time, 0.75), + Gate.new(:percentage_of_time, 0.75) ] } - assert ^expected = FunWithFlags.get_flag(name) + actual = FunWithFlags.get_flag(name) + assert ^expected = drop_timestamps(actual) + assert %DateTime{} = actual.last_modified_at end @tag :telemetry @@ -965,4 +976,12 @@ defmodule FunWithFlagsTest do :telemetry.detach(ref) end end + + defp drop_timestamps(%Gate{} = gate) do + %{gate | inserted_at: nil, updated_at: nil} + end + + defp drop_timestamps(%Flag{} = flag) do + %{flag | last_modified_at: nil, gates: Enum.map(flag.gates, &drop_timestamps/1)} + end end