defmodule Plausible.TestUtils do use Plausible.Repo use Plausible alias Plausible.Factory defmacro __using__(_) do quote do require Plausible.TestUtils import Plausible.TestUtils end end defmacro patch_env(env_key, value) do quote do if __MODULE__.__info__(:attributes)[:ex_unit_async] == [true] do raise "Patching env is unsafe in asynchronous tests. maybe extract the case elsewhere?" end original_env = Application.get_env(:plausible, unquote(env_key)) Application.put_env(:plausible, unquote(env_key), unquote(value)) on_exit(fn -> Application.put_env(:plausible, unquote(env_key), original_env) end) {:ok, %{patched_env: true}} end end defmacro setup_patch_env(env_key, value) do quote do setup do patch_env(unquote(env_key), unquote(value)) end end end def setup_do(context \\ %{}, step_fn) do case step_fn.(context) do {:ok, ctx} -> Map.merge(context, Map.new(ctx)) ctx -> Map.merge(context, Map.new(ctx)) end end def create_user(_) do {:ok, user: Plausible.Teams.Test.new_user()} end def create_site(%{user: user}) do {:ok, site: Plausible.Teams.Test.new_site(owner: user)} end def create_team(%{user: user}) do {:ok, team} = Plausible.Teams.get_or_create(user) {:ok, team: team} end def setup_team(%{conn: conn, team: team}) do team = Plausible.Teams.complete_setup(team) conn = conn |> Plug.Conn.fetch_session() |> Plug.Conn.put_session(:current_team_id, team.identifier) {:ok, conn: conn, team: team} end def create_legacy_site_import(%{site: site}) do create_site_import(%{site: site, create_legacy_import?: true}) end def create_site_import(%{site: site} = opts) do site_import = Factory.insert(:site_import, site: site, start_date: ~D[2005-01-01], end_date: Date.utc_today(), source: :universal_analytics, legacy: opts[:create_legacy_import?] == true ) {:ok, site_import: site_import} end def create_api_key(%{user: user}) do team = Plausible.Teams.Test.team_of(user) api_key = Factory.insert(:api_key, user: user, team: team) {:ok, api_key: api_key.key} end def use_api_key(%{conn: conn, api_key: api_key}) do conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key}") {:ok, conn: conn} end def log_in(%{user: user, conn: conn}) do conn = conn |> init_session() |> PlausibleWeb.UserAuth.log_in_user(user) |> Phoenix.ConnTest.recycle() |> Map.put(:secret_key_base, secret_key_base()) |> init_session() {:ok, conn: conn} end on_ee do alias Plausible.Auth.SSO def setup_sso(%{team: team} = ctx) do team = Plausible.Teams.complete_setup(team) integration = SSO.initiate_saml_integration(team) {:ok, sso_domain} = SSO.Domains.add(integration, ctx[:domain] || "example.com") _sso_domain = SSO.Domains.verify(sso_domain, skip_checks?: true) {:ok, team: team, sso_integration: integration, sso_domain: sso_domain} end def provision_sso_user(%{user: user, sso_integration: integration}) do identity = new_identity(user.name, user.email, integration) {:ok, _, _, sso_user} = SSO.provision_user(identity) {:ok, user: sso_user} end end def init_session(conn) do opts = Plug.Session.init( store: :cookie, key: "foobar", encryption_salt: "encrypted cookie salt", signing_salt: "signing salt", log: false, encrypt: false ) conn |> Plug.Session.call(opts) |> Plug.Conn.fetch_session() end def generate_usage_for(site, i, timestamp \\ NaiveDateTime.utc_now()) do events = for _i <- 1..i, do: Factory.build(:pageview, timestamp: timestamp) populate_stats(site, events) :ok end def populate_stats(site, import_id, events) do Enum.map(events, fn event -> event = Map.put(event, :site_id, site.id) case event do %Plausible.ClickhouseEventV2{} -> event imported_event -> Map.put(imported_event, :import_id, import_id) end end) |> populate_stats end def populate_stats(site, events) do Enum.map(events, fn event -> Map.put(event, :site_id, site.id) end) |> populate_stats end def populate_stats(events) do {native, imported} = events |> Enum.map(fn event -> case event do %{timestamp: timestamp} -> %{event | timestamp: to_naive_truncate(timestamp)} _other -> event end end) |> Enum.split_with(fn event -> case event do %Plausible.ClickhouseEventV2{} -> true _ -> false end end) populate_native_stats(native) populate_imported_stats(imported) end defp populate_native_stats(events) do for event_params <- events do {:ok, session} = Plausible.Session.CacheStore.on_event(event_params, event_params, nil, skip_balancer?: true ) event_params |> Plausible.ClickhouseEventV2.merge_session(session) |> Plausible.Event.WriteBuffer.insert() end Plausible.Session.WriteBuffer.flush() Plausible.Event.WriteBuffer.flush() end defp populate_imported_stats(events) do Enum.group_by(events, &Map.fetch!(&1, :table), &Map.delete(&1, :table)) |> Enum.map(fn {table, events} -> Plausible.Imported.Buffer.insert_all(table, events) end) end def relative_time(shifts) do NaiveDateTime.utc_now() |> NaiveDateTime.shift(shifts) |> NaiveDateTime.truncate(:second) end def to_naive_truncate(%DateTime{} = dt) do to_naive_truncate(DateTime.to_naive(dt)) end def to_naive_truncate(%NaiveDateTime{} = naive) do NaiveDateTime.truncate(naive, :second) end def to_naive_truncate(%Date{} = date) do NaiveDateTime.new!(date, ~T[00:00:00]) end def eventually(expectation, wait_time_ms \\ 50, retries \\ 10) do Enum.reduce_while(1..retries, nil, fn attempt, _acc -> case expectation.() do {true, result} -> {:halt, result} {false, _} -> Process.sleep(wait_time_ms * attempt) {:cont, nil} end end) end def await_clickhouse_count(query, expected) do eventually( fn -> count = Plausible.ClickhouseRepo.aggregate(query, :count) {count == expected, count} end, 100, 10 ) end def random_ip() do Enum.map_join(1..4, ".", fn _ -> Enum.random(1..254) end) end def htmlize_quotes(string) do String.replace(string, "'", "'") end def minio_running? do %{host: host, port: port} = ExAws.Config.new(:s3) healthcheck_req = Finch.build(:head, "http://#{host}:#{port}") case Finch.request(healthcheck_req, Plausible.Finch) do {:ok, %Finch.Response{}} -> true {:error, %Mint.TransportError{reason: :econnrefused}} -> false end end def ensure_minio do unless minio_running?() do %{host: host, port: port} = ExAws.Config.new(:s3) IO.puts(""" #{IO.ANSI.red()} You are trying to run MinIO tests (--include minio) \ but nothing is running on #{"http://#{host}:#{port}"}. #{IO.ANSI.blue()}Please make sure to start MinIO with `make minio`#{IO.ANSI.reset()} """) :init.stop(1) end end if Mix.env() == :test do def maybe_fake_minio(_context) do unless minio_running?() do %{port: port} = ExAws.Config.new(:s3) bypass = Bypass.open(port: port) Bypass.expect(bypass, fn conn -> # we only need to fake HeadObject, all the other S3 requests are "controlled" "HEAD" = conn.method # we pretent the object is not found Plug.Conn.send_resp(conn, 404, []) end) end :ok end else def maybe_fake_minio(_context) do :ok end end def monthly_pageview_usage_stub(penultimate_usage, last_usage) do last_bill_date = Date.utc_today() |> Date.shift(day: -1) Plausible.Teams.Billing |> Double.stub(:monthly_pageview_usage, fn _user -> %{ last_cycle: %{ date_range: Date.range( Date.shift(last_bill_date, month: -1), Date.shift(last_bill_date, day: -1) ), total: last_usage }, penultimate_cycle: %{ date_range: Date.range( Date.shift(last_bill_date, month: -2), Date.shift(last_bill_date, day: -1, month: -1) ), total: penultimate_usage } } end) end defp secret_key_base() do :plausible |> Application.fetch_env!(PlausibleWeb.Endpoint) |> Keyword.fetch!(:secret_key_base) end # normal `@tag :tmp_dir` might not work in Plausible.Session.Transfer tests # if the path is too long for unix domain sockets (>104) # this one makes paths a bit shorter def tmp_dir do name = "plausible-#{System.unique_integer([:positive])}" tmp_dir = Path.join(System.tmp_dir!(), name) File.rm_rf!(tmp_dir) File.mkdir_p!(tmp_dir) ExUnit.Callbacks.on_exit(fn -> File.rm_rf!(tmp_dir) end) tmp_dir end on_ee do def new_identity(name, email, integration, id \\ Ecto.UUID.generate()) do %Plausible.Auth.SSO.Identity{ id: id, integration_id: integration.identifier, name: name, email: email, expires_at: NaiveDateTime.add(NaiveDateTime.utc_now(:second), 6, :hour) } end end end