diff --git a/README.md b/README.md index 6a1e4ac..bdf8e01 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,15 @@ config :ex_ftp, }, # See "Choose a Storage Connector" storage_connector: ExFTP.Storage.FileConnector, - storage_config: %{} + storage_config: %{ + # Optional: Handler for file transfer completion notifications + # If not configured, logs "Transfer complete: " by default + # Called as: module.function(type, path, connector_state) + # - type: :retrieve (download) or :store (upload) + # - path: the file path + # - connector_state: contains current_working_directory and authenticator_state + on_transfer_complete: {MyApp.TransferHandler, :handle_complete} + } ``` @@ -446,7 +454,33 @@ This is the out-of-the-box behavior you'd expect from any FTP server. config :ex_ftp, #.... storage_connector: ExFTP.Storage.FileConnector, - storage_config: %{} + storage_config: %{ + # Optional: Handler for transfer completion notifications + # Default: Logs "Transfer complete: " + on_transfer_complete: {MyApp.TransferHandler, :handle_complete} + } +``` + +**Example Handler:** + +```elixir +defmodule MyApp.TransferHandler do + require Logger + + def handle_complete(type, path, connector_state) do + # type is :retrieve or :store + # path is the file path + # connector_state contains authenticator_state with username and other metadata + username = connector_state.authenticator_state.username + Logger.info("User #{username} #{type}: #{path}") + + # Trigger custom workflows + case type do + :store -> MyApp.notify_upload(username, path) + :retrieve -> MyApp.track_download(username, path) + end + end +end ``` [^ top](#top) @@ -471,7 +505,9 @@ config :ex_ftp, storage_connector: ExFTP.Storage.S3Connector, storage_config: %{ # the `/` path of the FTP server will point to s3://{my-storage-bucket}/ - storage_bucket: "my-storage-bucket" + storage_bucket: "my-storage-bucket", + # Optional: Handler for transfer completion notifications + on_transfer_complete: {MyApp.TransferHandler, :handle_complete} } ``` @@ -513,7 +549,9 @@ config :ex_ftp, storage_connector: ExFTP.Storage.S3Connector, storage_config: %{ # the `/` path of the FTP server will point to s3://{my-storage-bucket}/ - storage_bucket: "my-storage-bucket" + storage_bucket: "my-storage-bucket", + # Optional: Handler for transfer completion notifications + on_transfer_complete: {MyApp.TransferHandler, :handle_complete} } ``` @@ -635,7 +673,7 @@ defmodule MyStorageConnector do ) :: function() def create_write_func(path, connector_state, opts \\ []) do # Return a function that will write `stream` to your storage at path - # e.g + # e.g # fn stream -> # fs = File.stream!(path) # diff --git a/lib/ex_ftp/storage/common.ex b/lib/ex_ftp/storage/common.ex index acdeae3..246d717 100644 --- a/lib/ex_ftp/storage/common.ex +++ b/lib/ex_ftp/storage/common.ex @@ -16,6 +16,8 @@ defmodule ExFTP.Storage.Common do alias ExFTP.PassiveSocket + require Logger + @directory_action_ok 257 @directory_action_not_taken 521 @file_action_ok 250 @@ -398,6 +400,7 @@ defmodule ExFTP.Storage.Common do {:ok, stream} -> PassiveSocket.write(pasv, stream, close_after_write: true) send_resp(@closing_connection_success, "Transfer complete.", socket) + notify_transfer_complete(:retrieve, w_path, connector_state) _ -> send_resp(@action_aborted, "File not found.", socket) @@ -508,6 +511,7 @@ defmodule ExFTP.Storage.Common do ) ExFTP.Common.send_resp(@closing_connection_success, "Transfer Complete.", socket) + notify_transfer_complete(:store, w_path, connector_state) connector_state end @@ -693,4 +697,14 @@ defmodule ExFTP.Storage.Common do {key, val} end) end + + defp notify_transfer_complete(type, path, connector_state) do + case connector_state[:on_transfer_complete] do + {module, function} when is_atom(module) and is_atom(function) -> + apply(module, function, [type, path, connector_state]) + + _ -> + Logger.info("Transfer complete: #{type} #{path}") + end + end end diff --git a/lib/ex_ftp/storage/s3_connector.ex b/lib/ex_ftp/storage/s3_connector.ex index 874b9bf..dd335ff 100644 --- a/lib/ex_ftp/storage/s3_connector.ex +++ b/lib/ex_ftp/storage/s3_connector.ex @@ -287,7 +287,7 @@ defmodule ExFTP.Storage.S3Connector do * **path** :: `t:ExFTP.StorageConnector.path/0` * **connector_state** :: `t:ExFTP.StorageConnector.connector_state/0` - #{ExFTP.Doc.returns(success: "{:ok, data}", failure: "{:error, err}")} + #{ExFTP.Doc.returns(success: "{:ok, data}", failure: "{:error, err}")} #{ExFTP.Doc.related(["`c:ExFTP.StorageConnector.get_content/2`"])} diff --git a/lib/ex_ftp/worker.ex b/lib/ex_ftp/worker.ex index ebcaf57..7c8fc9c 100644 --- a/lib/ex_ftp/worker.ex +++ b/lib/ex_ftp/worker.ex @@ -35,6 +35,8 @@ defmodule ExFTP.Worker do connector = env[:storage_connector] || FileConnector authenticator = env[:authenticator] || PassthroughAuth server_name = env[:server_name] || :ExFTP + storage_config = env[:storage_config] || %{} + on_transfer_complete = storage_config[:on_transfer_complete] {:ok, host} = ftp_addr @@ -49,13 +51,22 @@ defmodule ExFTP.Worker do send_resp(220, "Hello from #{server_name}.", socket) + connector_state = %{current_working_directory: "/"} + + connector_state = + if on_transfer_complete do + Map.put(connector_state, :on_transfer_complete, on_transfer_complete) + else + connector_state + end + %Worker{ socket: socket, host: host, pasv_socket: nil, type: :ascii, storage_connector: connector, - connector_state: %{current_working_directory: "/"}, + connector_state: connector_state, authenticator: authenticator, authenticator_state: %{} } diff --git a/mix.exs b/mix.exs index f489639..e4b09f3 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,7 @@ defmodule ExFTP.MixProject do alias ExFTP.Storage.S3ConnectorConfig @source_url "https://github.com/camatcode/ex_ftp" - @version "1.2.0" + @version "1.3.0" def project do [ diff --git a/mix.lock b/mix.lock index 3053627..b8b700e 100644 --- a/mix.lock +++ b/mix.lock @@ -3,14 +3,14 @@ "cachex": {:hex, :cachex, "4.1.1", "574c5cd28473db313a0a76aac8c945fe44191659538ca6a1e8946ec300b1a19f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d6b7449ff98d6bb92dda58bd4fc3189cae9f99e7042054d669596f56dc503cd8"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "configparser_ex": {:hex, :configparser_ex, "4.0.0", "17e2b831cfa33a08c56effc610339b2986f0d82a9caa0ed18880a07658292ab6", [:mix], [], "hexpm", "02e6d1a559361a063cba7b75bc3eb2d6ad7e62730c551cc4703541fd11e65e5b"}, - "credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, + "credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "ex_aws": {:hex, :ex_aws, "2.5.9", "8e2455172f0e5cbe2f56dd68de514f0dae6bb26d6b6e2f435a06434cf9dbb412", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbdb6ffb0e6c6368de05ed8641fe1376298ba23354674428e5b153a541f23359"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"}, - "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.9", "862b7792f2e60d7010e2920d79964e3fab289bc0fd951b0ba8457a3f7f9d1199", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "a480d2bb2da64610014021629800e1e9457ca5e4a62f6775bffd963360c2bf90"}, + "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, "ex_license": {:hex, :ex_license, "0.1.1", "bbdaba704f861894da3ae80e4399984379f19de1cca4ecf7d799616c832b8821", [:mix], [], "hexpm", "dd95aaaba0c9c6f0e9be2db3e9fac0bf69784557e04f700f94e90629f5e24e1d"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, @@ -38,12 +38,12 @@ "poison": {:hex, :poison, "6.0.0", "9bbe86722355e36ffb62c51a552719534257ba53f3271dacd20fbbd6621a583a", [:mix], [{:decimal, "~> 2.1", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "bb9064632b94775a3964642d6a78281c07b7be1319e0016e1643790704e739a2"}, "proper_case": {:hex, :proper_case, "1.3.1", "5f51cabd2d422a45f374c6061b7379191d585b5154456b371432d0fa7cb1ffda", [:mix], [], "hexpm", "6cc715550cc1895e61608060bbe67aef0d7c9cf55d7ddb013c6d7073036811dd"}, "quokka": {:hex, :quokka, "2.11.2", "2856118154425f18547720d997199be54febed771a740ba3c988a17762328287", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "8208f5d814007cb35a2eb278462464d083fca8c463f62517ab94eef982f181cc"}, - "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, + "req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, - "thousand_island": {:hex, :thousand_island, "1.4.2", "735fa783005d1703359bbd2d3a5a3a398075ba4456e5afe3c5b7cf4666303d36", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c7637f16558fc1c35746d5ee0e83b18b8e59e18d28affd1f2fa1645f8bc7473"}, + "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, } diff --git a/test/ex_ftp/storage/transfer_complete_test.exs b/test/ex_ftp/storage/transfer_complete_test.exs new file mode 100644 index 0000000..b4fb189 --- /dev/null +++ b/test/ex_ftp/storage/transfer_complete_test.exs @@ -0,0 +1,150 @@ +defmodule ExFTP.Storage.TransferCompleteTest do + @moduledoc false + + use ExUnit.Case + + import ExFTP.StorageTester + import ExFTP.TestHelper + import ExUnit.CaptureLog + + alias ExFTP.Auth.PassthroughAuth + alias ExFTP.Storage.FileConnector + + defmodule TestHandler do + require Logger + + def handle_complete(type, path, _connector_state) do + Logger.info("Custom handler called: #{type} #{path}") + :ok + end + end + + describe "transfer completion with custom handler" do + setup do + Application.put_env(:ex_ftp, :authenticator, PassthroughAuth) + Application.put_env(:ex_ftp, :authenticator_config, %{}) + Application.put_env(:ex_ftp, :storage_connector, FileConnector) + + Application.put_env(:ex_ftp, :storage_config, %{ + on_transfer_complete: {TestHandler, :handle_complete} + }) + + socket = get_socket() + username = Faker.Internet.user_name() + password = Faker.Internet.slug() + + socket + |> send_and_expect("USER", [username], 331, "User name okay, need password") + |> send_and_expect("PASS", [password], 230, "Welcome.") + + on_exit(fn -> + Application.put_env(:ex_ftp, :storage_config, %{}) + end) + + %{socket: socket, username: username} + end + + test "calls custom handler on STOR (upload)", state do + w_dir = Path.join(System.tmp_dir!(), Faker.Internet.slug()) + on_exit(fn -> File.rm_rf!(w_dir) end) + + files_to_store = + File.cwd!() + |> File.ls!() + |> Enum.filter(fn file -> File.cwd!() |> Path.join(file) |> File.regular?() end) + |> Enum.take(1) + + refute Enum.empty?(files_to_store) + + log = + capture_log(fn -> + test_stor(state, w_dir, files_to_store) + end) + + # Verify custom handler was called (not default) + assert log =~ "Custom handler called: store" + refute log =~ "Transfer complete: store" + end + + test "calls custom handler on RETR (download)", state do + w_dir = File.cwd!() + + paths_to_download = + w_dir + |> File.ls!() + |> Enum.filter(fn file -> w_dir |> Path.join(file) |> File.regular?() end) + |> Enum.take(1) + + refute Enum.empty?(paths_to_download) + + log = + capture_log(fn -> + test_retr(state, w_dir, paths_to_download) + end) + + # Verify custom handler was called (not default) + assert log =~ "Custom handler called: retrieve" + refute log =~ "Transfer complete: retrieve" + end + end + + describe "transfer completion with default behavior" do + setup do + Application.put_env(:ex_ftp, :authenticator, PassthroughAuth) + Application.put_env(:ex_ftp, :authenticator_config, %{}) + Application.put_env(:ex_ftp, :storage_connector, FileConnector) + Application.put_env(:ex_ftp, :storage_config, %{}) + + socket = get_socket() + username = Faker.Internet.user_name() + password = Faker.Internet.slug() + + socket + |> send_and_expect("USER", [username], 331, "User name okay, need password") + |> send_and_expect("PASS", [password], 230, "Welcome.") + + %{socket: socket} + end + + test "logs default message on RETR when no handler configured", state do + w_dir = File.cwd!() + + paths_to_download = + w_dir + |> File.ls!() + |> Enum.filter(fn file -> w_dir |> Path.join(file) |> File.regular?() end) + |> Enum.take(1) + + refute Enum.empty?(paths_to_download) + + log = + capture_log(fn -> + test_retr(state, w_dir, paths_to_download) + end) + + # Verify default log message appears + assert log =~ "Transfer complete: retrieve" + end + + test "logs default message on STOR when no handler configured", state do + w_dir = Path.join(System.tmp_dir!(), Faker.Internet.slug()) + on_exit(fn -> File.rm_rf!(w_dir) end) + + files_to_store = + File.cwd!() + |> File.ls!() + |> Enum.filter(fn file -> File.cwd!() |> Path.join(file) |> File.regular?() end) + |> Enum.take(1) + + refute Enum.empty?(files_to_store) + + log = + capture_log(fn -> + test_stor(state, w_dir, files_to_store) + end) + + # Verify default log message appears + assert log =~ "Transfer complete: store" + end + end +end