mirror of
https://github.com/plausible/analytics.git
synced 2024-12-25 18:48:06 +03:00
0fa6b688af
* Make TestUtils module available in all tests * Add macros patching the application env in tests Unfortunately a lot of existing functionality relies on certain application env setup. This isn't ideal because the app config is a shared state that prevents us from running the tests in parallel. Those macros encapsulate setting up new env for test purposes and make sure the changes are reverted when the test finishes. * Allow passing request opts to HTTPClient.post/4 We need this to swap custom request building in Google Analytics import. * Unify errors when listing sites * React: propagate backend error messages if available * React: catch API errors in Search Terms component * Propagate google API errors on referrer drilldown * Handle verified properties errors in SC settings * Add missing tests for SC settings controller * Unify errors for fetching search analytics queries (list stats) * Unify errors refreshing Google Auth Token * Test fetch_stats/3 errors and replace Double with Mox * Fixup makrup * s/class/className * Simplify Search Terms display in case of errors * Fix warnings
421 lines
12 KiB
Elixir
421 lines
12 KiB
Elixir
defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
|
use PlausibleWeb.ConnCase
|
|
use Plausible.Repo
|
|
|
|
setup %{conn: conn} do
|
|
user = insert(:user)
|
|
api_key = insert(:api_key, user: user, scopes: ["sites:provision:*"])
|
|
conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}")
|
|
{:ok, user: user, api_key: api_key, conn: conn}
|
|
end
|
|
|
|
describe "POST /api/v1/sites" do
|
|
test "can create a site", %{conn: conn} do
|
|
conn =
|
|
post(conn, "/api/v1/sites", %{
|
|
"domain" => "some-site.domain",
|
|
"timezone" => "Europe/Tallinn"
|
|
})
|
|
|
|
assert json_response(conn, 200) == %{
|
|
"domain" => "some-site.domain",
|
|
"timezone" => "Europe/Tallinn"
|
|
}
|
|
end
|
|
|
|
test "timezone defaults to Etc/UTC", %{conn: conn} do
|
|
conn =
|
|
post(conn, "/api/v1/sites", %{
|
|
"domain" => "some-site.domain"
|
|
})
|
|
|
|
assert json_response(conn, 200) == %{
|
|
"domain" => "some-site.domain",
|
|
"timezone" => "Etc/UTC"
|
|
}
|
|
end
|
|
|
|
test "domain is required", %{conn: conn} do
|
|
conn = post(conn, "/api/v1/sites", %{})
|
|
|
|
assert json_response(conn, 400) == %{
|
|
"error" => "domain can't be blank"
|
|
}
|
|
end
|
|
|
|
test "accepts international domain names", %{conn: conn} do
|
|
["müllers-café.test", "音乐.cn", "до.101домен.рф/pages"]
|
|
|> Enum.each(fn idn_domain ->
|
|
conn = post(conn, "/api/v1/sites", %{"domain" => idn_domain})
|
|
assert %{"domain" => ^idn_domain} = json_response(conn, 200)
|
|
end)
|
|
end
|
|
|
|
test "validates uri breaking domains", %{conn: conn} do
|
|
["quero:café.test", "h&llo.test", "iamnotsur&about?this.com"]
|
|
|> Enum.each(fn bad_domain ->
|
|
conn = post(conn, "/api/v1/sites", %{"domain" => bad_domain})
|
|
|
|
assert %{"error" => error} = json_response(conn, 400)
|
|
assert error =~ "domain must not contain URI reserved characters"
|
|
end)
|
|
end
|
|
|
|
test "does not allow creating more sites than the limit", %{conn: conn, user: user} do
|
|
Application.put_env(:plausible, :site_limit, 3)
|
|
insert(:site, members: [user])
|
|
insert(:site, members: [user])
|
|
insert(:site, members: [user])
|
|
|
|
conn =
|
|
post(conn, "/api/v1/sites", %{
|
|
"domain" => "some-site.domain",
|
|
"timezone" => "Europe/Tallinn"
|
|
})
|
|
|
|
assert json_response(conn, 403) == %{
|
|
"error" =>
|
|
"Your account has reached the limit of 3 sites per account. Please contact hello@plausible.io to unlock more sites."
|
|
}
|
|
end
|
|
|
|
test "cannot access with a bad API key scope", %{conn: conn, user: user} do
|
|
api_key = insert(:api_key, user: user, scopes: ["stats:read:*"])
|
|
|
|
conn =
|
|
conn
|
|
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
|
|> post("/api/v1/sites", %{"site" => %{"domain" => "domain.com"}})
|
|
|
|
assert json_response(conn, 401) == %{
|
|
"error" =>
|
|
"Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested."
|
|
}
|
|
end
|
|
end
|
|
|
|
describe "DELETE /api/v1/sites/:site_id" do
|
|
setup :create_new_site
|
|
|
|
test "delete a site by it's domain", %{conn: conn, site: site} do
|
|
conn = delete(conn, "/api/v1/sites/" <> site.domain)
|
|
|
|
assert json_response(conn, 200) == %{"deleted" => true}
|
|
end
|
|
|
|
test "is 404 when site cannot be found", %{conn: conn} do
|
|
conn = delete(conn, "/api/v1/sites/foobar.baz")
|
|
|
|
assert json_response(conn, 404) == %{"error" => "Site could not be found"}
|
|
end
|
|
|
|
test "cannot delete a site that the user does not own", %{conn: conn, user: user} do
|
|
site = insert(:site, members: [])
|
|
insert(:site_membership, user: user, site: site, role: :admin)
|
|
conn = delete(conn, "/api/v1/sites/" <> site.domain)
|
|
|
|
assert json_response(conn, 404) == %{"error" => "Site could not be found"}
|
|
end
|
|
|
|
test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do
|
|
api_key = insert(:api_key, user: user, scopes: ["stats:read:*"])
|
|
|
|
conn =
|
|
conn
|
|
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
|
|> delete("/api/v1/sites/" <> site.domain)
|
|
|
|
assert json_response(conn, 401) == %{
|
|
"error" =>
|
|
"Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested."
|
|
}
|
|
end
|
|
end
|
|
|
|
describe "PUT /api/v1/sites/shared-links" do
|
|
setup :create_site
|
|
|
|
test "can add a shared link to a site", %{conn: conn, site: site} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/shared-links", %{
|
|
site_id: site.domain,
|
|
name: "Wordpress"
|
|
})
|
|
|
|
res = json_response(conn, 200)
|
|
assert res["name"] == "Wordpress"
|
|
assert String.starts_with?(res["url"], "http://")
|
|
end
|
|
|
|
test "is idempotent find or create op", %{conn: conn, site: site} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/shared-links", %{
|
|
site_id: site.domain,
|
|
name: "Wordpress"
|
|
})
|
|
|
|
%{"url" => url} = json_response(conn, 200)
|
|
|
|
conn =
|
|
put(conn, "/api/v1/sites/shared-links", %{
|
|
site_id: site.domain,
|
|
name: "Wordpress"
|
|
})
|
|
|
|
assert %{"url" => ^url} = json_response(conn, 200)
|
|
end
|
|
|
|
test "returns 400 when site id missing", %{conn: conn} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/shared-links", %{
|
|
name: "Wordpress"
|
|
})
|
|
|
|
res = json_response(conn, 400)
|
|
assert res["error"] == "Parameter `site_id` is required to create a shared link"
|
|
end
|
|
|
|
test "returns 404 when site id is non existent", %{conn: conn} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/shared-links", %{
|
|
name: "Wordpress",
|
|
site_id: "bad"
|
|
})
|
|
|
|
res = json_response(conn, 404)
|
|
assert res["error"] == "Site could not be found"
|
|
end
|
|
|
|
test "returns 404 when api key owner does not have permissions to create a shared link", %{
|
|
conn: conn,
|
|
site: site,
|
|
user: user
|
|
} do
|
|
Repo.update_all(
|
|
from(sm in Plausible.Site.Membership,
|
|
where: sm.site_id == ^site.id and sm.user_id == ^user.id
|
|
),
|
|
set: [role: :viewer]
|
|
)
|
|
|
|
conn =
|
|
put(conn, "/api/v1/sites/shared-links", %{
|
|
site_id: site.domain,
|
|
name: "Wordpress"
|
|
})
|
|
|
|
res = json_response(conn, 404)
|
|
assert res["error"] == "Site could not be found"
|
|
end
|
|
end
|
|
|
|
describe "PUT /api/v1/sites/goals" do
|
|
setup :create_site
|
|
|
|
test "can add a goal as event to a site", %{conn: conn, site: site} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
site_id: site.domain,
|
|
goal_type: "event",
|
|
event_name: "Signup"
|
|
})
|
|
|
|
res = json_response(conn, 200)
|
|
assert res["goal_type"] == "event"
|
|
assert res["event_name"] == "Signup"
|
|
end
|
|
|
|
test "can add a goal as page to a site", %{conn: conn, site: site} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
site_id: site.domain,
|
|
goal_type: "page",
|
|
page_path: "/signup"
|
|
})
|
|
|
|
res = json_response(conn, 200)
|
|
assert res["goal_type"] == "page"
|
|
assert res["page_path"] == "/signup"
|
|
end
|
|
|
|
test "is idempotent find or create op", %{conn: conn, site: site} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
site_id: site.domain,
|
|
goal_type: "event",
|
|
event_name: "Signup"
|
|
})
|
|
|
|
%{"id" => goal_id} = json_response(conn, 200)
|
|
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
site_id: site.domain,
|
|
goal_type: "event",
|
|
event_name: "Signup"
|
|
})
|
|
|
|
assert %{"id" => ^goal_id} = json_response(conn, 200)
|
|
end
|
|
|
|
test "returns 400 when site id missing", %{conn: conn} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
goal_type: "event",
|
|
event_name: "Signup"
|
|
})
|
|
|
|
res = json_response(conn, 400)
|
|
assert res["error"] == "Parameter `site_id` is required to create a goal"
|
|
end
|
|
|
|
test "returns 404 when site id is non existent", %{conn: conn} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
goal_type: "event",
|
|
event_name: "Signup",
|
|
site_id: "bad"
|
|
})
|
|
|
|
res = json_response(conn, 404)
|
|
assert res["error"] == "Site could not be found"
|
|
end
|
|
|
|
test "returns 404 when api key owner does not have permissions to create a goal", %{
|
|
conn: conn,
|
|
site: site,
|
|
user: user
|
|
} do
|
|
Repo.update_all(
|
|
from(sm in Plausible.Site.Membership,
|
|
where: sm.site_id == ^site.id and sm.user_id == ^user.id
|
|
),
|
|
set: [role: :viewer]
|
|
)
|
|
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
site_id: site.domain,
|
|
goal_type: "event",
|
|
event_name: "Signup"
|
|
})
|
|
|
|
res = json_response(conn, 404)
|
|
assert res["error"] == "Site could not be found"
|
|
end
|
|
|
|
test "returns 400 when goal type missing", %{conn: conn, site: site} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
site_id: site.domain,
|
|
event_name: "Signup"
|
|
})
|
|
|
|
res = json_response(conn, 400)
|
|
assert res["error"] == "Parameter `goal_type` is required to create a goal"
|
|
end
|
|
|
|
test "returns 400 when goal event name missing", %{conn: conn, site: site} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
site_id: site.domain,
|
|
goal_type: "event"
|
|
})
|
|
|
|
res = json_response(conn, 400)
|
|
assert res["error"] == "Parameter `event_name` is required to create a goal"
|
|
end
|
|
|
|
test "returns 400 when goal page path missing", %{conn: conn, site: site} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
site_id: site.domain,
|
|
goal_type: "page"
|
|
})
|
|
|
|
res = json_response(conn, 400)
|
|
assert res["error"] == "Parameter `page_path` is required to create a goal"
|
|
end
|
|
end
|
|
|
|
describe "DELETE /api/v1/sites/goals/:goal_id" do
|
|
setup :create_new_site
|
|
|
|
test "delete a goal by it's id", %{conn: conn, site: site} do
|
|
conn =
|
|
put(conn, "/api/v1/sites/goals", %{
|
|
site_id: site.domain,
|
|
goal_type: "event",
|
|
event_name: "Signup"
|
|
})
|
|
|
|
%{"id" => goal_id} = json_response(conn, 200)
|
|
|
|
conn =
|
|
delete(conn, "/api/v1/sites/goals/#{goal_id}", %{
|
|
site_id: site.domain
|
|
})
|
|
|
|
assert json_response(conn, 200) == %{"deleted" => true}
|
|
end
|
|
|
|
test "is 404 when goal cannot be found", %{conn: conn, site: site} do
|
|
conn =
|
|
delete(conn, "/api/v1/sites/goals/0", %{
|
|
site_id: site.domain
|
|
})
|
|
|
|
assert json_response(conn, 404) == %{"error" => "Goal could not be found"}
|
|
end
|
|
|
|
test "cannot delete a goal belongs to a site that the user does not own", %{
|
|
conn: conn,
|
|
user: user
|
|
} do
|
|
site = insert(:site, members: [])
|
|
insert(:site_membership, user: user, site: site, role: :viewer)
|
|
|
|
conn =
|
|
delete(conn, "/api/v1/sites/goals/1", %{
|
|
site_id: site.domain
|
|
})
|
|
|
|
assert json_response(conn, 404) == %{"error" => "Site could not be found"}
|
|
end
|
|
|
|
test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do
|
|
api_key = insert(:api_key, user: user, scopes: ["stats:read:*"])
|
|
|
|
conn =
|
|
conn
|
|
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
|
|
|
conn =
|
|
delete(conn, "/api/v1/sites/goals/1", %{
|
|
site_id: site.domain
|
|
})
|
|
|
|
assert json_response(conn, 401) == %{
|
|
"error" =>
|
|
"Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested."
|
|
}
|
|
end
|
|
end
|
|
|
|
describe "GET /api/v1/sites/:site_id" do
|
|
setup :create_new_site
|
|
|
|
test "get a site by it's domain", %{conn: conn, site: site} do
|
|
conn = get(conn, "/api/v1/sites/" <> site.domain)
|
|
|
|
assert json_response(conn, 200) == %{"domain" => site.domain, "timezone" => site.timezone}
|
|
end
|
|
|
|
test "is 404 when site cannot be found", %{conn: conn} do
|
|
conn = get(conn, "/api/v1/sites/foobar.baz")
|
|
|
|
assert json_response(conn, 404) == %{"error" => "Site could not be found"}
|
|
end
|
|
end
|
|
end
|