defmodule Plausible.Factory do @moduledoc """ See https://ecto.hexdocs.pm/test-factories.html """ use ExMachina.Ecto, repo: Plausible.Repo require Plausible.Billing.Subscription.Status alias Plausible.Billing.Subscription def team_factory do %Plausible.Teams.Team{ name: Plausible.Teams.default_name(), trial_expiry_date: Date.utc_today() |> Date.shift(day: 30), setup_complete: true, setup_at: NaiveDateTime.utc_now() } end def team_membership_factory do %Plausible.Teams.Membership{ user: build(:user), role: :viewer } end def guest_membership_factory do %Plausible.Teams.GuestMembership{ team_membership: build(:team_membership, role: :guest) } end def team_invitation_factory do %Plausible.Teams.Invitation{ invitation_id: Nanoid.generate(), email: sequence(:email, &"email-#{&1}@example.com"), role: :admin } end def guest_invitation_factory do %Plausible.Teams.GuestInvitation{ invitation_id: Nanoid.generate(), role: :editor, team_invitation: build(:team_invitation, role: :guest) } end def site_transfer_factory do %Plausible.Teams.SiteTransfer{ transfer_id: Nanoid.generate(), email: sequence(:email, &"email-#{&1}@example.com") } end def user_factory(attrs) do pw = Map.get(attrs, :password, "password") user = %Plausible.Auth.User{ name: "Jane Smith", email: sequence(:email, &"email-#{&1}@example.com"), password_hash: Plausible.Auth.Password.hash(pw), email_verified: true } merge_attributes(user, attrs) end def spike_notification_factory do %Plausible.Site.TrafficChangeNotification{ threshold: 10, type: :spike } end def drop_notification_factory do %Plausible.Site.TrafficChangeNotification{ threshold: 1, type: :drop } end def site_factory(attrs) do # The é exercises unicode support in domain names domain = sequence(:domain, &"é-#{&1}.example.com") site = %Plausible.Site{ native_stats_start_at: ~N[2000-01-01 00:00:00], domain: domain, timezone: "Etc/UTC" } merge_attributes(site, attrs) end def site_import_factory do today = Date.utc_today() %Plausible.Imported.SiteImport{ site: build(:site), imported_by: build(:user), start_date: ~D[2005-01-01], end_date: today, source: :universal_analytics, status: :completed, legacy: false } end def ch_session_factory do hostname = sequence(:domain, &"example-#{&1}.com") %Plausible.ClickhouseSessionV2{ sign: 1, session_id: SipHash.hash!(hash_key(), Ecto.UUID.generate()), user_id: SipHash.hash!(hash_key(), Ecto.UUID.generate()), hostname: hostname, site_id: Enum.random(1_000_000_000..2_000_000_000), entry_page: "/", pageviews: 1, events: 1, start: DateTime.utc_now(), timestamp: DateTime.utc_now(), is_bounce: false } end def pageview_factory(attrs) do Map.put(event_factory(attrs), :name, "pageview") end def engagement_factory(attrs) do Map.put(event_factory(attrs), :name, "engagement") end def event_factory(attrs) do if Map.get(attrs, :acquisition_channel) do raise "Acquisition channel cannot be written directly since it's a materialized column." end hostname = sequence(:domain, &"example-#{&1}.com") event = %Plausible.ClickhouseEventV2{ hostname: hostname, site_id: Enum.random(1_000_000_000..2_000_000_000), pathname: "/", timestamp: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second), user_id: SipHash.hash!(hash_key(), Ecto.UUID.generate()), session_id: SipHash.hash!(hash_key(), Ecto.UUID.generate()) } event |> merge_attributes(attrs) |> evaluate_lazy_attributes() end def goal_factory(attrs) do display_name_provided? = Map.has_key?(attrs, :display_name) attrs = case {attrs, display_name_provided?} do {%{page_path: path}, false} when is_binary(path) -> Map.put(attrs, :display_name, "Visit " <> path) {%{page_path: path}, false} when is_function(path, 0) -> attrs |> Map.put(:display_name, "Visit " <> path.()) |> Map.put(:page_path, path.()) {%{event_name: event_name}, false} when is_binary(event_name) -> Map.put(attrs, :display_name, event_name) {%{event_name: event_name}, false} when is_function(event_name, 0) -> attrs |> Map.put(:display_name, event_name.()) |> Map.put(:event_name, event_name.()) _ -> attrs end merge_attributes(%Plausible.Goal{}, attrs) end def subscription_factory do %Plausible.Billing.Subscription{ paddle_subscription_id: sequence(:paddle_subscription_id, &"subscription-#{&1}"), paddle_plan_id: sequence(:paddle_plan_id, &"plan-#{&1}"), cancel_url: "cancel.com", update_url: "cancel.com", status: Subscription.Status.active(), next_bill_amount: "6.00", next_bill_date: Date.utc_today(), last_bill_date: Date.utc_today(), currency_code: "USD" } end def starter_subscription_factory do build(:subscription, paddle_plan_id: "910413") end def growth_subscription_factory do build(:subscription, paddle_plan_id: "857097") end def business_subscription_factory do build(:subscription, paddle_plan_id: "857087") end def enterprise_plan_factory do %Plausible.Billing.EnterprisePlan{ paddle_plan_id: sequence(:paddle_plan_id, &"plan-#{&1}"), billing_interval: :monthly, monthly_pageview_limit: 1_000_000, hourly_api_request_limit: 3000, site_limit: 100, team_member_limit: 10 } end def google_auth_factory do %Plausible.Site.GoogleAuth{ email: sequence(:google_auth_email, &"email-#{&1}@example.com"), refresh_token: "123", access_token: "123", expires: DateTime.utc_now() |> DateTime.shift(day: 1) } end def weekly_report_factory do %Plausible.Site.WeeklyReport{} end def monthly_report_factory do %Plausible.Site.MonthlyReport{} end def shared_link_factory do slug = Nanoid.generate() name = "Link - #{slug}" %Plausible.Site.SharedLink{ name: name, slug: slug } end def api_key_factory do key = :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64) %Plausible.Auth.ApiKey{ name: "api-key-name", key: key, key_hash: Plausible.Auth.ApiKey.do_hash(key), key_prefix: binary_part(key, 0, 6) } end def imported_visitors_factory do %{ table: "imported_visitors", date: Date.utc_today(), visitors: 1, pageviews: 1, bounces: 0, visits: 1, visit_duration: 10 } end def imported_sources_factory do %{ table: "imported_sources", date: Date.utc_today(), source: "", visitors: 1, visits: 1, bounces: 0, visit_duration: 10 } end def imported_pages_factory do %{ table: "imported_pages", date: Date.utc_today(), page: "", visitors: 1, pageviews: 1, exits: 0, total_time_on_page: 10, total_time_on_page_visits: 1 } end def imported_entry_pages_factory do %{ table: "imported_entry_pages", date: Date.utc_today(), entry_page: "", visitors: 1, entrances: 1, bounces: 0, visit_duration: 10 } end def imported_exit_pages_factory do %{ table: "imported_exit_pages", date: Date.utc_today(), exit_page: "", visitors: 1, exits: 1 } end def imported_custom_events_factory do %{ table: "imported_custom_events", date: Date.utc_today(), name: "", link_url: "", visitors: 1, events: 1 } end def imported_locations_factory do %{ table: "imported_locations", date: Date.utc_today(), country: "", region: "", city: 0, visitors: 1, visits: 1, bounces: 0, visit_duration: 10 } end def imported_devices_factory do %{ table: "imported_devices", date: Date.utc_today(), device: "", visitors: 1, visits: 1, bounces: 0, visit_duration: 10 } end def imported_browsers_factory do %{ table: "imported_browsers", date: Date.utc_today(), browser: "", visitors: 1, visits: 1, bounces: 0, visit_duration: 10 } end def imported_operating_systems_factory do %{ table: "imported_operating_systems", date: Date.utc_today(), operating_system: "", visitors: 1, visits: 1, bounces: 0, visit_duration: 10 } end def ip_rule_factory do %Plausible.Shield.IPRule{ inet: Plausible.TestUtils.random_ip(), description: "Test IP Rule", added_by: "Mr Seed " } end def country_rule_factory do %Plausible.Shield.CountryRule{ added_by: "Mr Seed " } end def segment_factory do %Plausible.Segments.Segment{ segment_data: %{"filters" => [["is", "visit:entry_page", ["/blog"]]]} } end def annotation_factory do %Plausible.Annotations.Annotation{ note: "a test annotation", type: :site, datetime: ~U[2026-01-04 00:00:00Z], date: ~D[2026-01-04], granularity: :date } end defp hash_key() do Keyword.fetch!( Application.get_env(:plausible, PlausibleWeb.Endpoint), :secret_key_base ) |> binary_part(0, 16) end end