defmodule Plausible.Stats.Dashboard.QueryParserTest do use Plausible.DataCase import Plausible.Stats.Dashboard.QueryParser alias Plausible.Stats.{ParsedQueryParams, QueryError} @base_params %{ "date_range" => "28d", "relative_date" => nil, "filters" => [], "dimensions" => [], "metrics" => ["visitors"], "include" => %{ "imports" => true, "imports_meta" => false, "compare" => nil, "compare_match_day_of_week" => true } } describe "input_date_range" do test "is required" do params = Map.drop(@base_params, ["date_range"]) assert {:error, %QueryError{code: :invalid_date_range}} = parse(params) end for period <- [:realtime, :day, :month, :year, :all] do test "parses #{period} date_range" do params = Map.merge(@base_params, %{"date_range" => Atom.to_string(unquote(period))}) {:ok, parsed} = parse(params) assert_matches %ParsedQueryParams{input_date_range: ^unquote(period)} = parsed end end for i <- [7, 28, 30, 91] do test "parses #{i}d date_range" do params = Map.merge(@base_params, %{"date_range" => "#{unquote(i)}d"}) {:ok, parsed} = parse(params) assert_matches %ParsedQueryParams{ input_date_range: {:last_n_days, ^unquote(i)} } = parsed end end test "parses 24h period" do params = Map.merge(@base_params, %{"date_range" => "24h"}) {:ok, parsed} = parse(params) assert_matches %ParsedQueryParams{input_date_range: :"24h"} = parsed end for i <- [6, 12] do test "parses #{i}mo date_range" do params = Map.merge(@base_params, %{"date_range" => "#{unquote(i)}mo"}) {:ok, parsed} = parse(params) assert_matches %ParsedQueryParams{ input_date_range: {:last_n_months, ^unquote(i)} } = parsed end end test "parses custom date_range" do params = Map.merge(@base_params, %{"date_range" => ["2021-01-01", "2021-03-05"]}) {:ok, parsed} = parse(params) assert %ParsedQueryParams{ input_date_range: {:date_range, ~D[2021-01-01], ~D[2021-03-05]} } = parsed end end describe "relative_date" do test "parses a valid iso8601 date string" do params = Map.merge(@base_params, %{"relative_date" => "2021-05-05"}) {:ok, parsed} = parse(params) assert %ParsedQueryParams{relative_date: ~D[2021-05-05]} = parsed end test "nil is accepted" do params = Map.merge(@base_params, %{"relative_date" => nil}) {:ok, parsed} = parse(params) assert %ParsedQueryParams{relative_date: nil} = parsed end test "errors when invalid date" do params = Map.merge(@base_params, %{"relative_date" => "2021-13-32"}) {:error, %QueryError{code: :invalid_relative_date}} = parse(params) end end describe "include.imports" do test "true -> true" do params = %{@base_params | "include" => %{@base_params["include"] | "imports" => true}} {:ok, parsed} = parse(params) assert parsed.include.imports == true end test "invalid -> false" do params = %{@base_params | "include" => %{@base_params["include"] | "imports" => "foo"}} {:ok, parsed} = parse(params) assert parsed.include.imports == false end test "false -> false" do params = %{@base_params | "include" => %{@base_params["include"] | "imports" => false}} {:ok, parsed} = parse(params) assert parsed.include.imports == false end end describe "include.compare" do for mode <- [:previous_period, :year_over_year] do test "parses #{mode} mode" do params = %{ @base_params | "include" => %{@base_params["include"] | "compare" => "#{unquote(mode)}"} } {:ok, parsed} = parse(params) assert parsed.include.compare == unquote(mode) end end test "parses custom date range mode" do params = %{ @base_params | "include" => %{@base_params["include"] | "compare" => ["2021-01-01", "2021-04-30"]} } {:ok, parsed} = parse(params) assert parsed.include.compare == {:date_range, ~D[2021-01-01], ~D[2021-04-30]} end test "fails with invalid custom dates" do params = %{ @base_params | "include" => %{@base_params["include"] | "compare" => ["2021-13-01", "2021-04-30"]} } assert {:error, %QueryError{code: :invalid_include}} = parse(params) end test "fails with random invalid compare value" do params = %{ @base_params | "include" => %{@base_params["include"] | "compare" => "foo"} } assert {:error, %QueryError{code: :invalid_include}} = parse(params) end test "compare is nil if not provided" do params = %{ @base_params | "include" => Map.drop(@base_params["include"], ["compare"]) } {:ok, parsed} = parse(params) assert parsed.include.compare == nil end end describe "include.compare_match_day_of_week" do test "true -> true" do params = %{ @base_params | "include" => %{@base_params["include"] | "compare_match_day_of_week" => true} } {:ok, parsed} = parse(params) assert parsed.include.compare_match_day_of_week == true end test "invalid -> false" do params = %{ @base_params | "include" => %{@base_params["include"] | "compare_match_day_of_week" => "foo"} } {:ok, parsed} = parse(params) assert parsed.include.compare_match_day_of_week == false end test "false -> false" do params = %{ @base_params | "include" => %{@base_params["include"] | "compare_match_day_of_week" => false} } {:ok, parsed} = parse(params) assert parsed.include.compare_match_day_of_week == false end end describe "dimensions" do test "parses breakdown-only dimensions" do params = Map.merge(@base_params, %{"dimensions" => ["event:prop_key"]}) {:ok, parsed} = parse(params) assert parsed.dimensions == ["event:prop_key"] end end describe "order_by" do test "breakdown-only dimensions can be used in order_by" do params = Map.merge(@base_params, %{ "dimensions" => ["event:prop_key"], "order_by" => [["event:prop_key", "asc"]] }) {:ok, parsed} = parse(params) assert parsed.order_by == [{"event:prop_key", :asc}] end end describe "filters" do test "parses valid filters" do params = Map.merge(@base_params, %{ "filters" => [ ["has_not_done", ["is", "event:goal", ["Signup"]]], ["contains", "visit:exit_page", ["/:dashboard"]], ["is", "visit:source", ["Bing"]], ["is_not", "event:props:theme", ["system", "(none)"]] ] }) {:ok, parsed} = parse(params) assert parsed.filters == [ [:has_not_done, [:is, "event:goal", ["Signup"]]], [:contains, "visit:exit_page", ["/:dashboard"]], [:is, "visit:source", ["Bing"]], [:is_not, "event:props:theme", ["system", "(none)"]] ] end test "cannot filter by breakdown-only dimensions" do params = Map.merge(@base_params, %{ "filters" => [["is", "event:prop_key", ["author"]]] }) {:error, %QueryError{code: :invalid_filters}} = parse(params) end test "parses city filter with multiple clauses" do params = Map.merge(@base_params, %{ "filters" => [["is", "visit:city", ["2988507", "2950159"]]] }) {:ok, parsed} = parse(params) assert parsed.filters == [[:is, "visit:city", ["2988507", "2950159"]]] end test "parses a segment filter" do params = Map.merge(@base_params, %{ "filters" => [["is", "segment", [123]]] }) {:ok, parsed} = parse(params) assert parsed.filters == [[:is, "segment", [123]]] end end describe "metrics" do test "valid metrics" do params = Map.merge(@base_params, %{"metrics" => ["visitors", "group_conversion_rate"]}) assert {:ok, parsed} = parse(params) assert parsed.metrics == [:visitors, :group_conversion_rate] end test "at least one metric required" do params = Map.merge(@base_params, %{"metrics" => []}) assert {:error, %QueryError{code: :invalid_metrics}} = parse(params) end end describe "fixing now" do test "now can't be fixed externally via params" do params = Map.merge(@base_params, %{"now" => "2026-02-17T10:08:52.272894Z"}) {:ok, parsed} = parse(params) assert parsed.now == nil end test "now can be fixed as an optional extra argument to parse/2" do {:ok, parsed} = parse(@base_params, now: ~U[2026-02-17 10:08:00Z]) assert parsed.now == ~U[2026-02-17 10:08:00Z] end end end