473 lines
13 KiB
Elixir
473 lines
13 KiB
Elixir
defmodule Plausible.Ingestion.RequestTest do
|
|
use Plausible.DataCase, async: true
|
|
|
|
import Phoenix.ConnTest
|
|
import Plug.Conn
|
|
|
|
alias Plausible.Ingestion.Request
|
|
|
|
test "request cannot be built from conn without payload" do
|
|
conn = build_conn(:post, "/api/events", %{})
|
|
assert {:error, changeset} = Request.build(conn)
|
|
|
|
errors = Keyword.keys(changeset.errors)
|
|
assert :event_name in errors
|
|
assert :domain in errors
|
|
assert :url in errors
|
|
end
|
|
|
|
test "request cannot be built from non-json payload" do
|
|
conn = build_conn(:post, "/api/events", "defnotjson")
|
|
assert {:error, changeset} = Request.build(conn)
|
|
assert changeset.errors[:request]
|
|
end
|
|
|
|
test "request can be built from URL alone" do
|
|
payload = %{
|
|
name: "pageview",
|
|
url: "http://dummy.site"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:ok, request} = Request.build(conn)
|
|
|
|
assert request.domains == ["dummy.site"]
|
|
assert request.event_name == "pageview"
|
|
assert request.pathname == "/"
|
|
assert request.remote_ip == "127.0.0.1"
|
|
assert %NaiveDateTime{} = request.timestamp
|
|
assert request.user_agent == nil
|
|
assert request.hostname == "dummy.site"
|
|
assert request.uri.host == "dummy.site"
|
|
assert request.uri.scheme == "http"
|
|
assert request.props == %{}
|
|
end
|
|
|
|
@tag :slow
|
|
test "requests include moving timestamp" do
|
|
payload = %{
|
|
name: "pageview",
|
|
url: "http://dummy.site"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
|
|
assert {:ok, request1} = Request.build(conn)
|
|
:timer.sleep(1500)
|
|
assert {:ok, request2} = Request.build(conn)
|
|
|
|
ts1 = request1.timestamp
|
|
ts2 = request2.timestamp
|
|
|
|
assert NaiveDateTime.compare(ts1, ts2) == :lt
|
|
end
|
|
|
|
test "request can be built with domain" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/index"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert request.domains == ["dummy.site"]
|
|
assert request.uri.host == "dummy.site"
|
|
end
|
|
|
|
test "request can be built with domain using shorthands" do
|
|
payload = %{
|
|
n: "pageview",
|
|
d: "dummy.site",
|
|
u: "http://dummy.site/index"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert request.domains == ["dummy.site"]
|
|
assert request.uri.host == "dummy.site"
|
|
end
|
|
|
|
test "request can be built for multiple domains" do
|
|
payload = %{
|
|
n: "pageview",
|
|
d: "dummy.site,crash.site",
|
|
u: "http://dummy.site/index"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert request.domains == ["dummy.site", "crash.site"]
|
|
assert request.uri.host == "dummy.site"
|
|
end
|
|
|
|
test "request can be built with numeric event name" do
|
|
payload = %{
|
|
n: 404,
|
|
d: "dummy.site",
|
|
u: "http://dummy.site/index"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert request.event_name == "404"
|
|
end
|
|
|
|
test "hostname is (none) if host-less uri provided" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "about:config"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert request.hostname == "(none)"
|
|
end
|
|
|
|
test "hostname is set" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/index.html"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert request.hostname == "dummy.site"
|
|
end
|
|
|
|
test "user agent is set as-is" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/index.html"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload) |> put_req_header("user-agent", "Mozilla")
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert request.user_agent == "Mozilla"
|
|
end
|
|
|
|
test "request params are set" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/index.html",
|
|
referrer: "https://example.com",
|
|
hashMode: 1,
|
|
props: %{
|
|
"custom1" => "property1",
|
|
"custom2" => "property2"
|
|
}
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert request.referrer == "https://example.com"
|
|
assert request.hash_mode == 1
|
|
assert request.props["custom1"] == "property1"
|
|
assert request.props["custom2"] == "property2"
|
|
end
|
|
|
|
@tag :ee_only
|
|
test "parses revenue source field from a json string" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/index.html",
|
|
revenue: "{\"amount\":20.2,\"currency\":\"EUR\"}"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert %Money{amount: amount, currency: :EUR} = request.revenue_source
|
|
assert Decimal.new("20.2") == amount
|
|
end
|
|
|
|
@tag :ee_only
|
|
test "sets revenue source with integer amount" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/index.html",
|
|
revenue: %{
|
|
"amount" => 20,
|
|
"currency" => "USD"
|
|
}
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert %Money{amount: amount, currency: :USD} = request.revenue_source
|
|
assert Decimal.equal?(amount, Decimal.new("20.0"))
|
|
end
|
|
|
|
@tag :ee_only
|
|
test "sets revenue source with float amount" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/index.html",
|
|
revenue: %{
|
|
"amount" => 20.1,
|
|
"currency" => "USD"
|
|
}
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert %Money{amount: amount, currency: :USD} = request.revenue_source
|
|
assert Decimal.equal?(amount, Decimal.new("20.1"))
|
|
end
|
|
|
|
@tag :ee_only
|
|
test "parses string amounts into money structs" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/index.html",
|
|
revenue: %{
|
|
"amount" => "12.3",
|
|
"currency" => "USD"
|
|
}
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert %Money{amount: amount, currency: :USD} = request.revenue_source
|
|
assert Decimal.equal?(amount, Decimal.new("12.3"))
|
|
end
|
|
|
|
@tag :ee_only
|
|
test "ignores revenue data when currency is invalid" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/index.html",
|
|
revenue: %{
|
|
"amount" => 1233.2,
|
|
"currency" => "EEEE"
|
|
}
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert is_nil(request.revenue_source)
|
|
end
|
|
|
|
test "pathname is set" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/pictures/index.html#foo"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
|
|
assert {:ok, request} = Request.build(conn)
|
|
|
|
assert request.pathname == "/pictures/index.html"
|
|
end
|
|
|
|
test "pathname is set with hashMode" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/pictures/index.html#foo",
|
|
hashMode: 1
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
|
|
assert {:ok, request} = Request.build(conn)
|
|
|
|
assert request.pathname == "/pictures/index.html#foo"
|
|
end
|
|
|
|
test "query params are set" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/pictures/index.html?foo=bar&baz=bam"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert request.query_params["foo"] == "bar"
|
|
assert request.query_params["baz"] == "bam"
|
|
end
|
|
|
|
test "returns validation error when using data uris" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "data:text/html,%3Cscript%3Ealert%28%27hi%27%29%3B%3C%2Fscript%3E"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:error, changeset} = Request.build(conn)
|
|
assert {"scheme is not allowed", _} = changeset.errors[:url]
|
|
end
|
|
|
|
test "returns validation error when url is too long" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "https://dummy.site/#{String.duplicate("a", 5000)}"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:error, changeset} = Request.build(conn)
|
|
assert {"must be a valid url", _} = changeset.errors[:url]
|
|
end
|
|
|
|
test "returns validation error when event name is too long" do
|
|
payload = %{
|
|
name: String.duplicate("a", 500),
|
|
domain: "dummy.site",
|
|
url: "https://dummy.site/"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:error, changeset} = Request.build(conn)
|
|
assert {"should be at most %{count} character(s)", _} = changeset.errors[:event_name]
|
|
end
|
|
|
|
test "returns validation error when event name is blank" do
|
|
payload = %{
|
|
name: nil,
|
|
domain: "dummy.site",
|
|
url: "https://dummy.site/"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:error, changeset} = Request.build(conn)
|
|
assert {"can't be blank", _} = changeset.errors[:event_name]
|
|
end
|
|
|
|
test "returns validation error when event name cannot be cast to string" do
|
|
payload = %{
|
|
name: ["list", "of", "things"],
|
|
domain: "dummy.site",
|
|
url: "https://dummy.site/"
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:error, changeset} = Request.build(conn)
|
|
assert {"is invalid", _} = changeset.errors[:event_name]
|
|
end
|
|
|
|
test "truncates referrer when too long" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "https://dummy.site/",
|
|
referrer: String.duplicate("a", 2500)
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert request.referrer == String.duplicate("a", 2000)
|
|
end
|
|
|
|
test "returns validation error when props keys are too long" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "https://dummy.site/",
|
|
props: %{String.duplicate("a", 500) => "abc"}
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:error, changeset} = Request.build(conn)
|
|
|
|
assert {"keys should have at most 300 bytes and values 2000 bytes", _} =
|
|
changeset.errors[:props]
|
|
end
|
|
|
|
test "returns validation error when props values are too long" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "https://dummy.site/",
|
|
props: %{"abc" => String.duplicate("a", 2500)}
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
assert {:error, changeset} = Request.build(conn)
|
|
|
|
assert {"keys should have at most 300 bytes and values 2000 bytes", _} =
|
|
changeset.errors[:props]
|
|
end
|
|
|
|
test "trims prop list to 30 items when sending too many items" do
|
|
payload = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "http://dummy.site/index.html",
|
|
referrer: "https://example.com",
|
|
hashMode: 1,
|
|
props: for(i <- 1..50, do: {"#{i}", "foo"}, into: %{})
|
|
}
|
|
|
|
conn = build_conn(:post, "/api/events", payload)
|
|
|
|
assert {:ok, request} = Request.build(conn)
|
|
assert map_size(request.props) == 30
|
|
end
|
|
|
|
test "malicious input, technically valid json" do
|
|
conn = build_conn(:post, "/api/events", "\"<script>\"")
|
|
assert {:error, changeset} = Request.build(conn)
|
|
assert changeset.errors[:request]
|
|
end
|
|
|
|
@tag :ee_only
|
|
test "encodable" do
|
|
params = %{
|
|
name: "pageview",
|
|
domain: "dummy.site",
|
|
url: "https://dummy.site/pictures/index.html?foo=bar&baz=bam",
|
|
referrer: "https://example.com",
|
|
props: %{"abc" => "qwerty", "hello" => "world"},
|
|
hashMode: 1,
|
|
revenue: %{
|
|
"amount" => "12.3",
|
|
"currency" => "USD"
|
|
}
|
|
}
|
|
|
|
assert {:ok, request} =
|
|
build_conn(:post, "/api/events", params)
|
|
|> put_req_header("user-agent", "Mozilla")
|
|
|> Request.build()
|
|
|
|
request = request |> Jason.encode!() |> Jason.decode!()
|
|
|
|
assert Map.drop(request, ["timestamp"]) == %{
|
|
"domains" => ["dummy.site"],
|
|
"event_name" => "pageview",
|
|
"hash_mode" => 1,
|
|
"hostname" => "dummy.site",
|
|
"pathname" => "/pictures/index.html",
|
|
"props" => %{"abc" => "qwerty", "hello" => "world"},
|
|
"query_params" => %{"baz" => "bam", "foo" => "bar"},
|
|
"referrer" => "https://example.com",
|
|
"remote_ip" => "127.0.0.1",
|
|
"revenue_source" => %{"amount" => "12.3", "currency" => "USD"},
|
|
"uri" => "https://dummy.site/pictures/index.html?foo=bar&baz=bam",
|
|
"user_agent" => "Mozilla",
|
|
"ip_classification" => nil
|
|
}
|
|
|
|
assert %NaiveDateTime{} = NaiveDateTime.from_iso8601!(request["timestamp"])
|
|
end
|
|
end
|