From 531dfb4e844fa610edaea4f55fdbe1d7fae3217b Mon Sep 17 00:00:00 2001 From: njausteve Date: Sat, 5 Oct 2024 14:28:30 +0800 Subject: [PATCH 1/3] feat: Add validation for text alignment field in FieldContent struct --- lib/structs/field_content.ex | 39 ++++++++++--- lib/utils/validators.ex | 52 +++++++++++++++++ test/structs/field_content_test.exs | 89 +++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 9 deletions(-) diff --git a/lib/structs/field_content.ex b/lib/structs/field_content.ex index a5deb99..ceb8934 100644 --- a/lib/structs/field_content.ex +++ b/lib/structs/field_content.ex @@ -56,6 +56,13 @@ defmodule ExPass.Structs.FieldContent do * "PKNumberStyleScientific" * "PKNumberStyleSpellOut" + - `text_alignment`: The alignment of the text in the field. + Supported values are: + * "PKTextAlignmentLeft" + * "PKTextAlignmentCenter" + * "PKTextAlignmentRight" + * "PKTextAlignmentNatural" + - `value`: The value to use for the field. This can be a localizable string, ISO 8601 date, or number. This field is required. A date or time value must include a time zone. """ @@ -129,6 +136,17 @@ defmodule ExPass.Structs.FieldContent do """ @type number_style() :: String.t() + @typedoc """ + The alignment of the text in the field. + + Optional. Valid values are: + - "PKTextAlignmentLeft" + - "PKTextAlignmentCenter" + - "PKTextAlignmentRight" + - "PKTextAlignmentNatural" + """ + @type text_alignment() :: String.t() + @typedoc """ The value to use for the field. @@ -152,6 +170,7 @@ defmodule ExPass.Structs.FieldContent do field :key, String.t(), enforce: true field :label, String.t(), default: nil field :number_style, number_style(), default: nil + field :text_alignment, text_alignment(), default: nil field :value, value(), enforce: true end @@ -170,6 +189,7 @@ defmodule ExPass.Structs.FieldContent do • key • label • number_style + • text_alignment • value ## Parameters @@ -187,22 +207,22 @@ defmodule ExPass.Structs.FieldContent do ## Examples iex> FieldContent.new(%{key: "field1", value: "Hello, World!"}) - %FieldContent{key: "field1", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: nil, date_style: nil, ignores_time_zone: nil, is_relative: nil, label: nil, number_style: nil, value: "Hello, World!"} + %FieldContent{key: "field1", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: nil, date_style: nil, ignores_time_zone: nil, is_relative: nil, label: nil, number_style: nil, text_alignment: nil, value: "Hello, World!"} - iex> FieldContent.new(%{key: "field2", value: 42, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false, number_style: "PKNumberStyleDecimal"}) - %FieldContent{key: "field2", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false, label: nil, number_style: "PKNumberStyleDecimal", value: 42} + iex> FieldContent.new(%{key: "field2", value: 42, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false, number_style: "PKNumberStyleDecimal", text_alignment: "PKTextAlignmentCenter"}) + %FieldContent{key: "field2", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false, label: nil, number_style: "PKNumberStyleDecimal", text_alignment: "PKTextAlignmentCenter", value: 42} iex> datetime = ~U[2023-04-15 14:30:00Z] - iex> field_content = FieldContent.new(%{key: "field3", value: datetime, currency_code: "USD", date_style: "PKDateStyleLong", ignores_time_zone: true, is_relative: true}) - iex> %FieldContent{key: "field3", value: ^datetime, currency_code: "USD", date_style: "PKDateStyleLong", ignores_time_zone: true, is_relative: true} = field_content + iex> field_content = FieldContent.new(%{key: "field3", value: datetime, currency_code: "USD", date_style: "PKDateStyleLong", ignores_time_zone: true, is_relative: true, text_alignment: "PKTextAlignmentRight"}) + iex> %FieldContent{key: "field3", value: ^datetime, currency_code: "USD", date_style: "PKDateStyleLong", ignores_time_zone: true, is_relative: true, text_alignment: "PKTextAlignmentRight"} = field_content iex> field_content.change_message nil - iex> FieldContent.new(%{key: "field4", value: "Click here", data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", is_relative: false, number_style: "PKNumberStylePercent"}) - %FieldContent{key: "field4", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", ignores_time_zone: nil, is_relative: false, label: nil, number_style: "PKNumberStylePercent", value: "Click here"} + iex> FieldContent.new(%{key: "field4", value: "Click here", data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", is_relative: false, number_style: "PKNumberStylePercent", text_alignment: "PKTextAlignmentLeft"}) + %FieldContent{key: "field4", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", ignores_time_zone: nil, is_relative: false, label: nil, number_style: "PKNumberStylePercent", text_alignment: "PKTextAlignmentLeft", value: "Click here"} - iex> FieldContent.new(%{key: "field5", value: "No detectors", data_detector_types: [], change_message: "Updated to %@", ignores_time_zone: true, is_relative: true, label: "Field Label", number_style: "PKNumberStyleScientific"}) - %FieldContent{key: "field5", attributed_value: nil, change_message: "Updated to %@", currency_code: nil, data_detector_types: [], date_style: nil, ignores_time_zone: true, is_relative: true, label: "Field Label", number_style: "PKNumberStyleScientific", value: "No detectors"} + iex> FieldContent.new(%{key: "field5", value: "No detectors", data_detector_types: [], change_message: "Updated to %@", ignores_time_zone: true, is_relative: true, label: "Field Label", number_style: "PKNumberStyleScientific", text_alignment: "PKTextAlignmentNatural"}) + %FieldContent{key: "field5", attributed_value: nil, change_message: "Updated to %@", currency_code: nil, data_detector_types: [], date_style: nil, ignores_time_zone: true, is_relative: true, label: "Field Label", number_style: "PKNumberStyleScientific", text_alignment: "PKTextAlignmentNatural", value: "No detectors"} """ @spec new(map()) :: %__MODULE__{} def new(attrs \\ %{}) do @@ -219,6 +239,7 @@ defmodule ExPass.Structs.FieldContent do |> validate(:key, &Validators.validate_required_string(&1, :key)) |> validate(:label, &Validators.validate_optional_string(&1, :label)) |> validate(:number_style, &Validators.validate_number_style/1) + |> validate(:text_alignment, &Validators.validate_text_alignment/1) |> validate(:value, &Validators.validate_required_value(&1, :value)) struct!(__MODULE__, attrs) diff --git a/lib/utils/validators.ex b/lib/utils/validators.ex index 6c2751e..e6d9b34 100644 --- a/lib/utils/validators.ex +++ b/lib/utils/validators.ex @@ -33,6 +33,13 @@ defmodule ExPass.Utils.Validators do "PKNumberStyleSpellOut" ] + @valid_text_alignments [ + "PKTextAlignmentLeft", + "PKTextAlignmentCenter", + "PKTextAlignmentRight", + "PKTextAlignmentNatural" + ] + @doc """ Validates the type of the attributed value. @@ -488,6 +495,51 @@ defmodule ExPass.Utils.Validators do def validate_required_value(_value, field_name), do: {:error, "#{field_name} must be a string, number, DateTime, or Date"} + @doc """ + Validates the text alignment value. + + This function checks if the given value is a valid text alignment option. + Valid options are "PKTextAlignmentLeft", "PKTextAlignmentCenter", "PKTextAlignmentRight", and "PKTextAlignmentNatural". + + ## Parameters + + * `value` - The text alignment value to validate. + + ## Returns + + * `:ok` if the value is valid. + * `{:error, message}` if the value is invalid, where `message` is a string explaining the error. + + ## Examples + + iex> validate_text_alignment("PKTextAlignmentLeft") + :ok + + iex> validate_text_alignment("PKTextAlignmentCenter") + :ok + + iex> validate_text_alignment("PKTextAlignmentRight") + :ok + + iex> validate_text_alignment("PKTextAlignmentNatural") + :ok + + iex> validate_text_alignment("InvalidAlignment") + {:error, "Invalid text_alignment. Supported values are PKTextAlignmentLeft, PKTextAlignmentCenter, PKTextAlignmentRight, PKTextAlignmentNatural"} + + iex> validate_text_alignment(nil) + :ok + + """ + @spec validate_text_alignment(String.t() | nil) :: :ok | {:error, String.t()} + def validate_text_alignment(nil), do: :ok + def validate_text_alignment(value) when value in @valid_text_alignments, do: :ok + + def validate_text_alignment(_), + do: + {:error, + "Invalid text_alignment. Supported values are #{Enum.join(@valid_text_alignments, ", ")}"} + defp contains_unsupported_html_tags?(string) do # Remove all valid anchor tags string_without_anchors = String.replace(string, ~r{]*>.*?|]*/>}, "") diff --git a/test/structs/field_content_test.exs b/test/structs/field_content_test.exs index e0531bc..30eddf7 100644 --- a/test/structs/field_content_test.exs +++ b/test/structs/field_content_test.exs @@ -6,6 +6,25 @@ defmodule ExPass.Structs.FieldContentTest do doctest FieldContent + describe "new/1" do + test "creates a new FieldContent with default empty map" do + assert_raise ArgumentError, ~r/The :key field is required/, fn -> + FieldContent.new() + end + end + + test "creates a new FieldContent with minimum required fields" do + field_content = FieldContent.new(%{key: "test_key", value: "test_value"}) + assert %FieldContent{key: "test_key", value: "test_value"} = field_content + end + + test "raises ArgumentError for invalid attributes" do + assert_raise ArgumentError, ~r/Invalid data_detector_types/, fn -> + FieldContent.new(%{key: "test", value: "test", data_detector_types: ["InvalidType"]}) + end + end + end + describe "change_message" do test "new/1 raises ArgumentError for invalid change_message without '%@' placeholder" do message = "Balance updated" @@ -46,6 +65,24 @@ defmodule ExPass.Structs.FieldContentTest do assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("changeMessage":"Trimmed message %@") end + + test "validate_change_message/1 returns error for invalid change_message" do + invalid_message = "Invalid message without placeholder" + + assert {:error, + "The change_message must be a string containing the '%@' placeholder for the new value."} == + ExPass.Utils.Validators.validate_change_message(invalid_message) + end + + test "validate_change_message/1 returns error when change_message is not a string" do + invalid_types = [42, :atom, [], %{}] + + for invalid_type <- invalid_types do + assert {:error, + "The change_message must be a string containing the '%@' placeholder for the new value."} == + ExPass.Utils.Validators.validate_change_message(invalid_type) + end + end end describe "attributed_value" do @@ -547,6 +584,16 @@ defmodule ExPass.Structs.FieldContentTest do assert encoded =~ ~s("numberStyle":"#{style}") end) end + + test "new/1 raises ArgumentError when number_style is not a string" do + assert_raise ArgumentError, ~r/number_style must be a string/, fn -> + FieldContent.new(%{ + key: "test_key", + value: "test_value", + number_style: :PKNumberStyleDecimal + }) + end + end end describe "value" do @@ -590,4 +637,46 @@ defmodule ExPass.Structs.FieldContentTest do end end end + + describe "text_alignment" do + test "new/1 creates a valid FieldContent struct with text_alignment" do + alignments = [ + "PKTextAlignmentLeft", + "PKTextAlignmentCenter", + "PKTextAlignmentRight", + "PKTextAlignmentNatural" + ] + + Enum.each(alignments, fn alignment -> + result = + FieldContent.new(%{key: "test_key", value: "test_value", text_alignment: alignment}) + + assert %FieldContent{key: "test_key", value: "test_value", text_alignment: ^alignment} = + result + + encoded = Jason.encode!(result) + assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") + assert encoded =~ ~s("textAlignment":"#{alignment}") + end) + end + + test "new/1 raises ArgumentError for invalid text_alignment" do + assert_raise ArgumentError, ~r/Invalid text_alignment/, fn -> + FieldContent.new(%{ + key: "test_key", + value: "test_value", + text_alignment: "InvalidAlignment" + }) + end + end + + test "new/1 allows text_alignment to be nil" do + result = FieldContent.new(%{key: "test_key", value: "test_value", text_alignment: nil}) + + assert %FieldContent{key: "test_key", value: "test_value", text_alignment: nil} = result + encoded = Jason.encode!(result) + refute encoded =~ "textAlignment" + end + end end From 623a51cb9086cf4ffed092a277b670d9d4098730 Mon Sep 17 00:00:00 2001 From: njausteve Date: Sat, 5 Oct 2024 14:34:21 +0800 Subject: [PATCH 2/3] feat: Improve validation and error handling in FieldContent struct --- test/structs/field_content_test.exs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/structs/field_content_test.exs b/test/structs/field_content_test.exs index 30eddf7..9b3fe79 100644 --- a/test/structs/field_content_test.exs +++ b/test/structs/field_content_test.exs @@ -7,8 +7,8 @@ defmodule ExPass.Structs.FieldContentTest do doctest FieldContent describe "new/1" do - test "creates a new FieldContent with default empty map" do - assert_raise ArgumentError, ~r/The :key field is required/, fn -> + test "new/1 raises ArgumentError when called with no arguments" do + assert_raise ArgumentError, "key is a required field and must be a non-empty string", fn -> FieldContent.new() end end @@ -19,9 +19,15 @@ defmodule ExPass.Structs.FieldContentTest do end test "raises ArgumentError for invalid attributes" do - assert_raise ArgumentError, ~r/Invalid data_detector_types/, fn -> - FieldContent.new(%{key: "test", value: "test", data_detector_types: ["InvalidType"]}) - end + assert_raise ArgumentError, + "Invalid data detector type: InvalidType. Supported types are: PKDataDetectorTypePhoneNumber, PKDataDetectorTypeLink, PKDataDetectorTypeAddress, PKDataDetectorTypeCalendarEvent", + fn -> + FieldContent.new(%{ + key: "test", + value: "test", + data_detector_types: ["InvalidType"] + }) + end end end From 862c4db0719f61c6d8fb4462cd6f5f3729993d2e Mon Sep 17 00:00:00 2001 From: njausteve Date: Sat, 5 Oct 2024 14:34:42 +0800 Subject: [PATCH 3/3] chore: Update mix.exs to include test coverage threshold --- mix.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 9bbeabe..bff55f7 100644 --- a/mix.exs +++ b/mix.exs @@ -8,7 +8,8 @@ defmodule ExPass.MixProject do elixir: "~> 1.16", config_path: "./config/config.exs", start_permanent: Mix.env() == :prod, - deps: deps() + deps: deps(), + test_coverage: [threshold: 97.56] ] end