defmodule PlausibleWeb.Live.InstallationTest do use PlausibleWeb.ConnCase use Plausible.Test.Support.DNS import Phoenix.LiveViewTest alias Plausible.Site.TrackerScriptConfiguration @migration_guide_link "https://plausible.io/docs/script-update-guide" setup [:create_user, :log_in, :create_site] describe "GET /:domain/installation" do @tag :ee_only test "renders loading installation screen on EE", %{conn: conn, site: site} do resp = get(conn, "/#{site.domain}/installation") |> html_response(200) assert resp =~ "animate-spin" end @tag :ce_build_only test "no loading spinner, no GTM tab on CE", %{conn: conn, site: site} do resp = get(conn, "/#{site.domain}/installation") |> html_response(200) tabs_text = text_of_element(resp, "a[data-phx-link='patch']") assert length(String.split(tabs_text)) == 3 assert tabs_text =~ "Script" assert tabs_text =~ "WordPress" assert tabs_text =~ "NPM" refute resp =~ "animate-spin" end end describe "LiveView" do @tag :ee_only test "detects installation type when mounted", %{conn: conn, site: site} do stub_lookup_a_records(site.domain) stub_detection_wordpress() {lv, _} = get_lv(conn, site) html = render_async(lv, 500) assert text(html) =~ "Verify WordPress installation" end @tag :ee_only test "When ?type=wordpress URL parameter is supplied, detected type is unused", %{ conn: conn, site: site } do stub_lookup_a_records(site.domain) stub_detection_manual() {lv, _} = get_lv(conn, site, "?type=wordpress") html = render_async(lv, 500) assert text(html) =~ "Verify WordPress installation" end @tag :ee_only test "When ?type=gtm URL parameter is supplied, detected type is unused", %{ conn: conn, site: site } do stub_lookup_a_records(site.domain) stub_detection_wordpress() {lv, _} = get_lv(conn, site, "?type=gtm") html = render_async(lv, 500) assert text(html) =~ "Verify Tag Manager installation" end @tag :ee_only test "When ?type=npm URL parameter is supplied, detected type is unused", %{ conn: conn, site: site } do stub_lookup_a_records(site.domain) stub_detection_wordpress() {lv, _} = get_lv(conn, site, "?type=npm") html = render_async(lv, 500) assert text(html) =~ "Verify NPM installation" end @tag :ee_only test "When ?type=manual URL parameter is supplied, detected type is unused", %{ conn: conn, site: site } do stub_lookup_a_records(site.domain) stub_detection_wordpress() {lv, _} = get_lv(conn, site, "?type=manual") html = render_async(lv, 500) assert text(html) =~ "Verify Script installation" end @tag :ee_only test "allows switching between installation tabs (EE)", %{conn: conn, site: site} do stub_lookup_a_records(site.domain) stub_detection_manual() {lv, _html} = get_lv(conn, site, "?type=manual") html = render_async(lv, 500) assert html =~ "Verify Script installation" lv |> element("a[href*=\"type=wordpress\"]") |> render_click() html = render(lv) assert html =~ "Verify WordPress installation" lv |> element("a[href*=\"type=gtm\"]") |> render_click() html = render(lv) assert html =~ "Verify Tag Manager installation" lv |> element("a[href*=\"type=npm\"]") |> render_click() html = render(lv) assert html =~ "Verify NPM installation" end @tag :ce_build_only test "allows switching between installation tabs (CE)", %{conn: conn, site: site} do {lv, _html} = get_lv(conn, site) html = render_async(lv, 500) assert html =~ "Verify Script installation" lv |> element("a[href*=\"type=wordpress\"]") |> render_click() html = render(lv) assert html =~ "Verify WordPress installation" end test "manual installations has script snippet with expected ID", %{conn: conn, site: site} do on_ee do stub_lookup_a_records(site.domain) stub_detection_manual() end {lv, _html} = get_lv(conn, site, "?type=manual&flow=review") assert eventually(fn -> html = render(lv) {html =~ "Verify Script installation", html} end) html = render(lv) config = Plausible.Repo.get_by!(TrackerScriptConfiguration, site_id: site.id) assert html =~ "Privacy-friendly analytics by Plausible" assert html =~ "/js/#{config.id}.js" assert html =~ "async" end test "manual installation shows optional measurements", %{conn: conn, site: site} do on_ee do stub_lookup_a_records(site.domain) stub_detection_manual() end {lv, _html} = get_lv(conn, site, "?type=manual&flow=review") html = render_async(lv, 500) assert html =~ "Verify Script installation" assert html =~ "Optional measurements" assert html =~ "Outbound links" assert html =~ "File downloads" assert html =~ "Form submissions" end test "manual installation shows advanced options in disclosure", %{conn: conn, site: site} do on_ee do stub_lookup_a_records(site.domain) stub_detection_manual() end {lv, _html} = get_lv(conn, site, "?type=manual&flow=review") html = render_async(lv, 500) assert html =~ "Verify Script installation" assert html =~ "Advanced options" assert html =~ "Manual tagging" assert html =~ "404 error pages" assert html =~ "Hashed page paths" assert html =~ "Custom properties" assert html =~ "Ecommerce revenue" end test "toggling optional measurements updates tracker configuration", %{ conn: conn, site: site } do on_ee do stub_lookup_a_records(site.domain) stub_detection_manual() end {lv, _html} = get_lv(conn, site, "?type=manual&flow=review") html = render_async(lv, 500) assert html =~ "Verify Script installation" config = TrackerScriptConfiguration |> Plausible.Repo.get_by!(site_id: site.id) assert config.outbound_links == true assert config.file_downloads == true assert config.form_submissions == true lv |> element("form[phx-submit='submit']") |> render_submit(%{ "tracker_script_configuration" => %{ "installation_type" => "manual", "outbound_links" => "false", "file_downloads" => "true", "form_submissions" => "true" } }) updated_config = TrackerScriptConfiguration |> Plausible.Repo.get_by!(site_id: site.id) assert updated_config.outbound_links == false assert updated_config.file_downloads == true assert updated_config.form_submissions == true end on_ee do for {type, expected_text} <- [ {"manual", "Verify Script installation"}, {"wordpress", "Verify WordPress installation"}, {"gtm", "Verify Tag Manager installation"}, {"npm", "Verify NPM installation"} ] do test "submitting form with #{type} redirects to verification (EE)", %{ conn: conn, site: site } do stub_lookup_a_records(site.domain) stub_detection_manual() {lv, _html} = get_lv(conn, site, "?type=#{unquote(type)}") html = render_async(lv, 500) assert html =~ unquote(expected_text) lv |> element("form[phx-submit='submit']") |> render_submit(%{ "tracker_script_configuration" => %{ "installation_type" => unquote(type), "outbound_links" => "true", "file_downloads" => "true", "form_submissions" => "true" } }) assert_redirect( lv, Routes.site_path(conn, :verification, site.domain, flow: "provisioning", installation_type: unquote(type) ) ) end end end @tag :ce_build_only test "submitting the form redirects to verification (CE)", %{conn: conn, site: site} do {lv, _html} = get_lv(conn, site) lv |> element("form[phx-submit='submit']") |> render_submit(%{ "tracker_script_configuration" => %{ "installation_type" => "manual", "outbound_links" => "true", "file_downloads" => "true", "form_submissions" => "true" } }) assert_redirect( lv, Routes.site_path(conn, :verification, site.domain, flow: "provisioning", installation_type: "manual" ) ) end test "404 goal gets created regardless of user options", %{conn: conn, site: site} do on_ee do stub_lookup_a_records(site.domain) stub_detection_manual() end {lv, _html} = get_lv(conn, site, "?type=manual") html = render_async(lv, 500) assert html =~ "Verify Script installation" # Test with all options disabled lv |> element("form[phx-submit='submit']") |> render_submit(%{ "tracker_script_configuration" => %{ "installation_type" => "manual", "outbound_links" => "false", "file_downloads" => "false", "form_submissions" => "false" } }) # 404 goal should still be created goals = Plausible.Goals.for_site(site) assert Enum.any?(goals, &(&1.event_name == "404")) end test "submitting form with review flow redirects to verification with flow param", %{ conn: conn, site: site } do on_ee do stub_lookup_a_records(site.domain) stub_detection_manual() end {lv, _html} = get_lv(conn, site, "?type=manual&flow=review") html = render_async(lv, 500) assert html =~ "Verify Script installation" lv |> element("form[phx-submit='submit']") |> render_submit(%{ "tracker_script_configuration" => %{ "installation_type" => "manual", "outbound_links" => "true", "file_downloads" => "true", "form_submissions" => "true" } }) assert_redirect( lv, Routes.site_path(conn, :verification, site.domain, flow: "review", installation_type: "manual" ) ) end @tag :ee_only test "detected WordPress installation shows special message", %{conn: conn, site: site} do stub_lookup_a_records(site.domain) stub_detection_wordpress() {lv, _} = get_lv(conn, site) html = render_async(lv, 500) assert text(html) =~ "We've detected your website is using WordPress" end @tag :ee_only test "if ratelimit for detection is exceeded, does not make detection request and falls back to recommending manual installation", %{conn: conn, site: site} do stub_lookup_a_records(site.domain) # exceed the rate limit for site detection Plausible.RateLimit.check_rate( Plausible.RateLimit, "site_detection:#{site.domain}", :timer.minutes(60), 1, 100 ) # this won't be used: if it were used, the output would be different stub_detection_wordpress() {lv, _} = get_lv(conn, site) html = render_async(lv, 500) refute text(html) =~ "We've detected your website is using WordPress" assert text(html) =~ "Verify Script installation" end @tag :ee_only test "detected GTM installation shows special message", %{conn: conn, site: site} do stub_lookup_a_records(site.domain) stub_detection_gtm() {lv, _} = get_lv(conn, site) html = render_async(lv, 500) assert html =~ "Verify Tag Manager installation" assert text(html) =~ "We've detected your website is using Google Tag Manager" end @tag :ee_only test "detected NPM installation shows npm tab", %{conn: conn, site: site} do stub_lookup_a_records(site.domain) stub_detection_result(%{ "v1Detected" => false, "gtmLikely" => false, "npm" => true, "wordpressLikely" => false, "wordpressPlugin" => false }) {lv, _} = get_lv(conn, site) html = render_async(lv, 500) assert html =~ "Verify NPM installation" end @tag :ee_only test "shows v1 detection warning and migration guide link for manual installation", %{ conn: conn, site: site } do stub_lookup_a_records(site.domain) stub_detection_manual_with_v1() {lv, _} = get_lv(conn, site, "?type=manual") html = render_async(lv, 500) assert text(html) =~ "Your website is running an outdated version of the tracking script" assert element_exists?(html, "a[href='#{@migration_guide_link}']") end @tag :ce_build_only test "shows v1 migration guide link for manual instructions", %{conn: conn, site: site} do {lv, _} = get_lv(conn, site) html = render_async(lv, 500) assert text(html) =~ "Still using the legacy snippet" assert element_exists?(html, "a[href='#{@migration_guide_link}']") end test "does not render link to migrate guide on WordPress installation tab", %{ conn: conn, site: site } do on_ee do stub_lookup_a_records(site.domain) stub_detection_wordpress_with_v1() end {lv, _} = get_lv(conn, site, "?type=wordpress") html = render_async(lv, 500) assert html =~ "Verify WordPress installation" refute element_exists?(html, "a[href='#{@migration_guide_link}']") end @tag :ee_only test "falls back to manual installation when detection fails at dns check level", %{ conn: conn, site: site } do stub_lookup_a_records(site.domain, []) ExUnit.CaptureLog.capture_log(fn -> {lv, _} = get_lv(conn, site) assert eventually(fn -> html = render(lv) # Should default to manual installation when detection returns {:error, _} {html =~ "Verify Script installation", html} end) end) end @tag :ee_only test "falls back to manual installation when dns succeeds but detection fails", %{ conn: conn, site: site } do stub_lookup_a_records(site.domain) stub_detection_error() ExUnit.CaptureLog.capture_log(fn -> {lv, _} = get_lv(conn, site) html = render_async(lv, 500) # Should default to manual installation when detection returns {:error, _} assert html =~ "Verify Script installation" end) end end describe "Authorization" do test "requires site access permissions", %{conn: conn} do other_user = insert(:user) other_site = new_site(owner: other_user) assert_raise Ecto.NoResultsError, fn -> get_lv(conn, other_site) end end test "allows viewer access to installation page", %{conn: conn, user: user} do site = new_site() add_guest(site, user: user, role: :viewer) on_ee do stub_lookup_a_records(site.domain) stub_detection_manual() end {lv, _} = get_lv(conn, site) html = render_async(lv, 500) assert html =~ "Verify Script installation" end test "allows editor access to installation page", %{conn: conn, user: user} do site = new_site() add_guest(site, user: user, role: :editor) on_ee do stub_lookup_a_records(site.domain) stub_detection_manual() end {lv, _} = get_lv(conn, site) html = render_async(lv, 500) assert html =~ "Verify Script installation" end end describe "URL Parameter Handling" do test "falls back to manual installation when invalid installation type parameter supplied", %{ conn: conn, site: site } do on_ee do stub_lookup_a_records(site.domain) stub_detection_manual() end {lv, _} = get_lv(conn, site, "?type=invalid") html = render_async(lv, 500) assert html =~ "Verify Script installation" end test "falls back to provisioning flow when invalid flow parameter supplied", %{ conn: conn, site: site } do on_ee do stub_lookup_a_records(site.domain) stub_detection_manual() end {lv, _} = get_lv(conn, site, "?flow=invalid") html = render_async(lv, 500) assert html =~ "Verify Script installation" end end describe "Detection Result Combinations" do @describetag :ee_only test "When GTM + Wordpress detected, GTM takes precedence", %{conn: conn, site: site} do stub_lookup_a_records(site.domain) stub_detection_result(%{ "v1Detected" => false, "gtmLikely" => true, "wordpressLikely" => true, "wordpressPlugin" => false }) {lv, _} = get_lv(conn, site) html = render_async(lv, 500) assert html =~ "Verify Tag Manager installation" end end describe "Legacy Installations" do @tag :ee_only test "uses detected type in review flow when installation_type is nil", %{ conn: conn, site: site } do _config = PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site, %{ installation_type: nil, outbound_links: true, file_downloads: false, form_submissions: true }) stub_lookup_a_records(site.domain) stub_detection_wordpress() {lv, _} = get_lv(conn, site, "?flow=review") html = render_async(lv, 500) assert html =~ "Verify WordPress installation" end end defp stub_detection_manual do stub_detection_result(%{ "v1Detected" => false, "gtmLikely" => false, "npm" => false, "wordpressLikely" => false, "wordpressPlugin" => false }) end defp stub_detection_wordpress do stub_detection_result(%{ "v1Detected" => false, "gtmLikely" => false, "npm" => false, "wordpressLikely" => true, "wordpressPlugin" => false }) end defp stub_detection_gtm do stub_detection_result(%{ "v1Detected" => false, "gtmLikely" => true, "npm" => false, "wordpressLikely" => false, "wordpressPlugin" => false }) end defp stub_detection_manual_with_v1 do stub_detection_result(%{ "v1Detected" => true, "gtmLikely" => false, "npm" => false, "wordpressLikely" => false, "wordpressPlugin" => false }) end on_ee do defp stub_detection_wordpress_with_v1 do stub_detection_result(%{ "v1Detected" => true, "gtmLikely" => false, "npm" => false, "wordpressLikely" => true, "wordpressPlugin" => false }) end end defp stub_detection_result(js_data) do Req.Test.stub(Plausible.InstallationSupport.Checks.Detection, fn conn -> conn |> put_resp_content_type("application/json") |> send_resp(200, Jason.encode!(%{"data" => Map.put(js_data, "completed", true)})) end) end defp stub_detection_error do Req.Test.stub(Plausible.InstallationSupport.Checks.Detection, fn conn -> conn |> put_resp_content_type("application/json") |> send_resp( 200, Jason.encode!(%{"data" => %{"error" => %{"message" => "Simulated browser error"}}}) ) end) end defp get_lv(conn, site, qs \\ nil) do {:ok, lv, html} = live(conn, "/#{site.domain}/installation#{qs}") {lv, html} end end