|
| 1 | +defmodule Localize.Ecto.TypeEdgeCaseTest do |
| 2 | + use ExUnit.Case, async: true |
| 3 | + |
| 4 | + alias Localize.Currency.Ecto.Type, as: Currency |
| 5 | + alias Localize.Duration.Ecto.Type, as: LocalizeDuration |
| 6 | + alias Localize.Ecto.Type.DateRange |
| 7 | + alias Localize.Ecto.Type.Duration, as: ElixirDuration |
| 8 | + alias Localize.Ecto.Type.IntegerRange |
| 9 | + alias Localize.LanguageTag.Ecto.Type, as: LanguageTag |
| 10 | + alias Localize.Script.Ecto.Type, as: Script |
| 11 | + alias Localize.Territory.Ecto.Type, as: Territory |
| 12 | + |
| 13 | + # Every type must treat nil as nil in all three directions, and refuse |
| 14 | + # a value of the wrong shape rather than raising. |
| 15 | + describe "nil handling" do |
| 16 | + test "casts, loads and dumps nil" do |
| 17 | + for type <- [Currency, Territory, Script, LanguageTag, LocalizeDuration, ElixirDuration] do |
| 18 | + assert type.cast(nil) == {:ok, nil}, "#{inspect(type)} cast" |
| 19 | + assert type.load(nil) == {:ok, nil}, "#{inspect(type)} load" |
| 20 | + assert type.dump(nil) == {:ok, nil}, "#{inspect(type)} dump" |
| 21 | + end |
| 22 | + end |
| 23 | + |
| 24 | + test "range types treat nil as nil" do |
| 25 | + for type <- [IntegerRange, DateRange] do |
| 26 | + assert type.cast(nil) == {:ok, nil}, "#{inspect(type)} cast" |
| 27 | + assert type.load(nil) == {:ok, nil}, "#{inspect(type)} load" |
| 28 | + assert type.dump(nil) == {:ok, nil}, "#{inspect(type)} dump" |
| 29 | + end |
| 30 | + end |
| 31 | + end |
| 32 | + |
| 33 | + describe "wrong-shaped values are refused, not raised" do |
| 34 | + test "code types refuse non-codes" do |
| 35 | + for type <- [Currency, Territory, Script] do |
| 36 | + assert type.cast(1.5) == :error, "#{inspect(type)} cast" |
| 37 | + assert type.load(42) == :error, "#{inspect(type)} load" |
| 38 | + assert type.dump(1.5) == :error, "#{inspect(type)} dump" |
| 39 | + end |
| 40 | + end |
| 41 | + |
| 42 | + test "language tag refuses a non-identifier" do |
| 43 | + assert LanguageTag.cast(42) == :error |
| 44 | + assert LanguageTag.load(42) == :error |
| 45 | + assert LanguageTag.dump(42) == :error |
| 46 | + end |
| 47 | + |
| 48 | + test "duration types refuse non-durations" do |
| 49 | + for type <- [LocalizeDuration, ElixirDuration] do |
| 50 | + assert type.cast(3600) == :error, "#{inspect(type)} cast" |
| 51 | + assert type.load(3600) == :error, "#{inspect(type)} load" |
| 52 | + assert type.dump(3600) == :error, "#{inspect(type)} dump" |
| 53 | + end |
| 54 | + end |
| 55 | + |
| 56 | + test "range types refuse non-ranges" do |
| 57 | + for type <- [IntegerRange, DateRange] do |
| 58 | + assert type.load("not a range") == :error, "#{inspect(type)} load" |
| 59 | + assert type.dump("not a range") == :error, "#{inspect(type)} dump" |
| 60 | + end |
| 61 | + end |
| 62 | + end |
| 63 | + |
| 64 | + describe "code types accept every documented input form" do |
| 65 | + test "currency accepts atom, string and struct" do |
| 66 | + assert Currency.cast(:USD) == {:ok, :USD} |
| 67 | + assert Currency.cast("usd") == {:ok, :USD} |
| 68 | + assert Currency.cast(%Localize.Currency{code: :EUR}) == {:ok, :EUR} |
| 69 | + assert Currency.dump(%Localize.Currency{code: :EUR}) == {:ok, "EUR"} |
| 70 | + end |
| 71 | + |
| 72 | + test "territory accepts atom and string" do |
| 73 | + assert Territory.cast(:GB) == {:ok, :GB} |
| 74 | + assert Territory.dump("gb") == {:ok, "GB"} |
| 75 | + end |
| 76 | + |
| 77 | + test "script accepts atom and string" do |
| 78 | + assert Script.cast(:Latn) == {:ok, :Latn} |
| 79 | + assert Script.dump("cyrl") == {:ok, "Cyrl"} |
| 80 | + end |
| 81 | + |
| 82 | + test "unknown codes are refused on dump as well as cast" do |
| 83 | + assert Currency.dump(:ZZZ) == :error |
| 84 | + assert Territory.dump(:ZZZZ) == :error |
| 85 | + assert Script.dump(:Xyzq) == :error |
| 86 | + end |
| 87 | + end |
| 88 | + |
| 89 | + describe "language tag" do |
| 90 | + test "dumps a locale given as a string or atom" do |
| 91 | + assert LanguageTag.dump("en-US") == {:ok, "en-US"} |
| 92 | + assert LanguageTag.dump(:"en-US") == {:ok, "en-US"} |
| 93 | + end |
| 94 | + |
| 95 | + test "dumping an invalid identifier fails" do |
| 96 | + assert LanguageTag.dump("this-is-not-a-locale") == :error |
| 97 | + end |
| 98 | + |
| 99 | + test "a tag passes through cast unchanged" do |
| 100 | + {:ok, tag} = Localize.validate_locale("de-DE") |
| 101 | + assert LanguageTag.cast(tag) == {:ok, tag} |
| 102 | + end |
| 103 | + end |
| 104 | + |
| 105 | + describe "durations" do |
| 106 | + test "an empty duration round trips" do |
| 107 | + {:ok, dumped} = LocalizeDuration.dump(%Localize.Duration{}) |
| 108 | + assert {:ok, %Localize.Duration{}} = LocalizeDuration.load(dumped) |
| 109 | + end |
| 110 | + |
| 111 | + test "an Elixir duration with only microseconds round trips" do |
| 112 | + duration = Duration.new!(microsecond: {500, 6}) |
| 113 | + {:ok, dumped} = ElixirDuration.dump(duration) |
| 114 | + {:ok, loaded} = ElixirDuration.load(dumped) |
| 115 | + |
| 116 | + assert loaded.microsecond == {500, 6} |
| 117 | + end |
| 118 | + |
| 119 | + test "both report interval as their database type" do |
| 120 | + assert LocalizeDuration.type() == :interval |
| 121 | + assert ElixirDuration.type() == :interval |
| 122 | + end |
| 123 | + end |
| 124 | + |
| 125 | + describe "ranges" do |
| 126 | + test "report their database types" do |
| 127 | + assert IntegerRange.type() == :int8range |
| 128 | + assert DateRange.type() == :daterange |
| 129 | + end |
| 130 | + |
| 131 | + test "an integer range round trips through its dumped form" do |
| 132 | + {:ok, dumped} = IntegerRange.dump(1..10) |
| 133 | + assert IntegerRange.load(dumped) == {:ok, 1..10} |
| 134 | + end |
| 135 | + |
| 136 | + test "a date range round trips through its dumped form" do |
| 137 | + range = Date.range(~D[2024-01-01], ~D[2024-01-31]) |
| 138 | + {:ok, dumped} = DateRange.dump(range) |
| 139 | + assert DateRange.load(dumped) == {:ok, range} |
| 140 | + end |
| 141 | + |
| 142 | + test "an unbounded date range has no Elixir equivalent" do |
| 143 | + assert DateRange.load(%Postgrex.Range{ |
| 144 | + lower: nil, |
| 145 | + upper: ~D[2024-12-31], |
| 146 | + lower_inclusive: false, |
| 147 | + upper_inclusive: true |
| 148 | + }) == :error |
| 149 | + end |
| 150 | + |
| 151 | + test "a descending date range is refused on dump" do |
| 152 | + assert DateRange.dump(Date.range(~D[2024-12-31], ~D[2024-01-01], -1)) == :error |
| 153 | + end |
| 154 | + end |
| 155 | +end |
0 commit comments