| defmodule PlausibleWeb.StatsControllerTest do |
| use PlausibleWeb.ConnCase, async: false |
| use Plausible.Repo |
|
|
| @react_container "div#stats-react-container" |
|
|
| describe "GET /:domain - anonymous user" do |
| test "public site - shows site stats", %{conn: conn} do |
| site = new_site(public: true) |
| populate_stats(site, [build(:pageview)]) |
|
|
| conn = get(conn, "/#{site.domain}") |
| resp = html_response(conn, 200) |
| assert element_exists?(resp, @react_container) |
|
|
| assert text_of_attr(resp, @react_container, "data-domain") == site.domain |
| assert text_of_attr(resp, @react_container, "data-is-dbip") == "false" |
| assert text_of_attr(resp, @react_container, "data-has-goals") == "false" |
| assert text_of_attr(resp, @react_container, "data-conversions-opted-out") == "false" |
| assert text_of_attr(resp, @react_container, "data-funnels-opted-out") == "false" |
| assert text_of_attr(resp, @react_container, "data-props-opted-out") == "false" |
| assert text_of_attr(resp, @react_container, "data-props-available") == "true" |
| assert text_of_attr(resp, @react_container, "data-site-segments-available") == "true" |
| assert text_of_attr(resp, @react_container, "data-funnels-available") == "true" |
| assert text_of_attr(resp, @react_container, "data-exploration-available") == "true" |
| assert text_of_attr(resp, @react_container, "data-has-props") == "false" |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "false" |
| assert text_of_attr(resp, @react_container, "data-current-user-role") == "public" |
| assert text_of_attr(resp, @react_container, "data-current-user-id") == "null" |
| assert text_of_attr(resp, @react_container, "data-embedded") == "" |
| assert text_of_attr(resp, @react_container, "data-is-consolidated-view") == "false" |
| assert text_of_attr(resp, @react_container, "data-consolidated-view-available") == "false" |
| assert text_of_attr(resp, @react_container, "data-team-identifier") == site.team.identifier |
|
|
| assert "noindex, nofollow" == |
| resp |
| |> find("meta[name=robots]") |
| |> text_of_attr("content") |
|
|
| assert to_string(Plausible.InternalStatsApiVersion.api_version()) == |
| resp |
| |> find("meta[name=x-api-version]") |
| |> text_of_attr("content") |
|
|
| assert text_of_element(resp, "title") == "Plausible · #{site.domain}" |
| end |
|
|
| test "public site - all segments (personal or site) are stuffed into dataset, without their owner_id and owner_name", |
| %{conn: conn} do |
| user = new_user() |
| site = new_site(owner: user, public: true) |
|
|
| populate_stats(site, [build(:pageview)]) |
|
|
| emea_site_segment = |
| insert(:segment, |
| site: site, |
| owner: user, |
| type: :site, |
| name: "EMEA region" |
| ) |
|
|
| foo_personal_segment = |
| insert(:segment, |
| site: site, |
| owner: user, |
| type: :personal, |
| name: "FOO" |
| ) |
|
|
| conn = get(conn, "/#{site.domain}") |
| resp = html_response(conn, 200) |
| assert element_exists?(resp, @react_container) |
|
|
| assert text_of_attr(resp, @react_container, "data-segments") == |
| Jason.encode!([ |
| Plausible.Segments.to_response_map( |
| %{foo_personal_segment | owner_id: nil}, |
| site |
| ), |
| Plausible.Segments.to_response_map(%{emea_site_segment | owner_id: nil}, site) |
| ]) |
| end |
|
|
| @tag :ee_only |
| test "plausible.io live demo - shows site stats, header and footer", %{conn: conn} do |
| site = new_site(domain: "plausible.io", public: true) |
| populate_stats(site, [build(:pageview)]) |
|
|
| conn = get(conn, "/#{site.domain}") |
| resp = html_response(conn, 200) |
| assert element_exists?(resp, @react_container) |
|
|
| assert "index, nofollow" == |
| resp |
| |> find("meta[name=robots]") |
| |> text_of_attr("content") |
|
|
| assert text_of_element(resp, "title") == "Plausible Analytics: Live Demo" |
| assert resp =~ "Login" |
| assert resp =~ "You just saw how Plausible tracks plausible.io" |
| assert resp =~ "Start free trial" |
| assert resp =~ "See pricing" |
| assert resp =~ "Getting started" |
| end |
|
|
| test "public site - redirect to /login when no stats because verification requires it", %{ |
| conn: conn |
| } do |
| new_site(domain: "some-other-public-site.io", public: true) |
|
|
| conn = get(conn, conn |> get("/some-other-public-site.io") |> redirected_to()) |
|
|
| assert redirected_to(conn) == |
| Routes.auth_path(conn, :login_form, |
| return_to: "/some-other-public-site.io/verification" |
| ) |
| end |
|
|
| test "public site - no stats with skip_to_dashboard", %{ |
| conn: conn |
| } do |
| new_site(domain: "some-other-public-site.io", public: true) |
|
|
| conn = get(conn, "/some-other-public-site.io?skip_to_dashboard=true") |
| resp = html_response(conn, 200) |
|
|
| assert text_of_attr(resp, @react_container, "data-logged-in") == "false" |
| end |
|
|
| test "can not view stats of a private website", %{conn: conn} do |
| _ = insert(:user) |
| conn = get(conn, "/test-site.com") |
| assert html_response(conn, 404) =~ "There's nothing here" |
| end |
| end |
|
|
| describe "GET /:domain - as a logged in user" do |
| setup [:create_user, :log_in, :create_site] |
|
|
| test "can view stats of a website I've created", %{conn: conn, site: site, user: user} do |
| populate_stats(site, [build(:pageview)]) |
| conn = get(conn, "/" <> site.domain) |
| resp = html_response(conn, 200) |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "true" |
| assert text_of_attr(resp, @react_container, "data-current-user-role") == "owner" |
| assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}" |
| end |
|
|
| test "can view stats of a website I've created, enforcing pageviews check skip", %{ |
| conn: conn, |
| site: site |
| } do |
| resp = conn |> get(conn |> get("/" <> site.domain) |> redirected_to()) |> html_response(200) |
| refute text_of_attr(resp, @react_container, "data-logged-in") == "true" |
|
|
| resp = conn |> get("/" <> site.domain <> "?skip_to_dashboard=true") |> html_response(200) |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "true" |
| end |
|
|
| on_ee do |
| test "can't see exploration funnel UI if funnels feature unavailable", %{ |
| conn: conn, |
| site: site, |
| user: user |
| } do |
| subscribe_to_growth_plan(user) |
| populate_stats(site, [build(:pageview)]) |
| conn = get(conn, "/" <> site.domain) |
| resp = html_response(conn, 200) |
| assert text_of_attr(resp, @react_container, "data-exploration-available") == "false" |
| end |
|
|
| test "can see exploration funnel UI on trial", %{conn: conn, site: site} do |
| populate_stats(site, [build(:pageview)]) |
| conn = get(conn, "/" <> site.domain) |
| resp = html_response(conn, 200) |
| assert text_of_attr(resp, @react_container, "data-exploration-available") == "true" |
| end |
|
|
| test "can see exploration funnel UI past trial with funnels feature enabled", %{ |
| conn: conn, |
| site: site, |
| user: user |
| } do |
| populate_stats(site, [build(:pageview)]) |
|
|
| site.team |
| |> Plausible.Teams.Team.end_trial() |
| |> Plausible.Repo.update!() |
|
|
| subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.Funnels]) |
|
|
| conn = get(conn, "/" <> site.domain) |
| resp = html_response(conn, 200) |
| assert text_of_attr(resp, @react_container, "data-exploration-available") == "true" |
| end |
| end |
|
|
| on_ee do |
| test "first view of a consolidated dashboard sets stats_start_date and native_stats_start_at according to native_stats_start_at of the earliest team site", |
| %{ |
| conn: conn, |
| site: site, |
| user: user |
| } do |
| team = team_of(user) |
| now = NaiveDateTime.utc_now(:second) |
| ten_days_ago = NaiveDateTime.add(now, -10, :day) |
| twenty_days_ago = NaiveDateTime.add(now, -20, :day) |
|
|
| site |
| |> Plausible.Site.set_native_stats_start_at(ten_days_ago) |
| |> Plausible.Repo.update!() |
|
|
| new_site(team: team, native_stats_start_at: twenty_days_ago) |
| cv = new_consolidated_view(team) |
|
|
| conn = get(conn, "/" <> cv.domain) |
| resp = html_response(conn, 200) |
|
|
| assert text_of_attr(resp, @react_container, "data-domain") == cv.domain |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "true" |
| assert text_of_attr(resp, @react_container, "data-current-user-role") == "owner" |
| assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}" |
|
|
| cv = Plausible.Repo.reload(cv) |
| assert cv.stats_start_date == NaiveDateTime.to_date(twenty_days_ago) |
| assert cv.native_stats_start_at == twenty_days_ago |
| end |
|
|
| test "does not redirect consolidated views to verification", %{ |
| conn: conn, |
| user: user |
| } do |
| new_site(owner: user) |
| new_site(owner: user) |
| cv = user |> team_of() |> new_consolidated_view() |
|
|
| conn = get(conn, "/" <> cv.domain) |
| resp = html_response(conn, 200) |
|
|
| assert text_of_attr(resp, @react_container, "data-domain") == cv.domain |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "true" |
| assert text_of_attr(resp, @react_container, "data-current-user-role") == "owner" |
| assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}" |
| end |
|
|
| test "redirects to /sites if for some reason ineligible anymore", %{ |
| conn: conn, |
| user: user |
| } do |
| new_site(owner: user) |
| new_site(owner: user) |
| cv = user |> team_of() |> new_consolidated_view() |
|
|
| user |
| |> team_of() |
| |> Plausible.Teams.Team.end_trial() |
| |> Plausible.Repo.update!() |
|
|
| conn = get(conn, "/" <> cv.domain) |
| assert redirected_to(conn, 302) == "/sites" |
| end |
| end |
|
|
| @tag :ee_only |
| test "header, stats are shown; footer is not shown", %{conn: conn, site: site, user: user} do |
| populate_stats(site, [build(:pageview)]) |
| conn = get(conn, "/" <> site.domain) |
| resp = html_response(conn, 200) |
| assert resp =~ user.name |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "true" |
| refute resp =~ "Getting started" |
| end |
|
|
| @tag :ce_build_only |
| test "header, stats, footer are shown", %{conn: conn, site: site, user: user} do |
| populate_stats(site, [build(:pageview)]) |
| conn = get(conn, "/" <> site.domain) |
| resp = html_response(conn, 200) |
| assert resp =~ user.name |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "true" |
| assert resp =~ "Getting started" |
| end |
|
|
| test "shows locked page if site is locked", %{conn: conn, user: user} do |
| locked_site = new_site(owner: user) |
| locked_site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() |
| conn = get(conn, "/" <> locked_site.domain) |
| resp = html_response(conn, 200) |
| assert resp =~ "Your dashboard is unavailable" |
| assert resp =~ "Upgrade to the appropriate plan to restore access" |
| end |
|
|
| test "shows locked page if site is locked for billing role", %{conn: conn, user: user} do |
| other_user = new_user() |
| locked_site = new_site(owner: other_user) |
| locked_site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() |
| add_member(team_of(other_user), user: user, role: :billing) |
|
|
| conn = get(conn, "/" <> locked_site.domain) |
| resp = html_response(conn, 200) |
| assert resp =~ "Your dashboard is unavailable" |
| assert resp =~ "Upgrade to the appropriate plan to restore access" |
| end |
|
|
| test "shows locked page if site is locked for viewer role", %{conn: conn, user: user} do |
| other_user = new_user() |
| locked_site = new_site(owner: other_user) |
| locked_site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() |
| add_member(team_of(other_user), user: user, role: :viewer) |
|
|
| conn = get(conn, "/" <> locked_site.domain) |
| resp = html_response(conn, 200) |
| assert resp =~ "Your dashboard is unavailable" |
| refute resp =~ "Upgrade to the appropriate plan to restore access" |
| assert resp =~ "The owner of this site must upgrade their subscription plan" |
| end |
|
|
| test "shows locked page for anonymous" do |
| locked_site = new_site(public: true) |
| locked_site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() |
| conn = get(build_conn(), "/" <> locked_site.domain) |
| resp = html_response(conn, 200) |
| assert resp =~ "Your dashboard is unavailable" |
| assert resp =~ "You can check back later or contact the site owner" |
| end |
|
|
| test "can not view stats of someone else's website", %{conn: conn} do |
| site = new_site() |
| conn = get(conn, "/" <> site.domain) |
| assert html_response(conn, 404) =~ "There's nothing here" |
| end |
|
|
| test "does not show CRM link to the site", %{conn: conn, site: site} do |
| conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to()) |
|
|
| refute html_response(conn, 200) =~ "/cs/sites" |
| end |
|
|
| test "all segments (personal or site) are stuffed into dataset, with their associated owner_id and owner_name", |
| %{conn: conn, site: site, user: user} do |
| populate_stats(site, [build(:pageview)]) |
|
|
| emea_site_segment = |
| insert(:segment, |
| site: site, |
| owner: user, |
| type: :site, |
| name: "EMEA region" |
| ) |
|
|
| foo_personal_segment = |
| insert(:segment, |
| site: site, |
| owner: user, |
| type: :personal, |
| name: "FOO" |
| ) |
|
|
| conn = get(conn, "/#{site.domain}") |
| resp = html_response(conn, 200) |
| assert element_exists?(resp, @react_container) |
|
|
| assert text_of_attr(resp, @react_container, "data-segments") == |
| Jason.encode!([ |
| Plausible.Segments.to_response_map(foo_personal_segment, site), |
| Plausible.Segments.to_response_map(emea_site_segment, site) |
| ]) |
| end |
| end |
|
|
| describe "GET /:domain - as a super admin" do |
| @describetag :ee_only |
| setup [:create_user, :make_user_super_admin, :log_in] |
|
|
| test "can view a private dashboard with stats", %{conn: conn, user: user} do |
| site = new_site() |
| populate_stats(site, [build(:pageview)]) |
|
|
| conn = get(conn, "/" <> site.domain) |
| resp = html_response(conn, 200) |
| assert resp =~ "stats-react-container" |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "true" |
| assert text_of_attr(resp, @react_container, "data-current-user-role") == "super_admin" |
| assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}" |
| end |
|
|
| test "can enter verification when site is without stats", %{conn: conn} do |
| site = new_site() |
|
|
| conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to()) |
| assert html_response(conn, 200) =~ "Verifying your installation" |
| end |
|
|
| test "can view a private locked dashboard with stats", %{conn: conn} do |
| user = new_user() |
| site = new_site(owner: user) |
| site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() |
| populate_stats(site, [build(:pageview)]) |
|
|
| conn = get(conn, "/" <> site.domain) |
| resp = html_response(conn, 200) |
| assert resp =~ "This dashboard is actually locked" |
| end |
|
|
| test "can view private locked verification without stats", %{conn: conn} do |
| user = new_user() |
| site = new_site(owner: user) |
| site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() |
|
|
| conn = get(conn, conn |> get("/#{site.domain}") |> redirected_to()) |
| assert html_response(conn, 200) =~ "Verifying your installation" |
| end |
|
|
| test "can view a locked public dashboard", %{conn: conn} do |
| site = new_site(public: true) |
| site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() |
| populate_stats(site, [build(:pageview)]) |
|
|
| conn = get(conn, "/" <> site.domain) |
| resp = html_response(conn, 200) |
| assert resp =~ "This dashboard is actually locked" |
| end |
|
|
| on_ee do |
| test "shows CRM link to the site", %{conn: conn} do |
| site = new_site() |
| conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to()) |
|
|
| assert html_response(conn, 200) =~ |
| Routes.customer_support_site_path(PlausibleWeb.Endpoint, :show, site.id) |
| end |
| end |
| end |
|
|
| defp make_user_super_admin(%{user: user}) do |
| Application.put_env(:plausible, :super_admin_user_ids, [user.id]) |
| end |
|
|
| describe "GET /share/:domain?auth=:auth" do |
| test "prompts a password for a password-protected link", %{conn: conn} do |
| site = new_site() |
|
|
| link = |
| insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password")) |
|
|
| conn = get(conn, "/share/#{site.domain}?auth=#{link.slug}") |
| assert response(conn, 200) =~ "Enter password" |
| end |
|
|
| test "if the shared link is not protected with a password, passes user immediately to dashboard", |
| %{ |
| conn: conn |
| } do |
| site = new_site(domain: "test-site.com") |
| link = insert(:shared_link, site: site) |
|
|
| conn = get(conn, "/share/test-site.com/?auth=#{link.slug}") |
| resp = html_response(conn, 200) |
| assert resp =~ "stats-react-container" |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "false" |
| assert text_of_attr(resp, @react_container, "data-current-user-id") == "null" |
| assert text_of_attr(resp, @react_container, "data-current-user-role") == "public" |
| end |
|
|
| test "if the shared link is limited to a segment, only that segment is stuffed into data-segments", |
| %{ |
| conn: conn |
| } do |
| site = new_site(domain: "test-site.com") |
| emea_site_segment = insert(:segment, name: "EMEA", site: site, type: :site) |
| apac_site_segment = insert(:segment, name: "APAC", site: site, type: :site) |
| link = insert(:shared_link, site: site, segment: emea_site_segment) |
|
|
| conn = get(conn, "/share/test-site.com/?auth=#{link.slug}") |
| resp = html_response(conn, 200) |
| assert resp =~ "stats-react-container" |
|
|
| assert text_of_attr(resp, @react_container, "data-limited-to-segment-id") == |
| "#{emea_site_segment.id}" |
|
|
| assert text_of_attr(resp, @react_container, "data-segments") == |
| emea_site_segment |
| |> Plausible.Segments.to_response_map(site) |
| |> List.wrap() |
| |> Jason.encode!() |
|
|
| refute resp =~ apac_site_segment.name |
|
|
| assert text_of_attr(resp, @react_container, "data-current-user-role") == "public" |
| end |
|
|
| test "footer and header are shown when accessing shared link dashboard", %{ |
| conn: conn |
| } do |
| site = new_site(domain: "test-site.com") |
| link = insert(:shared_link, site: site) |
|
|
| conn = get(conn, "/share/test-site.com/?auth=#{link.slug}") |
| resp = html_response(conn, 200) |
| assert resp =~ "stats-react-container" |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "false" |
| assert text_of_attr(resp, @react_container, "data-current-user-id") == "null" |
| assert text_of_attr(resp, @react_container, "data-current-user-role") == "public" |
| assert resp =~ "Login" |
| assert resp =~ "Getting started" |
| end |
|
|
| test "returns page with X-Frame-Options disabled so it can be embedded in an iframe", %{ |
| conn: conn |
| } do |
| site = new_site(domain: "test-site.com") |
| link = insert(:shared_link, site: site) |
|
|
| conn = get(conn, "/share/test-site.com/?auth=#{link.slug}") |
| resp = html_response(conn, 200) |
| assert text_of_attr(resp, @react_container, "data-embedded") == "false" |
| assert Plug.Conn.get_resp_header(conn, "x-frame-options") == [] |
| end |
|
|
| test "returns page embedded page", %{ |
| conn: conn |
| } do |
| site = new_site(domain: "test-site.com") |
| link = insert(:shared_link, site: site) |
|
|
| conn = get(conn, "/share/test-site.com/?auth=#{link.slug}&embed=true") |
| resp = html_response(conn, 200) |
| assert text_of_attr(resp, @react_container, "data-embedded") == "true" |
| assert text_of_attr(resp, @react_container, "data-logged-in") == "false" |
| assert text_of_attr(resp, @react_container, "data-current-user-id") == "null" |
| assert text_of_attr(resp, @react_container, "data-current-user-role") == "public" |
| assert Plug.Conn.get_resp_header(conn, "x-frame-options") == [] |
| end |
|
|
| test "does not show header, does not show footer on embedded pages", %{conn: conn} do |
| site = new_site(domain: "test-site.com") |
| link = insert(:shared_link, site: site) |
|
|
| conn = get(conn, "/share/test-site.com/?auth=#{link.slug}&embed=true") |
| resp = html_response(conn, 200) |
| assert text_of_attr(resp, @react_container, "data-embedded") == "true" |
| refute resp =~ "Login" |
| refute resp =~ "Getting started" |
| end |
|
|
| test "shows locked page if page is locked", %{conn: conn} do |
| site = new_site(domain: "test-site.com") |
| site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() |
| link = insert(:shared_link, site: site) |
|
|
| conn = get(conn, "/share/test-site.com/?auth=#{link.slug}") |
|
|
| assert html_response(conn, 200) =~ "Your dashboard is unavailable" |
| refute String.contains?(html_response(conn, 200), "Back to my sites") |
| end |
|
|
| test "shows locked page if shared link is locked due to insufficient team subscription", %{ |
| conn: conn |
| } do |
| site = new_site(domain: "test-site.com") |
| link = insert(:shared_link, site: site) |
|
|
| insert(:starter_subscription, team: site.team) |
|
|
| conn = get(conn, "/share/test-site.com/?auth=#{link.slug}") |
|
|
| assert html_response(conn, 200) =~ "Shared link unavailable" |
| refute String.contains?(html_response(conn, 200), "Back to my sites") |
| end |
|
|
| for special_name <- Plausible.Sites.shared_link_special_names() do |
| test "shows dashboard if team subscription insufficient but shared link name is '#{special_name}'", |
| %{conn: conn} do |
| site = new_site(domain: "test-site.com") |
| link = insert(:shared_link, site: site, name: unquote(special_name)) |
|
|
| insert(:starter_subscription, team: site.team) |
|
|
| html = |
| conn |
| |> get("/share/test-site.com/?auth=#{link.slug}") |
| |> html_response(200) |
|
|
| assert element_exists?(html, @react_container) |
| refute html =~ "Shared Link Unavailable" |
| end |
| end |
|
|
| test "renders 404 not found when no auth parameter supplied", %{conn: conn} do |
| conn = get(conn, "/share/example.com") |
| assert response(conn, 404) =~ "nothing here" |
| end |
|
|
| test "renders 404 not found when non-existent auth parameter is supplied", %{conn: conn} do |
| conn = get(conn, "/share/example.com?auth=bad-token") |
| assert response(conn, 404) =~ "nothing here" |
| end |
|
|
| test "renders 404 not found when auth parameter for another site is supplied", %{conn: conn} do |
| site1 = insert(:site, domain: "test-site-1.com") |
| site2 = insert(:site, domain: "test-site-2.com") |
| site1_link = insert(:shared_link, site: site1) |
|
|
| conn = get(conn, "/share/#{site2.domain}/?auth=#{site1_link.slug}") |
| assert response(conn, 404) =~ "nothing here" |
| end |
|
|
| test "all segments (personal or site) are stuffed into dataset, without their owner_id and owner_name", |
| %{conn: conn} do |
| user = new_user() |
| site = new_site(domain: "test-site.com", owner: user) |
| link = insert(:shared_link, site: site) |
|
|
| emea_site_segment = |
| insert(:segment, |
| site: site, |
| owner: user, |
| type: :site, |
| name: "EMEA region" |
| ) |
|
|
| foo_personal_segment = |
| insert(:segment, |
| site: site, |
| owner: user, |
| type: :personal, |
| name: "FOO" |
| ) |
|
|
| conn = get(conn, "/share/#{site.domain}/?auth=#{link.slug}") |
| resp = html_response(conn, 200) |
|
|
| assert text_of_attr(resp, @react_container, "data-segments") == |
| Jason.encode!([ |
| Plausible.Segments.to_response_map( |
| %{foo_personal_segment | owner_id: nil}, |
| site |
| ), |
| Plausible.Segments.to_response_map(%{emea_site_segment | owner_id: nil}, site) |
| ]) |
| end |
| end |
|
|
| describe "GET /share/:slug - backwards compatibility" do |
| test "it redirects to new shared link format for historical links", %{conn: conn} do |
| site = insert(:site, domain: "test-site.com") |
| site_link = insert(:shared_link, site: site, inserted_at: ~N[2021-12-31 00:00:00]) |
|
|
| conn = get(conn, "/share/#{site_link.slug}") |
| assert redirected_to(conn, 302) == "/share/#{site.domain}/?auth=#{site_link.slug}" |
| end |
|
|
| test "it does nothing for newer links", %{conn: conn} do |
| site = insert(:site, domain: "test-site.com") |
| site_link = insert(:shared_link, site: site, inserted_at: ~N[2022-01-01 00:00:00]) |
|
|
| conn = get(conn, "/share/#{site_link.slug}") |
| assert response(conn, 404) =~ "nothing here" |
| end |
| end |
|
|
| describe "POST /share/:slug/authenticate" do |
| test "logs anonymous user in with correct password", %{conn: conn} do |
| site = new_site(domain: "test-site.com") |
|
|
| link = |
| insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password")) |
|
|
| conn = post(conn, "/share/#{link.slug}/authenticate", %{password: "password"}) |
| assert redirected_to(conn, 302) == "/share/#{site.domain}/?auth=#{link.slug}" |
|
|
| conn = get(conn, "/share/#{site.domain}?auth=#{link.slug}") |
| assert html_response(conn, 200) =~ "stats-react-container" |
| end |
|
|
| test "shows form again with wrong password", %{conn: conn} do |
| site = insert(:site, domain: "test-site.com") |
|
|
| link = |
| insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password")) |
|
|
| conn = post(conn, "/share/#{link.slug}/authenticate", %{password: "WRONG!"}) |
| assert html_response(conn, 200) =~ "Enter password" |
| end |
|
|
| test "only gives access to the correct dashboard", %{conn: conn} do |
| site = new_site(domain: "test-site.com") |
| site2 = new_site(domain: "test-site2.com") |
|
|
| link = |
| insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password")) |
|
|
| link2 = |
| insert(:shared_link, |
| site: site2, |
| password_hash: Plausible.Auth.Password.hash("password1") |
| ) |
|
|
| conn = post(conn, "/share/#{link.slug}/authenticate", %{password: "password"}) |
| assert redirected_to(conn, 302) == "/share/#{site.domain}/?auth=#{link.slug}" |
|
|
| conn = get(conn, "/share/#{site2.domain}?auth=#{link2.slug}") |
| assert html_response(conn, 200) =~ "Enter password" |
| end |
|
|
| test "preserves query parameters during password authentication", %{conn: conn} do |
| site = new_site() |
|
|
| link = |
| insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password")) |
|
|
| filters = "f=is,country,EE&l=EE,Estonia&f=is,browser,Firefox" |
|
|
| conn = |
| get( |
| conn, |
| "/share/#{URI.encode_www_form(site.domain)}?auth=#{link.slug}&#{filters}" |
| ) |
|
|
| assert html_response(conn, 200) =~ "Enter password" |
| html = html_response(conn, 200) |
|
|
| expected_action_string = |
| "/share/#{URI.encode_www_form(link.slug)}/authenticate?auth=#{link.slug}&#{filters}" |
|
|
| assert text_of_attr(html, "form", "action") == expected_action_string |
|
|
| conn = |
| post( |
| conn, |
| expected_action_string, |
| %{password: "WRONG!"} |
| ) |
|
|
| html = html_response(conn, 200) |
| assert html =~ "Enter password" |
| assert html =~ "Incorrect password" |
|
|
| assert text_of_attr(html, "form", "action") == expected_action_string |
|
|
| conn = |
| post( |
| conn, |
| expected_action_string, |
| %{password: "password"} |
| ) |
|
|
| expected_redirect = |
| "/share/#{URI.encode_www_form(site.domain)}/?auth=#{link.slug}&#{filters}" |
|
|
| assert redirected_to(conn, 302) == expected_redirect |
|
|
| conn = get(conn, expected_redirect) |
| assert html_response(conn, 200) =~ "stats-react-container" |
| end |
| end |
|
|
| test "handles return_to during password authentication", %{conn: conn} do |
| site = new_site() |
|
|
| link = |
| insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password")) |
|
|
| filters = "f=is,country,EE&l=EE,Estonia&f=is,browser,Firefox" |
|
|
| deep_path = "/filter/source" |
|
|
| conn = |
| get( |
| conn, |
| "/share/#{URI.encode_www_form(site.domain)}#{deep_path}?auth=#{link.slug}&#{filters}" |
| ) |
|
|
| assert html_response(conn, 200) =~ "Enter password" |
| html = html_response(conn, 200) |
|
|
| expected_action_string = |
| "/share/#{link.slug}/authenticate?auth=#{link.slug}&#{filters}&#{URI.encode_query(%{"return_to" => deep_path})}" |
|
|
| assert text_of_attr(html, "form", "action") == expected_action_string |
|
|
| conn = |
| post( |
| conn, |
| expected_action_string, |
| %{password: "password"} |
| ) |
|
|
| assert redirected_to(conn, 302) == |
| "/share/#{URI.encode_www_form(site.domain)}#{deep_path}?auth=#{link.slug}&#{filters}" |
| end |
|
|
| test "return_to from query_params is discarded", %{conn: conn} do |
| site = new_site() |
|
|
| link = |
| insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password")) |
|
|
| conn = |
| get( |
| conn, |
| "/share/#{URI.encode_www_form(site.domain)}/pages?auth=#{link.slug}&return_to=%2Ffoobar" |
| ) |
|
|
| assert html_response(conn, 200) =~ "Enter password" |
| html = html_response(conn, 200) |
|
|
| expected_action_string = |
| "/share/#{link.slug}/authenticate?auth=#{link.slug}&return_to=%2Fpages" |
|
|
| assert text_of_attr(html, "form", "action") == expected_action_string |
|
|
| conn = |
| post( |
| conn, |
| expected_action_string, |
| %{password: "password"} |
| ) |
|
|
| assert redirected_to(conn, 302) == |
| "/share/#{URI.encode_www_form(site.domain)}/pages?auth=#{link.slug}" |
| end |
|
|
| test "return_to doesn't allow navigating out of dashboard context", %{conn: conn} do |
| site = new_site() |
|
|
| link = |
| insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password")) |
|
|
| deep_path = "/../../settings/api-keys" |
| cleaned_deep_path = "/settings/api-keys" |
|
|
| conn = |
| get( |
| conn, |
| "/share/#{URI.encode_www_form(site.domain)}#{deep_path}?auth=#{link.slug}&theme=dark" |
| ) |
|
|
| assert html_response(conn, 200) =~ "Enter password" |
| html = html_response(conn, 200) |
|
|
| expected_action_string = |
| "/share/#{link.slug}/authenticate?auth=#{link.slug}&theme=dark&#{URI.encode_query(%{"return_to" => deep_path})}" |
|
|
| assert text_of_attr(html, "form", "action") == expected_action_string |
|
|
| conn = |
| post( |
| conn, |
| expected_action_string, |
| %{password: "password"} |
| ) |
|
|
| assert redirected_to(conn, 302) == |
| "/share/#{URI.encode_www_form(site.domain)}#{cleaned_deep_path}?auth=#{link.slug}&theme=dark" |
| end |
|
|
| describe "dogfood tracking" do |
| @describetag :ee_only |
|
|
| test "does not set domain_to_replace on live demo dashboard", %{conn: conn} do |
| site = new_site(domain: "plausible.io", public: true) |
| populate_stats(site, [build(:pageview)]) |
| conn = get(conn, "/#{site.domain}") |
| script_params = html_response(conn, 200) |> get_script_params() |
|
|
| assert %{ |
| "location_override" => nil, |
| "domain_to_replace" => nil |
| } = script_params |
| end |
|
|
| test "sets domain_to_replace on any other dashboard", %{conn: conn} do |
| site = new_site(domain: "öö.ee", public: true) |
| populate_stats(site, [build(:pageview)]) |
| conn = get(conn, "/#{site.domain}") |
| script_params = html_response(conn, 200) |> get_script_params() |
|
|
| assert %{ |
| "location_override" => nil, |
| "domain_to_replace" => "%C3%B6%C3%B6.ee" |
| } = script_params |
| end |
|
|
| test "sets domain_to_replace on live demo shared link", %{conn: conn} do |
| site = new_site(domain: "plausible.io", public: true) |
| link = insert(:shared_link, site: site) |
|
|
| populate_stats(site, [build(:pageview)]) |
|
|
| conn = get(conn, "/share/#{site.domain}/?auth=#{link.slug}") |
| script_params = html_response(conn, 200) |> get_script_params() |
|
|
| assert %{ |
| "location_override" => nil, |
| "domain_to_replace" => "plausible.io" |
| } = script_params |
| end |
|
|
| test "sets location_override on a locked dashboard", %{conn: conn} do |
| locked_site = new_site(public: true) |
| locked_site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() |
|
|
| conn = get(conn, "/" <> locked_site.domain) |
| html = html_response(conn, 200) |
|
|
| script_params = html |> get_script_params() |
|
|
| assert html =~ "Your dashboard is unavailable" |
| assert script_params["location_override"] == PlausibleWeb.Endpoint.url() <> "/:dashboard" |
| end |
|
|
| test "sets location_override on a locked shared link", %{conn: conn} do |
| locked_site = new_site() |
| link = insert(:shared_link, site: locked_site) |
|
|
| insert(:starter_subscription, team: locked_site.team) |
|
|
| conn = get(conn, "/share/#{locked_site.domain}/?auth=#{link.slug}") |
| html = html_response(conn, 200) |
|
|
| script_params = get_script_params(html) |
|
|
| assert html =~ "Shared link unavailable" |
|
|
| assert script_params["location_override"] == |
| PlausibleWeb.Endpoint.url() <> "/share/:dashboard" |
| end |
|
|
| test "sets location_override on shared_link_password.html", %{conn: conn} do |
| site = new_site() |
|
|
| link = |
| insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password")) |
|
|
| conn = get(conn, "/share/#{site.domain}?auth=#{link.slug}") |
| html = html_response(conn, 200) |
|
|
| script_params = get_script_params(html) |
|
|
| assert html =~ "Enter password" |
|
|
| assert script_params["location_override"] == |
| PlausibleWeb.Endpoint.url() <> "/share/:dashboard" |
| end |
| end |
|
|
| defp get_script_params(html) do |
| html |
| |> find("#dogfood-script") |
| |> text_of_attr("data-script-params") |
| |> JSON.decode!() |
| end |
| end |
|
|