| defmodule Plausible.TestUtils do |
| @moduledoc false |
|
|
| 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, %Finch.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 -> |
| |
| "HEAD" = conn.method |
|
|
| |
| 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 |
|
|
| |
| |
| |
| 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 |
|
|
| def prepare_conn(conn) do |
| conn |
| |> Map.put(:secret_key_base, secret_key_base()) |
| |> Plug.Conn.put_req_header("x-forwarded-for", random_ip()) |
| end |
| end |
|
|