Google APIs integration improvements (#2358)

* 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
This commit is contained in:
Adam Rutkowski 2022-10-24 09:34:02 +02:00 committed by GitHub
parent 93bb62ef27
commit 0fa6b688af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 427 additions and 191 deletions

View File

@ -4,9 +4,10 @@ let abortController = new AbortController()
let SHARED_LINK_AUTH = null
class ApiError extends Error {
constructor(message) {
super(message);
this.name = "ApiError";
constructor(message, payload) {
super(message)
this.name = "ApiError"
this.payload = payload
}
}
@ -56,7 +57,7 @@ export function get(url, query={}, ...extraQuery) {
.then( response => {
if (!response.ok) {
return response.json().then((msg) => {
throw new ApiError(msg.error)
throw new ApiError(msg.error, msg)
})
}
return response.json()

View File

@ -30,7 +30,11 @@ export default class SearchTerms extends React.Component {
searchTerms: res.search_terms || [],
notConfigured: res.not_configured,
isAdmin: res.is_admin
}))
})).catch((error) =>
{
this.setState({ loading: false, searchTerms: [], notConfigured: true, error: true, isAdmin: error.payload.is_admin })
}
)
}
renderSearchTerm(term) {
@ -67,8 +71,10 @@ export default class SearchTerms extends React.Component {
return (
<div className="text-center text-gray-700 dark:text-gray-300 text-sm mt-20">
<RocketIcon />
<div>The site is not connected to Google Search Keywords</div>
<div>Cannot show search terms</div>
<div>
This site is not connected to Search Console so we cannot show the search phrases.
{this.state.isAdmin && this.state.error && <><br/><br/><p>Please click below to connect your Search Console account.</p></>}
</div>
{this.state.isAdmin && <a href={`/${encodeURIComponent(this.props.site.domain)}/settings/search-console`} className="button mt-4">Connect with Google</a> }
</div>
)

View File

@ -182,9 +182,8 @@ defmodule Plausible.Google.Api do
buffer_pid = Keyword.get(opts, :buffer)
attempt = Keyword.get(opts, :attempt, 1)
sleep_time = Keyword.get(opts, :sleep_time, 1000)
http_client = Keyword.get(opts, :http_client, Finch)
case HTTP.get_report(http_client, report_request) do
case HTTP.get_report(report_request) do
{:ok, {rows, next_page_token}} ->
records = Plausible.Imported.from_google_analytics(rows, site.id, report_request.dataset)
:ok = Plausible.Google.Buffer.insert_many(buffer_pid, report_request.dataset, records)

View File

@ -2,82 +2,65 @@ defmodule Plausible.Google.HTTP do
require Logger
alias Plausible.HTTPClient
@spec get_report(module(), Plausible.Google.ReportRequest.t()) ::
@spec get_report(Plausible.Google.ReportRequest.t()) ::
{:ok, {[map()], String.t() | nil}} | {:error, any()}
def get_report(http_client, %Plausible.Google.ReportRequest{} = report_request) do
params =
Jason.encode!(%{
reportRequests: [
%{
viewId: report_request.view_id,
dateRanges: [
%{
startDate: report_request.date_range.first,
endDate: report_request.date_range.last
}
],
dimensions: Enum.map(report_request.dimensions, &%{name: &1, histogramBuckets: []}),
metrics: Enum.map(report_request.metrics, &%{expression: &1}),
hideTotals: true,
hideValueRanges: true,
orderBys: [%{fieldName: "ga:date", sortOrder: "DESCENDING"}],
pageSize: report_request.page_size,
pageToken: report_request.page_token
}
]
})
def get_report(%Plausible.Google.ReportRequest{} = report_request) do
params = %{
reportRequests: [
%{
viewId: report_request.view_id,
dateRanges: [
%{
startDate: report_request.date_range.first,
endDate: report_request.date_range.last
}
],
dimensions: Enum.map(report_request.dimensions, &%{name: &1, histogramBuckets: []}),
metrics: Enum.map(report_request.metrics, &%{expression: &1}),
hideTotals: true,
hideValueRanges: true,
orderBys: [%{fieldName: "ga:date", sortOrder: "DESCENDING"}],
pageSize: report_request.page_size,
pageToken: report_request.page_token
}
]
}
response =
:post
|> Finch.build(
HTTPClient.impl().post(
"#{reporting_api_url()}/v4/reports:batchGet",
[{"Authorization", "Bearer #{report_request.access_token}"}],
params
params,
receive_timeout: 60_000
)
|> http_client.request(Plausible.Finch, receive_timeout: 60_000)
with {:ok, %{status: 200, body: body}} <- response,
with {:ok, %{body: body}} <- response,
{:ok, report} <- parse_report_from_response(body),
token <- Map.get(report, "nextPageToken"),
{:ok, report} <- convert_to_maps(report) do
{:ok, {report, token}}
else
{:ok, %{status: _non_http_200, body: _body} = response} ->
report_failed_request_to_sentry(response)
{:error, %{reason: %{status: status, body: body}}} ->
Sentry.Context.set_extra_context(%{ga_response: %{body: body, status: status}})
{:error, :request_failed}
{:error, cause} ->
{:error, cause}
{:error, _} ->
{:error, :request_failed}
end
end
defp report_failed_request_to_sentry(%{status: status, body: body}) do
case Jason.decode(body) do
{:ok, %{} = body} ->
Sentry.Context.set_extra_context(%{ga_response: %{body: body, status: status}})
_error ->
Sentry.Context.set_extra_context(%{ga_response: %{body: body, status: status}})
end
end
defp parse_report_from_response(raw_body) do
with {:ok, map} <- Jason.decode(raw_body),
%{"reports" => [report | _]} <- map do
defp parse_report_from_response(body) do
with %{"reports" => [report | _]} <- body do
{:ok, report}
else
{:error, cause} ->
Logger.error("Google Analytics: Failed to parse JSON. Reason: #{inspect(cause)}")
Sentry.Context.set_extra_context(%{google_analytics_response: raw_body})
{:error, cause}
_ ->
Sentry.Context.set_extra_context(%{google_analytics_response: body})
%{} = response ->
Logger.error(
"Google Analytics: Failed to find report in response. Reason: #{inspect(response)}"
"Google Analytics: Failed to find report in response. Reason: #{inspect(body)}"
)
Sentry.Context.set_extra_context(%{google_analytics_response: response})
{:error, {:invalid_response, response}}
{:error, {:invalid_response, body}}
end
end
@ -114,13 +97,19 @@ defmodule Plausible.Google.HTTP do
url = "#{api_url()}/webmasters/v3/sites"
headers = [{"Content-Type", "application/json"}, {"Authorization", "Bearer #{access_token}"}]
case HTTPClient.get(url, headers) do
case HTTPClient.impl().get(url, headers) do
{:ok, %{body: body}} ->
{:ok, body}
{:error, reason} = e ->
{:error, %{reason: %{status: s}}} when s in [401, 403] ->
{:error, "google_auth_error"}
{:error, %{reason: %{body: %{"error" => error}}}} ->
{:error, error}
{:error, reason} ->
Logger.error("Google Analytics: failed to list sites: #{inspect(reason)}")
e
{:error, "failed_to_list_sites"}
end
end
@ -182,25 +171,20 @@ defmodule Plausible.Google.HTTP do
url = "#{api_url()}/webmasters/v3/sites/#{property}/searchAnalytics/query"
headers = [{"Authorization", "Bearer #{access_token}"}]
case HTTPClient.post(url, headers, params) do
case HTTPClient.impl().post(url, headers, params) do
{:ok, %Finch.Response{body: body, status: 200}} ->
{:ok, body}
{:error, %{reason: %Finch.Response{body: body, status: 401}}} ->
Sentry.capture_message("Error fetching Google queries", extra: %{body: inspect(body)})
{:error, :invalid_credentials}
{:error, %{reason: %Finch.Response{body: _body, status: status}}}
when status in [401, 403] ->
{:error, "google_auth_error"}
{:error, %{reason: %Finch.Response{body: body, status: 403}}} ->
Sentry.capture_message("Error fetching Google queries", extra: %{body: inspect(body)})
{:error, get_in(body, ["error", "message"])}
{:error, %{reason: %{body: %{"error" => error}}}} ->
{:error, error}
{:error, %{reason: %Finch.Response{body: body}}} ->
Sentry.capture_message("Error fetching Google queries", extra: %{body: inspect(body)})
{:error, :unknown}
{:error, %{reason: _} = e} ->
Sentry.capture_message("Error fetching Google queries", extra: %{error: inspect(e)})
{:error, :unknown}
{:error, reason} ->
Logger.error("Google Analytics: failed to list stats: #{inspect(reason)}")
{:error, "failed_to_list_stats"}
end
end
@ -223,14 +207,12 @@ defmodule Plausible.Google.HTTP do
{:ok, %Finch.Response{body: body, status: 200}} ->
{:ok, body}
{:error, %{reason: %Finch.Response{body: body, status: _non_http_200}}} ->
body
|> Map.get("error")
|> then(&{:error, &1})
{:error, %{reason: %Finch.Response{body: %{"error" => error}, status: _non_http_200}}} ->
{:error, error}
{:error, %{reason: _} = e} ->
Sentry.capture_message("Error fetching Google queries", extra: %{error: inspect(e)})
{:error, :unknown}
{:error, :unknown_error}
end
end

View File

@ -11,6 +11,7 @@ defmodule Plausible.HTTPClient.Non200Error do
end
defmodule Plausible.HTTPClient.Interface do
@type finch_request_opts() :: Keyword.t()
@type url() :: Finch.Request.url()
@type headers() :: Finch.Request.headers()
@type params() :: Finch.Request.body() | map()
@ -21,6 +22,7 @@ defmodule Plausible.HTTPClient.Interface do
@callback get(url(), headers()) :: response()
@callback get(url()) :: response()
@callback post(url(), headers(), params()) :: response()
@callback post(url(), headers(), params(), finch_request_opts()) :: response()
end
defmodule Plausible.HTTPClient do
@ -40,8 +42,8 @@ defmodule Plausible.HTTPClient do
@behaviour Plausible.HTTPClient.Interface
@impl Plausible.HTTPClient.Interface
def post(url, headers \\ [], params \\ nil) do
call(:post, url, headers, params)
def post(url, headers \\ [], params \\ nil, finch_req_opts \\ []) do
call(:post, url, headers, params, finch_req_opts)
end
@doc """
@ -59,12 +61,12 @@ defmodule Plausible.HTTPClient do
Application.get_env(:plausible, :http_impl, __MODULE__)
end
defp call(method, url, headers, params) do
defp call(method, url, headers, params, finch_req_opts \\ []) do
{params, headers} = maybe_encode_params(params, headers)
method
|> build_request(url, headers, params)
|> do_request()
|> do_request(finch_req_opts)
|> maybe_decode_body()
|> tag_error()
end
@ -73,8 +75,8 @@ defmodule Plausible.HTTPClient do
Finch.build(method, url, headers, params)
end
defp do_request(request) do
Finch.request(request, Plausible.Finch)
defp do_request(request, finch_req_opts) do
Finch.request(request, Plausible.Finch, finch_req_opts)
end
defp maybe_encode_params(params, headers) when is_binary(params) or is_nil(params) do
@ -123,7 +125,7 @@ defmodule Plausible.HTTPClient do
end
end
defp maybe_decode_body(resp), do: resp
defp maybe_decode_body(response), do: response
defp json?(headers) do
found =

View File

@ -429,18 +429,24 @@ defmodule PlausibleWeb.Api.StatsController do
%{:visitors => %{value: total_visitors}} = Stats.aggregate(site, query, [:visitors])
user_id = get_session(conn, :current_user_id)
is_admin = user_id && Plausible.Sites.has_admin_access?(user_id, site)
case search_terms do
nil ->
user_id = get_session(conn, :current_user_id)
is_admin = user_id && Plausible.Sites.has_admin_access?(user_id, site)
json(conn, %{not_configured: true, is_admin: is_admin, total_visitors: total_visitors})
{:ok, terms} ->
json(conn, %{search_terms: terms, total_visitors: total_visitors})
{:error, e} ->
put_status(conn, 500)
|> json(%{error: e})
{:error, _} ->
conn
|> put_status(502)
|> json(%{
not_configured: true,
is_admin: is_admin,
total_visitors: total_visitors
})
end
end

View File

@ -36,12 +36,24 @@
<%= submit "Save", class: "button" %>
<% end %>
<% {:error, error} -> %>
<p class="text-gray-700 dark:text-gray-300 mt-6">The following error happened when fetching your Google Search Console domains.</p>
<p class="text-gray-700 dark:text-gray-300 mt-6">The following error happened when fetching your Google Search Console domains:</p>
<%= if error == "invalid_grant" do %>
<p class="text-red-700 font-medium mt-3"><a href="https://plausible.io/docs/google-search-console-integration#i-get-the-invalid-grant-error">Invalid Grant error returned from Google. <span class="text-indigo-500">See here on how to fix it.</a></p>
<% else %>
<p class="text-red-700 font-medium mt-3">Something went wrong</p>
<%= case error do %>
<% "invalid_grant" -> %>
<p class="text-red-700 font-medium mt-3">
<a href="https://plausible.io/docs/google-search-console-integration#i-get-the-invalid-grant-error">
Invalid Grant error returned from Google. <span class="text-indigo-500">See here on how to fix it</span>.
</a>
</p>
<% "google_auth_error" -> %>
<p class="text-red-700 font-medium mt-3">
Your Search Console account hasn't been connected successfully. Please unlink your Google account and try linking it again.
</p>
<% _ -> %>
<p class="text-red-700 font-medium mt-3">
Something went wrong, but looks temporary. If the problem persists, try re-linking your Google account.
</p>
<% end %>
<% end %>
<% else %>

View File

@ -2,7 +2,6 @@ defmodule Plausible.BillingTest do
use Plausible.DataCase
use Bamboo.Test, shared: true
alias Plausible.Billing
import Plausible.TestUtils
describe "usage" do
test "is 0 with no events" do

View File

@ -2,13 +2,16 @@ defmodule Plausible.Google.ApiTest do
use Plausible.DataCase, async: true
use ExVCR.Mock, adapter: ExVCR.Adapter.Finch
alias Plausible.Google.Api
import Plausible.TestUtils
import Double
import ExUnit.CaptureLog
import Mox
setup :verify_on_exit!
setup [:create_user, :create_new_site]
describe "fetch_and_persist/4" do
@ok_response File.read!("fixture/ga_batch_report.json")
@ok_response Jason.decode!(File.read!("fixture/ga_batch_report.json"))
@no_report_response Jason.decode!(File.read!("fixture/ga_report_empty_rows.json"))
setup do
{:ok, pid} = Plausible.Google.Buffer.start_link()
@ -16,12 +19,6 @@ defmodule Plausible.Google.ApiTest do
end
test "will fetch and persist import data from Google Analytics", %{site: site, buffer: buffer} do
finch_double =
Finch
|> stub(:request, fn _, _, _ ->
{:ok, %Finch.Response{status: 200, body: @ok_response}}
end)
request = %Plausible.Google.ReportRequest{
dataset: "imported_exit_pages",
view_id: "123",
@ -33,8 +30,36 @@ defmodule Plausible.Google.ApiTest do
page_size: 10_000
}
expect(
Plausible.HTTPClient.Mock,
:post,
fn
"https://analyticsreporting.googleapis.com/v4/reports:batchGet",
[{"Authorization", "Bearer fake-token"}],
%{
reportRequests: [
%{
dateRanges: [%{endDate: ~D[2022-02-01], startDate: ~D[2022-01-01]}],
dimensions: [
%{histogramBuckets: [], name: "ga:date"},
%{histogramBuckets: [], name: "ga:exitPagePath"}
],
hideTotals: true,
hideValueRanges: true,
metrics: [%{expression: "ga:users"}, %{expression: "ga:exits"}],
orderBys: [%{fieldName: "ga:date", sortOrder: "DESCENDING"}],
pageSize: 10000,
pageToken: nil,
viewId: "123"
}
]
},
[receive_timeout: 60_000] ->
{:ok, %Finch.Response{status: 200, body: @ok_response}}
end
)
Api.fetch_and_persist(site, request,
http_client: finch_double,
sleep_time: 0,
buffer: buffer
)
@ -52,13 +77,21 @@ defmodule Plausible.Google.ApiTest do
site: site,
buffer: buffer
} do
finch_double =
Finch
|> stub(:request, fn _, _, _ -> {:error, :timeout} end)
|> stub(:request, fn _, _, _ -> {:error, :nx_domain} end)
|> stub(:request, fn _, _, _ -> {:error, :closed} end)
|> stub(:request, fn _, _, _ -> {:ok, %Finch.Response{status: 503}} end)
|> stub(:request, fn _, _, _ -> {:ok, %Finch.Response{status: 502}} end)
expect(
Plausible.HTTPClient.Mock,
:post,
5,
fn
"https://analyticsreporting.googleapis.com/v4/reports:batchGet",
_,
_,
[receive_timeout: 60_000] ->
Enum.random([
{:error, %Mint.TransportError{reason: :nxdomain}},
{:error, %{reason: %Finch.Response{status: 500}}}
])
end
)
request = %Plausible.Google.ReportRequest{
view_id: "123",
@ -72,25 +105,23 @@ defmodule Plausible.Google.ApiTest do
assert {:error, :request_failed} =
Api.fetch_and_persist(site, request,
http_client: finch_double,
sleep_time: 0,
buffer: buffer
)
assert_receive({Finch, :request, [_, _, [receive_timeout: 60_000]]})
assert_receive({Finch, :request, [_, _, [receive_timeout: 60_000]]})
assert_receive({Finch, :request, [_, _, [receive_timeout: 60_000]]})
assert_receive({Finch, :request, [_, _, [receive_timeout: 60_000]]})
assert_receive({Finch, :request, [_, _, [receive_timeout: 60_000]]})
end
test "does not fail when report does not have rows key", %{site: site, buffer: buffer} do
finch_double =
Finch
|> stub(:request, fn _, _, _ ->
{:ok,
%Finch.Response{status: 200, body: File.read!("fixture/ga_report_empty_rows.json")}}
end)
expect(
Plausible.HTTPClient.Mock,
:post,
fn
"https://analyticsreporting.googleapis.com/v4/reports:batchGet",
_,
_,
[receive_timeout: 60_000] ->
{:ok, %Finch.Response{status: 200, body: @no_report_response}}
end
)
request = %Plausible.Google.ReportRequest{
dataset: "imported_exit_pages",
@ -105,14 +136,92 @@ defmodule Plausible.Google.ApiTest do
assert :ok ==
Api.fetch_and_persist(site, request,
http_client: finch_double,
sleep_time: 0,
buffer: buffer
)
end
end
describe "fetch_stats/3" do
describe "fetch_stats/3 errors" do
setup %{user: user, site: site} do
insert(:google_auth,
user: user,
site: site,
property: "sc-domain:dummy.test",
expires: NaiveDateTime.add(NaiveDateTime.utc_now(), 3600)
)
:ok
end
test "returns generic google_auth_error on 401/403", %{site: site} do
expect(
Plausible.HTTPClient.Mock,
:post,
fn
"https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query",
[{"Authorization", "Bearer 123"}],
%{
dimensionFilterGroups: %{},
dimensions: ["query"],
endDate: "2022-01-05",
rowLimit: 5,
startDate: "2022-01-01"
} ->
{:error, %{reason: %Finch.Response{status: Enum.random([401, 403])}}}
end
)
query = %Plausible.Stats.Query{date_range: Date.range(~D[2022-01-01], ~D[2022-01-05])}
assert {:error, "google_auth_error"} = Plausible.Google.Api.fetch_stats(site, query, 5)
end
test "returns whatever error code google returns on API client error", %{site: site} do
expect(
Plausible.HTTPClient.Mock,
:post,
fn
"https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query",
_,
_ ->
{:error, %{reason: %Finch.Response{status: 400, body: %{"error" => "some_error"}}}}
end
)
query = %Plausible.Stats.Query{date_range: Date.range(~D[2022-01-01], ~D[2022-01-05])}
assert {:error, "some_error"} = Plausible.Google.Api.fetch_stats(site, query, 5)
end
test "returns generic HTTP error and logs it", %{site: site} do
expect(
Plausible.HTTPClient.Mock,
:post,
fn
"https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query",
_,
_ ->
{:error, Finch.Error.exception(:some_reason)}
end
)
query = %Plausible.Stats.Query{date_range: Date.range(~D[2022-01-01], ~D[2022-01-05])}
log =
capture_log(fn ->
assert {:error, "failed_to_list_stats"} =
Plausible.Google.Api.fetch_stats(site, query, 5)
end)
assert log =~ "Google Analytics: failed to list stats: %Finch.Error{reason: :some_reason}"
end
end
describe "fetch_stats/3 with VCR cassetes" do
# We need real HTTP Client for VCR tests
setup_patch_env(:http_impl, Plausible.HTTPClient)
test "returns name and visitor count", %{user: user, site: site} do
use_cassette "google_analytics_stats", match_requests_on: [:request_body] do
insert(:google_auth,

View File

@ -1,6 +1,6 @@
defmodule Plausible.Google.BufferTest do
use Plausible.DataCase, async: false
import Plausible.TestUtils
import Ecto.Query
alias Plausible.Google.Buffer
@ -8,10 +8,7 @@ defmodule Plausible.Google.BufferTest do
defp set_buffer_size(_setup_args) do
google_setting = Application.get_env(:plausible, :google)
on_exit(fn -> Application.put_env(:plausible, :google, google_setting) end)
test_setting = Keyword.put(google_setting, :max_buffer_size, 10)
Application.put_env(:plausible, :google, test_setting)
patch_env(:google, Keyword.put(google_setting, :max_buffer_size, 10))
:ok
end

View File

@ -2,9 +2,10 @@ defmodule Plausible.Google.Api.VCRTest do
use Plausible.DataCase, async: false
use ExVCR.Mock, adapter: ExVCR.Adapter.Finch
require Ecto.Query
import Plausible.TestUtils
setup [:create_user, :create_site]
# We need real HTTP Client for VCR tests
setup_patch_env(:http_impl, Plausible.HTTPClient)
test "imports page views from Google Analytics", %{site: site} do
use_cassette "google_analytics_import#1", match_requests_on: [:request_body] do

View File

@ -102,6 +102,18 @@ defmodule Plausible.HTTPClientTest do
HTTPClient.post(bypass_url(bypass, path: "/any"), headers_no_content_type, params)
end
test "post/4 accepts finch request opts", %{bypass: bypass} do
Bypass.expect_once(bypass, "POST", "/timeout", fn conn ->
Process.sleep(500)
Conn.resp(conn, 200, "ok")
end)
assert {:error, %Mint.TransportError{reason: :timeout}} ==
HTTPClient.post(bypass_url(bypass, path: "/timeout"), [], %{}, receive_timeout: 100)
Bypass.down(bypass)
end
test "non-200 responses are tagged as errors", %{bypass: bypass} do
Bypass.expect_once(bypass, "GET", "/get", fn conn ->
Conn.resp(conn, 300, "oops")

View File

@ -1,7 +1,6 @@
defmodule Plausible.ImportedTest do
use PlausibleWeb.ConnCase
use Timex
import Plausible.TestUtils
@user_id 123

View File

@ -1,6 +1,5 @@
defmodule Plausible.PurgeTest do
use Plausible.DataCase
import Plausible.TestUtils
setup do
site = insert(:site, stats_start_date: ~D[2020-01-01])

View File

@ -1,6 +1,6 @@
defmodule Plausible.SiteAdminTest do
use Plausible.DataCase, async: true
import Plausible.TestUtils
alias Plausible.{SiteAdmin, ClickhouseRepo, ClickhouseEvent, ClickhouseSession}
describe "transfer_data" do

View File

@ -1,6 +1,6 @@
defmodule Plausible.SitesTest do
use Plausible.DataCase
import Plausible.TestUtils
alias Plausible.Sites
describe "is_member?" do

View File

@ -50,14 +50,7 @@ defmodule PlausibleWeb.CaptchaTest do
end
describe "with patched application env" do
setup do
original_env = Application.get_env(:plausible, :hcaptcha)
Application.put_env(:plausible, :hcaptcha, sitekey: nil)
on_exit(fn ->
Application.put_env(:plausible, :hcaptcha, original_env)
end)
end
setup_patch_env(:hcaptcha, sitekey: nil)
test "returns true when disabled" do
assert Captcha.verify("disabled")

View File

@ -1,7 +1,6 @@
defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
use PlausibleWeb.ConnCase
use Plausible.Repo
import Plausible.TestUtils
setup %{conn: conn} do
user = insert(:user)

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
setup [:create_user, :create_new_site, :create_api_key, :use_api_key]
@user_id 123

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.ExternalStatsController.AuthTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
setup [:create_user, :create_api_key]
@ -69,12 +68,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AuthTest do
describe "super admin access" do
setup %{user: user} do
original_env = Application.get_env(:plausible, :super_admin_user_ids)
Application.put_env(:plausible, :super_admin_user_ids, [user.id])
on_exit(fn ->
Application.put_env(:plausible, :super_admin_user_ids, original_env)
end)
patch_env(:super_admin_user_ids, [user.id])
end
test "can access as a super admin", %{conn: conn, api_key: api_key} do

View File

@ -1,6 +1,6 @@
defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
@user_id 1231
setup [:create_user, :create_new_site, :create_api_key, :use_api_key]

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.ExternalStatsController.TimeseriesTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
setup [:create_user, :create_new_site, :create_api_key, :use_api_key]

View File

@ -1,7 +1,6 @@
defmodule PlausibleWeb.Api.InternalControllerTest do
use PlausibleWeb.ConnCase
use Plausible.Repo
import Plausible.TestUtils
describe "GET /api/:domain/status" do
setup [:create_user, :log_in]

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.StatsController.AuthorizationTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
describe "API authorization - as anonymous user" do
test "Sends 404 Not found for a site that doesn't exist", %{conn: conn} do

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
describe "GET /api/stats/:domain/browsers" do
setup [:create_user, :log_in, :create_new_site, :add_imported_data]

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.StatsController.CitiesTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
describe "GET /api/stats/:domain/cities" do
defp seed(%{site: site}) do

View File

@ -1,6 +1,6 @@
defmodule PlausibleWeb.Api.StatsController.ConversionsTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
@user_id 123
describe "GET /api/stats/:domain/conversions" do

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.StatsController.CountriesTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
describe "GET /api/stats/:domain/countries" do
setup [:create_user, :log_in, :create_new_site, :add_imported_data]

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.StatsController.CurrentVisitorsTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
describe "GET /api/stats/:domain/current-visitors" do
setup [:create_user, :log_in, :create_site]

View File

@ -1,6 +1,6 @@
defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
@user_id 123
describe "GET /api/stats/main-graph - plot" do

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
describe "GET /api/stats/:domain/operating_systems" do
setup [:create_user, :log_in, :create_new_site, :add_imported_data]

View File

@ -1,6 +1,6 @@
defmodule PlausibleWeb.Api.StatsController.PagesTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
@user_id 123
describe "GET /api/stats/:domain/pages" do

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.StatsController.RegionsTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
describe "GET /api/stats/:domain/regions" do
defp seed(%{site: site}) do

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
describe "GET /api/stats/:domain/browsers" do
setup [:create_user, :log_in, :create_new_site, :add_imported_data]

View File

@ -1,6 +1,6 @@
defmodule PlausibleWeb.Api.StatsController.SourcesTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
@user_id 123
describe "GET /api/stats/:domain/sources" do

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.Api.StatsController.SuggestionsTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
describe "GET /api/stats/:domain/suggestions/:filter_name" do
setup [:create_user, :log_in, :create_site]

View File

@ -1,6 +1,6 @@
defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
@user_id 123
describe "GET /api/stats/top-stats - default" do

View File

@ -2,7 +2,6 @@ defmodule PlausibleWeb.AuthControllerTest do
use PlausibleWeb.ConnCase
use Bamboo.Test
use Plausible.Repo
import Plausible.TestUtils
import Mox
setup :verify_on_exit!

View File

@ -1,6 +1,5 @@
defmodule PlausibleWeb.BillingControllerTest do
use PlausibleWeb.ConnCase
import Plausible.TestUtils
describe "GET /upgrade" do
setup [:create_user, :log_in]

View File

@ -2,7 +2,6 @@ defmodule PlausibleWeb.Site.InvitationControllerTest do
use PlausibleWeb.ConnCase
use Plausible.Repo
use Bamboo.Test
import Plausible.TestUtils
setup [:create_user, :log_in]

View File

@ -2,7 +2,6 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
use PlausibleWeb.ConnCase
use Plausible.Repo
use Bamboo.Test
import Plausible.TestUtils
setup [:create_user, :log_in]

View File

@ -3,7 +3,10 @@ defmodule PlausibleWeb.SiteControllerTest do
use Plausible.Repo
use Bamboo.Test
use Oban.Testing, repo: Plausible.Repo
import Plausible.TestUtils
import ExUnit.CaptureLog
import Mox
setup :verify_on_exit!
describe "GET /sites/new" do
setup [:create_user, :log_in]
@ -384,6 +387,118 @@ defmodule PlausibleWeb.SiteControllerTest do
end
end
describe "GET /:webiste/settings/search-console for self-hosting" do
setup [:create_user, :log_in, :create_site]
test "display search console settings", %{conn: conn, site: site} do
conn = get(conn, "/#{site.domain}/settings/search-console")
resp = html_response(conn, 200)
assert resp =~ "An extra step is needed"
assert resp =~ "Google Search Console integration"
assert resp =~ "self-hosting-configuration"
end
end
describe "GET /:webiste/settings/search-console" do
setup [:create_user, :log_in, :create_site]
setup_patch_env(:google, client_id: "some", api_url: "https://www.googleapis.com")
setup %{site: site, user: user} = context do
insert(:google_auth, user: user, site: site, property: "sc-domain:#{site.domain}")
context
end
test "displays appropriate error in case of google account `google_auth_error`", %{
conn: conn,
site: site
} do
expect(
Plausible.HTTPClient.Mock,
:get,
fn
"https://www.googleapis.com/webmasters/v3/sites",
[{"Content-Type", "application/json"}, {"Authorization", "Bearer 123"}] ->
{:error, %{reason: %Finch.Response{status: Enum.random([401, 403])}}}
end
)
conn = get(conn, "/#{site.domain}/settings/search-console")
resp = html_response(conn, 200)
assert resp =~ "Your Search Console account hasn't been connected successfully"
assert resp =~ "Please unlink your Google account and try linking it again"
end
test "displays docs link error in case of `invalid_grant`", %{
conn: conn,
site: site
} do
expect(
Plausible.HTTPClient.Mock,
:get,
fn
"https://www.googleapis.com/webmasters/v3/sites",
[{"Content-Type", "application/json"}, {"Authorization", "Bearer 123"}] ->
{:error, %{reason: %Finch.Response{status: 400, body: %{"error" => "invalid_grant"}}}}
end
)
conn = get(conn, "/#{site.domain}/settings/search-console")
resp = html_response(conn, 200)
assert resp =~
"https://plausible.io/docs/google-search-console-integration#i-get-the-invalid-grant-error"
end
test "displays generic error in case of random error code returned by google", %{
conn: conn,
site: site
} do
expect(
Plausible.HTTPClient.Mock,
:get,
fn
"https://www.googleapis.com/webmasters/v3/sites",
[{"Content-Type", "application/json"}, {"Authorization", "Bearer 123"}] ->
{:error, %{reason: %Finch.Response{status: 503, body: %{"error" => "some_error"}}}}
end
)
conn = get(conn, "/#{site.domain}/settings/search-console")
resp = html_response(conn, 200)
assert resp =~ "Something went wrong, but looks temporary"
assert resp =~ "try re-linking your Google account"
end
test "displays generic error and logs a message, in case of random HTTP failure calling google",
%{
conn: conn,
site: site
} do
expect(
Plausible.HTTPClient.Mock,
:get,
fn
"https://www.googleapis.com/webmasters/v3/sites",
[{"Content-Type", "application/json"}, {"Authorization", "Bearer 123"}] ->
{:error, :nxdomain}
end
)
log =
capture_log(fn ->
conn = get(conn, "/#{site.domain}/settings/search-console")
resp = html_response(conn, 200)
assert resp =~ "Something went wrong, but looks temporary"
assert resp =~ "try re-linking your Google account"
end)
assert log =~ "Google Analytics: failed to list sites: :nxdomain"
end
end
describe "GET /:website/goals/new" do
setup [:create_user, :log_in, :create_site]

View File

@ -1,7 +1,6 @@
defmodule PlausibleWeb.StatsControllerTest do
use PlausibleWeb.ConnCase, async: true
use Plausible.Repo
import Plausible.TestUtils
describe "GET /:website - anonymous user" do
test "public site - shows site stats", %{conn: conn} do

View File

@ -2,7 +2,6 @@ defmodule PlausibleWeb.AuthorizeSiteAccessTest do
use PlausibleWeb.ConnCase, async: true
alias PlausibleWeb.AuthorizeSiteAccess
import Plausible.TestUtils
setup [:create_user, :log_in]
test "doesn't allow :website bypass with :domain in body", %{conn: conn, user: me} do

View File

@ -18,6 +18,7 @@ defmodule PlausibleWeb.ConnCase do
using do
quote do
# Import conveniences for testing with connections
use Plausible.TestUtils
import Plug.Conn
import Phoenix.ConnTest
alias PlausibleWeb.Router.Helpers, as: Routes

View File

@ -17,6 +17,7 @@ defmodule Plausible.DataCase do
using do
quote do
use Plausible.Repo
use Plausible.TestUtils
import Ecto.Changeset
import Plausible.DataCase

View File

@ -2,6 +2,34 @@ defmodule Plausible.TestUtils do
use Plausible.Repo
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
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 create_user(_) do
{:ok, user: Factory.insert(:user)}
end

View File

@ -2,7 +2,7 @@ defmodule Plausible.Workers.CheckUsageTest do
use Plausible.DataCase, async: true
use Bamboo.Test
import Double
import Plausible.TestUtils
alias Plausible.Workers.CheckUsage
setup [:create_user, :create_site]

View File

@ -2,7 +2,7 @@ defmodule Plausible.Workers.ImportGoogleAnalyticsTest do
use Plausible.DataCase
use Bamboo.Test
import Double
import Plausible.TestUtils
alias Plausible.Workers.ImportGoogleAnalytics
@imported_data %Plausible.Site.ImportedData{

View File

@ -1,7 +1,7 @@
defmodule Plausible.Workers.NotifyAnnualRenewalTest do
use Plausible.DataCase, async: true
use Bamboo.Test
import Plausible.TestUtils
alias Plausible.Workers.NotifyAnnualRenewal
setup [:create_user, :create_site]

View File

@ -1,5 +1,4 @@
defmodule Plausible.Workers.SendEmailReportTest do
import Plausible.TestUtils
use Plausible.DataCase
use Bamboo.Test
use Oban.Testing, repo: Plausible.Repo

View File

@ -2,7 +2,7 @@ defmodule Plausible.Workers.SendSiteSetupEmailsTest do
use Plausible.DataCase, async: true
use Bamboo.Test
use Oban.Testing, repo: Plausible.Repo
import Plausible.TestUtils
alias Plausible.Workers.SendSiteSetupEmails
describe "when user has not managed to set up the site" do

View File

@ -2,7 +2,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
use Plausible.DataCase
use Bamboo.Test
use Oban.Testing, repo: Plausible.Repo
import Plausible.TestUtils
alias Plausible.Workers.SendTrialNotifications
test "does not send a notification if user didn't create a site" do