├── .formatter.exs ├── .gitignore ├── README.md ├── config └── config.exs ├── docker-compose.yml ├── lib └── testing_aws_in_elixir.ex ├── mix.exs ├── mix.lock └── test ├── support └── mocks.ex ├── test_helper.exs └── testing_aws_in_elixir_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | testing_aws_in_elixir-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Testing AWS In Elixir — Accompanying Code 2 | 3 | This repository contains the Elixir code to accompany [this blog 4 | post](https://andrealeopardi.com/posts/testing-aws-in-elixir/). 5 | 6 | This is a normal Mix project. 7 | 8 | To run tests locally, in the root of the repository run: 9 | 10 | ```bash 11 | mix deps.get 12 | mix test 13 | ``` 14 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if Mix.env() == :test do 4 | aws_uri = 5 | System.get_env("AWS_ENDPOINT_URL", "http://localhost:4566") 6 | |> URI.parse() 7 | 8 | config :ex_aws, :s3, 9 | scheme: aws_uri.scheme, 10 | host: aws_uri.host, 11 | port: aws_uri.port 12 | 13 | config :testing_aws_in_elixir, :test_doubles, ex_aws: ExAwsMock 14 | end 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | localstack: 5 | image: localstack/localstack 6 | environment: 7 | AWS_DEFAULT_REGION: "us-west-2" 8 | SERVICES: "s3" 9 | EDGE_PORT: "4566" 10 | ports: 11 | - "4566:4566" 12 | -------------------------------------------------------------------------------- /lib/testing_aws_in_elixir.ex: -------------------------------------------------------------------------------- 1 | defmodule TestinAWSInElixir do 2 | @ex_aws_mod Application.compile_env(:testing_aws_in_elixir, [:test_doubles, :ex_aws], ExAws) 3 | 4 | @spec download_file_from_s3!(String.t(), String.t(), keyword) :: :ok 5 | def download_file_from_s3!(bucket, path, options) do 6 | dest_path = Keyword.fetch!(options, :to) 7 | %{body: body} = @ex_aws_mod.request!(ExAws.S3.get_object(bucket, path)) 8 | File.write!(dest_path, body) 9 | :ok 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TestinAWSInElixir.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :testing_aws_in_elixir, 7 | version: "0.1.0", 8 | elixir: "~> 1.13", 9 | start_permanent: Mix.env() == :prod, 10 | elixirc_paths: elixirc_paths(Mix.env()), 11 | deps: deps() 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | defp elixirc_paths(:test), do: ["test/support", "lib"] 23 | defp elixirc_paths(_env), do: ["lib"] 24 | 25 | # Run "mix help deps" to learn about dependencies. 26 | defp deps do 27 | [ 28 | {:mox, "~> 1.0", only: :test}, 29 | {:ex_aws, "~> 2.2"}, 30 | {:ex_aws_s3, "~> 2.3"}, 31 | {:hackney, "~> 1.18"}, 32 | {:sweet_xml, "~> 0.7.2"}, 33 | {:jason, "~> 1.0"} 34 | ] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, 3 | "ex_aws": {:hex, :ex_aws, "2.2.10", "064139724335b00b6665af7277189afc9ed507791b1ccf2698dadc7c8ad892e8", [: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]}, {: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", "98acb63f74b2f0822be219c5c2f0e8d243c2390f5325ad0557b014d3360da47e"}, 4 | "ex_aws_s3": {:hex, :ex_aws_s3, "2.3.2", "92a63b72d763b488510626d528775b26831f5c82b066a63a3128054b7a09de28", [: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", "b235b27131409bcc293c343bf39f1fbdd32892aa237b3f13752e914dc2979960"}, 5 | "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, 6 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 7 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 8 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 9 | "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, 10 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 11 | "mox": {:hex, :mox, "1.0.1", "b651bf0113265cda0ba3a827fcb691f848b683c373b77e7d7439910a8d754d6e", [:mix], [], "hexpm", "35bc0dea5499d18db4ef7fe4360067a59b06c74376eb6ab3bd67e6295b133469"}, 12 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 13 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 14 | "sweet_xml": {:hex, :sweet_xml, "0.7.2", "4729f997286811fabdd8288f8474e0840a76573051062f066c4b597e76f14f9f", [:mix], [], "hexpm", "6894e68a120f454534d99045ea3325f7740ea71260bc315f82e29731d570a6e8"}, 15 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, 16 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 17 | } 18 | -------------------------------------------------------------------------------- /test/support/mocks.ex: -------------------------------------------------------------------------------- 1 | Mox.defmock(ExAwsMock, for: ExAws.Behaviour) 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/testing_aws_in_elixir_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestinAWSInElixirTest do 2 | use ExUnit.Case 3 | 4 | setup do 5 | Mox.stub_with(ExAwsMock, ExAws) 6 | :ok 7 | end 8 | 9 | test "download_file_from_s3!/3" do 10 | # Create some random bytes to store in the file. 11 | contents = :crypto.strong_rand_bytes(100) 12 | 13 | # We set up an on_exit callback to empty and then delete the bucket 14 | # when the test exit, so that the next test has a clean slate. 15 | on_exit(fn -> 16 | "test-bucket" 17 | |> ExAws.S3.list_objects() 18 | |> ExAws.stream!() 19 | |> Enum.each(&ExAws.request!(ExAws.S3.delete_object("test-bucket", &1.key))) 20 | 21 | ExAws.request!(ExAws.S3.delete_bucket("test-bucket")) 22 | 23 | # Also, delete the file we'll create. 24 | File.rm_rf!("localfile") 25 | end) 26 | 27 | # Create bucket. 28 | ExAws.request!(ExAws.S3.put_bucket("test-bucket", "us-west-2")) 29 | 30 | # Upload a file. 31 | ExAws.request!(ExAws.S3.put_object("test-bucket", "my/random/file", contents)) 32 | 33 | # Now, we run our code and assert on its behavior. 34 | TestinAWSInElixir.download_file_from_s3!("test-bucket", "my/random/file", to: "localfile") 35 | assert File.read!("localfile") == contents 36 | end 37 | end 38 | --------------------------------------------------------------------------------