diff --git a/lib/core/structure/root.ex b/lib/core/structure/root.ex index f882490..16d72b9 100644 --- a/lib/core/structure/root.ex +++ b/lib/core/structure/root.ex @@ -24,9 +24,14 @@ defmodule Structure.Root do "{app_snake}/resources/cloud/Dockerfile-build" => @base <> "Dockerfile-build.txt", "{app_snake}/test/infrastructure/entry_points/api_rest_test.exs" => @base <> "api_rest_test.exs", + "{app_snake}/test/infrastructure/entry_points/health_check_test.exs" => + @base <> "health_check_test.exs", "{app_snake}/test/{app_snake}_application_test.exs" => @base <> "test_application.exs", "{app_snake}/test/test_helper.exs" => @base <> "test_helper.exs", - "{app_snake}/coveralls.json" => @base <> "coveralls.json" + "{app_snake}/coveralls.json" => @base <> "coveralls.json", + "{app_snake}/test/utils/custom_telemetry_test.exs" => + @base <> "custom_telemetry_test.exs", + "{app_snake}/test/utils/data_type_utils_test.exs" => @base <> "data_type_utils_test.exs" }, folders: [ "{app_snake}/lib/infrastructure/driven_adapters/", diff --git a/priv/templates/structure/api_rest.ex b/priv/templates/structure/api_rest.ex index 998a296..2d01b2c 100644 --- a/priv/templates/structure/api_rest.ex +++ b/priv/templates/structure/api_rest.ex @@ -1,5 +1,5 @@ defmodule {app}.Infrastructure.EntryPoint.ApiRest do - + @compile if Mix.env() == :test, do: :export_all @moduledoc """ Access point to the rest exposed services """ diff --git a/priv/templates/structure/api_rest_test.exs b/priv/templates/structure/api_rest_test.exs index dee1bd7..1007151 100644 --- a/priv/templates/structure/api_rest_test.exs +++ b/priv/templates/structure/api_rest_test.exs @@ -13,7 +13,8 @@ defmodule {app}.Infrastructure.EntryPoint.ApiRestTets do |> ApiRest.call(@opts) assert conn.state == :sent - assert conn.status in [200, 500] # TODO: Implement mocks correctly when needed + # TODO: Implement mocks correctly when needed + assert conn.status in [200, 500] end test "test Hello" do @@ -26,4 +27,56 @@ defmodule {app}.Infrastructure.EntryPoint.ApiRestTets do assert conn.status == 200 end + test "put_resp_content_type/2" do + conn = conn(:get, "/test_content_type") + conn = put_resp_content_type(conn, "application/json") + + assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"] + end + + test "test non-existent endpoint" do + conn = + :get + |> conn("/api/nonexistent", "") + |> ApiRest.call(@opts) + + assert conn.state == :sent + assert conn.status == 404 + end + + test "test POST on /api/health" do + conn = + :post + |> conn("/api/health", "") + |> ApiRest.call(@opts) + + assert conn.state == :sent + assert conn.status in [200, 500] + end + + describe "handle_not_found/2" do + test "returns 404 status with path in debug mode" do + Logger.configure(level: :debug) + + # Simulate a request to a nonexistent path + conn = conn(:get, "/nonexistent_path") + conn = ApiRest.call(conn, @opts) + + # Verify the response + assert conn.state == :sent + assert conn.status == 404 + assert conn.resp_body == Poison.encode!(%{status: 404, path: "/nonexistent_path"}) + end + + test "returns 404 status without body in non-debug mode" do + Logger.configure(level: :info) + + conn = conn(:get, "/test_not_found/info") + conn = ApiRest.call(conn, @opts) + + assert conn.state == :sent + assert conn.status == 404 + assert conn.resp_body == "" + end + end end diff --git a/priv/templates/structure/custom_telemetry_test.exs b/priv/templates/structure/custom_telemetry_test.exs new file mode 100644 index 0000000..1e24f10 --- /dev/null +++ b/priv/templates/structure/custom_telemetry_test.exs @@ -0,0 +1,153 @@ +defmodule {app}.Utils.CustomTelemetryTest do + alias {app}.Utils.CustomTelemetry + use ExUnit.Case + + setup do + :telemetry.detach("test-handler") + :ok + end + + test "execute_custom_event emits the event with correct metric, value, and metadata" do + event_name = [:elixir, :custom, :metric] + metric = [:custom, :metric] + value = 200 + metadata = %{source: "api"} + service_name = "{app_snake}_test" + + Application.put_env(:test, :service_name, service_name) + + :telemetry.attach( + "test-handler", + event_name, + fn _event_name, measurements, event_metadata, _config -> + send(self(), {:event_received, measurements, event_metadata}) + end, + nil + ) + + CustomTelemetry.execute_custom_event(metric, value, metadata) + + assert_receive {:event_received, %{duration: ^value}, + %{source: "api", service: ^service_name}} + + :telemetry.detach("test-handler") + end + + test "execute_custom_event adds service name to metadata" do + event_name = [:elixir, :another, :metric] + metric = [:another, :metric] + value = 100 + metadata = %{} + service_name = "{app_snake}_test" + + Application.put_env(:test, :service_name, service_name) + + :telemetry.attach( + "test-handler", + event_name, + fn _event_name, measurements, event_metadata, _config -> + send(self(), {:event_received, measurements, event_metadata}) + end, + nil + ) + + CustomTelemetry.execute_custom_event(metric, value, metadata) + + assert_receive {:event_received, %{duration: ^value}, %{service: ^service_name}} + + :telemetry.detach("test-handler") + end + + test "handle_custom_event emits the event with correct metric, measures, and metadata" do + event_name = [:elixir, :custom, :metric] + metric = [:custom, :metric] + measures = %{duration: 200} + metadata = %{source: "api"} + service_name = "{app_snake}_test" + + Application.put_env(:test, :service_name, service_name) + + :telemetry.attach( + "test-handler", + event_name, + fn _event_name, event_measures, event_metadata, _config -> + send(self(), {:event_received, event_measures, event_metadata}) + end, + nil + ) + + CustomTelemetry.handle_custom_event(metric, measures, metadata, nil) + + assert_receive {:event_received, ^measures, %{source: "api", service: ^service_name}} + + :telemetry.detach("test-handler") + end + + test "handle_custom_event adds service name to metadata" do + event_name = [:elixir, :another, :metric] + metric = [:another, :metric] + measures = %{count: 100} + metadata = %{} + service_name = "{app_snake}_test" + + Application.put_env(:{app_snake}, :service_name, service_name) + + :telemetry.attach( + "test-handler", + event_name, + fn _event_name, event_measures, event_metadata, _config -> + send(self(), {:event_received, event_measures, event_metadata}) + end, + nil + ) + + CustomTelemetry.handle_custom_event(metric, measures, metadata, nil) + + assert_receive {:event_received, ^measures, %{service: ^service_name}} + + :telemetry.detach("test-handler") + end + + test "execute_custom_event handles atom metric by delegating to list implementation" do + metric = :custom_event + value = 100 + metadata = %{source: "test_source"} + expected_metric_list = [metric] + + :telemetry.attach( + "test-handler", + [:elixir | expected_metric_list], + fn _event_name, measurements, event_metadata, _config -> + send(self(), {:event_received, measurements, event_metadata}) + end, + nil + ) + + CustomTelemetry.execute_custom_event(metric, value, metadata) + + assert_receive {:event_received, %{duration: ^value}, event_metadata} + + assert event_metadata[:source] == "test_source" + end + + test "execute_custom_event emits the correct event with default metadata" do + metric = [:custom, :event] + value = 200 + expected_metadata = %{service: "{app_snake}_test"} + + :telemetry.attach( + "test-handler-default-metadata", + [:elixir | metric], + fn _event_name, measurements, event_metadata, _config -> + send(self(), {:event_received, measurements, event_metadata}) + end, + nil + ) + + CustomTelemetry.execute_custom_event(metric, value) + + assert_receive {:event_received, %{duration: ^value}, event_metadata} + assert event_metadata == expected_metadata + end + +end diff --git a/priv/templates/structure/data_type_utils_test.exs b/priv/templates/structure/data_type_utils_test.exs new file mode 100644 index 0000000..e2e32fd --- /dev/null +++ b/priv/templates/structure/data_type_utils_test.exs @@ -0,0 +1,135 @@ +defmodule {app}.Utils.DataTypeUtilsTest do + alias {app}.Utils.DataTypeUtils + use ExUnit.Case + + describe "normalize/1" do + test "normalizes a struct" do + struct = %DateTime{ + year: 2023, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + time_zone: "Etc/UTC", + zone_abbr: "UTC", + utc_offset: 0, + std_offset: 0 + } + + assert DataTypeUtils.normalize(struct) == struct + end + + test "normalizes a map" do + map = %{"key" => "value"} + assert DataTypeUtils.normalize(map) == %{key: "value"} + end + + test "normalizes a list" do + list = [%{"key" => "value"}] + assert DataTypeUtils.normalize(list) == [%{key: "value"}] + end + end + + describe "base64_decode/1" do + test "decodes a base64 string" do + assert DataTypeUtils.base64_decode("SGVsbG8gd29ybGQ=") == "Hello world" + end + end + + describe "extract_header/2" do + test "returns {:ok, value} when the header is found and value is not nil" do + headers = [{"content-type", "application/json"}, {"authorization", "Bearer token"}] + assert DataTypeUtils.extract_header(headers, "content-type") == {:ok, "application/json"} + end + + test "returns {:error, :not_found} when the header is not found" do + headers = [{"content-type", "application/json"}, {"authorization", "Bearer token"}] + assert DataTypeUtils.extract_header(headers, "accept") == {:error, :not_found} + end + end + + describe "extract_header/2 with non-list headers" do + test "returns an error when headers is a map" do + headers = %{"content-type" => "application/json"} + + assert DataTypeUtils.extract_header(headers, "content-type") == + {:error, + "headers is not a list when finding \"content-type\": %{\"content-type\" => \"application/json\"}"} + end + + test "returns an error when headers is a string" do + headers = "content-type: application/json" + + assert DataTypeUtils.extract_header(headers, "content-type") == + {:error, + "headers is not a list when finding \"content-type\": \"content-type: application/json\""} + end + + test "returns an error when headers is a tuple" do + headers = {"content-type", "application/json"} + + assert DataTypeUtils.extract_header(headers, "content-type") == + {:error, + "headers is not a list when finding \"content-type\": {\"content-type\", \"application/json\"}"} + end + + test "returns an error when headers is nil" do + headers = nil + + assert DataTypeUtils.extract_header(headers, "content-type") == + {:error, "headers is not a list when finding \"content-type\": nil"} + end + + test "returns an error when headers is an integer" do + headers = 123 + + assert DataTypeUtils.extract_header(headers, "content-type") == + {:error, "headers is not a list when finding \"content-type\": 123"} + end + end + + describe "format/2" do + test "returns true when input is 'true' and type is 'boolean'" do + assert DataTypeUtils.format("true", "boolean") == true + end + + test "returns false when input is 'false' and type is 'boolean'" do + assert DataTypeUtils.format("false", "boolean") == false + end + end + + test "returns the input value when type is ignored" do + assert DataTypeUtils.format(123, "any_type") == 123 + end + + test "converts system time in nanoseconds to milliseconds" do + assert DataTypeUtils.system_time_to_milliseconds(1_000_000_000) == 1000 + end + + test "converts monotonic time in native units to milliseconds" do + assert DataTypeUtils.monotonic_time_to_milliseconds(1_000_000) == 1 + end + + test "returns different numbers on subsequent calls" do + {:ok, confirm_number1} = DataTypeUtils.create_confirm_number() + {:ok, confirm_number2} = DataTypeUtils.create_confirm_number() + + refute confirm_number1 == confirm_number2 + end + + test "returns the current monotonic time as an integer" do + start_time = DataTypeUtils.start_time() + + assert is_integer(start_time) + end + + test "calculates the duration time in milliseconds" do + start_time = System.monotonic_time() + :timer.sleep(100) + duration = DataTypeUtils.duration_time(start_time) + + assert is_integer(duration) + assert duration >= 100 + end +end diff --git a/priv/templates/structure/health_check_test.exs b/priv/templates/structure/health_check_test.exs new file mode 100644 index 0000000..f402d49 --- /dev/null +++ b/priv/templates/structure/health_check_test.exs @@ -0,0 +1,12 @@ +defmodule {app}.Infrastructure.EntryPoints.HealthCheckTest do + alias {app}.Infrastructure.EntryPoint.HealthCheck + + use ExUnit.Case + + describe "check_http/0" do + test "returns :ok" do + assert HealthCheck.check_http() == :ok + end + end + +end diff --git a/priv/templates/structure/test_application.exs b/priv/templates/structure/test_application.exs index d65398d..9b1a338 100644 --- a/priv/templates/structure/test_application.exs +++ b/priv/templates/structure/test_application.exs @@ -1,9 +1,34 @@ defmodule {app}.ApplicationTest do use ExUnit.Case doctest {app}.Application - alias {app}.Config.AppConfig + alias {app}.Config.{ConfigHolder, AppConfig} test "test childrens" do assert {app}.Application.env_children(:test, %AppConfig{}) == [] end + + setup do + if :ets.info(:{app_snake}_config) == :undefined do + :ets.new(:{app_snake}_config, [:public, :named_table, read_concurrency: true]) + end + + :ets.delete_all_objects(:{app_snake}_config) + :ok + end + + test "conf/0 returns the current config when it exists" do + config = %AppConfig{env: :test, enable_server: true, http_port: 8083} + + :ets.insert(:{app_snake}_config, {:config, config}) + + assert ConfigHolder.conf() == config + end + + test "get!/1 raises an error when the key does not exist" do + :ets.delete_all_objects(:{app_snake}_config) + + assert_raise RuntimeError, "Config with key :nonexistent_key not found", fn -> + ConfigHolder.get!(:nonexistent_key) + end + end end