diff --git a/test/default_session_registry_test.exs b/test/default_session_registry_test.exs new file mode 100644 index 0000000..ead516c --- /dev/null +++ b/test/default_session_registry_test.exs @@ -0,0 +1,42 @@ +defmodule ExFix.DefaultSessionRegistryTest do + use ExUnit.Case + + alias ExFix.DefaultSessionRegistry + + test "session_on_init replies according to status" do + table = :ex_fix_registry + :ets.delete_all_objects(table) + + assert {:error, :notfound} = DefaultSessionRegistry.session_on_init("s1") + + DefaultSessionRegistry.session_update_status("s2", :connecting) + assert :ok = DefaultSessionRegistry.session_on_init("s2") + + DefaultSessionRegistry.session_update_status("s3", :connected) + assert :wait_to_reconnect = DefaultSessionRegistry.session_on_init("s3") + + DefaultSessionRegistry.session_update_status("s4", :disconnecting) + assert {:error, :disconnected} = DefaultSessionRegistry.session_on_init("s4") + end + + test "handles process DOWN messages" do + table = :ex_fix_registry + :ets.delete_all_objects(table) + + DefaultSessionRegistry.session_update_status("dn1", :connecting) + :ok = DefaultSessionRegistry.session_on_init("dn1") + state = :sys.get_state(DefaultSessionRegistry) + ref1 = Enum.find_value(state.monitor_map, fn {ref, name} -> if name == "dn1", do: ref end) + send(DefaultSessionRegistry, {:DOWN, ref1, :process, self(), :normal}) + Process.sleep(10) + assert DefaultSessionRegistry.get_session_status("dn1") == :disconnected + + DefaultSessionRegistry.session_update_status("dn2", :connecting) + :ok = DefaultSessionRegistry.session_on_init("dn2") + state = :sys.get_state(DefaultSessionRegistry) + ref2 = Enum.find_value(state.monitor_map, fn {ref, name} -> if name == "dn2", do: ref end) + send(DefaultSessionRegistry, {:DOWN, ref2, :process, self(), :shutdown}) + Process.sleep(10) + assert DefaultSessionRegistry.get_session_status("dn2") == :reconnecting + end +end diff --git a/test/ex_fix_api_test.exs b/test/ex_fix_api_test.exs new file mode 100644 index 0000000..4e44d70 --- /dev/null +++ b/test/ex_fix_api_test.exs @@ -0,0 +1,57 @@ +defmodule ExFix.ApiTest do + use ExUnit.Case + + alias ExFix.SessionConfig + alias ExFix.TestHelper.FixEmptySessionHandler + + defmodule CaptureRegistry do + @behaviour ExFix.SessionRegistry + + def get_session_status(_), do: :disconnected + + def start_session(name, %SessionConfig{} = config) do + send(self(), {:start_session, name, config}) + :ok + end + + def stop_session(name) do + send(self(), {:stop_session, name}) + :ok + end + + def session_on_init(_), do: :ok + def session_update_status(_, _), do: :ok + end + + test "start_session_initiator builds config and delegates" do + ExFix.start_session_initiator( + "test_sess", + "S", + "T", + FixEmptySessionHandler, + session_registry: CaptureRegistry, + hostname: "myhost", + port: 12_345, + log_incoming_msg: false + ) + + assert_receive {:start_session, "test_sess", config} + assert %SessionConfig{name: "test_sess", sender_comp_id: "S", target_comp_id: "T"} = config + assert config.hostname == "myhost" + assert config.port == 12_345 + assert config.log_incoming_msg == false + + ExFix.stop_session("test_sess", CaptureRegistry) + assert_receive {:stop_session, "test_sess"} + end + + test "start_session_initiator uses defaults" do + ExFix.start_session_initiator("defaults", "S", "T", FixEmptySessionHandler, session_registry: CaptureRegistry) + + assert_receive {:start_session, "defaults", config} + assert config.hostname == "localhost" + assert config.port == 9876 + assert config.log_incoming_msg == true + assert config.transport_mod == :gen_tcp + end +end diff --git a/test/in_message_test.exs b/test/in_message_test.exs index 8dc1713..e0c8d0c 100644 --- a/test/in_message_test.exs +++ b/test/in_message_test.exs @@ -18,11 +18,14 @@ defmodule ExFix.InMessageTest do %{bin_msg: bin_msg} end - test "InMessage tests", %{bin_msg: bin_msg} do + test "get_field returns value", %{bin_msg: bin_msg} do fix_msg = Parser.parse1(bin_msg, Dictionary, 12_345) assert InMessage.get_field(fix_msg, "49") == "MARKET" assert InMessage.get_field(fix_msg, "52") == "20161007-16:28:50.802" + end - # assert InMessage.get_field(fix_msg, "SendingTime", TestDict) == Calendar.DateTime.Parse.rfc3339_utc + test "get_field returns nil for missing field", %{bin_msg: bin_msg} do + fix_msg = Parser.parse1(bin_msg, Dictionary, 12_345) + assert InMessage.get_field(fix_msg, "9999") == nil end end diff --git a/test/parser_test.exs b/test/parser_test.exs index 10052ad..2e41c00 100644 --- a/test/parser_test.exs +++ b/test/parser_test.exs @@ -124,6 +124,23 @@ defmodule ExFix.ParserTest do assert fix_msg.original_fix_msg == data end + test "Parse message with invalid begin string" do + data = msg("8=FIX4.2|9=12|10=000|") + fix_msg = Parser.parse1(data, Dictionary, 1) + assert fix_msg.valid == false + assert fix_msg.error_reason == :begin_string_error + assert fix_msg.original_fix_msg == data + end + + test "Parse message with unexpected seqnum" do + now = DateTime.from_naive!(~N[2017-07-17 17:50:56], "Etc/UTC") + data = build_message("0", 10, "SENDER", "TARGET", now) + fix_msg = Parser.parse1(data, Dictionary, 5) + assert fix_msg.valid == false + assert fix_msg.error_reason == :unexpected_seqnum + assert fix_msg.seqnum == 10 + end + test "Parse message - stage 1 - subject with 2 fields" do data = msg( diff --git a/test/session_sup_test.exs b/test/session_sup_test.exs new file mode 100644 index 0000000..55cf373 --- /dev/null +++ b/test/session_sup_test.exs @@ -0,0 +1,14 @@ +defmodule ExFix.SessionSupTest do + use ExUnit.Case + + test "start_link when already started" do + assert {:error, {:already_started, _}} = ExFix.SessionSup.start_link([]) + end + + test "dynamic supervisor can start child" do + {:ok, pid} = DynamicSupervisor.start_child(ExFix.SessionSup, {Task, fn -> :timer.sleep(10) end}) + assert is_pid(pid) + ref = Process.monitor(pid) + assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 50 + end +end diff --git a/test/session_worker_failure_test.exs b/test/session_worker_failure_test.exs new file mode 100644 index 0000000..60c3bce --- /dev/null +++ b/test/session_worker_failure_test.exs @@ -0,0 +1,36 @@ +defmodule ExFix.SessionWorkerFailureTest do + use ExUnit.Case + + alias ExFix.SessionConfig + alias ExFix.TestHelper.FixEmptySessionHandler + + defmodule FailTransport do + def connect(_host, _port, _opts), do: {:error, :econnrefused} + def send(_conn, _data), do: :ok + def close(_conn), do: :ok + end + + alias ExFix.TestHelper.TestSessionRegistry + + test "worker reports reconnecting on connection failure" do + Process.flag(:trap_exit, true) + {:ok, _} = TestSessionRegistry.start_link() + config = %SessionConfig{ + name: "fail1", + mode: :initiator, + sender_comp_id: "S", + target_comp_id: "T", + session_handler: FixEmptySessionHandler, + transport_mod: FailTransport, + transport_options: [], + log_incoming_msg: false, + log_outgoing_msg: false, + reconnect_interval: 0 + } + + {:ok, pid} = ExFix.SessionWorker.start_link(config, TestSessionRegistry) + ref = Process.monitor(pid) + assert_receive {:DOWN, ^ref, :process, ^pid, :econnrefused} + assert TestSessionRegistry.get_session_status("fail1") == :reconnecting + end +end diff --git a/test/session_worker_test.exs b/test/session_worker_test.exs index 0f463b3..d15ccd2 100644 --- a/test/session_worker_test.exs +++ b/test/session_worker_test.exs @@ -145,4 +145,65 @@ defmodule ExFix.SessionWorkerTest do SessionWorker.stop("sessiontest1") assert TestSessionRegistry.get_session_status("sessiontest1") == :disconnected end + + test "higher seqnum triggers resend request" do + + cfg = %SessionConfig{ + name: "resend_sess", + mode: :initiator, + sender_comp_id: "SENDER", + target_comp_id: "TARGET", + session_handler: FixEmptySessionHandler, + dictionary: DefaultDictionary, + transport_mod: TestTransport, + transport_options: [test_pid: self()] + } + + {:ok, _pid} = SessionWorker.start_link(cfg, TestSessionRegistry) + assert_receive {:data, _logon_msg} + + now = DateTime.utc_now() + logon = %MessageToSend{seqnum: 1, sender: "TARGET", orig_sending_time: now, target: "SENDER", msg_type: "A", body: []} + TestTransport.receive_data("resend_sess", Serializer.serialize(logon, now)) + Process.sleep(20) + + msg = %MessageToSend{seqnum: 12, sender: "TARGET", orig_sending_time: now, target: "SENDER", msg_type: "8", body: []} + TestTransport.receive_data("resend_sess", Serializer.serialize(msg, now)) + + assert_receive {:data, resend} + parsed = Parser.parse(resend, DefaultDictionary, 1) + assert parsed.msg_type == "2" + SessionWorker.stop("resend_sess") + end + + test "invalid SenderCompID triggers logout" do + cfg = %SessionConfig{ + name: "badcomp", + mode: :initiator, + sender_comp_id: "SENDER", + target_comp_id: "TARGET", + session_handler: FixEmptySessionHandler, + dictionary: DefaultDictionary, + transport_mod: TestTransport, + transport_options: [test_pid: self()] + } + + {:ok, pid} = SessionWorker.start_link(cfg, TestSessionRegistry) + ref = Process.monitor(pid) + assert_receive {:data, _} + + now = DateTime.utc_now() + logon = %MessageToSend{seqnum: 1, sender: "TARGET", orig_sending_time: now, target: "SENDER", msg_type: "A", body: []} + TestTransport.receive_data("badcomp", Serializer.serialize(logon, now)) + Process.sleep(20) + + msg = %MessageToSend{seqnum: 2, sender: "OTHER", orig_sending_time: now, target: "SENDER", msg_type: "8", body: []} + TestTransport.receive_data("badcomp", Serializer.serialize(msg, now)) + + assert_receive {:data, reject} + assert_receive {:data, logout} + assert Parser.parse(reject, DefaultDictionary, 1).msg_type == "3" + assert Parser.parse(logout, DefaultDictionary, 1).msg_type == "5" + assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 1000 + end end