Rework `send_params` env key test

Rework `send_params` env key test

The default value of `send_params` is already `true` so testing if the
`true` value gets set doesn't actually test anything. It passed before
the `send_params` option was supported, but the test didn't actually
test anything.

defmodule Appsignal.ConfigTest do
  @moduledoc """
  Test the configuratoin
  """

  use ExUnit.Case
  import AppsignalTest.Utils
  import ExUnit.CaptureIO
  alias Appsignal.{Config, Nif}

  setup do
    environment = freeze_environment()
    Application.delete_env(:appsignal, :config)
    Application.delete_env(:appsignal, :config_sources)

    ExUnit.Callbacks.on_exit(fn ->
      unfreeze_environment(environment)
    end)
  end

  describe "initialize" do
    test "stores sources in Application" do
      init_config()

      default =
        default_configuration()
        |> Map.delete(:valid)

      assert Application.get_env(:appsignal, :config_sources) == %{
               default: default,
               system: %{},
               file: %{},
               env: %{}
             }
    end

    test "stores file sources in Application" do
      config = %{name: "My app", active: true}

      assert with_config(
               config,
               fn ->
                 init_config()
                 Application.get_env(:appsignal, :config_sources)[:file]
               end
             ) == config
    end
  end

  test "unconfigured" do
    assert {:error, :invalid_config} = Config.initialize()
  end

  test "minimum config from OS env" do
    assert with_env(
             %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
             &Config.initialize/0
           ) == :ok
  end

  test "minimum config from application env" do
    assert with_config(
             %{push_api_key: "00000000-0000-0000-0000-000000000000"},
             &Config.initialize/0
           ) == :ok
  end

  test "default configuration" do
    assert default_configuration() == init_config()
  end

  describe "configured_as_active?" do
    test "when active" do
      assert with_config(
               %{active: true, valid: true},
               &Config.configured_as_active?/0
             )
    end

    test "when not active" do
      refute with_config(
               %{active: false, valid: true},
               &Config.configured_as_active?/0
             )
    end
  end

  describe "active?" do
    test "when active and valid" do
      assert with_config(
               %{active: true, valid: true},
               &Config.active?/0
             )
    end

    test "when active but not valid" do
      refute with_config(
               %{active: true, valid: false},
               &Config.active?/0
             )
    end

    test "when not active and not valid" do
      refute with_config(
               %{active: false, valid: true},
               &Config.active?/0
             )
    end
  end

  describe "request_headers" do
    test "is nil by default" do
      assert with_config(%{}, &Config.request_headers/0) == nil
    end

    test "returns the request_headers config" do
      assert with_config(%{request_headers: []}, &Config.request_headers/0) == []
    end
  end

  describe "ca_file_path" do
    setup do
      Config.initialize()
      :ok
    end

    test "uses the priv path by default" do
      assert with_config(%{}, &Config.ca_file_path/0) ==
               Path.join(:code.priv_dir(:appsignal), "cacert.pem")
    end

    test "returns user override when set" do
      assert with_config(%{ca_file_path: "/foo/bar/bat.ca"}, &Config.ca_file_path/0) ==
               "/foo/bar/bat.ca"
    end

    test "returns nil when set to nil" do
      assert with_config(%{ca_file_path: nil}, &Config.ca_file_path/0) == nil
    end
  end

  describe "using the application environment" do
    test "active" do
      assert with_config(%{active: true}, &init_config/0) ==
               default_configuration() |> Map.put(:active, true)
    end

    test "ca_file_path" do
      assert with_config(%{ca_file_path: "/foo/bar/baz.ca"}, &init_config/0) ==
               default_configuration() |> Map.put(:ca_file_path, "/foo/bar/baz.ca")
    end

    test "debug" do
      assert with_config(%{debug: true}, &init_config/0) ==
               default_configuration() |> Map.put(:debug, true)
    end

    test "dns_servers" do
      assert with_config(%{dns_servers: ["8.8.8.8", "8.8.4.4"]}, &init_config/0) ==
               default_configuration() |> Map.put(:dns_servers, ["8.8.8.8", "8.8.4.4"])
    end

    test "enable_host_metrics" do
      assert with_config(%{enable_host_metrics: false}, &init_config/0) ==
               default_configuration() |> Map.put(:enable_host_metrics, false)
    end

    test "endpoint" do
      assert with_config(%{endpoint: "https://push.staging.lol"}, &init_config/0) ==
               default_configuration() |> Map.put(:endpoint, "https://push.staging.lol")
    end

    test "env" do
      assert with_config(%{env: :prod}, &init_config/0) ==
               default_configuration() |> Map.put(:env, :prod)
    end

    test "filter_parameters" do
      assert with_config(%{filter_parameters: ~w(password secret)}, &init_config/0) ==
               default_configuration() |> Map.put(:filter_parameters, ~w(password secret))
    end

    test "filter_session_data" do
      assert with_config(%{filter_session_data: ~w(accept connection)}, &init_config/0) ==
               default_configuration() |> Map.put(:filter_session_data, ~w(accept connection))
    end

    test "frontend_error_catching_path" do
      assert with_config(
               %{frontend_error_catching_path: "/appsignal_error_catcher"},
               &init_config/0
             ) ==
               default_configuration()
               |> Map.put(:frontend_error_catching_path, "/appsignal_error_catcher")
    end

    test "hostname" do
      assert with_config(%{hostname: "Bobs-MBP.example.com"}, &init_config/0) ==
               default_configuration() |> Map.put(:hostname, "Bobs-MBP.example.com")
    end

    test "http_proxy" do
      assert with_config(%{http_proxy: "http://10.10.10.10:8888"}, &init_config/0) ==
               default_configuration() |> Map.put(:http_proxy, "http://10.10.10.10:8888")
    end

    test "ignore_actions" do
      actions = ~w(
          ExampleApplication.PageController#ignored
          ExampleApplication.PageController#also_ignored
      )

      assert with_config(%{ignore_actions: actions}, &init_config/0) ==
               default_configuration() |> Map.put(:ignore_actions, actions)
    end

    test "ignore_errors" do
      errors = ~w(VerySpecificError AnotherError)

      assert with_config(%{ignore_errors: errors}, &init_config/0) ==
               default_configuration() |> Map.put(:ignore_errors, errors)
    end

    test "ignore_namespaces" do
      namespaces = ~w(admin private_namespace)

      assert with_config(%{ignore_namespaces: namespaces}, &init_config/0) ==
               default_configuration() |> Map.put(:ignore_namespaces, namespaces)
    end

    test "log" do
      assert with_config(%{log: "stdout"}, &init_config/0) ==
               default_configuration() |> Map.put(:log, "stdout")
    end

    test "log_path" do
      log_path = File.cwd!()

      assert with_config(%{log_path: log_path}, &init_config/0) ==
               default_configuration() |> Map.put(:log_path, log_path)
    end

    test "name" do
      assert with_config(%{name: "AppSignal test suite app"}, &init_config/0) ==
               default_configuration() |> Map.put(:name, "AppSignal test suite app")
    end

    test "push_api_key" do
      assert with_config(%{push_api_key: "00000000-0000-0000-0000-000000000000"}, &init_config/0) ==
               valid_configuration() |> Map.put(:active, false)
    end

    test "running_in_container" do
      assert with_config(%{running_in_container: true}, &init_config/0) ==
               default_configuration() |> Map.put(:running_in_container, true)
    end

    test "send_params" do
      assert with_config(%{send_params: true}, &init_config/0) ==
               default_configuration() |> Map.put(:send_params, true)
    end

    test "skip_session_data" do
      assert with_config(%{skip_session_data: true}, &init_config/0) ==
               default_configuration() |> Map.put(:skip_session_data, true)
    end

    test "files_world_accessible" do
      assert with_config(%{files_world_accessible: true}, &init_config/0) ==
               default_configuration() |> Map.put(:files_world_accessible, true)
    end

    test "working_dir_path" do
      without_logger(fn ->
        assert with_config(%{working_dir_path: "/tmp/appsignal"}, &init_config/0) ==
                 default_configuration() |> Map.put(:working_dir_path, "/tmp/appsignal")
      end)
    end

    test "working_directory_path" do
      assert with_config(%{working_directory_path: "/tmp/appsignal"}, &init_config/0) ==
               default_configuration() |> Map.put(:working_directory_path, "/tmp/appsignal")
    end

    test "request_headers" do
      assert with_config(%{request_headers: ~w(accept accept-charset)}, &init_config/0) ==
               default_configuration() |> Map.put(:request_headers, ~w(accept accept-charset))
    end

    test "revision" do
      assert with_config(%{revision: "03bd9e"}, &init_config/0) ==
               default_configuration() |> Map.put(:revision, "03bd9e")
    end
  end

  describe "using the system environment" do
    test "stores system env source in Application" do
      assert with_env(
               %{"APPSIGNAL_ACTIVE" => "true", "APPSIGNAL_DEBUG" => "true"},
               fn ->
                 init_config()
                 Application.get_env(:appsignal, :config_sources)[:env]
               end
             ) == %{active: true, debug: true}
    end

    test "active" do
      assert with_env(
               %{"APPSIGNAL_ACTIVE" => "true"},
               &init_config/0
             ) == default_configuration() |> Map.put(:active, true)
    end

    test "ca_file_path" do
      assert with_env(
               %{"APPSIGNAL_CA_FILE_PATH" => "/foo/bar/baz.ca"},
               &init_config/0
             ) == default_configuration() |> Map.put(:ca_file_path, "/foo/bar/baz.ca")
    end

    test "debug" do
      assert with_env(
               %{"APPSIGNAL_DEBUG" => "true"},
               &init_config/0
             ) == default_configuration() |> Map.put(:debug, true)
    end

    test "dns_servers" do
      assert with_env(
               %{"APPSIGNAL_DNS_SERVERS" => "8.8.8.8,8.8.4.4"},
               &init_config/0
             ) == default_configuration() |> Map.put(:dns_servers, ["8.8.8.8", "8.8.4.4"])
    end

    test "enable_host_metrics" do
      assert with_env(
               %{"APPSIGNAL_ENABLE_HOST_METRICS" => "false"},
               &init_config/0
             ) == default_configuration() |> Map.put(:enable_host_metrics, false)
    end

    test "endpoint" do
      assert with_env(
               %{"APPSIGNAL_PUSH_API_ENDPOINT" => "https://push.staging.lol"},
               &init_config/0
             ) == default_configuration() |> Map.put(:endpoint, "https://push.staging.lol")
    end

    test "env" do
      assert with_env(
               %{"APPSIGNAL_APP_ENV" => "prod"},
               &init_config/0
             ) == default_configuration() |> Map.put(:env, :prod)
    end

    test "filter_parameters" do
      assert with_env(
               %{"APPSIGNAL_FILTER_PARAMETERS" => "password,secret"},
               &init_config/0
             ) == default_configuration() |> Map.put(:filter_parameters, ~w(password secret))
    end

    test "filter_session_data" do
      assert with_env(
               %{"APPSIGNAL_FILTER_SESSION_DATA" => "accept,connection"},
               &init_config/0
             ) == default_configuration() |> Map.put(:filter_session_data, ~w(accept connection))
    end

    test "frontend_error_catching_path" do
      assert with_env(
               %{"APPSIGNAL_FRONTEND_ERROR_CATCHING_PATH" => "/appsignal_error_catcher"},
               &init_config/0
             ) ==
               default_configuration()
               |> Map.put(:frontend_error_catching_path, "/appsignal_error_catcher")
    end

    test "hostname" do
      assert with_env(
               %{"APPSIGNAL_HOSTNAME" => "Bobs-MBP.example.com"},
               &init_config/0
             ) == default_configuration() |> Map.put(:hostname, "Bobs-MBP.example.com")
    end

    test "http_proxy" do
      assert with_env(
               %{"APPSIGNAL_HTTP_PROXY" => "http://10.10.10.10:8888"},
               &init_config/0
             ) == default_configuration() |> Map.put(:http_proxy, "http://10.10.10.10:8888")
    end

    test "ignore_actions" do
      assert with_env(
               %{
                 "APPSIGNAL_IGNORE_ACTIONS" =>
                   "ExampleApplication.PageController#ignored,ExampleApplication.PageController#also_ignored"
               },
               &init_config/0
             ) == default_configuration() |> Map.put(:ignore_actions, ~w(
          ExampleApplication.PageController#ignored
          ExampleApplication.PageController#also_ignored
      ))
    end

    test "ignore_errors" do
      assert with_env(
               %{"APPSIGNAL_IGNORE_ERRORS" => "VerySpecificError,AnotherError"},
               &init_config/0
             ) ==
               default_configuration()
               |> Map.put(:ignore_errors, ~w(VerySpecificError AnotherError))
    end

    test "ignore_namespaces" do
      assert with_env(
               %{"APPSIGNAL_IGNORE_NAMESPACES" => "admin,private_namespace"},
               &init_config/0
             ) ==
               default_configuration() |> Map.put(:ignore_namespaces, ~w(admin private_namespace))
    end

    test "log" do
      assert with_env(
               %{"APPSIGNAL_LOG" => "stdout"},
               &init_config/0
             ) == default_configuration() |> Map.put(:log, "stdout")
    end

    test "log_path" do
      log_path = File.cwd!()

      assert with_env(
               %{"APPSIGNAL_LOG_PATH" => log_path},
               &init_config/0
             ) == default_configuration() |> Map.put(:log_path, log_path)
    end

    test "name" do
      assert with_env(
               %{"APPSIGNAL_APP_NAME" => "AppSignal test suite app"},
               &init_config/0
             ) == default_configuration() |> Map.put(:name, "AppSignal test suite app")
    end

    test "push_api_key" do
      assert with_env(
               %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
               &init_config/0
             ) == valid_configuration()
    end

    test "running_in_container" do
      assert with_env(
               %{"APPSIGNAL_RUNNING_IN_CONTAINER" => "true"},
               &init_config/0
             ) == default_configuration() |> Map.put(:running_in_container, true)
    end

    test "send_params" do
      assert with_env(
               %{"APPSIGNAL_SEND_PARAMS" => "false"},
               &init_config/0
             ) == default_configuration() |> Map.put(:send_params, false)
    end

    test "skip_session_data" do
      assert with_env(
               %{"APPSIGNAL_SKIP_SESSION_DATA" => "true"},
               &init_config/0
             ) == default_configuration() |> Map.put(:skip_session_data, true)
    end

    test "files_world_accessible" do
      assert with_env(
               %{"APPSIGNAL_FILES_WORLD_ACCESSIBLE" => "false"},
               &init_config/0
             ) == default_configuration() |> Map.put(:files_world_accessible, false)
    end

    test "working_dir_path" do
      without_logger(fn ->
        assert with_env(
                 %{"APPSIGNAL_WORKING_DIR_PATH" => "/tmp/appsignal"},
                 &init_config/0
               ) == default_configuration() |> Map.put(:working_dir_path, "/tmp/appsignal")
      end)
    end

    test "working_directory_path" do
      assert with_env(
               %{"APPSIGNAL_WORKING_DIRECTORY_PATH" => "/tmp/appsignal"},
               &init_config/0
             ) == default_configuration() |> Map.put(:working_directory_path, "/tmp/appsignal")
    end

    test "request_headers" do
      assert with_env(
               %{"APPSIGNAL_REQUEST_HEADERS" => "accept,accept-charset"},
               &init_config/0
             ) == default_configuration() |> Map.put(:request_headers, ~w(accept accept-charset))
    end

    test "revision" do
      assert with_env(
               %{"APP_REVISION" => "03bd9e"},
               &init_config/0
             ) == default_configuration() |> Map.put(:revision, "03bd9e")
    end
  end

  describe "config based on system" do
    test "system environment overwrites application environment configuration" do
      assert with_env(
               %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
               &init_config/0
             ) == valid_configuration() |> Map.put(:active, true)

      assert with_config(%{active: false}, fn ->
               with_env(
                 %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
                 &init_config/0
               )
             end) == valid_configuration() |> Map.put(:active, false)
    end

    test "stores system source in Application" do
      assert with_env(
               %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
               fn ->
                 init_config()
                 Application.get_env(:appsignal, :config_sources)[:system]
               end
             ) == %{active: true}

      assert with_config(%{active: false}, fn ->
               with_env(
                 %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
                 fn ->
                   init_config()
                   Application.get_env(:appsignal, :config_sources)[:system]
                 end
               )
             end) == %{active: true}
    end
  end

  describe "when on Heroku" do
    setup do
      setup_with_env(%{"DYNO" => "web.1"})
    end

    test "stores system source in Application" do
      init_config()

      assert Application.get_env(:appsignal, :config_sources)[:system] == %{
               log: "stdout",
               running_in_container: true
             }
    end

    test ":running_in_container and :log" do
      config =
        default_configuration()
        |> Map.put(:running_in_container, true)
        |> Map.put(:log, "stdout")

      assert config == init_config()
    end

    test "application environment overwrites :running_in_container config on Heroku" do
      assert with_config(%{running_in_container: false}, &init_config/0) ==
               default_configuration()
               |> Map.put(:running_in_container, false)
               |> Map.put(:log, "stdout")
    end

    test "application environment overwrites :log config on Heroku" do
      assert with_config(%{log: "file"}, &init_config/0) ==
               default_configuration()
               |> Map.put(:running_in_container, true)
               |> Map.put(:log, "file")
    end
  end

  describe "log_file_path/0" do
    test "defaults to /tmp/appsignal.log" do
      system_tmp_dir = Appsignal.Utils.FileSystem.system_tmp_dir()

      with_config(%{}, fn ->
        assert Config.log_file_path() == Path.join(system_tmp_dir, "appsignal.log")
      end)
    end

    test "overrides user specified filename when set" do
      output =
        capture_io(:stderr, fn ->
          system_tmp_dir = Appsignal.Utils.FileSystem.system_tmp_dir()

          with_config(%{log_path: Path.join(system_tmp_dir, "custom.log")}, fn ->
            assert Config.log_file_path() == Path.join(system_tmp_dir, "appsignal.log")
          end)
        end)

      assert output =~ "Deprecation warning: File names are no longer supported in the 'log_path'"
    end

    test "falls back on /tmp/appsignal.log when user path is not writable" do
      log_path = "/non_existent_path"
      system_tmp_dir = Appsignal.Utils.FileSystem.system_tmp_dir()

      output =
        capture_io(:stderr, fn ->
          with_config(%{log_path: log_path}, fn ->
            assert Config.log_file_path() == Path.join(system_tmp_dir, "appsignal.log")
          end)
        end)

      assert output =~
               "appsignal: Unable to log to '#{log_path}' or the " <>
                 "'#{system_tmp_dir}' fallback. " <>
                 "Please check the write permissions for the log directory."
    end
  end

  describe "reset_environment_config!" do
    test "deletes existing configuration in environment" do
      assert with_env(
               %{"_APPSIGNAL_APP_NAME" => "AppSignal test suite app"},
               fn ->
                 Appsignal.Config.reset_environment_config!()
                 Nif.env_get("_APPSIGNAL_APP_NAME")
               end
             ) == ''
    end
  end

  describe "write_to_environment" do
    defp write_to_environment do
      init_config()
      Appsignal.Config.write_to_environment()
    end

    @tag :skip_env_test_no_nif
    test "empty config options get written to the env" do
      write_to_environment()
      assert Nif.env_get("_APPSIGNAL_APP_NAME") == ''
      assert Nif.env_get("_APPSIGNAL_HTTP_PROXY") == ''
      assert Nif.env_get("_APPSIGNAL_IGNORE_ACTIONS") == ''
      assert Nif.env_get("_APPSIGNAL_IGNORE_ERRORS") == ''
      assert Nif.env_get("_APPSIGNAL_IGNORE_NAMESPACES") == ''
      assert Nif.env_get("_APPSIGNAL_LOG_FILE_PATH") == '/tmp/appsignal.log'
      assert Nif.env_get("_APPSIGNAL_WORKING_DIR_PATH") == ''
      assert Nif.env_get("_APPSIGNAL_WORKING_DIRECTORY_PATH") == ''
      assert Nif.env_get("_APPSIGNAL_RUNNING_IN_CONTAINER") == ''
      assert Nif.env_get("_APP_REVISION") == ''
    end

    test "deletes existing configuration in environment" do
      with_env(
        # Name is present in the configuration
        %{"_APPSIGNAL_APP_NAME" => "AppSignal test suite app"},
        fn ->
          # The new config doesn't have a name
          with_config(%{name: ""}, fn ->
            write_to_environment()
            # So it doesn't get written to the new agent environment configuration
            assert Nif.env_get("_APPSIGNAL_APP_NAME") == ''
          end)
        end
      )
    end

    @tag :skip_env_test_no_nif
    test "writes valid AppSignal config options to the env" do
      with_config(
        %{
          active: true,
          ca_file_path: "/foo/bar/zab.ca",
          debug: true,
          dns_servers: ["8.8.8.8", "8.8.4.4"],
          enable_host_metrics: false,
          endpoint: "https://push.staging.lol",
          env: :prod,
          push_api_key: "00000000-0000-0000-0000-000000000000",
          hostname: "My hostname",
          http_proxy: "http://10.10.10.10:8888",
          ignore_actions: ~w(
          ExampleApplication.PageController#ignored
          ExampleApplication.PageController#also_ignored
        ),
          ignore_errors: ~w(VerySpecificError AnotherError),
          ignore_namespaces: ~w(admin private_namespace),
          log: "stdout",
          log_path: "/tmp",
          name: "AppSignal test suite app",
          running_in_container: false,
          working_dir_path: "/tmp/appsignal-deprecated",
          working_directory_path: "/tmp/appsignal",
          files_world_accessible: false,
          revision: "03bd9e"
        },
        fn ->
          without_logger(&write_to_environment/0)

          assert Nif.env_get("_APPSIGNAL_ACTIVE") == 'true'
          assert Nif.env_get("_APPSIGNAL_AGENT_PATH") == :code.priv_dir(:appsignal)
          assert Nif.env_get("_APPSIGNAL_APP_NAME") == 'AppSignal test suite app'
          assert Nif.env_get("_APPSIGNAL_CA_FILE_PATH") == '/foo/bar/zab.ca'
          assert Nif.env_get("_APPSIGNAL_DEBUG_LOGGING") == 'true'
          assert Nif.env_get("_APPSIGNAL_DNS_SERVERS") == '8.8.8.8,8.8.4.4'
          assert Nif.env_get("_APPSIGNAL_ENABLE_HOST_METRICS") == 'false'
          assert Nif.env_get("_APPSIGNAL_ENVIRONMENT") == 'prod'
          assert Nif.env_get("_APPSIGNAL_HOSTNAME") == 'My hostname'
          assert Nif.env_get("_APPSIGNAL_HTTP_PROXY") == 'http://10.10.10.10:8888'

          assert Nif.env_get("_APPSIGNAL_IGNORE_ACTIONS") ==
                   'ExampleApplication.PageController#ignored,ExampleApplication.PageController#also_ignored'

          assert Nif.env_get("_APPSIGNAL_IGNORE_ERRORS") == 'VerySpecificError,AnotherError'
          assert Nif.env_get("_APPSIGNAL_IGNORE_NAMESPACES") == 'admin,private_namespace'

          assert Nif.env_get("_APPSIGNAL_LANGUAGE_INTEGRATION_VERSION") ==
                   'elixir-' ++ String.to_charlist(Mix.Project.config()[:version])

          assert Nif.env_get("_APPSIGNAL_LOG") == 'stdout'
          assert Nif.env_get("_APPSIGNAL_LOG_FILE_PATH") == '/tmp/appsignal.log'
          assert Nif.env_get("_APPSIGNAL_PUSH_API_ENDPOINT") == 'https://push.staging.lol'
          assert Nif.env_get("_APPSIGNAL_PUSH_API_KEY") == '00000000-0000-0000-0000-000000000000'
          assert Nif.env_get("_APPSIGNAL_RUNNING_IN_CONTAINER") == 'false'
          assert Nif.env_get("_APPSIGNAL_WORKING_DIR_PATH") == '/tmp/appsignal-deprecated'
          assert Nif.env_get("_APPSIGNAL_WORKING_DIRECTORY_PATH") == '/tmp/appsignal'
          assert Nif.env_get("_APPSIGNAL_FILES_WORLD_ACCESSIBLE") == 'false'
          assert Nif.env_get("_APP_REVISION") == '03bd9e'
        end
      )
    end

    @tag :skip_env_test_no_nif
    test "name as atom" do
      with_config(%{name: :appsignal_test_suite_app}, fn ->
        write_to_environment()
        assert Nif.env_get("_APPSIGNAL_APP_NAME") == 'appsignal_test_suite_app'
      end)
    end

    @tag :skip_env_test_no_nif
    test "name as string" do
      with_config(%{name: "AppSignal test suite app"}, fn ->
        write_to_environment()
        assert Nif.env_get("_APPSIGNAL_APP_NAME") == 'AppSignal test suite app'
      end)
    end

    test "writes dns_servers to env if empty" do
      with_config(%{dns_servers: []}, fn ->
        write_to_environment()
        assert Nif.env_get("_APPSIGNAL_DNS_SERVERS") == ''
      end)
    end

    @tag :skip_env_test_no_nif
    test "handles atom fields as strings" do
      with_config(
        %{
          active: "true",
          debug: "false",
          enable_host_metrics: "true",
          env: "prod",
          running_in_container: "false",
          files_world_accessible: "false"
        },
        fn ->
          write_to_environment()

          assert Nif.env_get("_APPSIGNAL_ACTIVE") == 'true'
          assert Nif.env_get("_APPSIGNAL_DEBUG_LOGGING") == 'false'
          assert Nif.env_get("_APPSIGNAL_ENABLE_HOST_METRICS") == 'true'
          assert Nif.env_get("_APPSIGNAL_ENVIRONMENT") == 'prod'
          assert Nif.env_get("_APPSIGNAL_RUNNING_IN_CONTAINER") == 'false'
          assert Nif.env_get("_APPSIGNAL_FILES_WORLD_ACCESSIBLE") == 'false'
        end
      )
    end

    @tag :skip_env_test_no_nif
    test "writes default ca_file_path to env if not user configured" do
      with_config(%{}, fn ->
        write_to_environment()

        assert Nif.env_get("_APPSIGNAL_CA_FILE_PATH") ==
                 to_charlist(default_configuration()[:ca_file_path])
      end)
    end

    @tag :skip_env_test_no_nif
    test "writes empty strint ca_file_path to env if user configured to nil" do
      with_config(%{ca_file_path: nil}, fn ->
        write_to_environment()

        assert Nif.env_get("_APPSIGNAL_CA_FILE_PATH") == ''
      end)
    end
  end

  defp default_configuration do
    %{
      active: false,
      debug: false,
      dns_servers: [],
      enable_host_metrics: true,
      endpoint: "https://push.appsignal.com",
      diagnose_endpoint: "https://appsignal.com/diag",
      env: :dev,
      filter_parameters: [],
      filter_session_data: [],
      ignore_actions: [],
      ignore_errors: [],
      ignore_namespaces: [],
      send_params: true,
      skip_session_data: false,
      files_world_accessible: true,
      valid: false,
      log: "file",
      request_headers: ~w(
        accept accept-charset accept-encoding accept-language cache-control
        connection content-length path-info range request-method request-uri
        server-name server-port server-protocol
      ),
      ca_file_path: Path.join(:code.priv_dir(:appsignal), "cacert.pem")
    }
  end

  defp valid_configuration do
    default_configuration()
    |> Map.put(:active, true)
    |> Map.put(:valid, true)
    |> Map.put(:push_api_key, "00000000-0000-0000-0000-000000000000")
  end

  defp init_config do
    Config.initialize()
    Application.get_all_env(:appsignal)[:config]
  end

  defp without_logger(fun) do
    Logger.disable(self())
    fun.()
    Logger.enable(self())
  end
end

defmodule Appsignal.ConfigTest do
  @moduledoc """
  Test the configuratoin
  """

  use ExUnit.Case
  import AppsignalTest.Utils
  import ExUnit.CaptureIO
  alias Appsignal.{Config, Nif}

  setup do
    environment = freeze_environment()
    Application.delete_env(:appsignal, :config)
    Application.delete_env(:appsignal, :config_sources)

    ExUnit.Callbacks.on_exit(fn ->
      unfreeze_environment(environment)
    end)
  end

  describe "initialize" do
    test "stores sources in Application" do
      init_config()

      default =
        default_configuration()
        |> Map.delete(:valid)

      assert Application.get_env(:appsignal, :config_sources) == %{
               default: default,
               system: %{},
               file: %{},
               env: %{}
             }
    end

    test "stores file sources in Application" do
      config = %{name: "My app", active: true}

      assert with_config(
               config,
               fn ->
                 init_config()
                 Application.get_env(:appsignal, :config_sources)[:file]
               end
             ) == config
    end
  end

  test "unconfigured" do
    assert {:error, :invalid_config} = Config.initialize()
  end

  test "minimum config from OS env" do
    assert with_env(
             %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
             &Config.initialize/0
           ) == :ok
  end

  test "minimum config from application env" do
    assert with_config(
             %{push_api_key: "00000000-0000-0000-0000-000000000000"},
             &Config.initialize/0
           ) == :ok
  end

  test "default configuration" do
    assert default_configuration() == init_config()
  end

  describe "configured_as_active?" do
    test "when active" do
      assert with_config(
               %{active: true, valid: true},
               &Config.configured_as_active?/0
             )
    end

    test "when not active" do
      refute with_config(
               %{active: false, valid: true},
               &Config.configured_as_active?/0
             )
    end
  end

  describe "active?" do
    test "when active and valid" do
      assert with_config(
               %{active: true, valid: true},
               &Config.active?/0
             )
    end

    test "when active but not valid" do
      refute with_config(
               %{active: true, valid: false},
               &Config.active?/0
             )
    end

    test "when not active and not valid" do
      refute with_config(
               %{active: false, valid: true},
               &Config.active?/0
             )
    end
  end

  describe "request_headers" do
    test "is nil by default" do
      assert with_config(%{}, &Config.request_headers/0) == nil
    end

    test "returns the request_headers config" do
      assert with_config(%{request_headers: []}, &Config.request_headers/0) == []
    end
  end

  describe "ca_file_path" do
    setup do
      Config.initialize()
      :ok
    end

    test "uses the priv path by default" do
      assert with_config(%{}, &Config.ca_file_path/0) ==
               Path.join(:code.priv_dir(:appsignal), "cacert.pem")
    end

    test "returns user override when set" do
      assert with_config(%{ca_file_path: "/foo/bar/bat.ca"}, &Config.ca_file_path/0) ==
               "/foo/bar/bat.ca"
    end

    test "returns nil when set to nil" do
      assert with_config(%{ca_file_path: nil}, &Config.ca_file_path/0) == nil
    end
  end

  describe "using the application environment" do
    test "active" do
      assert with_config(%{active: true}, &init_config/0) ==
               default_configuration() |> Map.put(:active, true)
    end

    test "ca_file_path" do
      assert with_config(%{ca_file_path: "/foo/bar/baz.ca"}, &init_config/0) ==
               default_configuration() |> Map.put(:ca_file_path, "/foo/bar/baz.ca")
    end

    test "debug" do
      assert with_config(%{debug: true}, &init_config/0) ==
               default_configuration() |> Map.put(:debug, true)
    end

    test "dns_servers" do
      assert with_config(%{dns_servers: ["8.8.8.8", "8.8.4.4"]}, &init_config/0) ==
               default_configuration() |> Map.put(:dns_servers, ["8.8.8.8", "8.8.4.4"])
    end

    test "enable_host_metrics" do
      assert with_config(%{enable_host_metrics: false}, &init_config/0) ==
               default_configuration() |> Map.put(:enable_host_metrics, false)
    end

    test "endpoint" do
      assert with_config(%{endpoint: "https://push.staging.lol"}, &init_config/0) ==
               default_configuration() |> Map.put(:endpoint, "https://push.staging.lol")
    end

    test "env" do
      assert with_config(%{env: :prod}, &init_config/0) ==
               default_configuration() |> Map.put(:env, :prod)
    end

    test "filter_parameters" do
      assert with_config(%{filter_parameters: ~w(password secret)}, &init_config/0) ==
               default_configuration() |> Map.put(:filter_parameters, ~w(password secret))
    end

    test "filter_session_data" do
      assert with_config(%{filter_session_data: ~w(accept connection)}, &init_config/0) ==
               default_configuration() |> Map.put(:filter_session_data, ~w(accept connection))
    end

    test "frontend_error_catching_path" do
      assert with_config(
               %{frontend_error_catching_path: "/appsignal_error_catcher"},
               &init_config/0
             ) ==
               default_configuration()
               |> Map.put(:frontend_error_catching_path, "/appsignal_error_catcher")
    end

    test "hostname" do
      assert with_config(%{hostname: "Bobs-MBP.example.com"}, &init_config/0) ==
               default_configuration() |> Map.put(:hostname, "Bobs-MBP.example.com")
    end

    test "http_proxy" do
      assert with_config(%{http_proxy: "http://10.10.10.10:8888"}, &init_config/0) ==
               default_configuration() |> Map.put(:http_proxy, "http://10.10.10.10:8888")
    end

    test "ignore_actions" do
      actions = ~w(
          ExampleApplication.PageController#ignored
          ExampleApplication.PageController#also_ignored
      )

      assert with_config(%{ignore_actions: actions}, &init_config/0) ==
               default_configuration() |> Map.put(:ignore_actions, actions)
    end

    test "ignore_errors" do
      errors = ~w(VerySpecificError AnotherError)

      assert with_config(%{ignore_errors: errors}, &init_config/0) ==
               default_configuration() |> Map.put(:ignore_errors, errors)
    end

    test "ignore_namespaces" do
      namespaces = ~w(admin private_namespace)

      assert with_config(%{ignore_namespaces: namespaces}, &init_config/0) ==
               default_configuration() |> Map.put(:ignore_namespaces, namespaces)
    end

    test "log" do
      assert with_config(%{log: "stdout"}, &init_config/0) ==
               default_configuration() |> Map.put(:log, "stdout")
    end

    test "log_path" do
      log_path = File.cwd!()

      assert with_config(%{log_path: log_path}, &init_config/0) ==
               default_configuration() |> Map.put(:log_path, log_path)
    end

    test "name" do
      assert with_config(%{name: "AppSignal test suite app"}, &init_config/0) ==
               default_configuration() |> Map.put(:name, "AppSignal test suite app")
    end

    test "push_api_key" do
      assert with_config(%{push_api_key: "00000000-0000-0000-0000-000000000000"}, &init_config/0) ==
               valid_configuration() |> Map.put(:active, false)
    end

    test "running_in_container" do
      assert with_config(%{running_in_container: true}, &init_config/0) ==
               default_configuration() |> Map.put(:running_in_container, true)
    end

    test "send_params" do
      assert with_config(%{send_params: true}, &init_config/0) ==
               default_configuration() |> Map.put(:send_params, true)
    end

    test "skip_session_data" do
      assert with_config(%{skip_session_data: true}, &init_config/0) ==
               default_configuration() |> Map.put(:skip_session_data, true)
    end

    test "files_world_accessible" do
      assert with_config(%{files_world_accessible: true}, &init_config/0) ==
               default_configuration() |> Map.put(:files_world_accessible, true)
    end

    test "working_dir_path" do
      without_logger(fn ->
        assert with_config(%{working_dir_path: "/tmp/appsignal"}, &init_config/0) ==
                 default_configuration() |> Map.put(:working_dir_path, "/tmp/appsignal")
      end)
    end

    test "working_directory_path" do
      assert with_config(%{working_directory_path: "/tmp/appsignal"}, &init_config/0) ==
               default_configuration() |> Map.put(:working_directory_path, "/tmp/appsignal")
    end

    test "request_headers" do
      assert with_config(%{request_headers: ~w(accept accept-charset)}, &init_config/0) ==
               default_configuration() |> Map.put(:request_headers, ~w(accept accept-charset))
    end

    test "revision" do
      assert with_config(%{revision: "03bd9e"}, &init_config/0) ==
               default_configuration() |> Map.put(:revision, "03bd9e")
    end
  end

  describe "using the system environment" do
    test "stores system env source in Application" do
      assert with_env(
               %{"APPSIGNAL_ACTIVE" => "true", "APPSIGNAL_DEBUG" => "true"},
               fn ->
                 init_config()
                 Application.get_env(:appsignal, :config_sources)[:env]
               end
             ) == %{active: true, debug: true}
    end

    test "active" do
      assert with_env(
               %{"APPSIGNAL_ACTIVE" => "true"},
               &init_config/0
             ) == default_configuration() |> Map.put(:active, true)
    end

    test "ca_file_path" do
      assert with_env(
               %{"APPSIGNAL_CA_FILE_PATH" => "/foo/bar/baz.ca"},
               &init_config/0
             ) == default_configuration() |> Map.put(:ca_file_path, "/foo/bar/baz.ca")
    end

    test "debug" do
      assert with_env(
               %{"APPSIGNAL_DEBUG" => "true"},
               &init_config/0
             ) == default_configuration() |> Map.put(:debug, true)
    end

    test "dns_servers" do
      assert with_env(
               %{"APPSIGNAL_DNS_SERVERS" => "8.8.8.8,8.8.4.4"},
               &init_config/0
             ) == default_configuration() |> Map.put(:dns_servers, ["8.8.8.8", "8.8.4.4"])
    end

    test "enable_host_metrics" do
      assert with_env(
               %{"APPSIGNAL_ENABLE_HOST_METRICS" => "false"},
               &init_config/0
             ) == default_configuration() |> Map.put(:enable_host_metrics, false)
    end

    test "endpoint" do
      assert with_env(
               %{"APPSIGNAL_PUSH_API_ENDPOINT" => "https://push.staging.lol"},
               &init_config/0
             ) == default_configuration() |> Map.put(:endpoint, "https://push.staging.lol")
    end

    test "env" do
      assert with_env(
               %{"APPSIGNAL_APP_ENV" => "prod"},
               &init_config/0
             ) == default_configuration() |> Map.put(:env, :prod)
    end

    test "filter_parameters" do
      assert with_env(
               %{"APPSIGNAL_FILTER_PARAMETERS" => "password,secret"},
               &init_config/0
             ) == default_configuration() |> Map.put(:filter_parameters, ~w(password secret))
    end

    test "filter_session_data" do
      assert with_env(
               %{"APPSIGNAL_FILTER_SESSION_DATA" => "accept,connection"},
               &init_config/0
             ) == default_configuration() |> Map.put(:filter_session_data, ~w(accept connection))
    end

    test "frontend_error_catching_path" do
      assert with_env(
               %{"APPSIGNAL_FRONTEND_ERROR_CATCHING_PATH" => "/appsignal_error_catcher"},
               &init_config/0
             ) ==
               default_configuration()
               |> Map.put(:frontend_error_catching_path, "/appsignal_error_catcher")
    end

    test "hostname" do
      assert with_env(
               %{"APPSIGNAL_HOSTNAME" => "Bobs-MBP.example.com"},
               &init_config/0
             ) == default_configuration() |> Map.put(:hostname, "Bobs-MBP.example.com")
    end

    test "http_proxy" do
      assert with_env(
               %{"APPSIGNAL_HTTP_PROXY" => "http://10.10.10.10:8888"},
               &init_config/0
             ) == default_configuration() |> Map.put(:http_proxy, "http://10.10.10.10:8888")
    end

    test "ignore_actions" do
      assert with_env(
               %{
                 "APPSIGNAL_IGNORE_ACTIONS" =>
                   "ExampleApplication.PageController#ignored,ExampleApplication.PageController#also_ignored"
               },
               &init_config/0
             ) == default_configuration() |> Map.put(:ignore_actions, ~w(
          ExampleApplication.PageController#ignored
          ExampleApplication.PageController#also_ignored
      ))
    end

    test "ignore_errors" do
      assert with_env(
               %{"APPSIGNAL_IGNORE_ERRORS" => "VerySpecificError,AnotherError"},
               &init_config/0
             ) ==
               default_configuration()
               |> Map.put(:ignore_errors, ~w(VerySpecificError AnotherError))
    end

    test "ignore_namespaces" do
      assert with_env(
               %{"APPSIGNAL_IGNORE_NAMESPACES" => "admin,private_namespace"},
               &init_config/0
             ) ==
               default_configuration() |> Map.put(:ignore_namespaces, ~w(admin private_namespace))
    end

    test "log" do
      assert with_env(
               %{"APPSIGNAL_LOG" => "stdout"},
               &init_config/0
             ) == default_configuration() |> Map.put(:log, "stdout")
    end

    test "log_path" do
      log_path = File.cwd!()

      assert with_env(
               %{"APPSIGNAL_LOG_PATH" => log_path},
               &init_config/0
             ) == default_configuration() |> Map.put(:log_path, log_path)
    end

    test "name" do
      assert with_env(
               %{"APPSIGNAL_APP_NAME" => "AppSignal test suite app"},
               &init_config/0
             ) == default_configuration() |> Map.put(:name, "AppSignal test suite app")
    end

    test "push_api_key" do
      assert with_env(
               %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
               &init_config/0
             ) == valid_configuration()
    end

    test "running_in_container" do
      assert with_env(
               %{"APPSIGNAL_RUNNING_IN_CONTAINER" => "true"},
               &init_config/0
             ) == default_configuration() |> Map.put(:running_in_container, true)
    end

    test "send_params" do
      assert with_env(
               %{"APPSIGNAL_SEND_PARAMS" => "true"},
               &init_config/0
             ) == default_configuration() |> Map.put(:send_params, true)
    end

    test "skip_session_data" do
      assert with_env(
               %{"APPSIGNAL_SKIP_SESSION_DATA" => "true"},
               &init_config/0
             ) == default_configuration() |> Map.put(:skip_session_data, true)
    end

    test "files_world_accessible" do
      assert with_env(
               %{"APPSIGNAL_FILES_WORLD_ACCESSIBLE" => "false"},
               &init_config/0
             ) == default_configuration() |> Map.put(:files_world_accessible, false)
    end

    test "working_dir_path" do
      without_logger(fn ->
        assert with_env(
                 %{"APPSIGNAL_WORKING_DIR_PATH" => "/tmp/appsignal"},
                 &init_config/0
               ) == default_configuration() |> Map.put(:working_dir_path, "/tmp/appsignal")
      end)
    end

    test "working_directory_path" do
      assert with_env(
               %{"APPSIGNAL_WORKING_DIRECTORY_PATH" => "/tmp/appsignal"},
               &init_config/0
             ) == default_configuration() |> Map.put(:working_directory_path, "/tmp/appsignal")
    end

    test "request_headers" do
      assert with_env(
               %{"APPSIGNAL_REQUEST_HEADERS" => "accept,accept-charset"},
               &init_config/0
             ) == default_configuration() |> Map.put(:request_headers, ~w(accept accept-charset))
    end

    test "revision" do
      assert with_env(
               %{"APP_REVISION" => "03bd9e"},
               &init_config/0
             ) == default_configuration() |> Map.put(:revision, "03bd9e")
    end
  end

  describe "config based on system" do
    test "system environment overwrites application environment configuration" do
      assert with_env(
               %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
               &init_config/0
             ) == valid_configuration() |> Map.put(:active, true)

      assert with_config(%{active: false}, fn ->
               with_env(
                 %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
                 &init_config/0
               )
             end) == valid_configuration() |> Map.put(:active, false)
    end

    test "stores system source in Application" do
      assert with_env(
               %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
               fn ->
                 init_config()
                 Application.get_env(:appsignal, :config_sources)[:system]
               end
             ) == %{active: true}

      assert with_config(%{active: false}, fn ->
               with_env(
                 %{"APPSIGNAL_PUSH_API_KEY" => "00000000-0000-0000-0000-000000000000"},
                 fn ->
                   init_config()
                   Application.get_env(:appsignal, :config_sources)[:system]
                 end
               )
             end) == %{active: true}
    end
  end

  describe "when on Heroku" do
    setup do
      setup_with_env(%{"DYNO" => "web.1"})
    end

    test "stores system source in Application" do
      init_config()

      assert Application.get_env(:appsignal, :config_sources)[:system] == %{
               log: "stdout",
               running_in_container: true
             }
    end

    test ":running_in_container and :log" do
      config =
        default_configuration()
        |> Map.put(:running_in_container, true)
        |> Map.put(:log, "stdout")

      assert config == init_config()
    end

    test "application environment overwrites :running_in_container config on Heroku" do
      assert with_config(%{running_in_container: false}, &init_config/0) ==
               default_configuration()
               |> Map.put(:running_in_container, false)
               |> Map.put(:log, "stdout")
    end

    test "application environment overwrites :log config on Heroku" do
      assert with_config(%{log: "file"}, &init_config/0) ==
               default_configuration()
               |> Map.put(:running_in_container, true)
               |> Map.put(:log, "file")
    end
  end

  describe "log_file_path/0" do
    test "defaults to /tmp/appsignal.log" do
      system_tmp_dir = Appsignal.Utils.FileSystem.system_tmp_dir()

      with_config(%{}, fn ->
        assert Config.log_file_path() == Path.join(system_tmp_dir, "appsignal.log")
      end)
    end

    test "overrides user specified filename when set" do
      output =
        capture_io(:stderr, fn ->
          system_tmp_dir = Appsignal.Utils.FileSystem.system_tmp_dir()

          with_config(%{log_path: Path.join(system_tmp_dir, "custom.log")}, fn ->
            assert Config.log_file_path() == Path.join(system_tmp_dir, "appsignal.log")
          end)
        end)

      assert output =~ "Deprecation warning: File names are no longer supported in the 'log_path'"
    end

    test "falls back on /tmp/appsignal.log when user path is not writable" do
      log_path = "/non_existent_path"
      system_tmp_dir = Appsignal.Utils.FileSystem.system_tmp_dir()

      output =
        capture_io(:stderr, fn ->
          with_config(%{log_path: log_path}, fn ->
            assert Config.log_file_path() == Path.join(system_tmp_dir, "appsignal.log")
          end)
        end)

      assert output =~
               "appsignal: Unable to log to '#{log_path}' or the " <>
                 "'#{system_tmp_dir}' fallback. " <>
                 "Please check the write permissions for the log directory."
    end
  end

  describe "reset_environment_config!" do
    test "deletes existing configuration in environment" do
      assert with_env(
               %{"_APPSIGNAL_APP_NAME" => "AppSignal test suite app"},
               fn ->
                 Appsignal.Config.reset_environment_config!()
                 Nif.env_get("_APPSIGNAL_APP_NAME")
               end
             ) == ''
    end
  end

  describe "write_to_environment" do
    defp write_to_environment do
      init_config()
      Appsignal.Config.write_to_environment()
    end

    @tag :skip_env_test_no_nif
    test "empty config options get written to the env" do
      write_to_environment()
      assert Nif.env_get("_APPSIGNAL_APP_NAME") == ''
      assert Nif.env_get("_APPSIGNAL_HTTP_PROXY") == ''
      assert Nif.env_get("_APPSIGNAL_IGNORE_ACTIONS") == ''
      assert Nif.env_get("_APPSIGNAL_IGNORE_ERRORS") == ''
      assert Nif.env_get("_APPSIGNAL_IGNORE_NAMESPACES") == ''
      assert Nif.env_get("_APPSIGNAL_LOG_FILE_PATH") == '/tmp/appsignal.log'
      assert Nif.env_get("_APPSIGNAL_WORKING_DIR_PATH") == ''
      assert Nif.env_get("_APPSIGNAL_WORKING_DIRECTORY_PATH") == ''
      assert Nif.env_get("_APPSIGNAL_RUNNING_IN_CONTAINER") == ''
      assert Nif.env_get("_APP_REVISION") == ''
    end

    test "deletes existing configuration in environment" do
      with_env(
        # Name is present in the configuration
        %{"_APPSIGNAL_APP_NAME" => "AppSignal test suite app"},
        fn ->
          # The new config doesn't have a name
          with_config(%{name: ""}, fn ->
            write_to_environment()
            # So it doesn't get written to the new agent environment configuration
            assert Nif.env_get("_APPSIGNAL_APP_NAME") == ''
          end)
        end
      )
    end

    @tag :skip_env_test_no_nif
    test "writes valid AppSignal config options to the env" do
      with_config(
        %{
          active: true,
          ca_file_path: "/foo/bar/zab.ca",
          debug: true,
          dns_servers: ["8.8.8.8", "8.8.4.4"],
          enable_host_metrics: false,
          endpoint: "https://push.staging.lol",
          env: :prod,
          push_api_key: "00000000-0000-0000-0000-000000000000",
          hostname: "My hostname",
          http_proxy: "http://10.10.10.10:8888",
          ignore_actions: ~w(
          ExampleApplication.PageController#ignored
          ExampleApplication.PageController#also_ignored
        ),
          ignore_errors: ~w(VerySpecificError AnotherError),
          ignore_namespaces: ~w(admin private_namespace),
          log: "stdout",
          log_path: "/tmp",
          name: "AppSignal test suite app",
          running_in_container: false,
          working_dir_path: "/tmp/appsignal-deprecated",
          working_directory_path: "/tmp/appsignal",
          files_world_accessible: false,
          revision: "03bd9e"
        },
        fn ->
          without_logger(&write_to_environment/0)

          assert Nif.env_get("_APPSIGNAL_ACTIVE") == 'true'
          assert Nif.env_get("_APPSIGNAL_AGENT_PATH") == :code.priv_dir(:appsignal)
          assert Nif.env_get("_APPSIGNAL_APP_NAME") == 'AppSignal test suite app'
          assert Nif.env_get("_APPSIGNAL_CA_FILE_PATH") == '/foo/bar/zab.ca'
          assert Nif.env_get("_APPSIGNAL_DEBUG_LOGGING") == 'true'
          assert Nif.env_get("_APPSIGNAL_DNS_SERVERS") == '8.8.8.8,8.8.4.4'
          assert Nif.env_get("_APPSIGNAL_ENABLE_HOST_METRICS") == 'false'
          assert Nif.env_get("_APPSIGNAL_ENVIRONMENT") == 'prod'
          assert Nif.env_get("_APPSIGNAL_HOSTNAME") == 'My hostname'
          assert Nif.env_get("_APPSIGNAL_HTTP_PROXY") == 'http://10.10.10.10:8888'

          assert Nif.env_get("_APPSIGNAL_IGNORE_ACTIONS") ==
                   'ExampleApplication.PageController#ignored,ExampleApplication.PageController#also_ignored'

          assert Nif.env_get("_APPSIGNAL_IGNORE_ERRORS") == 'VerySpecificError,AnotherError'
          assert Nif.env_get("_APPSIGNAL_IGNORE_NAMESPACES") == 'admin,private_namespace'

          assert Nif.env_get("_APPSIGNAL_LANGUAGE_INTEGRATION_VERSION") ==
                   'elixir-' ++ String.to_charlist(Mix.Project.config()[:version])

          assert Nif.env_get("_APPSIGNAL_LOG") == 'stdout'
          assert Nif.env_get("_APPSIGNAL_LOG_FILE_PATH") == '/tmp/appsignal.log'
          assert Nif.env_get("_APPSIGNAL_PUSH_API_ENDPOINT") == 'https://push.staging.lol'
          assert Nif.env_get("_APPSIGNAL_PUSH_API_KEY") == '00000000-0000-0000-0000-000000000000'
          assert Nif.env_get("_APPSIGNAL_RUNNING_IN_CONTAINER") == 'false'
          assert Nif.env_get("_APPSIGNAL_WORKING_DIR_PATH") == '/tmp/appsignal-deprecated'
          assert Nif.env_get("_APPSIGNAL_WORKING_DIRECTORY_PATH") == '/tmp/appsignal'
          assert Nif.env_get("_APPSIGNAL_FILES_WORLD_ACCESSIBLE") == 'false'
          assert Nif.env_get("_APP_REVISION") == '03bd9e'
        end
      )
    end

    @tag :skip_env_test_no_nif
    test "name as atom" do
      with_config(%{name: :appsignal_test_suite_app}, fn ->
        write_to_environment()
        assert Nif.env_get("_APPSIGNAL_APP_NAME") == 'appsignal_test_suite_app'
      end)
    end

    @tag :skip_env_test_no_nif
    test "name as string" do
      with_config(%{name: "AppSignal test suite app"}, fn ->
        write_to_environment()
        assert Nif.env_get("_APPSIGNAL_APP_NAME") == 'AppSignal test suite app'
      end)
    end

    test "writes dns_servers to env if empty" do
      with_config(%{dns_servers: []}, fn ->
        write_to_environment()
        assert Nif.env_get("_APPSIGNAL_DNS_SERVERS") == ''
      end)
    end

    @tag :skip_env_test_no_nif
    test "handles atom fields as strings" do
      with_config(
        %{
          active: "true",
          debug: "false",
          enable_host_metrics: "true",
          env: "prod",
          running_in_container: "false",
          send_params: "true",
          files_world_accessible: "false"
        },
        fn ->
          write_to_environment()

          assert Nif.env_get("_APPSIGNAL_ACTIVE") == 'true'
          assert Nif.env_get("_APPSIGNAL_DEBUG_LOGGING") == 'false'
          assert Nif.env_get("_APPSIGNAL_ENABLE_HOST_METRICS") == 'true'
          assert Nif.env_get("_APPSIGNAL_ENVIRONMENT") == 'prod'
          assert Nif.env_get("_APPSIGNAL_RUNNING_IN_CONTAINER") == 'false'
          assert Nif.env_get("_APPSIGNAL_FILES_WORLD_ACCESSIBLE") == 'false'
        end
      )
    end

    @tag :skip_env_test_no_nif
    test "writes default ca_file_path to env if not user configured" do
      with_config(%{}, fn ->
        write_to_environment()

        assert Nif.env_get("_APPSIGNAL_CA_FILE_PATH") ==
                 to_charlist(default_configuration()[:ca_file_path])
      end)
    end

    @tag :skip_env_test_no_nif
    test "writes empty strint ca_file_path to env if user configured to nil" do
      with_config(%{ca_file_path: nil}, fn ->
        write_to_environment()

        assert Nif.env_get("_APPSIGNAL_CA_FILE_PATH") == ''
      end)
    end
  end

  defp default_configuration do
    %{
      active: false,
      debug: false,
      dns_servers: [],
      enable_host_metrics: true,
      endpoint: "https://push.appsignal.com",
      diagnose_endpoint: "https://appsignal.com/diag",
      env: :dev,
      filter_parameters: [],
      filter_session_data: [],
      ignore_actions: [],
      ignore_errors: [],
      ignore_namespaces: [],
      send_params: true,
      skip_session_data: false,
      files_world_accessible: true,
      valid: false,
      log: "file",
      request_headers: ~w(
        accept accept-charset accept-encoding accept-language cache-control
        connection content-length path-info range request-method request-uri
        server-name server-port server-protocol
      ),
      ca_file_path: Path.join(:code.priv_dir(:appsignal), "cacert.pem")
    }
  end

  defp valid_configuration do
    default_configuration()
    |> Map.put(:active, true)
    |> Map.put(:valid, true)
    |> Map.put(:push_api_key, "00000000-0000-0000-0000-000000000000")
  end

  defp init_config do
    Config.initialize()
    Application.get_all_env(:appsignal)[:config]
  end

  defp without_logger(fun) do
    Logger.disable(self())
    fun.()
    Logger.enable(self())
  end
end


bugfix, we need a sender address!

bugfix, we need a sender address!

defmodule Logger.Backends.ExceptionNotification do
  use GenEvent

  def handle_event({:error, _group_leader, {Logger, message, timestamp, metadata}}, state) do
    unless String.contains?(inspect(message), ["Fatal error: handshake failure",
                                               "SSL: :hello: ssl_alert.erl:85:Warning: unrecognised name",
                                               ":unexpected_end"]) do
      Logger.Formatter.compile("$time $metadata[$level] $message\n")
      |> Logger.Formatter.format(:error, message, timestamp, metadata)
      |> IO.iodata_to_binary
      |> Pan.Email.error_notification("exeception_notification@panoptikum.io",
                                      "stefan@panoptikum.io")
      |> Pan.Mailer.deliver_now()
    end

    {:ok, state}
  end

  def handle_event({_level, group_leader, {Logger, _, _, _}}, state)
    when node(group_leader) != node(), do: {:ok, state}

  def handle_event({level, _group_leader, {Logger, _, _, _}}, state)
    when level in [:debug, :info, :warn], do: {:ok, state}
end

defmodule Logger.Backends.ExceptionNotification do
  use GenEvent

  def handle_event({:error, _group_leader, {Logger, message, timestamp, metadata}}, state) do
    unless String.contains?(inspect(message), ["Fatal error: handshake failure",
                                               "SSL: :hello: ssl_alert.erl:85:Warning: unrecognised name",
                                               ":unexpected_end"]) do
      Logger.Formatter.compile("$time $metadata[$level] $message\n")
      |> Logger.Formatter.format(:error, message, timestamp, metadata)
      |> IO.iodata_to_binary
      |> Pan.Email.error_notification("stefan@panoptikum.io")
      |> Pan.Mailer.deliver_now()
    end

    {:ok, state}
  end

  def handle_event({_level, group_leader, {Logger, _, _, _}}, state)
    when node(group_leader) != node(), do: {:ok, state}

  def handle_event({level, _group_leader, {Logger, _, _, _}}, state)
    when level in [:debug, :info, :warn], do: {:ok, state}
end


Remove potential for duplicates by introducing repo name.

defmodule SlackCoder.Github.Watchers.Supervisor do
  import Supervisor.Spec
  alias SlackCoder.Repo
  alias SlackCoder.Models.PR
  require Logger

  def start_link() do
    prs = Repo.all PR.active
    children = prs |> Enum.map(&worker_for/1)

    opts = [strategy: :one_for_one, name: __MODULE__]
    Supervisor.start_link(children, opts)
  end

  defp worker_for(pr) do
    worker(SlackCoder.Github.Watchers.PullRequest, [pr], id: worker_id(pr), restart: :transient)
  end

  @worker_id_prefix "PR-"
  defp worker_id(pr), do: "#{@worker_id_prefix}#{pr.repo}-#{pr.number}"

  def start_watcher(pr) do
    case Supervisor.start_child(__MODULE__, worker_for(pr)) do
      {:ok, watcher} ->
        Logger.debug "Starting watcher for: #{worker_id(pr)} #{pr.title}"
        watcher
      {:error, {:already_started, watcher}} ->
        watcher
      {:error, :already_present} ->
        find_watcher(pr)
    end
  end

  def stop_watcher(nil), do: nil
  def stop_watcher(pr) do
    Supervisor.terminate_child(__MODULE__, worker_id(pr))
    Supervisor.delete_child(__MODULE__, worker_id(pr))
  end

  def find_or_start_watcher(pr) do
    case find_watcher(pr) do
      nil -> start_watcher(pr)
      pid -> pid
    end
  end

  def find_watcher(pr) do
    number = pr.number |> to_string
    child = Supervisor.which_children(__MODULE__)
            |> Enum.find(&(match?({@worker_id_prefix <> ^number, _, _, _}, &1)))
    case child do
      nil -> nil
      {_, worker, _, _} -> worker
    end
  end

  def pull_requests() do
    Supervisor.which_children(__MODULE__)
    |> Stream.filter(fn
      {"PR-" <> _number, _, _, _} -> true
      _ -> false
    end)
    |> Stream.map(fn
      {_, pid, _, _} ->
        SlackCoder.Github.Watchers.PullRequest.fetch(pid)
    end)
    |> Stream.filter(&(&1)) # Remove nils
    |> Enum.reduce(%{}, fn
      %{github_user: github_user} = pr, prs when not is_nil(github_user) -> # Only take PRs where there is a github user
        user = github_user |> String.downcase() |> String.to_atom
        list = prs[user] || []
        Map.put(prs, user, [pr | list])
      _, prs ->
        prs
    end)
  end

end

defmodule SlackCoder.Github.Watchers.Supervisor do
  import Supervisor.Spec
  alias SlackCoder.Repo
  alias SlackCoder.Models.PR
  require Logger

  def start_link() do
    prs = Repo.all PR.active
    children = prs |> Enum.map(&worker_for/1)

    opts = [strategy: :one_for_one, name: __MODULE__]
    Supervisor.start_link(children, opts)
  end

  defp worker_for(pr) do
    worker(SlackCoder.Github.Watchers.PullRequest, [pr], id: worker_id(pr), restart: :transient)
  end

  @worker_id_prefix "PR-"
  defp worker_id(pr), do: "#{@worker_id_prefix}#{pr.number}"

  def start_watcher(pr) do
    case Supervisor.start_child(__MODULE__, worker_for(pr)) do
      {:ok, watcher} ->
        Logger.debug "Starting watcher for: #{worker_id(pr)} #{pr.title}"
        watcher
      {:error, {:already_started, watcher}} ->
        watcher
      {:error, :already_present} ->
        find_watcher(pr)
    end
  end

  def stop_watcher(nil), do: nil
  def stop_watcher(pr) do
    Supervisor.terminate_child(__MODULE__, worker_id(pr))
    Supervisor.delete_child(__MODULE__, worker_id(pr))
  end

  def find_or_start_watcher(pr) do
    case find_watcher(pr) do
      nil -> start_watcher(pr)
      pid -> pid
    end
  end

  def find_watcher(pr) do
    number = pr.number |> to_string
    child = Supervisor.which_children(__MODULE__)
            |> Enum.find(&(match?({@worker_id_prefix <> ^number, _, _, _}, &1)))
    case child do
      nil -> nil
      {_, worker, _, _} -> worker
    end
  end

  def pull_requests() do
    Supervisor.which_children(__MODULE__)
    |> Stream.filter(fn
      {"PR-" <> _number, _, _, _} -> true
      _ -> false
    end)
    |> Stream.map(fn
      {_, pid, _, _} ->
        SlackCoder.Github.Watchers.PullRequest.fetch(pid)
    end)
    |> Stream.filter(&(&1)) # Remove nils
    |> Enum.reduce(%{}, fn
      %{github_user: github_user} = pr, prs when not is_nil(github_user) -> # Only take PRs where there is a github user
        user = github_user |> String.downcase() |> String.to_atom
        list = prs[user] || []
        Map.put(prs, user, [pr | list])
      _, prs ->
        prs
    end)
  end

end


to fix travis

to fix travis

defmodule DrabTestApp.CoreTest do
  use DrabTestApp.IntegrationCase
  import Drab.Core
  import ExUnit.CaptureLog

  defp core_index do
    core_url(DrabTestApp.Endpoint, :core)
  end

  setup do
    core_index() |> navigate_to()
    # wait for a page to load
    find_element(:id, "page_loaded_indicator")
    [socket: drab_socket()]
  end

  describe "Drab.Core" do
    # we don't want to see Drab Errors in the log
    @tag capture_log: true
    test "exec_js and broadcast_js" do
      # test execjs and broadcastjs
      standard_click_and_get_test("core1")
      standard_click_and_get_test("core2")
    end

    @tag capture_log: true
    test "multiple events on object", fixture do
      standard_click_and_get_test("core4")

      assert Drab.Core.exec_js(fixture.socket, "document.getElementById('core5_out').innerHTML") ==
               {:ok, "core5"}
    end

    @tag capture_log: true
    test "multiple events on object defined with shorthand form", fixture do
      standard_click_and_get_test("core6")

      assert Drab.Core.exec_js(fixture.socket, "document.getElementById('core7_out').innerHTML") ==
               {:ok, "core7"}
    end

    @tag capture_log: true
    test "capturing custom events", fixture do
      hijack_click = """
        var event;
        event = document.createEvent("HTMLEvents");
        event.initEvent("custom.event", true, true);

        var node = document.getElementById('core8_button');

        node.addEventListener('click', function(e) {
          e.preventDefault();
          node.dispatchEvent(event);
        });
      """

      Drab.Core.exec_js(fixture.socket, hijack_click)

      standard_click_and_get_test("core8")
    end

    @tag capture_log: true
    test "debounce", fixture do
      for t <- ["1", "2", "3"] do
        input = find_element(:id, "core#{t}_input")
        fill_field(input, "something")

        assert Drab.Core.exec_js(
                 fixture.socket,
                 "document.getElementById('input#{t}_out').innerHTML"
               ) == {:ok, ""}

        Process.sleep(600)

        assert Drab.Core.exec_js(
                 fixture.socket,
                 "document.getElementById('input#{t}_out').innerHTML"
               ) == {:ok, "input#{t}"}
      end
    end

    @tag capture_log: true
    test "session" do
      # this session value should be visible
      session_value = find_element(:id, "test_session_value1")
      assert visible_text(session_value) == "test session value 1"

      # and this one not, as it is not listed in `access_session`
      session_value = find_element(:id, "test_session_value2")
      assert visible_text(session_value) == ""
    end

    @tag capture_log: true
    test "store" do
      # test if the store is set up correctly
      click_and_wait("set_store_button")
      click_and_wait("get_store_button")

      store_value = find_element(:id, "store1_out")
      assert visible_text(store_value) == "test store value"
    end

    @tag capture_log: true
    test "return values of exec_js", context do
      assert exec_js(context[:socket], "2 + 2") == {:ok, 4}
      assert exec_js(context[:socket], "nonexisting") == {:error, "nonexisting is not defined"}
    end

    @tag capture_log: true
    test "return values of exec_js!", context do
      assert exec_js!(context[:socket], "2 + 2") == 4

      assert_raise Drab.JSExecutionError, "nonexisting is not defined", fn ->
        exec_js!(context[:socket], "nonexisting")
      end
    end
  end

  describe "Drab.Core callbacks" do
    @tag capture_log: true
    test "before all should set the store", context do
      socket = context[:socket]
      click_and_wait("core1_button")

      assert get_store(socket, :set_in_before_all) == :before
    end

    @tag capture_log: true
    test "after all should get the handler return value", context do
      click_and_wait("core1_button")

      assert get_store(context[:socket], :set_in_after_all) == 42
    end

    @tag capture_log: true
    test "before handler which returns false should stop processing", context do
      click_and_wait("core3_button")

      assert get_store(context[:socket], :should_never_be_assigned) == nil
      assert find_element(:id, "core3_out") |> visible_text() == ""
    end

    @tag capture_log: true
    test "after handler `except` test 1", context do
      click_and_wait("core1_button")
      assert get_store(context[:socket], :shouldnt_be_set_in_core3) == true
    end

    @tag capture_log: true
    test "after handler `except` test 2", context do
      click_and_wait("core2_button")
      assert get_store(context[:socket], :shouldnt_be_set_in_core3) == true
    end

    @tag capture_log: true
    test "after handler `except` test 3", context do
      click_and_wait("core3_button")
      assert get_store(context[:socket], :shouldnt_be_set_in_core3) == nil
    end

    @tag capture_log: true
    test "onconnect should go before onload" do
      assert find_element(:id, "onconnect_counter") |> visible_text() == "1"
    end

    @tag capture_log: true
    test "onload should go after onconnect" do
      assert find_element(:id, "onload_counter") |> visible_text() == "2"
    end
  end

  describe "after disconnect" do
    @tag capture_log: true
    test "should return disconnection error" do
      log = capture_log(fn ->
        click_and_wait("disconnect_button")
        navigate_to(index_url(DrabTestApp.Endpoint, :index))
        Process.sleep(1000)
      end)
      assert String.contains?(log, "(Drab.ConnectionError) Disconnected")
    end
  end

  describe "presence" do
    @tag capture_log: true
    test "presence should be started", feature do
      assert Drab.Presence.count_connections(feature.socket) == 1

      change_to_secondary_session()
      core_index() |> navigate_to()
      assert Drab.Presence.count_connections(feature.socket) == 2
    end

    @tag capture_log: true
    test "presence with subscribtion to some topic", feature do
      topic = same_topic("my_topic")
      Drab.Commander.subscribe(feature.socket, topic)
      assert Drab.Presence.count_connections(topic) == 1

      change_to_secondary_session()
      core_index() |> navigate_to()
      find_element(:id, "page_loaded_indicator")
      assert Drab.Presence.count_connections(topic) == 1

      socket = drab_socket()
      Drab.Commander.subscribe(socket, topic)
      Process.sleep(500)
      assert Drab.Presence.count_connections(topic) == 2

      Drab.Commander.unsubscribe(socket, topic)
      assert Drab.Presence.count_connections(topic) == 1
    end
  end
end

defmodule DrabTestApp.CoreTest do
  use DrabTestApp.IntegrationCase
  import Drab.Core
  import ExUnit.CaptureLog

  defp core_index do
    core_url(DrabTestApp.Endpoint, :core)
  end

  setup do
    core_index() |> navigate_to()
    # wait for a page to load
    find_element(:id, "page_loaded_indicator")
    [socket: drab_socket()]
  end

  describe "Drab.Core" do
    # we don't want to see Drab Errors in the log
    @tag capture_log: true
    test "exec_js and broadcast_js" do
      # test execjs and broadcastjs
      standard_click_and_get_test("core1")
      standard_click_and_get_test("core2")
    end

    @tag capture_log: true
    test "multiple events on object", fixture do
      standard_click_and_get_test("core4")

      assert Drab.Core.exec_js(fixture.socket, "document.getElementById('core5_out').innerHTML") ==
               {:ok, "core5"}
    end

    @tag capture_log: true
    test "multiple events on object defined with shorthand form", fixture do
      standard_click_and_get_test("core6")

      assert Drab.Core.exec_js(fixture.socket, "document.getElementById('core7_out').innerHTML") ==
               {:ok, "core7"}
    end

    @tag capture_log: true
    test "capturing custom events", fixture do
      hijack_click = """
        var event;
        event = document.createEvent("HTMLEvents");
        event.initEvent("custom.event", true, true);

        var node = document.getElementById('core8_button');

        node.addEventListener('click', function(e) {
          e.preventDefault();
          node.dispatchEvent(event);
        });
      """

      Drab.Core.exec_js(fixture.socket, hijack_click)

      standard_click_and_get_test("core8")
    end

    @tag capture_log: true
    test "debounce", fixture do
      for t <- ["1", "2", "3"] do
        input = find_element(:id, "core#{t}_input")
        fill_field(input, "something")

        assert Drab.Core.exec_js(
                 fixture.socket,
                 "document.getElementById('input#{t}_out').innerHTML"
               ) == {:ok, ""}

        Process.sleep(600)

        assert Drab.Core.exec_js(
                 fixture.socket,
                 "document.getElementById('input#{t}_out').innerHTML"
               ) == {:ok, "input#{t}"}
      end
    end

    @tag capture_log: true
    test "session" do
      # this session value should be visible
      session_value = find_element(:id, "test_session_value1")
      assert visible_text(session_value) == "test session value 1"

      # and this one not, as it is not listed in `access_session`
      session_value = find_element(:id, "test_session_value2")
      assert visible_text(session_value) == ""
    end

    @tag capture_log: true
    test "store" do
      # test if the store is set up correctly
      click_and_wait("set_store_button")
      click_and_wait("get_store_button")

      store_value = find_element(:id, "store1_out")
      assert visible_text(store_value) == "test store value"
    end

    @tag capture_log: true
    test "return values of exec_js", context do
      assert exec_js(context[:socket], "2 + 2") == {:ok, 4}
      assert exec_js(context[:socket], "nonexisting") == {:error, "nonexisting is not defined"}
    end

    @tag capture_log: true
    test "return values of exec_js!", context do
      assert exec_js!(context[:socket], "2 + 2") == 4

      assert_raise Drab.JSExecutionError, "nonexisting is not defined", fn ->
        exec_js!(context[:socket], "nonexisting")
      end
    end
  end

  describe "Drab.Core callbacks" do
    @tag capture_log: true
    test "before all should set the store", context do
      socket = context[:socket]
      click_and_wait("core1_button")

      assert get_store(socket, :set_in_before_all) == :before
    end

    @tag capture_log: true
    test "after all should get the handler return value", context do
      click_and_wait("core1_button")

      assert get_store(context[:socket], :set_in_after_all) == 42
    end

    @tag capture_log: true
    test "before handler which returns false should stop processing", context do
      click_and_wait("core3_button")

      assert get_store(context[:socket], :should_never_be_assigned) == nil
      assert find_element(:id, "core3_out") |> visible_text() == ""
    end

    @tag capture_log: true
    test "after handler `except` test 1", context do
      click_and_wait("core1_button")
      assert get_store(context[:socket], :shouldnt_be_set_in_core3) == true
    end

    @tag capture_log: true
    test "after handler `except` test 2", context do
      click_and_wait("core2_button")
      assert get_store(context[:socket], :shouldnt_be_set_in_core3) == true
    end

    @tag capture_log: true
    test "after handler `except` test 3", context do
      click_and_wait("core3_button")
      assert get_store(context[:socket], :shouldnt_be_set_in_core3) == nil
    end

    @tag capture_log: true
    test "onconnect should go before onload" do
      assert find_element(:id, "onconnect_counter") |> visible_text() == "1"
    end

    @tag capture_log: true
    test "onload should go after onconnect" do
      assert find_element(:id, "onload_counter") |> visible_text() == "2"
    end
  end

  describe "after disconnect" do
    @tag capture_log: true
    test "should return disconnection error" do
      log = capture_log(fn ->
        click_and_wait("disconnect_button")
        navigate_to(index_url(DrabTestApp.Endpoint, :index))
        Process.sleep(1000)
      end)
      assert String.contains?(log, "(Drab.ConnectionError) Disconnected")
    end
  end

  describe "presence" do
    @tag capture_log: true
    test "presence should be started", feature do
      assert Drab.Presence.count_connections(feature.socket) == 1

      change_to_secondary_session()
      core_index() |> navigate_to()
      assert Drab.Presence.count_connections(feature.socket) == 2
    end

    @tag capture_log: true
    test "presence with subscribtion to some topic", feature do
      topic = same_topic("my_topic")
      Drab.Commander.subscribe(feature.socket, topic)
      assert Drab.Presence.count_connections(topic) == 1

      change_to_secondary_session()
      core_index() |> navigate_to()
      find_element(:id, "page_loaded_indicator")
      assert Drab.Presence.count_connections(topic) == 1

      socket = drab_socket()
      Drab.Commander.subscribe(socket, topic)
      assert Drab.Presence.count_connections(topic) == 2

      Drab.Commander.unsubscribe(socket, topic)
      assert Drab.Presence.count_connections(topic) == 1
    end
  end
end


Change Enum protocol to return a function that expect one argument.

# Evalutes the items in the given collection according to the
# Enum::Iterator protocol. Most functions in this module
# will automatically retrieve the protocol given the collection
# and iterator, for example:
#
#     Enum.map [1,2,3], fun(x) { x * 2 }
#
# However, one can use their own iteration function for any
# collection by passing the iterator function as the first
# argument:
#
#     Enum.map my_iteration_function, [1,2,3], fun(x) { x * 2 }
#
# ## The protocol
#
# When `Enum.map` is invoked without the iterator function,
# it invokes `Enum::Iterator.iterator(collection)` with the
# given collection in order to retrieve the default iterator
# for that collection. You can implement the protocol for any
# data type you wish. Elixir ships with a default iterator
# for lists, implemented as follow:
#
#     defimpl Enum::Iterator, for: List do
#       def iterator(_), do: iterate(_)
#
#       defp iterate([h|t]) do
#         { h, t }
#       end
#
#       defp iterate([]) do
#         __STOP_ITERATOR__
#       end
#     end
#
# The __STOP_ITERATOR__ is a special Elixir token that
# marks when iteration should finish.
defmodule Enum do
  defprotocol Iterator, [iterator(collection)], only: [List, Tuple]
  refer Enum::Iterator, as: I

  # Invokes the given `fun` for each item in the `collection`
  # checking if all results evalutes to true. If any does not,
  # abort and return false. Otherwise, true.
  #
  # ## Examples
  #
  #     Enum.all? [2,4,6], fn(x) { rem(x, 2) == 0 }
  #     #=> true
  #
  #     Enum.all? [2,3,4], fn(x) { rem(x, 2) == 0 }
  #     #=> false
  #
  # If no function is given, it defaults to checking if
  # all items in the collection evalutes to true.
  #
  #     Enum.all? [1,2,3]   #=> true
  #     Enum.all? [1,nil,3] #=> false
  #
  def all?(collection, fun // fn(x) { x }) do
    all?(I.iterator(collection), collection, fun)
  end

  def all?(iterator, collection, fun) do
    do_all?(iterator.(collection), iterator, fun)
  end

  # Invokes the given `fun` for each item in the `collection`
  # checking if any of the results returns true. If one does,
  # aborts and returns true. If not, returns false.
  #
  # ## Examples
  #
  #     Enum.any? [2,4,6], fn(x) { rem(x, 2) == 1 }
  #     #=> false
  #
  #     Enum.any? [2,3,4], fn(x) { rem(x, 2) == 1 }
  #     #=> true
  #
  # If no function is given, it defaults to checking if
  # any item in the collection evalutes to true.
  #
  #     Enum.any? [false,false,false] #=> false
  #     Enum.any? [false,true,false]  #=> true
  #
  def any?(collection, fun // fn(x) { x }) do
    any?(I.iterator(collection), collection, fun)
  end

  def any?(iterator, collection, fun) do
    do_any?(iterator.(collection), iterator, fun)
  end

  # Invokes the `fun` for each item in collection
  # and returns the first the function returns a truthy
  # value. If no item is found, returns `ifnone`.
  #
  # ## Examples
  #
  #     Enum.detect [2,4,6], fn(x) { rem(x, 2) == 1 }
  #     # => nil
  #
  #     Enum.detect [2,4,6], 0, fn(x) { rem(x, 2) == 1 }
  #     # => 0
  #
  #     Enum.detect [2,3,4], fn(x) { rem(x, 2) == 1 }
  #     # => 3
  #
  def detect(collection, ifnone // nil, fun) do
    detect(I.iterator(collection), collection, ifnone, fun)
  end

  def detect(iterator, collection, ifnone, fun) do
    do_detect(iterator.(collection), iterator, ifnone, fun)
  end

  # Similar to detect, but returns the value of the function
  # invocation instead of the element iterated.
  #
  # ## Examples
  #
  #     Enum.detect_value [2,4,6], fn(x) { rem(x, 2) == 1 }
  #     # => nil
  #
  #     Enum.detect_value [2,4,6], 0, fn(x) { rem(x, 2) == 1 }
  #     # => 0
  #
  #     Enum.detect_value [2,3,4], fn(x) { rem(x, 2) == 1 }
  #     # => true
  #
  def detect_value(collection, ifnone // nil, fun) do
    detect_value(I.iterator(collection), collection, ifnone, fun)
  end

  def detect_value(iterator, collection, ifnone, fun) do
    do_detect_value(iterator.(collection), iterator, ifnone, fun)
  end

  # Invokes the given `fun` for each item in the `collection`.
  # Returns the `collection` itself.
  #
  # ## Examples
  #
  #     Enum.each ['some', 'example'], fn(x) { IO.puts x }
  #
  def each(collection, fun) do
    each(I.iterator(collection), collection, fun)
  end

  def each(iterator, collection, fun) do
    do_each(iterator.(collection), iterator, fun)
    collection
  end

  # Returns if the collection is empty or not.
  #
  # ## Examples
  #
  #     Enum.empty? [] #=> true
  #     Enum.empty? [1,2,3] #=> false
  #
  def empty?(collection) do
    empty?(I.iterator(collection), collection)
  end

  def empty?(iterator, collection) do
    iterator.(collection) == __STOP_ITERATOR__
  end

  # Invokes the given `fun` for each item in the `collection`.
  # Returns only the items the function evalutes to true.
  #
  # ## Examples
  #
  #     Enum.filter [1, 2, 3], fn(x) { rem(x, 2) == 0 }
  #     #=> [2]
  #
  def filter(collection, fun) do
    filter(I.iterator(collection), collection, fun)
  end

  def filter(iterator, collection, fun) do
    do_filter(iterator.(collection), iterator, fun)
  end

  # Iterates the collection from left to right passing an
  # accumulator as parameter. Returns the accumulator.
  #
  # ## Examples
  #
  #     Enum.foldl [1, 2, 3], 0, fn(x, acc) { x + acc }
  #     #=> 6
  #
  def foldl(collection, acc, f) do
    foldl(I.iterator(collection), collection, acc, f)
  end

  def foldl(iterator, collection, acc, f) do
    do_foldl(iterator.(collection), iterator, acc, f)
  end

  # Join the given `collection` according to `joiner`.
  # Joiner can be either a binary or a list and the
  # result will be of the same type of joiner.
  #
  # ## Examples
  #
  #     Enum.join([1,2,3], " = ") #=> "1 = 2 = 3"
  #     Enum.join([1,2,3], ' = ') #=> '1 = 2 = 3'
  #
  def join(collection, joiner) do
    join(I.iterator(collection), collection, joiner)
  end

  def join(iterator, collection, joiner) when is_list(joiner) do
    binary_to_list join(iterator, collection, list_to_binary(joiner))
  end

  def join(iterator, collection, joiner) do
    do_join(iterator.(collection), iterator, joiner, nil)
  end

  # Invokes the given `fun` for each item in the `collection`.
  # Returns the result of all function calls.
  #
  # ## Examples
  #
  #     Enum.map [1, 2, 3], fn(x) { x * 2 }
  #     #=> [2, 4, 6]
  #
  def map(collection, fun) do
    map(I.iterator(collection), collection, fun)
  end

  def map(iterator, collection, fun) do
    do_map(iterator.(collection), iterator, fun)
  end

  # Invokes the given `fun` for each item in the `collection`
  # while also keeping an accumulator. Returns a tuple where
  # the first element is the iterated collection and the second
  # one is the final accumulator.
  #
  # ## Examples
  #
  #     Enum.mapfoldl [1, 2, 3], 0, fn(x, acc) { { x * 2, x + acc } }
  #     #=> { [2, 4, 6], 6 }
  #
  def mapfoldl(collection, acc, fun) do
    mapfoldl(I.iterator(collection), collection, acc, fun)
  end

  def mapfoldl(iterator, collection, acc, fun) do
    do_mapfoldl(iterator.(collection), iterator, acc, fun)
  end

  ## Implementations

  ## all?

  defp do_all?({ h, next }, iterator, fun) do
    case fun.(h) do
    match: false
      false
    match: nil
      false
    else:
      do_all?(iterator.(next), iterator, fun)
    end
  end

  defp do_all?(__STOP_ITERATOR__, _, _) do
    true
  end

  ## any?

  defp do_any?({ h, next }, iterator, fun) do
    case fun.(h) do
    match: false
      do_any?(iterator.(next), iterator, fun)
    match: nil
      do_any?(iterator.(next), iterator, fun)
    else:
      true
    end
  end

  defp do_any?(__STOP_ITERATOR__, _, _) do
    false
  end

  ## detect

  defp do_detect({ h, next }, iterator, ifnone, fun) do
    case fun.(h) do
    match: false
      do_detect(iterator.(next), iterator, ifnone, fun)
    match: nil
      do_detect(iterator.(next), iterator, ifnone, fun)
    else:
      h
    end
  end

  defp do_detect(__STOP_ITERATOR__, _, ifnone, _) do
    ifnone
  end

  ## detect_value

  defp do_detect_value({ h, next }, iterator, ifnone, fun) do
    case fun.(h) do
    match: false
      do_detect_value(iterator.(next), iterator, ifnone, fun)
    match: nil
      do_detect_value(iterator.(next), iterator, ifnone, fun)
    match: other
      other
    end
  end

  defp do_detect_value(__STOP_ITERATOR__, _, ifnone, _) do
    ifnone
  end

  ## each

  defp do_each({ h, next }, iterator, fun) do
    fun.(h)
    do_each(iterator.(next), iterator, fun)
  end

  defp do_each(__STOP_ITERATOR__, _, _) do
    []
  end

  ## filter

  defp do_filter({ h, next }, iterator, fun) do
    case fun.(h) do
    match: false
      do_filter(iterator.(next), iterator, fun)
    match: nil
      do_filter(iterator.(next), iterator, fun)
    else:
      [h|do_filter(iterator.(next), iterator, fun)]
    end
  end

  defp do_filter(__STOP_ITERATOR__, _, _) do
    []
  end

  ## foldl

  defp do_foldl({ h, next }, iterator, acc, fun) do
    do_foldl(iterator.(next), iterator, fun.(h, acc), fun)
  end

  defp do_foldl(__STOP_ITERATOR__, _, acc, _) do
    acc
  end

  ## join

  # The first item is simply stringified unless ...
  defp do_join({ h, next }, iterator, joiner, nil) do
    do_join(iterator.(next), iterator, joiner, to_binary(h))
  end

  # The first item is __STOP_ITERATOR__, then we return an empty string;
  defp do_join(__STOP_ITERATOR__, _, _joiner, nil) do
    ""
  end

  # All other items are concatenated to acc, by first adding the joiner;
  defp do_join({ h, next }, iterator, joiner, acc) do
    acc = << acc | :binary, joiner | :binary, to_binary(h) | :binary >>
    do_join(iterator.(next), iterator, joiner, acc)
  end

  # Until we have to stop iteration, then we return acc.
  defp do_join(__STOP_ITERATOR__, _, _joiner, acc) do
    acc
  end

  ## map

  defp do_map({ h, next }, iterator, fun) do
    [fun.(h)|do_map(iterator.(next), iterator, fun)]
  end

  defp do_map(__STOP_ITERATOR__, _, _) do
    []
  end

  ## mapfoldl

  defp do_mapfoldl({ h, next }, iterator, acc, f) do
    { result, acc } = f.(h, acc)
    { rest, acc }   = do_mapfoldl(iterator.(next), iterator, acc, f)
    { [result|rest], acc }
  end

  defp do_mapfoldl(__STOP_ITERATOR__, _, acc, _f) do
    { [], acc }
  end
end

defimpl Enum::Iterator, for: List do
  def iterator(_), do: iterate(_)

  defp iterate([h|t]) do
    { h, t }
  end

  defp iterate([]) do
    __STOP_ITERATOR__
  end
end

defmodule Enum do
  defprotocol Iterator, [iterator(collection)], only: [List, Tuple]
  refer Enum::Iterator, as: I

  # Invokes the given `fun` for each item in the `collection`
  # checking if all results evalutes to true. If any does not,
  # abort and return false. Otherwise, true.
  #
  # ## Examples
  #
  #     Enum.all? [2,4,6], fn(x) { rem(x, 2) == 0 }
  #     #=> true
  #
  #     Enum.all? [2,3,4], fn(x) { rem(x, 2) == 0 }
  #     #=> false
  #
  # If no function is given, it defaults to checking if
  # all items in the collection evalutes to true.
  #
  #     Enum.all? [1,2,3]   #=> true
  #     Enum.all? [1,nil,3] #=> false
  #
  def all?(collection, fun // fn(x) { x }) do
    _all?(I.iterator(collection).(), fun)
  end

  # Invokes the given `fun` for each item in the `collection`
  # checking if any of the results returns true. If one does,
  # aborts and returns true. If not, returns false.
  #
  # ## Examples
  #
  #     Enum.any? [2,4,6], fn(x) { rem(x, 2) == 1 }
  #     #=> false
  #
  #     Enum.any? [2,3,4], fn(x) { rem(x, 2) == 1 }
  #     #=> true
  #
  # If no function is given, it defaults to checking if
  # any item in the collection evalutes to true.
  #
  #     Enum.any? [false,false,false] #=> false
  #     Enum.any? [false,true,false]  #=> true
  #
  def any?(collection, fun // fn(x) { x }) do
    _any?(I.iterator(collection).(), fun)
  end

  # Invokes the `fun` for each item in collection
  # and returns the first the function returns a truthy
  # value. If no item is found, returns `ifnone`.
  #
  # ## Examples
  #
  #     Enum.detect [2,4,6], fn(x) { rem(x, 2) == 1 }
  #     # => nil
  #
  #     Enum.detect [2,4,6], 0, fn(x) { rem(x, 2) == 1 }
  #     # => 0
  #
  #     Enum.detect [2,3,4], fn(x) { rem(x, 2) == 1 }
  #     # => 3
  #
  def detect(collection, ifnone // nil, fun) do
    _detect(I.iterator(collection).(), ifnone, fun)
  end

  # Similar to detect, but returns the value of the function
  # invocation instead of the element iterated.
  #
  # ## Examples
  #
  #     Enum.detect_value [2,4,6], fn(x) { rem(x, 2) == 1 }
  #     # => nil
  #
  #     Enum.detect_value [2,4,6], 0, fn(x) { rem(x, 2) == 1 }
  #     # => 0
  #
  #     Enum.detect_value [2,3,4], fn(x) { rem(x, 2) == 1 }
  #     # => true
  #
  def detect_value(collection, ifnone // nil, fun) do
    _detect_value(I.iterator(collection).(), ifnone, fun)
  end

  # Invokes the given `fun` for each item in the `collection`.
  # Returns the `collection` itself.
  #
  # ## Examples
  #
  #     Enum.each ['some', 'example'], fn(x) { IO.puts x }
  #
  def each(collection, fun) do
    _each(I.iterator(collection).(), fun)
    collection
  end

  # Returns if the collection is empty or not.
  #
  # ## Examples
  #
  #     Enum.empty? [] #=> true
  #     Enum.empty? [1,2,3] #=> false
  #
  def empty?(collection) do
    I.iterator(collection).() == __STOP_ITERATOR__
  end

  # Invokes the given `fun` for each item in the `collection`.
  # Returns only the items the function evalutes to true.
  #
  # ## Examples
  #
  #     Enum.filter [1, 2, 3], fn(x) { rem(x, 2) == 0 }
  #     #=> [2]
  #
  def filter(collection, fun) do
    _filter(I.iterator(collection).(), fun)
  end

  # Iterates the collection from left to right passing an
  # accumulator as parameter. Returns the accumulator.
  #
  # ## Examples
  #
  #     Enum.foldl [1, 2, 3], 0, fn(x, acc) { x + acc }
  #     #=> 6
  #
  def foldl(collection, acc, f) do
    _foldl(I.iterator(collection).(), acc, f)
  end

  # Join the given `collection` according to `joiner`.
  # Joiner can be either a binary or a list and the
  # result will be of the same type of joiner.
  #
  # ## Examples
  #
  #     Enum.join([1,2,3], " = ") #=> "1 = 2 = 3"
  #     Enum.join([1,2,3], ' = ') #=> '1 = 2 = 3'
  #
  def join(collection, joiner) when is_list(joiner) do
    binary_to_list join(collection, list_to_binary(joiner))
  end

  def join(collection, joiner) do
    _join(I.iterator(collection).(), joiner, nil)
  end

  # Invokes the given `fun` for each item in the `collection`.
  # Returns the result of all function calls.
  #
  # ## Examples
  #
  #     Enum.map [1, 2, 3], fn(x) { x * 2 }
  #     #=> [2, 4, 6]
  #
  def map(collection, fun) do
    _map(I.iterator(collection).(), fun)
  end

  # Invokes the given `fun` for each item in the `collection`
  # while also keeping an accumulator. Returns a tuple where
  # the first element is the iterated collection and the second
  # one is the final accumulator.
  #
  # ## Examples
  #
  #     Enum.mapfoldl [1, 2, 3], 0, fn(x, acc) { { x * 2, x + acc } }
  #     #=> { [2, 4, 6], 6 }
  #
  def mapfoldl(collection, acc, fun) do
    _mapfoldl(I.iterator(collection).(), acc, fun)
  end

  ## Implementations

  ## all?

  defp _all?({ h, next }, fun) do
    case fun.(h) do
    match: false
      false
    match: nil
      false
    else:
      _all?(next.(), fun)
    end
  end

  defp _all?(__STOP_ITERATOR__, _) do
    true
  end

  ## any?

  defp _any?({ h, next }, fun) do
    case fun.(h) do
    match: false
      _any?(next.(), fun)
    match: nil
      _any?(next.(), fun)
    else:
      true
    end
  end

  defp _any?(__STOP_ITERATOR__, _) do
    false
  end

  ## detect

  defp _detect({ h, next }, ifnone, fun) do
    case fun.(h) do
    match: false
      _detect(next.(), ifnone, fun)
    match: nil
      _detect(next.(), ifnone, fun)
    else:
      h
    end
  end

  defp _detect(__STOP_ITERATOR__, ifnone, _) do
    ifnone
  end

  ## detect_value

  defp _detect_value({ h, next }, ifnone, fun) do
    case fun.(h) do
    match: false
      _detect_value(next.(), ifnone, fun)
    match: nil
      _detect_value(next.(), ifnone, fun)
    match: other
      other
    end
  end

  defp _detect_value(__STOP_ITERATOR__, ifnone, _) do
    ifnone
  end

  ## each

  defp _each({ h, next }, fun) do
    fun.(h)
    _each(next.(), fun)
  end

  defp _each(__STOP_ITERATOR__, _fun) do
    []
  end

  ## filter

  defp _filter({ h, next }, fun) do
    case fun.(h) do
    match: false
      _filter(next.(), fun)
    match: nil
      _filter(next.(), fun)
    else:
      [h|_filter(next.(), fun)]
    end
  end

  defp _filter(__STOP_ITERATOR__, _fun) do
    []
  end

  ## foldl

  defp _foldl({ h, next }, acc, f) do
    _foldl(next.(), f.(h, acc), f)
  end

  defp _foldl(__STOP_ITERATOR__, acc, _f) do
    acc
  end

  ## join

  # The first item is simply stringified unless ...
  defp _join({ h, next }, joiner, nil) do
    _join(next.(), joiner, to_binary(h))
  end

  # The first item is __STOP_ITERATOR__, then we return an empty string;
  defp _join(__STOP_ITERATOR__, _joiner, nil) do
    ""
  end

  # All other items are concatenated to acc, by first adding the joiner;
  defp _join({ h, next }, joiner, acc) do
    acc = << acc | :binary, joiner | :binary, to_binary(h) | :binary >>
    _join(next.(), joiner, acc)
  end

  # Until we have to stop iteration, then we return acc.
  defp _join(__STOP_ITERATOR__, _joiner, acc) do
    acc
  end

  ## map

  defp _map({ h, next }, fun) do
    [fun.(h)|_map(next.(), fun)]
  end

  defp _map(__STOP_ITERATOR__, _fun) do
    []
  end

  ## mapfoldl

  defp _mapfoldl({ h, next }, acc, f) do
    { result, acc } = f.(h, acc)
    { rest, acc }   = _mapfoldl(next.(), acc, f)
    { [result|rest], acc }
  end

  defp _mapfoldl(__STOP_ITERATOR__, acc, _f) do
    { [], acc }
  end
end

defimpl Enum::Iterator, for: List do
  def iterator(list) do
    fn { iterate(list) }
  end

  defp iterate([h|t]) do
    { h, fn { iterate(t) } }
  end

  defp iterate([]) do
    __STOP_ITERATOR__
  end
end


Add a note to José solution

Add a note to José solution

# NOTE: José's solution is much more functional here
# https://gist.github.com/josevalim/978771f4f5c948504db1e88383cac8d0
defmodule AdventOfCode2017.Captcha do
  @doc ~S"""
  The captcha requires you to review a sequence of digits (your puzzle input) and
  find the sum of all digits that match the next digit in the list. The list is circular,
  so the digit after the last digit is the first digit in the list.

  ## Examples

      iex> AdventOfCode2017.Captcha.solve("1122")
      3
      iex> AdventOfCode2017.Captcha.solve("1111")
      4
      iex> AdventOfCode2017.Captcha.solve("1234")
      0
      iex> AdventOfCode2017.Captcha.solve("91212129")
      9
      iex> AdventOfCode2017.Captcha.solve("")
      0
      # This spec depends on how you interpret the request
      iex> AdventOfCode2017.Captcha.solve("1")
      1
  """
  def solve(input) do
    digits = input |> String.graphemes() |> Enum.map(&String.to_integer/1)
    # cycle the digits to the left
    next_digits = Enum.take(digits, -(Enum.count(digits) - 1)) ++ Enum.take(digits, 1)

    digits
    |> Enum.zip(next_digits)
    |> Enum.reduce(0, fn {digit, next_digit}, acc ->
      if digit == next_digit do
        acc + digit
      else
        acc
      end
    end)
  end
end


Don't update the name upon redirects. Fix #105