From 65c2fcd1308cd25d513952dc724a4e841099eefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Gali=C4=87?= Date: Wed, 24 Jan 2018 23:28:44 +0100 Subject: [PATCH 1/7] test(stream_data): add dependency --- mix.exs | 1 + mix.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 868fcc0..dbe77a6 100644 --- a/mix.exs +++ b/mix.exs @@ -47,6 +47,7 @@ defmodule AuthToken.Mixfile do {:phoenix, "~> 1.3"}, {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, + {:stream_data, "~> 0.1", only: [:dev, :test]}, {:ex_doc, "~> 0.18", only: :dev} ] end diff --git a/mix.lock b/mix.lock index c4bd822..5dc176e 100644 --- a/mix.lock +++ b/mix.lock @@ -7,4 +7,5 @@ "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [], [], "hexpm"}, "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [], [], "hexpm"}} + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [], [], "hexpm"}, + "stream_data": {:hex, :stream_data, "0.4.0", "128c01bfd0fae0108d169eee1772aeed6958604f8782abc2d6e11da4e52468b0", [], [], "hexpm"}} From 3cdfb29cf5ea4e751102bdfea923e16d1e1c8a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Gali=C4=87?= Date: Thu, 25 Jan 2018 18:07:52 +0100 Subject: [PATCH 2/7] test(stream_data): replace checks for byte_size(key) == 16 --- mix.exs | 4 ++-- test/authtoken_test.exs | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/mix.exs b/mix.exs index dbe77a6..cb3ad2d 100644 --- a/mix.exs +++ b/mix.exs @@ -28,8 +28,8 @@ defmodule AuthToken.Mixfile do ] end - def application do - [applications: [:logger, :plug], + def extra_application do + [applications: [:logger], env: [ timeout: 86400, refresh: 1800 diff --git a/test/authtoken_test.exs b/test/authtoken_test.exs index db8a959..fa68975 100644 --- a/test/authtoken_test.exs +++ b/test/authtoken_test.exs @@ -1,5 +1,8 @@ defmodule AuthTokenTest do use ConnCase + import StreamData + import ExUnitProperties + @user %{id: 123} @@ -9,10 +12,12 @@ defmodule AuthTokenTest do end describe "keys" do - test "generate_key/0 returns a valid AES128 key" do - {:ok, key} = AuthToken.generate_key() + property "generate_key/0 returns a valid AES128 key" do + authtoken_key_generator = map(AuthToken.generate_key, fn {x} -> x end) + check all {:ok, key} <- authtoken_key_generator do - assert byte_size(key) == 16 + assert byte_size(key) == 16 + end end end From e42bb9194a4e5b1544fc802dcc7dc151adc0ed91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Gali=C4=87?= Date: Fri, 26 Jan 2018 12:55:19 +0100 Subject: [PATCH 3/7] test(properties): fix our first property test thanks to the helpful people of the community, we now have a working property test: https://elixirforum.com/t/how-to-create-a-custom-streamdata-generator/11935/3 --- test/authtoken_test.exs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/authtoken_test.exs b/test/authtoken_test.exs index fa68975..b6a0b5f 100644 --- a/test/authtoken_test.exs +++ b/test/authtoken_test.exs @@ -1,11 +1,15 @@ defmodule AuthTokenTest do use ConnCase - import StreamData import ExUnitProperties - @user %{id: 123} + defp gen_authtoken_key() do + StreamData.map(StreamData.list_of(StreamData.constant(:unused_tick)), fn _ -> + AuthToken.generate_key() + end) + end + setup do Application.put_env(:authtoken, :timeout, 86400) Application.put_env(:authtoken, :refresh, 1800) @@ -13,9 +17,8 @@ defmodule AuthTokenTest do describe "keys" do property "generate_key/0 returns a valid AES128 key" do - authtoken_key_generator = map(AuthToken.generate_key, fn {x} -> x end) - check all {:ok, key} <- authtoken_key_generator do - + check all authtoken_key <- gen_authtoken_key() do + {:ok, key} = authtoken_key assert byte_size(key) == 16 end end From 3e9df738332842577df27e5d7a12bfdc5c83a238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Gali=C4=87?= Date: Fri, 26 Jan 2018 22:17:34 +0100 Subject: [PATCH 4/7] test(properties): got even better way of generating keys generally, bind() is the way to create custom generators. But I couldn't figure this out from the documentation! Maybe it'll get fixed by elixir 1.7 :D --- test/authtoken_test.exs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/authtoken_test.exs b/test/authtoken_test.exs index b6a0b5f..b70d1c6 100644 --- a/test/authtoken_test.exs +++ b/test/authtoken_test.exs @@ -5,9 +5,11 @@ defmodule AuthTokenTest do @user %{id: 123} defp gen_authtoken_key() do - StreamData.map(StreamData.list_of(StreamData.constant(:unused_tick)), fn _ -> - AuthToken.generate_key() - end) + StreamData.unshrinkable( + StreamData.bind(StreamData.constant(:unused), fn _ -> + StreamData.constant(AuthToken.generate_key()) + end) + ) end setup do From 2da86ba37b8380df5e90e1af4da46d564bbfd37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Gali=C4=87?= Date: Mon, 29 Jan 2018 17:31:31 +0100 Subject: [PATCH 5/7] test(properties): generate_key/0 always returns unique values --- test/authtoken_test.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/authtoken_test.exs b/test/authtoken_test.exs index b70d1c6..e6c89ac 100644 --- a/test/authtoken_test.exs +++ b/test/authtoken_test.exs @@ -26,6 +26,16 @@ defmodule AuthTokenTest do end end + describe "unique keys" do + property "generate_key/0 always returns a unique AES128 key" do + # n.b.: not using `check all` here, since that would require we we + # construct a StreamData list. but all we want to do is ensure a large list is unique + authtoken_keys = Enum.take(gen_authtoken_key(), 9_999) + + assert authtoken_keys == Enum.uniq(authtoken_keys) + end + end + describe "tokens" do test "token generation" do {:ok, encrypted_token} = AuthToken.generate_token(@user) From c464bc192b1f253c99fe185379511a023517b8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Gali=C4=87?= Date: Tue, 30 Jan 2018 11:13:47 +0100 Subject: [PATCH 6/7] test(properties): use |> for better readability This makes our custom generator far more readable, so I've updated the solution too: https://elixirforum.com/t/how-to-create-a-custom-streamdata-generator/11935/3?u=igalic --- test/authtoken_test.exs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/authtoken_test.exs b/test/authtoken_test.exs index e6c89ac..2ece3b2 100644 --- a/test/authtoken_test.exs +++ b/test/authtoken_test.exs @@ -5,11 +5,13 @@ defmodule AuthTokenTest do @user %{id: 123} defp gen_authtoken_key() do - StreamData.unshrinkable( - StreamData.bind(StreamData.constant(:unused), fn _ -> - StreamData.constant(AuthToken.generate_key()) - end) - ) + gen_ticker = fn _ -> + StreamData.constant(AuthToken.generate_key()) + end + + StreamData.constant(:unused) + |> StreamData.bind(gen_ticker) + |> StreamData.unshrinkable() end setup do From 7e56fec2b5c1b5d21824e65ac980165f37455f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Gali=C4=87?= Date: Fri, 16 Feb 2018 17:44:49 +0100 Subject: [PATCH 7/7] test(properites): pull setup into function transform unit into prop tests setup being an ExUnit macro is only called once, so we pull its body into an extra function which we can call when needed. We've discovered that `async: true` does nothing for ExUnitProperties, so one of the tests is commented out for now. We've also split the use-case driven unit test into a better property test shape --- test/authtoken_test.exs | 104 +++++++++++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 28 deletions(-) diff --git a/test/authtoken_test.exs b/test/authtoken_test.exs index 2ece3b2..04cba35 100644 --- a/test/authtoken_test.exs +++ b/test/authtoken_test.exs @@ -1,5 +1,5 @@ defmodule AuthTokenTest do - use ConnCase + use ConnCase, async: true import ExUnitProperties @user %{id: 123} @@ -14,11 +14,39 @@ defmodule AuthTokenTest do |> StreamData.unshrinkable() end - setup do + defp gen_usermap() do + StreamData.fixed_map(%{ id: StreamData.integer() }) + end + + defp gen_authtoken_token(user) do + gen_ticker = fn user -> + StreamData.constant(AuthToken.generate_token(user)) + end + + StreamData.constant(user) + |> StreamData.bind(gen_ticker) + |> StreamData.unshrinkable() + end + + defp gen_authtoken_stale_token(user) do + gen_ticker = fn user -> + StreamData.constant(AuthToken.generate_token(user)) + end + + StreamData.constant(user) + |> StreamData.bind(gen_ticker) + |> StreamData.unshrinkable() + end + + defp reset_timeout_env() do Application.put_env(:authtoken, :timeout, 86400) Application.put_env(:authtoken, :refresh, 1800) end + setup do + reset_timeout_env() + end + describe "keys" do property "generate_key/0 returns a valid AES128 key" do check all authtoken_key <- gen_authtoken_key() do @@ -32,48 +60,68 @@ defmodule AuthTokenTest do property "generate_key/0 always returns a unique AES128 key" do # n.b.: not using `check all` here, since that would require we we # construct a StreamData list. but all we want to do is ensure a large list is unique - authtoken_keys = Enum.take(gen_authtoken_key(), 9_999) + authtoken_keys = Enum.take(gen_authtoken_key(), 99_999) assert authtoken_keys == Enum.uniq(authtoken_keys) end end - describe "tokens" do - test "token generation" do - {:ok, encrypted_token} = AuthToken.generate_token(@user) + describe "token properties (and life-cycle)" do + property "generate_token/1 and decrypt_token/1 are reversible" do + check all user <- gen_usermap(), + authtoken_token <- gen_authtoken_token(user) do + + {:ok, encrypted_token} = authtoken_token + assert {:ok, token} = AuthToken.decrypt_token(encrypted_token) + assert token["id"] == user.id + end + end + + property "authtoken can timeout" do + check all user <- gen_usermap(), + authtoken_token <- gen_authtoken_token(user) do - assert {:ok, token} = AuthToken.decrypt_token(encrypted_token) - assert token["id"] == @user.id + reset_timeout_env() - refute AuthToken.is_timedout?(token) - refute AuthToken.needs_refresh?(token) + {:ok, encrypted_token} = authtoken_token + {:ok, token} = AuthToken.decrypt_token(encrypted_token) - Application.put_env(:authtoken, :timeout, -1) - Application.put_env(:authtoken, :refresh, -1) + refute AuthToken.is_timedout?(token) + refute AuthToken.needs_refresh?(token) - assert AuthToken.is_timedout?(token) - assert AuthToken.needs_refresh?(token) + Application.put_env(:authtoken, :timeout, -1) + Application.put_env(:authtoken, :refresh, -1) + + assert AuthToken.is_timedout?(token) + assert AuthToken.needs_refresh?(token) + end end - test "token refresh" do - {:ok, encrypted_token} = AuthToken.generate_token(@user) - {:ok, token} = AuthToken.decrypt_token(encrypted_token) + # property "token refresh" do + # check all user <- gen_usermap(), + # authtoken_token <- gen_authtoken_token(user) do - assert AuthToken.refresh_token(token) == {:error, :stillfresh} - assert AuthToken.refresh_token(encrypted_token) == {:error, :stillfresh} + # reset_timeout_env() - :timer.sleep(1000) + # {:ok, encrypted_token} = authtoken_token + # {:ok, token} = AuthToken.decrypt_token(encrypted_token) - Application.put_env(:authtoken, :refresh, -1) - assert {:ok, fresh_token} = AuthToken.refresh_token(token) - assert {:ok, fresh_token} = AuthToken.refresh_token(encrypted_token) + # assert AuthToken.refresh_token(token) == {:error, :stillfresh} + # assert AuthToken.refresh_token(encrypted_token) == {:error, :stillfresh} - {:ok, token} = AuthToken.decrypt_token(fresh_token) - assert token["ct"] < token["rt"] + # :timer.sleep(1000) - Application.put_env(:authtoken, :timeout, -1) - assert AuthToken.refresh_token(token) == {:error, :timedout} - end + # Application.put_env(:authtoken, :refresh, -1) + # assert {:ok, fresh_token} = AuthToken.refresh_token(token) + # assert {:ok, fresh_token} = AuthToken.refresh_token(encrypted_token) + + # {:ok, token} = AuthToken.decrypt_token(fresh_token) + # assert token["ct"] < token["rt"] + + # Application.put_env(:authtoken, :timeout, -1) + # assert AuthToken.refresh_token(token) == {:error, :timedout} + # end + # end end describe "plug verifying and testing tokens" do