From df6342382216cb6d8c9d55e865dbfeea6fb7b5fd Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Wed, 14 Mar 2018 22:20:31 -0300 Subject: [PATCH 1/5] Expand Storyline to include NastyVirus sub-chapter --- lib/software/make/file.ex | 3 + lib/story/mission/tutorial/steps.ex | 82 ++++++++++++++++++- lib/story/model/step/macros.ex | 33 +++++++- .../storyline/quests/tutorial_test.exs | 48 ++++++++++- test/support/story/macros.ex | 12 ++- test/support/story/vars.ex | 10 +++ 6 files changed, 179 insertions(+), 9 deletions(-) diff --git a/lib/software/make/file.ex b/lib/software/make/file.ex index bcb3af38..dbaaa5aa 100644 --- a/lib/software/make/file.ex +++ b/lib/software/make/file.ex @@ -36,6 +36,9 @@ defmodule Helix.Software.Make.File do file end + def spyware(parent, modules, data \\ %{}), + do: file(parent, :virus_spyware, modules, data) + @spec file(file_parent, Software.type, modules, data) :: file_return(Software.type) defp file(server = %Server{}, type, modules, data) do diff --git a/lib/story/mission/tutorial/steps.ex b/lib/story/mission/tutorial/steps.ex index b9a0911c..99c44148 100644 --- a/lib/story/mission/tutorial/steps.ex +++ b/lib/story/mission/tutorial/steps.ex @@ -168,20 +168,96 @@ defmodule Helix.Story.Mission.Tutorial do step NastyVirus do + alias Helix.Server.Model.Server + alias Helix.Server.Query.Server, as: ServerQuery + alias Helix.Software.Model.File + alias Helix.Story.Action.Context, as: ContextAction + + alias Helix.Software.Make.File, as: MakeFile + email "nasty_virus1" - empty_setup() + on_email "nasty_virus1", + send: "nasty_virus2", + send_opts: [sleep: 3] + + email "nasty_virus2", + replies: ["punks1"] + + email "punks2" + + on_reply "punks1", + send: "punks2", + send_opts: [sleep: 3] + + email "punks3" + + filter_email "punks2" do + {{:send_email, "punks3", %{ip: step.meta.ip}, [sleep: 2]}, step, []} + end + + def setup(step) do + # Create the underlying character (:rcn) and its server + {:ok, server, _, e1} = + setup_once :char, {step.entity_id, :rcn} do + result = {:ok, server, %{entity: entity}, _} = + StoryMake.char(step.manager.network_id) + + ContextAction.save( + step.entity_id, :rcn, :server_id, server.server_id + ) + ContextAction.save( + step.entity_id, :rcn, :entity_id, entity.entity_id + ) + + result + end + + # Create the Spyware the player is supposed to download + {:ok, spyware, _, e2} = + setup_once :file, step.meta[:spyware_id] do + result = {:ok, spyware, _, _} = + MakeFile.spyware(server, %{vir_spyware: 30}) + + ContextAction.save( + step.entity_id, :rcn, :spyware_id, spyware.file_id + ) + + result + end + + meta = + %{ + ip: ServerQuery.get_ip(server, step.manager.network_id), + server_id: server.server_id, + spyware_id: spyware.file_id + } + + {meta, %{}, e1 ++ e2} + end def start(step) do - e1 = send_email step, "nasty_virus1", %{} + {meta, _, e1} = setup(step) + + e2 = send_email step, "nasty_virus1", %{} - {:ok, step, e1, sleep: 4} + step = %{step| meta: meta} + + {:ok, step, e1 ++ e2, sleep: 4} end def complete(step) do {:ok, step, []} end + format_meta do + %{ + ip: meta.ip, + server_id: meta.server_id |> Server.ID.cast!(), + spyware_id: meta.spyware_id |> File.ID.cast!() + } + end + next_step __MODULE__ end diff --git a/lib/story/model/step/macros.ex b/lib/story/model/step/macros.ex index e3eea131..3fb524eb 100644 --- a/lib/story/model/step/macros.ex +++ b/lib/story/model/step/macros.ex @@ -388,7 +388,7 @@ defmodule Helix.Story.Model.Step.Macros do quote do @doc false - def handle_event(step = unquote(step), unquote(event), unquote(meta)) do + def handle_event(step = unquote(step), unquote(event), m = unquote(meta)) do unquote( case opts do [do: :complete, send_opts: send_opts] -> @@ -491,6 +491,37 @@ defmodule Helix.Story.Model.Step.Macros do [email_block] ++ [reply_block] end + @doc """ + `filter_email` is workaround to a limitation on `filter` macro that is lacking + some flexibility. Works for now, but probably should be reworked later, in + order to make `filter` a more flexible method. + """ + defmacro filter_email(email_id, do: block) do + quote do + + @doc false + def handle_event( + step, + %StoryEmailSentEvent{ + email: %{id: unquote(email_id)} + }, + meta + ) + do + var!(step) = step + var!(meta) = meta + + # Mark unhygienic variables as used + var!(step) + var!(meta) + + unquote(block) + + end + + end + end + @doc """ Interface used to declare what should happen when `email_id` is sent. """ diff --git a/test/features/storyline/quests/tutorial_test.exs b/test/features/storyline/quests/tutorial_test.exs index 5594f71d..4cba433c 100644 --- a/test/features/storyline/quests/tutorial_test.exs +++ b/test/features/storyline/quests/tutorial_test.exs @@ -98,7 +98,7 @@ defmodule Helix.Test.Features.Storyline.Quests.Tutorial do [story_email_sent, process_created] = wait_events [:story_email_sent, :process_created], timeout() - assert_email story_email_sent, :msg2, [], s.tutorial.dl_crc + assert_email story_email_sent, :msg2, [:msg3], s.tutorial.dl_crc # Flush pending messages EventHelper.flush_timer() @@ -127,6 +127,52 @@ defmodule Helix.Test.Features.Storyline.Quests.Tutorial do assert_email story_email_sent, :msg5, [], s.tutorial.dl_crc assert_transition story_step_proceeded, s.tutorial.dl_crc + + # Fetch setup data + %{object: cur_step} = StoryQuery.fetch_step(entity_id, cur_step.contact) + + # Generated metadata from the Tutorial.NastyVirus setup + assert cur_step.meta.ip + assert cur_step.meta.server_id + assert cur_step.meta.spyware_id + + EventHelper.flush_timer() + + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg1, [], s.tutorial.nasty + + EventHelper.flush_timer() + + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg2, [:msg3], s.tutorial.nasty + + # Let's reply to :msg2 using :msg3 + params = + %{ + "reply_id" => s.tutorial.nasty.msg3, + "contact_id" => s.contact.friend + } + ref = push account_socket, "email.reply", params + assert_reply ref, :ok, _, timeout() + + [story_reply_sent] = wait_events [:story_reply_sent] + + assert_reply story_reply_sent, :msg3, :msg2, [], s.tutorial.nasty + + EventHelper.flush_timer() + + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg4, [], s.tutorial.nasty + + EventHelper.flush_timer() + + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg5, [], s.tutorial.nasty + assert story_email_sent.data.meta["ip"] == cur_step.meta.ip end end end diff --git a/test/support/story/macros.ex b/test/support/story/macros.ex index 0d0e5765..28c9da96 100644 --- a/test/support/story/macros.ex +++ b/test/support/story/macros.ex @@ -33,10 +33,14 @@ defmodule Helix.Test.Story.Macros do assert unquote(event).event == "story_email_sent" assert event_data.email_id == expected_email - Enum.each(replies, fn reply -> - reply = Map.fetch!(unquote(step_var), reply) - assert Enum.member?(event_data.replies, reply) - end) + if Enum.empty?(replies) do + assert Enum.empty?(event_data.replies) + else + Enum.each(replies, fn reply -> + reply = Map.fetch!(unquote(step_var), reply) + assert Enum.member?(event_data.replies, reply) + end) + end assert event_data.contact_id == to_string(unquote(step_var).contact) assert event_data.step =~ to_string(unquote(step_var).name) diff --git a/test/support/story/vars.ex b/test/support/story/vars.ex index ad3f8f10..e0ad7491 100644 --- a/test/support/story/vars.ex +++ b/test/support/story/vars.ex @@ -27,6 +27,16 @@ defmodule Helix.Test.Story.Vars do msg3: "yeah_right", msg4: "downloaded", msg5: "nothing_now" + }, + nasty: %{ + contact: "friend", + name: "nasty_virus", + next: "IDONTKNOW", + msg1: "nasty_virus1", + msg2: "nasty_virus2", + msg3: "punks1", + msg4: "punks2", + msg5: "punks3" } } } From 09c85c026b65d40ffaef26ee4db36fc502d6085b Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Fri, 6 Apr 2018 01:19:11 -0300 Subject: [PATCH 2/5] Complete NastyVirus sub-chapter --- lib/client/web1/event/action.ex | 6 - lib/client/web1/model/web1.ex | 6 +- lib/event/dispatcher.ex | 5 + lib/process/event/process.ex | 7 +- lib/server/event/server/password.ex | 11 +- lib/software/make/file.ex | 10 +- lib/story/event/handler/story.ex | 2 + lib/story/mission/tutorial/steps.ex | 99 ++++++++++- lib/story/model/step/macros.ex | 99 ++++++++++- .../storyline/quests/tutorial_test.exs | 165 +++++++++++++++++- test/support/channel/setup.ex | 4 + test/support/process/helper/top.ex | 10 ++ test/support/server/helper.ex | 3 + test/support/story/vars.ex | 15 +- 14 files changed, 416 insertions(+), 26 deletions(-) diff --git a/lib/client/web1/event/action.ex b/lib/client/web1/event/action.ex index 3f6c4c95..bd7a6d24 100644 --- a/lib/client/web1/event/action.ex +++ b/lib/client/web1/event/action.ex @@ -31,11 +31,5 @@ defmodule Helix.Client.Web1.Event.Action do action: action } end - - listenable do - listen(event) do - [event.entity_id] - end - end end end diff --git a/lib/client/web1/model/web1.ex b/lib/client/web1/model/web1.ex index d1649389..7c5da8d8 100644 --- a/lib/client/web1/model/web1.ex +++ b/lib/client/web1/model/web1.ex @@ -1,8 +1,10 @@ defmodule Helix.Client.Web1.Model.Web1 do - @type action :: :tutorial_accessed_task_manager + @type action :: + :tutorial_accessed_task_manager + | :tutorial_spotted_nasty_virus - @actions [:tutorial_accessed_task_manager] + @actions [:tutorial_accessed_task_manager, :tutorial_spotted_nasty_virus] @actions_str Enum.map(@actions, &to_string/1) def valid_actions, diff --git a/lib/event/dispatcher.ex b/lib/event/dispatcher.ex index 507ef92e..2a0b7287 100644 --- a/lib/event/dispatcher.ex +++ b/lib/event/dispatcher.ex @@ -88,6 +88,11 @@ defmodule Helix.Event.Dispatcher do # All event Web1Event.Action.Performed + # Custom handlers + event Web1Event.Action.Performed, + StoryHandler.Story, + :event_handler + ############################################################################## # Entity events ############################################################################## diff --git a/lib/process/event/process.ex b/lib/process/event/process.ex index dcf836ac..e816c349 100644 --- a/lib/process/event/process.ex +++ b/lib/process/event/process.ex @@ -121,7 +121,12 @@ defmodule Helix.Process.Event.Process do listenable do listen(event = %_{confirmed: true}) do - [event.process.src_file_id, event.process.tgt_file_id] + [ + event.process.gateway_id, + event.process.target_id, + event.process.src_file_id, + event.process.tgt_file_id, + ] end end end diff --git a/lib/server/event/server/password.ex b/lib/server/event/server/password.ex index c51cd814..6ba311bd 100644 --- a/lib/server/event/server/password.ex +++ b/lib/server/event/server/password.ex @@ -4,8 +4,9 @@ defmodule Helix.Server.Event.Server.Password do event Acquired do @moduledoc """ - The ServerPasswordAcquiredEvent is fired after a Bruteforce attack has been - completed, and the attacker discovered the target's server root password. + The `ServerPasswordAcquiredEvent` is fired after a Bruteforce attack has + been completed, and the attacker discovered the target's server root + password. """ alias Helix.Entity.Model.Entity @@ -61,5 +62,11 @@ defmodule Helix.Server.Event.Server.Password do def whom_to_notify(event), do: %{account: event.entity_id} end + + listenable do + listen(event) do + [event.server_id] + end + end end end diff --git a/lib/software/make/file.ex b/lib/software/make/file.ex index dbaaa5aa..66cdf9f8 100644 --- a/lib/software/make/file.ex +++ b/lib/software/make/file.ex @@ -9,9 +9,12 @@ defmodule Helix.Software.Make.File do alias Helix.Software.Event.File.Added, as: FileAddedEvent - @type modules :: cracker_modules + @type modules :: + cracker_modules + | spyware_modules @type cracker_modules :: %{bruteforce: version, overflow: version} + @type spyware_modules :: %{vir_spyware: version} @type data :: %{optional(:path) => File.path} @@ -36,6 +39,11 @@ defmodule Helix.Software.Make.File do file end + @spec spyware(file_parent, spyware_modules, data) :: + file_return(:virus_spyware) + @doc """ + Generates a spyware. + """ def spyware(parent, modules, data \\ %{}), do: file(parent, :virus_spyware, modules, data) diff --git a/lib/story/event/handler/story.ex b/lib/story/event/handler/story.ex index 0a424411..dc917f7b 100644 --- a/lib/story/event/handler/story.ex +++ b/lib/story/event/handler/story.ex @@ -223,6 +223,8 @@ defmodule Helix.Story.Event.Handler.Story do do: emit(event, [], from: source_event) defp emit(event, [], from: source_event), do: Event.emit(event, from: source_event) + defp emit(event, [sleep: 0], from: source_event), + do: emit(event, from: source_event) defp emit(event, [sleep: interval], from: source_event), do: Event.emit_after(event, interval * 1000, from: source_event) end diff --git a/lib/story/mission/tutorial/steps.ex b/lib/story/mission/tutorial/steps.ex index 99c44148..847967c8 100644 --- a/lib/story/mission/tutorial/steps.ex +++ b/lib/story/mission/tutorial/steps.ex @@ -117,7 +117,7 @@ defmodule Helix.Story.Mission.Tutorial do hespawn fn -> # Send `about_that` when download starts - on_process_started :file_download, cracker.file_id, email: "about_that" + on_download_started cracker.file_id, email: "about_that", sleep: 2 # Reply `downloaded` when the cracker has been downloaded story_listen cracker.file_id, FileDownloadedEvent, reply: "downloaded" @@ -173,6 +173,10 @@ defmodule Helix.Story.Mission.Tutorial do alias Helix.Software.Model.File alias Helix.Story.Action.Context, as: ContextAction + alias Helix.Client.Web1.Event.Action.Performed, as: Web1ActionPerformedEvent + alias Helix.Server.Event.Server.Password.Acquired, + as: ServerPasswordAcquiredEvent + alias Helix.Software.Make.File, as: MakeFile email "nasty_virus1" @@ -182,7 +186,7 @@ defmodule Helix.Story.Mission.Tutorial do send_opts: [sleep: 3] email "nasty_virus2", - replies: ["punks1"] + replies: "punks1" email "punks2" @@ -196,6 +200,60 @@ defmodule Helix.Story.Mission.Tutorial do {{:send_email, "punks3", %{ip: step.meta.ip}, [sleep: 2]}, step, []} end + email "dlayd_much1", + replies: "dlayd_much2" + + email "dlayd_much3" + + on_reply "dlayd_much2", + send: "dlayd_much3", + send_opts: [sleep: 2] + + email "dlayd_much4", + replies: "noice" + + on_email "dlayd_much4", + reply: "noice", + send_opts: [sleep: 2] + + email "nasty_virus3", + replies: ["virus_spotted1"] + + reply "virus_spotted1" + + on_reply "virus_spotted1", + send: "virus_spotted2", + send_opts: [sleep: 2] + + email "virus_spotted2", + replies: ["pointless_convo1"] + + reply "pointless_convo1" + + on_reply "pointless_convo1", + send: "pointless_convo2", + send_opts: [sleep: 3] + + email "pointless_convo2", + replies: ["pointless_convo3"] + + on_email "pointless_convo2", + reply: "pointless_convo3", + send_opts: [sleep: 3] + + reply "pointless_convo3" + + on_reply "pointless_convo3", + send: "pointless_convo4", + send_opts: [sleep: 4] + + email "pointless_convo4", + replies: ["pointless_convo5"] + + on_email "pointless_convo4", + reply: "pointless_convo5", + send_opts: [sleep: 2] + def setup(step) do # Create the underlying character (:rcn) and its server {:ok, server, _, e1} = @@ -233,9 +291,46 @@ defmodule Helix.Story.Mission.Tutorial do spyware_id: spyware.file_id } + # Listeners + hespawn fn -> + + # Send `dlayd_much1` when bruteforce starts + on_bruteforce_started server.server_id, email: "dlayd_much1", sleep: 2 + + # Send `nasty_virus3` when bruteforce finishes + story_listen server.server_id, ServerPasswordAcquiredEvent, + email: "nasty_virus3", sleep: 2 + + # Send `pointless_convo1` when download starts + on_download_started spyware.file_id, reply: "pointless_convo1", sleep: 2 + + end + {meta, %{}, e1 ++ e2} end + # Filters + + # Send `dlayd_much4` email when player opens TaskManager app + filter( + _step, + %Web1ActionPerformedEvent{ + action: :tutorial_accessed_task_manager + }, + _meta, + send: "dlayd_much4", send_opts: [sleep: 1] + ) + + # Send `virus_spotted1` reply when player spots the virus + filter( + _step, + %Web1ActionPerformedEvent{ + action: :tutorial_spotted_nasty_virus + }, + _meta, + reply: "virus_spotted1", send_opts: [sleep: 1] + ) + def start(step) do {meta, _, e1} = setup(step) diff --git a/lib/story/model/step/macros.ex b/lib/story/model/step/macros.ex index 3fb524eb..3bde9440 100644 --- a/lib/story/model/step/macros.ex +++ b/lib/story/model/step/macros.ex @@ -130,28 +130,40 @@ defmodule Helix.Story.Model.Step.Macros do Predefined callback when user asks to `:send_email`. `meta` must contain required data, in this case at least `email_id`. """ - callback :cb_send_email, _event, meta = %{email_id: email_id} do + callback :cb_send_email, event, meta = %{email_id: email_id} do email_meta = Map.get(meta, :email_meta, %{}) + sleep = Map.get(meta, :sleep, 0) + email_opts = [sleep: sleep] - {{:send_email, email_id, email_meta, []}, []} + {{:send_email, email_id, email_meta, email_opts}, []} end @doc """ Predefined callback when user asks to `:send_reply`. `meta` must contain required data, in this case at least `reply_id`. """ - callback :cb_send_reply, _event, %{reply_id: reply_id} do - {{:send_reply, reply_id, []}, []} + callback :cb_send_reply, _event, meta = %{reply_id: reply_id} do + sleep = Map.get(meta, :sleep, 0) + reply_opts = [sleep: sleep] + + {{:send_reply, reply_id, reply_opts}, []} end @doc """ Predefined callback that is used by `on_process_started` listener. """ callback :cb_process_started, event, meta do - if to_string(event.process.type) == meta.type do + field = String.to_existing_atom(meta.process_field) + target_value = Map.fetch!(event.process, field) + + with \ + true <- to_string(event.process.type) == meta.type, + true <- to_string(target_value) == meta.element_id + do relay_callback meta.relay_cb, event, meta else - {:noop, []} + _ -> + {:noop, []} end end @@ -243,10 +255,60 @@ defmodule Helix.Story.Model.Step.Macros do end end + @doc """ + Removes the registered listener for `event` on `object_id`. + + It's a wrapper for `Core.Listener` + """ + defmacro story_unlisten(object_id, event, owner_id) do + # Import `Helix.Core.Listener` only once within the Step context (ENV) + macro = has_macro?(__CALLER__, Helix.Core.Listener) + import_block = macro && [] || quote(do: import Helix.Core.Listener) + + quote do + + unquote(import_block) + + unlisten unquote(owner_id), unquote(object_id), unquote(event), @step_name + + end + end + + @doc """ + Allows the step to deregister a listener based on a top-level action. + + `opts` may receive `step_entity`, which has the Entity.ID for the entity that + step belongs to. If no `step_entity` is given, the macro will attempt to get + the Entity.ID from the `step` var, which does not exist if called from within + a callback! + + It's syntactic sugar for `story_unlisten/2`, allowing the step to unsubscribe + from a listener without it having to know the underlying event. + """ + defmacro unsubscribe(action, element_id, opts \\ []) do + owner_id = + if opts == [] do + quote(do: var!(step).entity_id) + else + quote(do: unquote(opts)[:step_entity]) + end + + event = get_event_from_action(action) + + quote do + + story_unlisten(unquote(element_id), unquote(event), unquote(owner_id)) + + end + end + + defp get_event_from_action(:client_action), + do: Web1ActionPerformedEvent + @doc """ Listener that triggers once the process of type `type` acts over `element_id`. """ - defmacro on_process_started(type, element_id, callback) do + defmacro on_process_started(type, {process_field, element_id}, callback) do quote do {callback_name, extra_meta} = get_callback_data(unquote(callback)) @@ -254,6 +316,8 @@ defmodule Helix.Story.Model.Step.Macros do meta = %{ type: unquote(type), + element_id: unquote(element_id), + process_field: unquote(process_field), relay_cb: callback_name } |> Map.merge(extra_meta) @@ -263,6 +327,19 @@ defmodule Helix.Story.Model.Step.Macros do end end + defmacro on_download_started(elem_id, callback), + do: process_listener(:file_download, elem_id, callback, :tgt_file_id) + + defmacro on_bruteforce_started(elem_id, callback), + do: process_listener(:cracker_bruteforce, elem_id, callback, :target_id) + + defp process_listener(type, element_id, callback, field) do + quote do + on_process_started unquote(type), {unquote(field), unquote(element_id)}, + unquote(callback) + end + end + @doc """ Listeners that triggers once the player has performed `action` on the client. """ @@ -276,7 +353,7 @@ defmodule Helix.Story.Model.Step.Macros do meta = %{ - action: unquote(action), + action: to_string(unquote(action)), relay_cb: callback_name } |> Map.merge(extra_meta) @@ -424,6 +501,7 @@ defmodule Helix.Story.Model.Step.Macros do [send: email_id, send_opts: send_opts] -> quote do meta = Keyword.get(unquote(opts), :meta, %{}) + { {:send_email, unquote(email_id), meta, unquote(send_opts)}, step, @@ -621,12 +699,17 @@ defmodule Helix.Story.Model.Step.Macros do Helper that analyzes the `story_listen` opts and returns the corresponding callback name, as well as extra metadata that we should feed to the callback. """ + def get_callback_data(email: email_id, sleep: sleep), + do: {:cb_send_email, %{email_id: email_id, sleep: sleep}} def get_callback_data(email: {email_id, email_meta}), do: {:cb_send_email, %{email_id: email_id, email_meta: email_meta}} def get_callback_data(email: email_id) when is_binary(email_id), do: {:cb_send_email, %{email_id: email_id}} + def get_callback_data(reply: reply_id, sleep: sleep), + do: {:cb_send_reply, %{reply_id: reply_id, sleep: sleep}} def get_callback_data(reply: reply_id) when is_binary(reply_id), do: {:cb_send_reply, %{reply_id: reply_id}} + def get_callback_data(:complete), do: {:cb_complete, %{}} def get_callback_data(callback) when is_atom(callback), diff --git a/test/features/storyline/quests/tutorial_test.exs b/test/features/storyline/quests/tutorial_test.exs index 4cba433c..98922821 100644 --- a/test/features/storyline/quests/tutorial_test.exs +++ b/test/features/storyline/quests/tutorial_test.exs @@ -10,6 +10,7 @@ defmodule Helix.Test.Features.Storyline.Quests.Tutorial do alias Helix.Story.Model.Step alias Helix.Story.Query.Story, as: StoryQuery + alias Helix.Test.Channel.Helper, as: ChannelHelper alias Helix.Test.Channel.Setup, as: ChannelSetup alias Helix.Test.Event.Helper, as: EventHelper alias Helix.Test.Entity.Helper, as: EntityHelper @@ -24,10 +25,12 @@ defmodule Helix.Test.Features.Storyline.Quests.Tutorial do skip_on_travis_slowpoke() test "flow" do {server_socket, %{account: account, manager: manager}} = - ChannelSetup.join_storyline_server() + ChannelSetup.join_storyline_server(socket_opts: [client: :web1]) entity = EntityHelper.fetch_entity_from_account(account) entity_id = entity.entity_id + gateway = ServerHelper.fetch(server_socket.assigns.gateway.server_id) + gateway_ip = ServerHelper.get_ip(gateway, manager.network_id) {account_socket, _} = ChannelSetup.join_account( @@ -95,8 +98,11 @@ defmodule Helix.Test.Features.Storyline.Quests.Tutorial do assert_reply ref, :ok, _, timeout(:slow) # Right after the download has started, we receive an email - [story_email_sent, process_created] = - wait_events [:story_email_sent, :process_created], timeout() + [process_created] = wait_events [:process_created], timeout() + + EventHelper.flush_timer() + + [story_email_sent] = wait_events [:story_email_sent] assert_email story_email_sent, :msg2, [:msg3], s.tutorial.dl_crc @@ -173,6 +179,159 @@ defmodule Helix.Test.Features.Storyline.Quests.Tutorial do assert_email story_email_sent, :msg5, [], s.tutorial.nasty assert story_email_sent.data.meta["ip"] == cur_step.meta.ip + + # Let's bruteforce that IP + + # Now I'll download the requested file + params = + %{ + "ip" => cur_step.meta.ip, + "network_id" => to_string(manager.network_id), + "bounce_id" => nil + } + + # Start the download (using the PublicFTP) + ref = push server_socket, "cracker.bruteforce", params + assert_reply ref, :ok, _, timeout(:slow) + + [process_created] = wait_events [:process_created] + + process = TOPHelper.fetch_process(process_created) + + EventHelper.flush_timer() + + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg6, [:msg7], s.tutorial.nasty + + # Let's answer with the expected reply + # Now we'll reply back and finally proceed to the next step + params = + %{ + "reply_id" => s.tutorial.nasty.msg7, + "contact_id" => s.contact.friend + } + ref = push account_socket, "email.reply", params + assert_reply ref, :ok, _, timeout(:slow) + + [story_reply_sent] = wait_events [:story_reply_sent] + + assert_reply story_reply_sent, :msg7, :msg6, [], s.tutorial.nasty + + EventHelper.flush_timer() + + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg8, [], s.tutorial.nasty + + # Now we'll simulate the user opening the task manager + params = + %{ + "action" => "tutorial_accessed_task_manager" + } + + ref = push account_socket, "client.action", params + assert_reply ref, :ok, _, timeout(:slow) + + # After some time, Contact replies with another message + EventHelper.flush_timer() + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg9, [:msg10], s.tutorial.nasty + + # Automatic reply comes next + EventHelper.flush_timer() + + [story_reply_sent] = wait_events [:story_reply_sent] + + assert_reply story_reply_sent, :msg10, :msg9, [], s.tutorial.nasty + + # Simulate completion of the process now + TOPHelper.force_completion(process) + + [server_password_acquired] = wait_events [:server_password_acquired] + + # Automatic email comes right after process completion + EventHelper.flush_timer() + + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg11, [:msg12], s.tutorial.nasty + + # Next email is sent when user "spots" the targeted virus. Let's simulate + # this by sending this custom action to the backend. + params = + %{ + "action" => "tutorial_spotted_nasty_virus" + } + + ref = push account_socket, "client.action", params + assert_reply ref, :ok, _, timeout(:slow) + + # Soon after auto reply `spotted` will be sent + EventHelper.flush_timer() + + [story_reply_sent] = wait_events [:story_reply_sent] + + assert_reply story_reply_sent, :msg12, :msg11, [], s.tutorial.nasty + + # After which `spotted2` will be automatically sent by the contact + EventHelper.flush_timer() + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg13, [:msg14], s.tutorial.nasty + + # Let's join the target server + topic = + ChannelHelper.server_topic_name(manager.network_id, cur_step.meta.ip) + + join_params = + %{ + "gateway_ip" => gateway_ip, + "password" => server_password_acquired.data.password + } + + # Joins the RCN server + {:ok, _, target_socket} = join(account_socket, topic, join_params) + + # Now we'll start the download of the spyware virus + params = + %{ + "file_id" => cur_step.meta.spyware_id + } + + ref = push target_socket, "file.download", params + assert_reply ref, :ok, _, timeout(:slow) + + # And soon after that, player will automatically start pointless_convo + EventHelper.flush_timer() + [story_reply_sent] = wait_events [:story_reply_sent] + + assert_reply story_reply_sent, :msg14, :msg13, [], s.tutorial.nasty + + # After which Contact automatically replies... + EventHelper.flush_timer() + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg15, [:msg16], s.tutorial.nasty + + # And player automatically bounces back... + EventHelper.flush_timer() + [story_reply_sent] = wait_events [:story_reply_sent] + + assert_reply story_reply_sent, :msg16, :msg15, [], s.tutorial.nasty + + # One more round; auto reply from contact... + EventHelper.flush_timer() + [story_email_sent] = wait_events [:story_email_sent] + + assert_email story_email_sent, :msg17, [:msg18], s.tutorial.nasty + + # And auto reply from player... + EventHelper.flush_timer() + [story_reply_sent] = wait_events [:story_reply_sent] + + assert_reply story_reply_sent, :msg18, :msg17, [], s.tutorial.nasty end end end diff --git a/test/support/channel/setup.ex b/test/support/channel/setup.ex index 65caf198..0ffa16b6 100644 --- a/test/support/channel/setup.ex +++ b/test/support/channel/setup.ex @@ -263,6 +263,10 @@ defmodule Helix.Test.Channel.Setup do end end + @doc """ + Opts: + - socket_opts: Opts to be relayed to `create_socket/1` + """ def join_storyline_server(opts \\ []) do {socket, %{account: account}} = create_socket(opts[:socket_opts] || []) diff --git a/test/support/process/helper/top.ex b/test/support/process/helper/top.ex index e05ff6da..77c22e10 100644 --- a/test/support/process/helper/top.ex +++ b/test/support/process/helper/top.ex @@ -8,6 +8,16 @@ defmodule Helix.Test.Process.TOPHelper do alias Helix.Process.Action.TOP, as: TOPAction + @doc """ + Fetches a process from an event or its ID (binary or Helix.ID). + """ + def fetch_process(event = %{event: "process_created"}), + do: fetch_process(event.data.process_id) + def fetch_process(process_id) when is_binary(process_id), + do: fetch_process(Process.ID.cast!(process_id)) + def fetch_process(process_id = %Process.ID{}), + do: ProcessQuery.fetch(process_id) + @doc """ Stops the TOP of a server. """ diff --git a/test/support/server/helper.ex b/test/support/server/helper.ex index 0cfe9276..59fe09c9 100644 --- a/test/support/server/helper.ex +++ b/test/support/server/helper.ex @@ -18,6 +18,9 @@ defmodule Helix.Test.Server.Helper do @internet_id NetworkHelper.internet_id() + def fetch(server_id = %Server.ID{}), + do: ServerQuery.fetch(server_id) + def get_ip(server, network_id \\ @internet_id) def get_ip(server = %Server{}, network_id), do: get_ip(server.server_id, network_id) diff --git a/test/support/story/vars.ex b/test/support/story/vars.ex index e0ad7491..b50a1802 100644 --- a/test/support/story/vars.ex +++ b/test/support/story/vars.ex @@ -36,7 +36,20 @@ defmodule Helix.Test.Story.Vars do msg2: "nasty_virus2", msg3: "punks1", msg4: "punks2", - msg5: "punks3" + msg5: "punks3", + msg6: "dlayd_much1", + msg7: "dlayd_much2", + msg8: "dlayd_much3", + msg9: "dlayd_much4", + msg10: "noice", + msg11: "nasty_virus3", + msg12: "virus_spotted1", + msg13: "virus_spotted2", + msg14: "pointless_convo1", + msg15: "pointless_convo2", + msg16: "pointless_convo3", + msg17: "pointless_convo4", + msg18: "pointless_convo5" } } } From f73641d823bd07f629aed2945d6f575cd824c8c9 Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Fri, 6 Apr 2018 01:27:43 -0300 Subject: [PATCH 3/5] Use a single event for all ClientActions --- lib/client/{web1 => }/event/action.ex | 18 ++++++------ lib/client/public/public.ex | 10 ++----- lib/event/dispatcher.ex | 6 ++-- lib/story/mission/tutorial/steps.ex | 8 ++++-- lib/story/model/step/macros.ex | 40 ++------------------------- 5 files changed, 22 insertions(+), 60 deletions(-) rename lib/client/{web1 => }/event/action.ex (52%) diff --git a/lib/client/web1/event/action.ex b/lib/client/event/action.ex similarity index 52% rename from lib/client/web1/event/action.ex rename to lib/client/event/action.ex index bd7a6d24..36522806 100644 --- a/lib/client/web1/event/action.ex +++ b/lib/client/event/action.ex @@ -1,32 +1,32 @@ import Helix.Event -# Note to self: When implementing `web2`, use a macro that generates the -# skeleton of this event and each client then specializes it. -defmodule Helix.Client.Web1.Event.Action do +defmodule Helix.Client.Event.Action do event Performed do @moduledoc """ - `Web1ActionPerformedEvent` is emitted when the client performed a custom + `ClientActionPerformedEvent` is emitted when the client performed a custom action that should be tracked by the backend for a specific behaviour. Examples include the tutorial quest, which awaits for the player to open apps in order to proceed with the storyline. """ alias Helix.Entity.Model.Entity - alias Helix.Client.Web1.Model.Web1 + alias Helix.Client.Model.Client - event_struct [:entity_id, :action] + event_struct [:client, :entity_id, :action] @type t :: %__MODULE__{ + client: Client.t, entity_id: Entity.id, - action: Web1.action + action: Client.action } - @spec new(Entity.id, Web1.action) :: + @spec new(Client.t, Entity.id, Client.action) :: t - def new(entity_id, action) do + def new(client, entity_id, action) do %__MODULE__{ + client: client, entity_id: entity_id, action: action } diff --git a/lib/client/public/public.ex b/lib/client/public/public.ex index e76fdd68..7f013c4b 100644 --- a/lib/client/public/public.ex +++ b/lib/client/public/public.ex @@ -9,7 +9,7 @@ defmodule Helix.Client.Public.Client do alias Helix.Client.Model.Client alias Helix.Client.Web1.Public.Bootstrap, as: Web1Bootstrap - alias Helix.Client.Web1.Event.Action.Performed, as: Web1ActionPerformedEvent + alias Helix.Client.Event.Action.Performed, as: ClientActionPerformedEvent @typep bootstrap_result :: Web1Bootstrap.bootstrap @typep render_bootstrap_result :: Web1Bootstrap.rendered_bootstrap @@ -39,12 +39,11 @@ defmodule Helix.Client.Public.Client do It's up to the handlers of the `ClientActionPerformedEvent` to determine what should be done with such information. - Emits: `Web1ActionPerformedEvent` + Emits: `ClientActionPerformedEvent` """ def broadcast_action(client, entity_id, action) do client - |> get_action_event() - |> apply(:new, [entity_id, action]) + |> ClientActionPerformedEvent.new(entity_id, action) |> Event.emit() end @@ -61,7 +60,4 @@ defmodule Helix.Client.Public.Client do do: Web1Bootstrap.render_bootstrap(bootstrap) defp dispatch(_, :render_bootstrap, _), do: %{} - - defp get_action_event(:web1), - do: Web1ActionPerformedEvent end diff --git a/lib/event/dispatcher.ex b/lib/event/dispatcher.ex index 2a0b7287..3cadc6e7 100644 --- a/lib/event/dispatcher.ex +++ b/lib/event/dispatcher.ex @@ -40,7 +40,7 @@ defmodule Helix.Event.Dispatcher do alias Helix.Core.Listener.Event.Handler.Listener, as: ListenerHandler alias Helix.Account.Event, as: AccountEvent alias Helix.Account.Event.Handler, as: AccountHandler - alias Helix.Client.Web1.Event, as: Web1Event + alias Helix.Client.Event, as: ClientEvent alias Helix.Entity.Event, as: EntityEvent alias Helix.Entity.Event.Handler, as: EntityHandler alias Helix.Log.Event, as: LogEvent @@ -86,10 +86,10 @@ defmodule Helix.Event.Dispatcher do ############################################################################## # All - event Web1Event.Action.Performed + event ClientEvent.Action.Performed # Custom handlers - event Web1Event.Action.Performed, + event ClientEvent.Action.Performed, StoryHandler.Story, :event_handler diff --git a/lib/story/mission/tutorial/steps.ex b/lib/story/mission/tutorial/steps.ex index 847967c8..04ccadb1 100644 --- a/lib/story/mission/tutorial/steps.ex +++ b/lib/story/mission/tutorial/steps.ex @@ -173,7 +173,7 @@ defmodule Helix.Story.Mission.Tutorial do alias Helix.Software.Model.File alias Helix.Story.Action.Context, as: ContextAction - alias Helix.Client.Web1.Event.Action.Performed, as: Web1ActionPerformedEvent + alias Helix.Client.Event.Action.Performed, as: ClientActionPerformedEvent alias Helix.Server.Event.Server.Password.Acquired, as: ServerPasswordAcquiredEvent @@ -314,7 +314,8 @@ defmodule Helix.Story.Mission.Tutorial do # Send `dlayd_much4` email when player opens TaskManager app filter( _step, - %Web1ActionPerformedEvent{ + %ClientActionPerformedEvent{ + client: _, action: :tutorial_accessed_task_manager }, _meta, @@ -324,7 +325,8 @@ defmodule Helix.Story.Mission.Tutorial do # Send `virus_spotted1` reply when player spots the virus filter( _step, - %Web1ActionPerformedEvent{ + %ClientActionPerformedEvent{ + client: _, action: :tutorial_spotted_nasty_virus }, _meta, diff --git a/lib/story/model/step/macros.ex b/lib/story/model/step/macros.ex index 3bde9440..6beb20e0 100644 --- a/lib/story/model/step/macros.ex +++ b/lib/story/model/step/macros.ex @@ -16,7 +16,6 @@ defmodule Helix.Story.Model.Step.Macros do alias Helix.Story.Action.Story, as: StoryAction alias Helix.Story.Query.Story, as: StoryQuery - alias Helix.Client.Web1.Event.Action.Performed, as: Web1ActionPerformedEvent alias Helix.Process.Event.Process.Created, as: ProcessCreatedEvent alias Helix.Story.Event.Email.Sent, as: StoryEmailSentEvent alias Helix.Story.Event.Reply.Sent, as: StoryReplySentEvent @@ -166,18 +165,6 @@ defmodule Helix.Story.Model.Step.Macros do {:noop, []} end end - - @doc """ - Callback used to filter the client action, making sure the action - broadcasted is the desired one. If so, relays to the proper callback. - """ - callback :cb_client_action, event, meta do - if to_string(event.action) == meta.action do - relay_callback meta.relay_cb, event, meta - else - {:noop, []} - end - end end end end @@ -302,8 +289,8 @@ defmodule Helix.Story.Model.Step.Macros do end end - defp get_event_from_action(:client_action), - do: Web1ActionPerformedEvent + defp get_event_from_action(:process_started), + do: ProcessCreatedEvent @doc """ Listener that triggers once the process of type `type` acts over `element_id`. @@ -340,29 +327,6 @@ defmodule Helix.Story.Model.Step.Macros do end end - @doc """ - Listeners that triggers once the player has performed `action` on the client. - """ - defmacro on_client_action(_client, action, entity_id, callback) do - quote do - - # TODO: Proper handler for multiple clients based on the `client` var - event = Web1ActionPerformedEvent - - {callback_name, extra_meta} = get_callback_data(unquote(callback)) - - meta = - %{ - action: to_string(unquote(action)), - relay_cb: callback_name - } - |> Map.merge(extra_meta) - - story_listen unquote(entity_id), event, meta, :cb_client_action - - end - end - @doc """ Formats the step metadata, automatically handling empty maps or atomizing existing map keys. From 31724acef666828644e1170458016451175335d0 Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Tue, 29 May 2018 10:42:20 -0300 Subject: [PATCH 4/5] Finish NastyVirus chapter --- events.json | 29 +++++++++--- lib/event/dispatcher.ex | 3 ++ lib/process/event/process.ex | 11 ----- lib/story/mission/tutorial/steps.ex | 46 +++++++------------ lib/story/model/step.ex | 2 + lib/story/model/step/macros.ex | 71 +++++++++++++++++++++++++++++ 6 files changed, 114 insertions(+), 48 deletions(-) diff --git a/events.json b/events.json index 54687af3..8ea3dbc9 100644 --- a/events.json +++ b/events.json @@ -115,9 +115,11 @@ }, "Story.Story": { "receives": [ + "Client.Action.Performed", + "Process.Created", "Story.Step.Email.Sent", "Story.Step.Reply.Sent", - "Story.Step.ActionRequested" + "Story.Step.ActionRequested", ], "emits": [ "Story.Step.Proceeded", @@ -188,8 +190,8 @@ "File.Added", "File.Deleted", "File.Uploaded", - "StoryEmailSentEvent", - "StoryReplySentEvent", + "Story.Email.Sent", + "Story.Reply.Sent", "Story.Step.Restarted", "Story.Step.Proceeded", "Virus.Installed", @@ -199,16 +201,29 @@ "Tutorial": { "steps": { "SetupPC": { - "filters": ["Account.Created"], - "emits": [] + "filters": [], + "emits": ["Story.Email.Sent"] }, "DownloadCracker": { "filters": [ "File.Downloaded", - "File.Deleted" + "File.Deleted", + "Process.Created" + ], + "emits": [ + "File.Added", + "Story.Email.Sent" + ] + }, + "NastyVirus": { + "filters": [ + "Process.Created", + "Client.Action.Performed", + "Server.Password.Acquired" ], "emits": [ - "File.Added" + "File.Added", + "Story.Email.Sent" ] } } diff --git a/lib/event/dispatcher.ex b/lib/event/dispatcher.ex index 3cadc6e7..926efa6a 100644 --- a/lib/event/dispatcher.ex +++ b/lib/event/dispatcher.ex @@ -152,6 +152,9 @@ defmodule Helix.Event.Dispatcher do event ProcessEvent.Process.Created, ProcessHandler.TOP, :recalque_handler + event ProcessEvent.Process.Created, + StoryHandler.Story, + :event_handler event ProcessEvent.TOP.BringMeToLife, ProcessHandler.TOP, diff --git a/lib/process/event/process.ex b/lib/process/event/process.ex index e816c349..e7680d62 100644 --- a/lib/process/event/process.ex +++ b/lib/process/event/process.ex @@ -118,17 +118,6 @@ defmodule Helix.Process.Event.Process do def whom_to_notify(event), do: %{server: [event.gateway_id, event.target_id]} end - - listenable do - listen(event = %_{confirmed: true}) do - [ - event.process.gateway_id, - event.process.target_id, - event.process.src_file_id, - event.process.tgt_file_id, - ] - end - end end event Completed do diff --git a/lib/story/mission/tutorial/steps.ex b/lib/story/mission/tutorial/steps.ex index 04ccadb1..42170424 100644 --- a/lib/story/mission/tutorial/steps.ex +++ b/lib/story/mission/tutorial/steps.ex @@ -116,9 +116,6 @@ defmodule Helix.Story.Mission.Tutorial do # Listeners hespawn fn -> - # Send `about_that` when download starts - on_download_started cracker.file_id, email: "about_that", sleep: 2 - # Reply `downloaded` when the cracker has been downloaded story_listen cracker.file_id, FileDownloadedEvent, reply: "downloaded" @@ -129,6 +126,11 @@ defmodule Helix.Story.Mission.Tutorial do {meta, %{}, e1 ++ e2 ++ e3 ++ e4} end + # Filters + + # Send `about_that` when download starts + filter_download_started :cracker_id, send: "about_that", sleep: 2 + # Callbacks callback :on_file_deleted, _event do @@ -173,7 +175,6 @@ defmodule Helix.Story.Mission.Tutorial do alias Helix.Software.Model.File alias Helix.Story.Action.Context, as: ContextAction - alias Helix.Client.Event.Action.Performed, as: ClientActionPerformedEvent alias Helix.Server.Event.Server.Password.Acquired, as: ServerPasswordAcquiredEvent @@ -294,16 +295,10 @@ defmodule Helix.Story.Mission.Tutorial do # Listeners hespawn fn -> - # Send `dlayd_much1` when bruteforce starts - on_bruteforce_started server.server_id, email: "dlayd_much1", sleep: 2 - # Send `nasty_virus3` when bruteforce finishes story_listen server.server_id, ServerPasswordAcquiredEvent, email: "nasty_virus3", sleep: 2 - # Send `pointless_convo1` when download starts - on_download_started spyware.file_id, reply: "pointless_convo1", sleep: 2 - end {meta, %{}, e1 ++ e2} @@ -311,27 +306,18 @@ defmodule Helix.Story.Mission.Tutorial do # Filters + # Send `dlayd_much1` when bruteforce starts + filter_bruteforce_started :server_id, send: "dlayd_much1", sleep: 2 + + # Send `pointless_convo1` when download starts + filter_download_started :spyware_id, reply: "pointless_convo1", sleep: 2 + # Send `dlayd_much4` email when player opens TaskManager app - filter( - _step, - %ClientActionPerformedEvent{ - client: _, - action: :tutorial_accessed_task_manager - }, - _meta, - send: "dlayd_much4", send_opts: [sleep: 1] - ) - - # Send `virus_spotted1` reply when player spots the virus - filter( - _step, - %ClientActionPerformedEvent{ - client: _, - action: :tutorial_spotted_nasty_virus - }, - _meta, - reply: "virus_spotted1", send_opts: [sleep: 1] - ) + filter_client_action :tutorial_accessed_task_manager, + send: "dlayd_much4", sleep: 1 + + filter_client_action :tutorial_spotted_nasty_virus, + reply: "virus_spotted1", sleep: 1 def start(step) do {meta, _, e1} = setup(step) diff --git a/lib/story/model/step.ex b/lib/story/model/step.ex index bafb9123..9ec32861 100644 --- a/lib/story/model/step.ex +++ b/lib/story/model/step.ex @@ -203,6 +203,8 @@ defmodule Helix.Story.Model.Step do do: entity_id def get_entity(%_{source_entity_id: entity_id}), do: entity_id + def get_entity(%_{process: %{source_entity_id: entity_id}}), + do: entity_id def get_entity(_), do: false diff --git a/lib/story/model/step/macros.ex b/lib/story/model/step/macros.ex index 6beb20e0..dc7c0184 100644 --- a/lib/story/model/step/macros.ex +++ b/lib/story/model/step/macros.ex @@ -16,6 +16,7 @@ defmodule Helix.Story.Model.Step.Macros do alias Helix.Story.Action.Story, as: StoryAction alias Helix.Story.Query.Story, as: StoryQuery + alias Helix.Client.Event.Action.Performed, as: ClientActionPerformedEvent alias Helix.Process.Event.Process.Created, as: ProcessCreatedEvent alias Helix.Story.Event.Email.Sent, as: StoryEmailSentEvent alias Helix.Story.Event.Reply.Sent, as: StoryReplySentEvent @@ -314,6 +315,56 @@ defmodule Helix.Story.Model.Step.Macros do end end + defmacro filter_download_started(meta_field, callback), + do: process_filter(:file_download, :tgt_file_id, meta_field, callback) + + defmacro filter_bruteforce_started(meta_field, callback), + do: process_filter(:cracker_bruteforce, :target_id, meta_field, callback) + + defmacro filter_process_created(type, field, meta_field, callback) do + quote do + + filter( + _step, + %ProcessCreatedEvent{ + process: %{ + :type => unquote(type), + unquote(:"#{field}") => expected_id + }, + }, + %{ + unquote(:"#{meta_field}") => expected_id + }, + unquote(callback) + ) + + end + end + + defp process_filter(type, field, meta_field, callback) do + quote do + filter_process_created( + unquote(type), unquote(field), unquote(meta_field), unquote(callback) + ) + end + end + + defmacro filter_client_action(action, callback, client \\ quote(do: _)) do + quote do + + filter( + _step, + %ClientActionPerformedEvent{ + client: unquote(client), + action: unquote(action) + }, + _meta, + unquote(callback) + ) + + end + end + defmacro on_download_started(elem_id, callback), do: process_listener(:file_download, elem_id, callback, :tgt_file_id) @@ -455,6 +506,15 @@ defmodule Helix.Story.Model.Step.Macros do {{:send_reply, unquote(reply_id), unquote(send_opts)}, step, []} end + [reply: reply_id, sleep: sleep] -> + quote do + { + {:send_reply, unquote(reply_id), [sleep: unquote(sleep)]}, + step, + [] + } + end + [send: email_id] -> quote do meta = Keyword.get(unquote(opts), :meta, %{}) @@ -473,6 +533,17 @@ defmodule Helix.Story.Model.Step.Macros do } end + [send: email_id, sleep: slp] -> + quote do + meta = Keyword.get(unquote(opts), :meta, %{}) + + { + {:send_email, unquote(email_id), meta, [sleep: unquote(slp)]}, + step, + [] + } + end + [restart: true, reason: reason, checkpoint: checkpoint] -> quote do {{:restart, unquote(reason), unquote(checkpoint)}, step, []} From ed6eb1eb750d5e066a26772293db908965059df6 Mon Sep 17 00:00:00 2001 From: Renato Massaro Date: Thu, 14 Jun 2018 18:35:51 -0300 Subject: [PATCH 5/5] Adapt tests to slower Jenkins system --- test/features/process/progress_test.exs | 18 +++++++++++++----- .../storyline/quests/tutorial_test.exs | 5 +++++ test/features/virus/collect_test.exs | 6 +++--- .../websocket/requests/virus/collect_test.exs | 16 +++++++++++++--- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/test/features/process/progress_test.exs b/test/features/process/progress_test.exs index 63a62a57..40f23c61 100644 --- a/test/features/process/progress_test.exs +++ b/test/features/process/progress_test.exs @@ -49,23 +49,23 @@ defmodule Helix.Test.Features.Process.Progress do # We've just started the downloaded, so it ran ~0% download0 = ProcessQuery.fetch(download_id) - assert_in_delta download0.percentage, 0, 0.07 - assert_in_delta download0.time_left, 1.0, 0.07 + assert_in_delta download0.percentage, 0, error_delta() + assert_in_delta download0.time_left, 1.0, error_delta() # Sleep 100ms, which is about 10% :timer.sleep(100) # Yep, we are somewhere near 10% download1 = ProcessQuery.fetch(download_id) - assert_in_delta download1.percentage, 0.1, 0.07 - assert_in_delta download1.time_left, 0.9, 0.07 + assert_in_delta download1.percentage, 0.1, error_delta() + assert_in_delta download1.time_left, 0.9, error_delta() # Sleep another 100ms (+10%) :timer.sleep(100) # Percentage is at 20% download2 = ProcessQuery.fetch(download_id) - assert_in_delta download2.percentage, 0.2, 0.07 + assert_in_delta download2.percentage, 0.2, error_delta() # Now we'll tragically make increase the download time. # The time_left will increase, but the percentage shouldn't change @@ -82,5 +82,13 @@ defmodule Helix.Test.Features.Process.Progress do # Percentage barely changed assert_in_delta download3.percentage, download2.percentage, 0.01 end + + def error_delta do + if System.get_env("HELIX_TEST_ENV") == "jenkins" do + 0.09 + else + 0.05 + end + end end end diff --git a/test/features/storyline/quests/tutorial_test.exs b/test/features/storyline/quests/tutorial_test.exs index 98922821..e6b1f8f8 100644 --- a/test/features/storyline/quests/tutorial_test.exs +++ b/test/features/storyline/quests/tutorial_test.exs @@ -125,6 +125,11 @@ defmodule Helix.Test.Features.Storyline.Quests.Tutorial do # Flush pending messages EventHelper.flush_timer() + # Jenkins slowpoke + if System.get_env("HELIX_TEST_ENV") == "jenkins" do + :timer.sleep(500) + end + # And soon after that we'll receive yet another email and proceed to the # next step [story_email_sent, story_step_proceeded] = diff --git a/test/features/virus/collect_test.exs b/test/features/virus/collect_test.exs index 0e7fd8ae..69495447 100644 --- a/test/features/virus/collect_test.exs +++ b/test/features/virus/collect_test.exs @@ -192,13 +192,13 @@ defmodule Helix.Test.Features.Virus.CollectTest do bank_acc.balance + expected_earnings1 + expected_earnings2 # Both viruses had their `running_time` reset to 0 - # (returning 1 is OK because slower systems (travis)) + # (returning 1 or 2 is OK because slower systems (travis)) new_virus1 = VirusQuery.fetch(file1.file_id) - assert_in_delta new_virus1.running_time, 0, 1 + assert_in_delta new_virus1.running_time, 0, 2.01 assert new_virus1.is_active? new_virus2 = VirusQuery.fetch(file2.file_id) - assert_in_delta new_virus2.running_time, 0, 1 + assert_in_delta new_virus2.running_time, 0, 2.01 assert new_virus2.is_active? # Processes no longer exist diff --git a/test/software/websocket/requests/virus/collect_test.exs b/test/software/websocket/requests/virus/collect_test.exs index 04aa11d3..0117492f 100644 --- a/test/software/websocket/requests/virus/collect_test.exs +++ b/test/software/websocket/requests/virus/collect_test.exs @@ -2,6 +2,8 @@ defmodule Helix.Software.Websocket.Requests.Virus.CollectTest do use Helix.Test.Case.Integration + import Helix.Test.Macros + alias Helix.Websocket.Requestable alias Helix.Process.Query.Process, as: ProcessQuery alias Helix.Software.Websocket.Requests.Virus.Collect, as: VirusCollectRequest @@ -113,6 +115,7 @@ defmodule Helix.Software.Websocket.Requests.Virus.CollectTest do end describe "check_permissions/2" do + skip_on_travis_slowpoke() test "accepts when data is valid" do {gateway, %{entity: entity}} = ServerSetup.server() @@ -155,10 +158,17 @@ defmodule Helix.Software.Websocket.Requests.Virus.CollectTest do assert request.meta.gateway == gateway assert request.meta.payment_info == {bank_account, nil} assert request.meta.bounce == bounce + assert [ - %{file: file1, virus: virus1}, - %{file: file2, virus: virus2}, - ] == request.meta.viruses + %{file: req_file1, virus: req_virus1}, + %{file: req_file2, virus: req_virus2}, + ] = request.meta.viruses + + assert req_file1 == file1 + assert_map req_virus1, virus1, skip: :running_time + + assert req_file2 == file2 + assert_map req_virus2, virus2, skip: :running_time end test "rejects when bad things happen" do