Implement token-based sessions (#4463)

* Turn `Plausible.Auth.UserSession` into full schema

* Implement token based sessions and use them as default

* Ignore expired user sessions during retrieval from DB

* Implement plug bumping user session last used and timeout timestamps

* Implement Oban worker removing expired user sessions with grace period

* Implement legacy session conversion on touch, when applicable

* Update `UserAuth` moduledoc

* Extend `UserAuth` tests to account for db-backed session tokens

* Update CHANGELOG

* Add tests for `UserSessionTouch` plug

* Add test for `CleanUserSessions` worker

* Add logging of legacy session retrievals

* Use single update permitting stale records  when touching user session

* Don't fetch session and user for external API endpoints (/api/event too)

* Refactor `Users.with_subscription/1` and expose helper query

* Skip fetching session in legacy `SessionTimeoutPlug`

* Rely on user session assign from `AuthContext` in `SentryContext`

* Silence legacy session warnings in `UserSessionTouchTest`

* Rely on session assign from `AuthPlug` in `SuperAdminOnlyPlug`

* Change `UserAuth` to get session, user and last subscription in one go

* Avoid refetching user session in `AuthorizeSiteAccess` plug

* Fix code formatting

* Refactor `UserAuth.get_user_token/1` (h/t @aerosol)

* Remove bogus empty opts from `scope` declarations in router

* Only touch session once an hour and keep `user.last_seen` in sync

* Bring back logging of legacy token use
This commit is contained in:
Adrian Gruntkowski 2024-09-03 11:34:37 +02:00 committed by GitHub
parent 533bf90329
commit 373d4dd665
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 884 additions and 369 deletions

View File

@ -35,6 +35,7 @@ All notable changes to this project will be documented in this file.
- Make `TOTP_VAULT_KEY` optional plausible/analytics#4317
- Sources like 'google' and 'facebook' are now stored in capitalized forms ('Google', 'Facebook') plausible/analytics#4417
- `DATABASE_CACERTFILE` now forces TLS for PostgreSQL connections, so you don't need to add `?ssl=true` in `DATABASE_URL`
- Change auth session cookies to token-based ones with server-side expiration management.
### Fixed

View File

@ -587,6 +587,8 @@ base_cron = [
# Every day at 1am
{"0 1 * * *", Plausible.Workers.CleanInvitations},
# Every 2 hours
{"30 */2 * * *", Plausible.Workers.CleanUserSessions},
# Every 2 hours
{"0 */2 * * *", Plausible.Workers.ExpireDomainChangeTransitions},
# Daily at midnight
{"0 0 * * *", Plausible.Workers.LocationsSync}
@ -617,6 +619,7 @@ base_queues = [
check_stats_emails: 1,
site_setup_emails: 1,
clean_invitations: 1,
clean_user_sessions: 1,
analytics_imports: 1,
analytics_exports: 1,
notify_exported_analytics: 1,

View File

@ -47,6 +47,7 @@ defmodule Plausible.Auth.User do
embeds_one :grace_period, Plausible.Auth.GracePeriod, on_replace: :update
has_many :sessions, Plausible.Auth.UserSession
has_many :site_memberships, Plausible.Site.Membership
has_many :sites, through: [:site_memberships, :site]
has_many :api_keys, Plausible.Auth.ApiKey

View File

@ -5,9 +5,48 @@ defmodule Plausible.Auth.UserSession do
use Ecto.Schema
import Ecto.Changeset
alias Plausible.Auth
@type t() :: %__MODULE__{}
embedded_schema do
field :user_id, :integer
@rand_size 32
@timeout Duration.new!(day: 14)
schema "user_sessions" do
field :token, :binary
field :device, :string
field :last_used_at, :naive_datetime
field :timeout_at, :naive_datetime
belongs_to :user, Plausible.Auth.User
timestamps(updated_at: false)
end
@spec timeout_duration() :: Duration.t()
def timeout_duration(), do: @timeout
@spec new_session(Auth.User.t(), String.t(), NaiveDateTime.t()) :: Ecto.Changeset.t()
def new_session(user, device, now \\ NaiveDateTime.utc_now(:second)) do
%__MODULE__{}
|> cast(%{device: device}, [:device])
|> generate_token()
|> put_assoc(:user, user)
|> touch_session(now)
end
@spec touch_session(t() | Ecto.Changeset.t(), NaiveDateTime.t()) :: Ecto.Changeset.t()
def touch_session(session, now \\ NaiveDateTime.utc_now(:second)) do
session
|> change()
|> put_change(:last_used_at, now)
|> put_change(:timeout_at, NaiveDateTime.shift(now, @timeout))
end
defp generate_token(changeset) do
token = :crypto.strong_rand_bytes(@rand_size)
put_change(changeset, :token, token)
end
end

View File

@ -35,6 +35,19 @@ defmodule Plausible.Users do
|> Repo.update!()
end
@spec bump_last_seen(Auth.User.t() | pos_integer(), NaiveDateTime.t()) :: :ok
def bump_last_seen(%Auth.User{id: user_id}, now) do
bump_last_seen(user_id, now)
end
def bump_last_seen(user_id, now) do
q = from(u in Auth.User, where: u.id == ^user_id)
Repo.update_all(q, set: [last_seen: now])
:ok
end
@spec accept_traffic_until(Auth.User.t()) :: Date.t()
on_ee do
def accept_traffic_until(user) do
@ -64,19 +77,18 @@ defmodule Plausible.Users do
end
end
def with_subscription(%Auth.User{id: user_id} = user) do
Repo.preload(user, subscription: last_subscription_query(user_id))
def with_subscription(%Auth.User{} = user) do
Repo.preload(user, subscription: last_subscription_query())
end
def with_subscription(user_id) when is_integer(user_id) do
Repo.one(
from(user in Auth.User,
left_join: last_subscription in subquery(last_subscription_query(user_id)),
on: last_subscription.user_id == user.id,
left_join: subscription in Subscription,
on: subscription.id == last_subscription.id,
as: :user,
left_lateral_join: s in subquery(last_subscription_join_query()),
on: true,
where: user.id == ^user_id,
preload: [subscription: subscription]
preload: [subscription: s]
)
)
end
@ -102,9 +114,14 @@ defmodule Plausible.Users do
end
end
defp last_subscription_query(user_id) do
def last_subscription_join_query() do
from(subscription in last_subscription_query(),
where: subscription.user_id == parent_as(:user).id
)
end
defp last_subscription_query() do
from(subscription in Subscription,
where: subscription.user_id == ^user_id,
order_by: [desc: subscription.inserted_at],
limit: 1
)

View File

@ -5,12 +5,12 @@ defmodule PlausibleWeb do
use Phoenix.LiveView, global_prefixes: ~w(x-)
use PlausibleWeb.Live.Flash
use PlausibleWeb.Live.AuthContext
unless :no_sentry_context in unquote(opts) do
use PlausibleWeb.Live.SentryContext
end
use PlausibleWeb.Live.AuthContext
alias PlausibleWeb.Router.Helpers, as: Routes
alias Phoenix.LiveView.JS
end

View File

@ -26,10 +26,8 @@ defmodule PlausibleWeb.Live.AuthContext do
end
end)
|> assign_new(:current_user, fn context ->
with %{} = user_session <- context.current_user_session,
{:ok, user} <- UserAuth.get_user(user_session) do
user
else
case context.current_user_session do
%{user: user} -> user
_ -> nil
end
end)

View File

@ -18,7 +18,7 @@ defmodule PlausibleWeb.Live.SentryContext do
end
end
def on_mount(:default, _params, session, socket) do
def on_mount(:default, _params, _session, socket) do
if Phoenix.LiveView.connected?(socket) do
peer = Phoenix.LiveView.get_connect_info(socket, :peer_data)
uri = Phoenix.LiveView.get_connect_info(socket, :uri)
@ -49,14 +49,10 @@ defmodule PlausibleWeb.Live.SentryContext do
Sentry.Context.set_request_context(request_context)
case PlausibleWeb.UserAuth.get_user_session(session) do
{:ok, user_session} ->
if current_user = socket.assigns[:current_user] do
Sentry.Context.set_user_context(%{
id: user_session.user_id
id: current_user.id
})
_ ->
:pass
end
end

View File

@ -16,15 +16,17 @@ defmodule PlausibleWeb.AuthPlug do
end
def call(conn, _opts) do
with {:ok, user_session} <- UserAuth.get_user_session(conn),
{:ok, user} <- UserAuth.get_user(user_session) do
case UserAuth.get_user_session(conn) do
{:ok, user_session} ->
user = user_session.user
Plausible.OpenTelemetry.add_user_attributes(user)
Sentry.Context.set_user_context(%{id: user.id, name: user.name, email: user.email})
conn
|> assign(:current_user, user)
|> assign(:current_user_session, user_session)
else
_ ->
conn
end

View File

@ -1,37 +0,0 @@
defmodule PlausibleWeb.LastSeenPlug do
import Plug.Conn
use Plausible.Repo
@one_hour 60 * 60
def init(opts) do
opts
end
def call(conn, _opts) do
last_seen = get_session(conn, :last_seen)
user = conn.assigns[:current_user]
cond do
user && last_seen && last_seen < unix_now() - @one_hour ->
persist_last_seen(user)
put_session(conn, :last_seen, unix_now())
user && !last_seen ->
put_session(conn, :last_seen, unix_now())
true ->
conn
end
end
defp persist_last_seen(user) do
q = from(u in Plausible.Auth.User, where: u.id == ^user.id)
Repo.update_all(q, set: [last_seen: DateTime.utc_now()])
end
defp unix_now do
DateTime.utc_now() |> DateTime.to_unix()
end
end

View File

@ -6,24 +6,19 @@ defmodule PlausibleWeb.SessionTimeoutPlug do
"""
import Plug.Conn
alias PlausibleWeb.UserAuth
def init(opts \\ []) do
opts
end
def call(conn, opts) do
timeout_at = get_session(conn, :session_timeout_at)
user_id =
case UserAuth.get_user_session(conn) do
{:ok, session} -> session.user_id
_ -> nil
end
user_id = get_session(conn, :current_user_id)
cond do
user_id && timeout_at && now() > timeout_at ->
PlausibleWeb.UserAuth.log_out_user(conn)
conn
|> PlausibleWeb.UserAuth.log_out_user()
|> halt()
user_id ->
put_session(

View File

@ -1,20 +1,24 @@
defmodule PlausibleWeb.SuperAdminOnlyPlug do
@moduledoc false
import Plug.Conn
use Plausible.Repo
import Plug.Conn
def init(options) do
options
end
def call(conn, _opts) do
with {:ok, user} <- PlausibleWeb.UserAuth.get_user(conn),
true <- Plausible.Auth.is_super_admin?(user) do
assign(conn, :current_user, user)
current_user = conn.assigns[:current_user]
if current_user && Plausible.Auth.is_super_admin?(current_user) do
conn
else
_ ->
conn |> send_resp(403, "Not allowed") |> halt
conn
|> PlausibleWeb.UserAuth.log_out_user()
|> send_resp(403, "Not allowed")
|> halt()
end
end
end

View File

@ -0,0 +1,28 @@
defmodule PlausibleWeb.Plugs.UserSessionTouch do
@moduledoc """
Plug for bumping timeout on user session on every dashboard request.
"""
import Plug.Conn
alias PlausibleWeb.UserAuth
def init(opts \\ []) do
opts
end
def call(conn, _opts) do
# NOTE: Needed only during transitional 14-day period
conn = UserAuth.convert_legacy_session(conn)
if user_session = conn.assigns[:current_user_session] do
assign(
conn,
:current_user_session,
UserAuth.touch_user_session(user_session)
)
else
conn
end
end
end

View File

@ -13,7 +13,7 @@ defmodule PlausibleWeb.Router do
on_ee(do: nil, else: plug(PlausibleWeb.FirstLaunchPlug, redirect_to: "/register"))
plug PlausibleWeb.SessionTimeoutPlug, timeout_after_seconds: @two_weeks_in_seconds
plug PlausibleWeb.AuthPlug
plug PlausibleWeb.LastSeenPlug
plug PlausibleWeb.Plugs.UserSessionTouch
end
pipeline :shared_link do
@ -30,6 +30,10 @@ defmodule PlausibleWeb.Router do
plug :put_root_layout, html: {PlausibleWeb.LayoutView, :app}
end
pipeline :external_api do
plug :accepts, ["json"]
end
pipeline :api do
plug :accepts, ["json"]
plug :fetch_session
@ -39,6 +43,7 @@ defmodule PlausibleWeb.Router do
pipeline :internal_stats_api do
plug :accepts, ["json"]
plug :fetch_session
plug PlausibleWeb.AuthPlug
plug PlausibleWeb.AuthorizeSiteAccess
plug PlausibleWeb.Plugs.NoRobots
end
@ -54,6 +59,7 @@ defmodule PlausibleWeb.Router do
plug PlausibleWeb.Plugs.NoRobots
plug :fetch_session
plug PlausibleWeb.AuthPlug
plug PlausibleWeb.SuperAdminOnlyPlug
end
end
@ -65,7 +71,11 @@ defmodule PlausibleWeb.Router do
on_ee do
use Kaffy.Routes,
scope: "/crm",
pipe_through: [PlausibleWeb.Plugs.NoRobots, PlausibleWeb.SuperAdminOnlyPlug]
pipe_through: [
PlausibleWeb.Plugs.NoRobots,
PlausibleWeb.AuthPlug,
PlausibleWeb.SuperAdminOnlyPlug
]
end
on_ee do
@ -180,7 +190,7 @@ defmodule PlausibleWeb.Router do
scope "/api/docs", PlausibleWeb.Api do
get "/query/schema.json", ExternalQueryApiController, :schema
scope assigns: %{} do
scope [] do
pipe_through :internal_stats_api
post "/query", ExternalQueryApiController, :query
@ -213,13 +223,17 @@ defmodule PlausibleWeb.Router do
end
scope "/api", PlausibleWeb do
pipe_through :api
scope [] do
pipe_through :external_api
post "/event", Api.ExternalController, :event
get "/error", Api.ExternalController, :error
get "/health", Api.ExternalController, :health
get "/system", Api.ExternalController, :info
end
scope [] do
pipe_through :api
post "/paddle/webhook", Api.PaddleController, :webhook
get "/paddle/currency", Api.PaddleController, :currency
@ -227,6 +241,7 @@ defmodule PlausibleWeb.Router do
get "/sites", Api.InternalController, :sites
end
end
scope "/", PlausibleWeb do
pipe_through [:browser, :csrf]

View File

@ -5,21 +5,23 @@ defmodule PlausibleWeb.UserAuth do
In it's current shape, both current (legacy) and soon to be implemented (new)
user sessions are supported side by side.
Once the token-based sessions are implemented, `create_user_session/1` will
start returning new token instead of the legacy one. At the same time,
`put_token_in_session/2` will always set the new token. The legacy token will
still be accepted from the session cookie. Once 14 days pass (the current time
window for which session cookie is valid without any activity), the legacy
cookies won't be accepted anymore (token retrieval will most likely be
instrumented to confirm the usage falls in the mentioned time window as
expected) and the logic will be cleaned of branching for legacy session.
The legacy token is still accepted from the session cookie. Once 14 days
pass (the current time window for which session cookie is valid without
any activity), the legacy cookies won't be accepted anymore (legacy token
retrieval is tracked with logging) and the logic will be cleaned of branching
for legacy session.
"""
import Ecto.Query, only: [from: 2]
alias Plausible.Auth
alias Plausible.Repo
alias PlausibleWeb.TwoFactor
alias PlausibleWeb.Router.Helpers, as: Routes
require Logger
@spec log_in_user(Plug.Conn.t(), Auth.User.t(), String.t() | nil) :: Plug.Conn.t()
def log_in_user(conn, user, redirect_path \\ nil) do
login_dest =
@ -47,30 +49,36 @@ defmodule PlausibleWeb.UserAuth do
|> clear_logged_in_cookie()
end
@spec get_user(Plug.Conn.t() | Auth.UserSession.t() | map()) ::
{:ok, Auth.User.t()} | {:error, :no_valid_token | :session_not_found | :user_not_found}
def get_user(%Auth.UserSession{} = user_session) do
if user = Plausible.Users.with_subscription(user_session.user_id) do
{:ok, user}
else
{:error, :user_not_found}
end
end
def get_user(conn_or_session) do
with {:ok, user_session} <- get_user_session(conn_or_session) do
get_user(user_session)
end
end
@spec get_user_session(Plug.Conn.t() | map()) ::
{:ok, map()} | {:error, :no_valid_token | :session_not_found}
{:ok, Auth.UserSession.t()} | {:error, :no_valid_token | :session_not_found}
def get_user_session(%Plug.Conn{assigns: %{current_user_session: user_session}}) do
{:ok, user_session}
end
def get_user_session(conn_or_session) do
with {:ok, token} <- get_user_token(conn_or_session) do
get_session_by_token(token)
end
end
@spec touch_user_session(Auth.UserSession.t()) :: Auth.UserSession.t()
def touch_user_session(%{token: nil} = user_session) do
# NOTE: Legacy token sessions can't be touched.
user_session
end
def touch_user_session(user_session, now \\ NaiveDateTime.utc_now(:second)) do
if NaiveDateTime.diff(now, user_session.last_used_at, :hour) >= 1 do
Plausible.Users.bump_last_seen(user_session.user_id, now)
user_session
|> Auth.UserSession.touch_session(now)
|> Repo.update!(allow_stale: true)
else
user_session
end
end
@doc """
Sets the `logged_in` cookie share with the static site for determining
whether client is authenticated.
@ -86,16 +94,58 @@ defmodule PlausibleWeb.UserAuth do
)
end
defp get_session_by_token({:legacy, user_id}) do
{:ok, %Auth.UserSession{user_id: user_id}}
@spec convert_legacy_session(Plug.Conn.t()) :: Plug.Conn.t()
def convert_legacy_session(conn) do
current_user = conn.assigns[:current_user]
if current_user && Plug.Conn.get_session(conn, :current_user_id) do
{token, user_session} = create_user_session(conn, current_user)
conn
|> put_token_in_session(token)
|> Plug.Conn.delete_session(:current_user_id)
|> Plug.Conn.assign(:current_user_session, user_session)
else
conn
end
end
defp get_session_by_token({:new, _token}) do
defp get_session_by_token({:legacy, user_id}) do
case Plausible.Users.with_subscription(user_id) do
%Auth.User{} = user ->
{:ok, %Auth.UserSession{user_id: user.id, user: user}}
nil ->
{:error, :session_not_found}
end
end
defp get_session_by_token({:new, token}) do
now = NaiveDateTime.utc_now(:second)
last_subscription_query = Plausible.Users.last_subscription_join_query()
token_query =
from(us in Auth.UserSession,
inner_join: u in assoc(us, :user),
as: :user,
left_lateral_join: s in subquery(last_subscription_query),
on: true,
where: us.token == ^token and us.timeout_at > ^now,
preload: [user: {u, subscription: s}]
)
case Repo.one(token_query) do
%Auth.UserSession{} = user_session ->
{:ok, user_session}
nil ->
{:error, :session_not_found}
end
end
defp set_user_session(conn, user) do
{token, _} = create_user_session(user)
{token, _} = create_user_session(conn, user)
conn
|> renew_session()
@ -118,11 +168,7 @@ defmodule PlausibleWeb.UserAuth do
defp put_token_in_session(conn, {:new, token}) do
conn
|> Plug.Conn.put_session(:user_token, token)
|> Plug.Conn.put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
end
defp put_token_in_session(conn, {:legacy, user_id}) do
Plug.Conn.put_session(conn, :current_user_id, user_id)
|> Plug.Conn.put_session(:live_socket_id, "user_sessions:#{Base.url_encode64(token)}")
end
defp get_user_token(%Plug.Conn{} = conn) do
@ -131,26 +177,92 @@ defmodule PlausibleWeb.UserAuth do
|> get_user_token()
end
defp get_user_token(session) do
case Enum.map(["user_token", "current_user_id"], &Map.get(session, &1)) do
[token, nil] when is_binary(token) -> {:ok, {:new, token}}
[nil, current_user_id] when is_integer(current_user_id) -> {:ok, {:legacy, current_user_id}}
[nil, nil] -> {:error, :no_valid_token}
end
defp get_user_token(%{"user_token" => token}) when is_binary(token) do
{:ok, {:new, token}}
end
defp create_user_session(user) do
# NOTE: a temporary fix for for dialyzer
# complaining about unreachable code
# path.
if :erlang.phash2(1, 1) == 0 do
{{:legacy, user.id}, %{}}
else
{{:new, "disabled-for-now"}, %{}}
end
defp get_user_token(%{"current_user_id" => user_id}) when is_integer(user_id) do
Logger.warning("Legacy user session detected (user: #{user_id})")
{:ok, {:legacy, user_id}}
end
defp remove_user_session(_token) do
defp get_user_token(_) do
{:error, :no_valid_token}
end
defp create_user_session(conn, user) do
device_name = get_device_name(conn)
user_session =
user
|> Auth.UserSession.new_session(device_name)
|> Repo.insert!()
{{:new, user_session.token}, user_session}
end
defp remove_user_session({:legacy, _}), do: :ok
defp remove_user_session({:new, token}) do
Repo.delete_all(from us in Auth.UserSession, where: us.token == ^token)
:ok
end
@unknown_label "Unknown"
defp get_device_name(%Plug.Conn{} = conn) do
conn
|> Plug.Conn.get_req_header("user-agent")
|> List.first()
|> get_device_name()
end
defp get_device_name(user_agent) when is_binary(user_agent) do
case UAInspector.parse(user_agent) do
%UAInspector.Result{client: %UAInspector.Result.Client{name: "Headless Chrome"}} ->
"Headless Chrome"
%UAInspector.Result.Bot{name: name} when is_binary(name) ->
name
%UAInspector.Result{} = ua ->
browser = browser_name(ua)
if os = os_name(ua) do
browser <> " (#{os})"
else
browser
end
_ ->
@unknown_label
end
end
defp get_device_name(_), do: @unknown_label
defp browser_name(ua) do
case ua.client do
:unknown -> @unknown_label
%UAInspector.Result.Client{name: "Mobile Safari"} -> "Safari"
%UAInspector.Result.Client{name: "Chrome Mobile"} -> "Chrome"
%UAInspector.Result.Client{name: "Chrome Mobile iOS"} -> "Chrome"
%UAInspector.Result.Client{name: "Firefox Mobile"} -> "Firefox"
%UAInspector.Result.Client{name: "Firefox Mobile iOS"} -> "Firefox"
%UAInspector.Result.Client{name: "Opera Mobile"} -> "Opera"
%UAInspector.Result.Client{name: "Opera Mini"} -> "Opera"
%UAInspector.Result.Client{name: "Opera Mini iOS"} -> "Opera"
%UAInspector.Result.Client{name: "Yandex Browser Lite"} -> "Yandex Browser"
%UAInspector.Result.Client{name: "Chrome Webview"} -> "Mobile App"
%UAInspector.Result.Client{type: "mobile app"} -> "Mobile App"
client -> client.name || @unknown_label
end
end
defp os_name(ua) do
case ua.os do
:unknown -> nil
os -> os.name
end
end
end

View File

@ -0,0 +1,27 @@
defmodule Plausible.Workers.CleanUserSessions do
@moduledoc """
Job removing expired user sessions. A grace period is applied.
"""
use Plausible.Repo
use Oban.Worker, queue: :clean_user_sessions
@grace_period Duration.new!(day: -7)
@spec grace_period_duration() :: Duration.t()
def grace_period_duration(), do: @grace_period
@impl Oban.Worker
def perform(_job) do
grace_cutoff =
NaiveDateTime.utc_now(:second)
|> NaiveDateTime.shift(@grace_period)
Repo.delete_all(
from us in Plausible.Auth.UserSession,
where: us.timeout_at < ^grace_cutoff
)
:ok
end
end

View File

@ -123,15 +123,15 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do
build(:imported_visitors, visitors: 2)
])
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day")
conn1 = get(conn, "/api/stats/#{site.domain}/browsers?period=day")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{"name" => "Chrome", "visitors" => 1, "percentage" => 100}
]
conn = get(conn, "/api/stats/#{site.domain}/browsers?period=day&with_imported=true")
conn2 = get(conn, "/api/stats/#{site.domain}/browsers?period=day&with_imported=true")
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{"name" => "Chrome", "visitors" => 2, "percentage" => 66.7},
%{"name" => "Firefox", "visitors" => 1, "percentage" => 33.3}
]

View File

@ -14,9 +14,9 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
build(:imported_visitors, visitors: 2)
])
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day")
conn1 = get(conn, "/api/stats/#{site.domain}/countries?period=day")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"code" => "EE",
"alpha_3" => "EST",
@ -35,9 +35,9 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
}
]
conn = get(conn, "/api/stats/#{site.domain}/countries?period=day&with_imported=true")
conn2 = get(conn, "/api/stats/#{site.domain}/countries?period=day&with_imported=true")
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"code" => "EE",
"alpha_3" => "EST",

View File

@ -29,19 +29,19 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
)
])
conn = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
conn1 = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{"name" => "(not set)", "visitors" => 1, "percentage" => 50},
%{"name" => "Linux", "visitors" => 1, "percentage" => 50}
]
filters = Jason.encode!(%{os: "(not set)"})
conn =
conn2 =
get(conn, "/api/stats/#{site.domain}/operating-systems?period=day&filters=#{filters}")
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{"name" => "(not set)", "visitors" => 1, "percentage" => 100}
]
end
@ -172,17 +172,17 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do
build(:imported_visitors, visitors: 2)
])
conn = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
conn1 = get(conn, "/api/stats/#{site.domain}/operating-systems?period=day")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{"name" => "Mac", "visitors" => 2, "percentage" => 66.7},
%{"name" => "Android", "visitors" => 1, "percentage" => 33.3}
]
conn =
conn2 =
get(conn, "/api/stats/#{site.domain}/operating-systems?period=day&with_imported=true")
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{"name" => "Mac", "visitors" => 3, "percentage" => 60},
%{"name" => "Android", "visitors" => 2, "percentage" => 40}
]

View File

@ -860,17 +860,17 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
build(:pageview, pathname: "/contact")
])
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day")
conn1 = get(conn, "/api/stats/#{site.domain}/pages?period=day")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{"visitors" => 3, "name" => "/"},
%{"visitors" => 2, "name" => "/register"},
%{"visitors" => 1, "name" => "/contact"}
]
conn = get(conn, "/api/stats/#{site.domain}/pages?period=day&with_imported=true")
conn2 = get(conn, "/api/stats/#{site.domain}/pages?period=day&with_imported=true")
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{"visitors" => 4, "name" => "/"},
%{"visitors" => 3, "name" => "/register"},
%{"visitors" => 1, "name" => "/contact"}
@ -1529,9 +1529,9 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
)
])
conn = get(conn, "/api/stats/#{site.domain}/entry-pages?period=day&date=2021-01-01")
conn1 = get(conn, "/api/stats/#{site.domain}/entry-pages?period=day&date=2021-01-01")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"visitors" => 2,
"visits" => 2,
@ -1546,13 +1546,13 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/entry-pages?period=day&date=2021-01-01&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"visitors" => 3,
"visits" => 5,
@ -1947,20 +1947,20 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
)
])
conn = get(conn, "/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01")
conn1 = get(conn, "/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{"name" => "/page1", "visitors" => 2, "visits" => 2, "exit_rate" => 66},
%{"name" => "/page2", "visitors" => 1, "visits" => 1, "exit_rate" => 100}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"name" => "/page2",
"visitors" => 3,

View File

@ -87,19 +87,17 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
)
])
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
conn1 = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{"name" => "(not set)", "visitors" => 1, "percentage" => 50},
%{"name" => "Desktop", "visitors" => 1, "percentage" => 50}
]
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
filters = Jason.encode!(%{screen: "(not set)"})
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
conn2 = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&filters=#{filters}")
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{"name" => "(not set)", "visitors" => 1, "percentage" => 100}
]
end
@ -203,16 +201,16 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do
build(:imported_visitors, visitors: 2)
])
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
conn1 = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{"name" => "Desktop", "visitors" => 2, "percentage" => 66.7},
%{"name" => "Laptop", "visitors" => 1, "percentage" => 33.3}
]
conn = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&with_imported=true")
conn2 = get(conn, "/api/stats/#{site.domain}/screen-sizes?period=day&with_imported=true")
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{"name" => "Desktop", "visitors" => 2, "percentage" => 40},
%{"name" => "Laptop", "visitors" => 2, "percentage" => 40},
%{"name" => "Mobile", "visitors" => 1, "percentage" => 20}

View File

@ -268,16 +268,16 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn = get(conn, "/api/stats/#{site.domain}/sources")
conn1 = get(conn, "/api/stats/#{site.domain}/sources")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{"name" => "Google", "visitors" => 2},
%{"name" => "DuckDuckGo", "visitors" => 1}
]
conn = get(conn, "/api/stats/#{site.domain}/sources?with_imported=true")
conn2 = get(conn, "/api/stats/#{site.domain}/sources?with_imported=true")
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{"name" => "Google", "visitors" => 4},
%{"name" => "DuckDuckGo", "visitors" => 2}
]
@ -369,13 +369,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn =
conn1 =
get(
conn,
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&detailed=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"name" => "DuckDuckGo",
"visitors" => 1,
@ -390,13 +390,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/sources?period=day&date=2021-01-01&detailed=true&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"name" => "Google",
"visitors" => 3,
@ -461,15 +461,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn = get(conn, "/api/stats/#{site.domain}/sources?limit=1&page=2")
conn1 = get(conn, "/api/stats/#{site.domain}/sources?limit=1&page=2")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{"name" => "DuckDuckGo", "visitors" => 1}
]
conn = get(conn, "/api/stats/#{site.domain}/sources?limit=1&page=2&with_imported=true")
conn2 = get(conn, "/api/stats/#{site.domain}/sources?limit=1&page=2&with_imported=true")
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{"name" => "Google", "visitors" => 2}
]
end
@ -688,13 +688,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn =
conn1 =
get(
conn,
"/api/stats/#{site.domain}/utm_mediums?period=day&date=2021-01-01"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"name" => "email",
"visitors" => 1,
@ -709,13 +709,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/utm_mediums?period=day&date=2021-01-01&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"name" => "email",
"visitors" => 2,
@ -768,13 +768,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn =
conn1 =
get(
conn,
"/api/stats/#{site.domain}/utm_mediums?period=day&date=2021-01-01"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"name" => "social",
"visitors" => 1,
@ -783,13 +783,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/utm_mediums?period=day&date=2021-01-01&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"name" => "social",
"visitors" => 2,
@ -844,13 +844,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn =
conn1 =
get(
conn,
"/api/stats/#{site.domain}/utm_campaigns?period=day&date=2021-01-01"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"name" => "august",
"visitors" => 2,
@ -865,13 +865,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/utm_campaigns?period=day&date=2021-01-01&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"name" => "august",
"visitors" => 3,
@ -928,13 +928,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn =
conn1 =
get(
conn,
"/api/stats/#{site.domain}/utm_campaigns?period=day&date=2021-01-01"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"name" => "profile",
"visitors" => 1,
@ -943,13 +943,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/utm_campaigns?period=day&date=2021-01-01&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"name" => "profile",
"visitors" => 2,
@ -1052,13 +1052,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn =
conn1 =
get(
conn,
"/api/stats/#{site.domain}/utm_terms?period=day&date=2021-01-01"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"name" => "Sweden",
"visitors" => 2,
@ -1073,13 +1073,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/utm_terms?period=day&date=2021-01-01&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"name" => "Sweden",
"visitors" => 3,
@ -1136,13 +1136,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn =
conn1 =
get(
conn,
"/api/stats/#{site.domain}/utm_terms?period=day&date=2021-01-01"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"name" => "oat milk",
"visitors" => 1,
@ -1151,13 +1151,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/utm_terms?period=day&date=2021-01-01&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"name" => "oat milk",
"visitors" => 2,
@ -1212,13 +1212,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn =
conn1 =
get(
conn,
"/api/stats/#{site.domain}/utm_contents?period=day&date=2021-01-01"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"name" => "blog",
"visitors" => 2,
@ -1233,13 +1233,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/utm_contents?period=day&date=2021-01-01&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"name" => "blog",
"visitors" => 3,
@ -1296,13 +1296,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn =
conn1 =
get(
conn,
"/api/stats/#{site.domain}/utm_contents?period=day&date=2021-01-01"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{
"name" => "ad",
"visitors" => 1,
@ -1311,13 +1311,13 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
}
]
conn =
conn2 =
get(
conn,
"/api/stats/#{site.domain}/utm_contents?period=day&date=2021-01-01&with_imported=true"
)
assert json_response(conn, 200)["results"] == [
assert json_response(conn2, 200)["results"] == [
%{
"name" => "ad",
"visitors" => 2,
@ -1757,15 +1757,15 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
)
])
conn = get(conn, "/api/stats/#{site.domain}/referrers/!Google?period=day")
conn1 = get(conn, "/api/stats/#{site.domain}/referrers/!Google?period=day")
assert json_response(conn, 200)["results"] == [
assert json_response(conn1, 200)["results"] == [
%{"name" => "duckduckgo.com", "visitors" => 1}
]
conn = get(conn, "/api/stats/#{site.domain}/referrers/Google|DuckDuckGo?period=day")
conn2 = get(conn, "/api/stats/#{site.domain}/referrers/Google|DuckDuckGo?period=day")
assert [entry1, entry2] = json_response(conn, 200)["results"]
assert [entry1, entry2] = json_response(conn2, 200)["results"]
assert %{"name" => "google.com", "visitors" => 2} in [entry1, entry2]
assert %{"name" => "duckduckgo.com", "visitors" => 1} in [entry1, entry2]
end

View File

@ -764,13 +764,13 @@ defmodule PlausibleWeb.Api.StatsController.SuggestionsTest do
build(:imported_locations, country: "EE", region: "Hiiumaa", pageviews: 1)
])
conn =
conn1 =
get(
conn,
"/api/stats/#{site.domain}/suggestions/region?q=&with_imported=true"
)
assert json_response(conn, 200) == [
assert json_response(conn1, 200) == [
%{"value" => "EE-37", "label" => "Harjumaa"},
%{"value" => "Hiiumaa", "label" => "Hiiumaa"}
]

View File

@ -75,6 +75,7 @@ defmodule PlausibleWeb.AuthControllerTest do
end
test "logs the user in", %{conn: conn} do
user =
Repo.insert!(
User.new(%{
name: "Jane Doe",
@ -93,7 +94,8 @@ defmodule PlausibleWeb.AuthControllerTest do
}
)
assert get_session(conn, :current_user_id)
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn, :user_token) == token
end
end
@ -129,6 +131,7 @@ defmodule PlausibleWeb.AuthControllerTest do
role: :admin
)
user =
Repo.insert!(
User.new(%{
name: "Jane Doe",
@ -138,7 +141,7 @@ defmodule PlausibleWeb.AuthControllerTest do
})
)
{:ok, %{site: site, invitation: invitation}}
{:ok, %{site: site, invitation: invitation, user: user}}
end
test "registering sends an activation link", %{conn: conn} do
@ -172,7 +175,7 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == "/activate?flow=invitation"
end
test "logs the user in", %{conn: conn} do
test "logs the user in", %{conn: conn, user: user} do
conn =
post(conn, "/login",
user: %{
@ -184,7 +187,8 @@ defmodule PlausibleWeb.AuthControllerTest do
}
)
assert get_session(conn, :current_user_id)
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn, :user_token) == token
end
end
@ -334,7 +338,8 @@ defmodule PlausibleWeb.AuthControllerTest do
conn = post(conn, "/login", email: user.email, password: "password")
assert get_session(conn, :current_user_id) == user.id
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn, :user_token) == token
assert redirected_to(conn) == "/sites"
end
@ -365,7 +370,7 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == Routes.auth_path(conn, :verify_2fa_form)
assert fetch_cookies(conn).cookies["session_2fa"].current_2fa_user_id == user.id
refute get_session(conn)["current_user_id"]
refute get_session(conn)["user_token"]
end
test "valid email and password with 2FA enabled and remember 2FA cookie set - logs the user in",
@ -383,7 +388,8 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert conn.resp_cookies["session_2fa"].max_age == 0
assert get_session(conn, :current_user_id) == user.id
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn, :user_token) == token
end
test "valid email and password with 2FA enabled and rogue remember 2FA cookie set - logs the user in",
@ -402,13 +408,13 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == Routes.auth_path(conn, :verify_2fa_form)
assert fetch_cookies(conn).cookies["session_2fa"].current_2fa_user_id == user.id
refute get_session(conn, :current_user_id)
refute get_session(conn, :user_token)
end
test "email does not exist - renders login form again", %{conn: conn} do
conn = post(conn, "/login", email: "user@example.com", password: "password")
assert get_session(conn, :current_user_id) == nil
assert get_session(conn, :user_token) == nil
assert html_response(conn, 200) =~ "Enter your account credentials"
end
@ -416,7 +422,7 @@ defmodule PlausibleWeb.AuthControllerTest do
user = insert(:user, password: "password")
conn = post(conn, "/login", email: user.email, password: "wrong")
assert get_session(conn, :current_user_id) == nil
assert get_session(conn, :user_token) == nil
assert html_response(conn, 200) =~ "Enter your account credentials"
end
@ -506,7 +512,7 @@ defmodule PlausibleWeb.AuthControllerTest do
# cookie state is as expected for logged out user
assert conn.private[:plug_session_info] == :renew
assert conn.resp_cookies["logged_in"].max_age == 0
assert get_session(conn, :current_user_id) == nil
assert get_session(conn, :user_token) == nil
{:ok, %{conn: conn}} = PlausibleWeb.FirstLaunchPlug.Test.skip(%{conn: recycle(conn)})
conn = get(conn, location)
@ -525,7 +531,7 @@ defmodule PlausibleWeb.AuthControllerTest do
# cookie state is as expected for logged out user
assert conn.private[:plug_session_info] == :renew
assert conn.resp_cookies["logged_in"].max_age == 0
assert get_session(conn, :current_user_id) == nil
assert get_session(conn, :user_token) == nil
{:ok, %{conn: conn}} = PlausibleWeb.FirstLaunchPlug.Test.skip(%{conn: recycle(conn)})
conn = get(conn, location)
@ -1697,7 +1703,8 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert get_session(conn)["current_user_id"] == user.id
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn)["user_token"] == token
# 2FA session terminated
assert conn.resp_cookies["session_2fa"].max_age == 0
# Remember cookie unset
@ -1740,7 +1747,8 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert get_session(conn)["current_user_id"] == user.id
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn)["user_token"] == token
# 2FA session terminated
assert conn.resp_cookies["session_2fa"].max_age == 0
# Remember cookie set
@ -1765,7 +1773,8 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert get_session(conn)["current_user_id"] == user.id
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn)["user_token"] == token
# 2FA session terminated
assert conn.resp_cookies["session_2fa"].max_age == 0
# Remember cookie set
@ -1791,7 +1800,8 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert get_session(conn)["current_user_id"] == user.id
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn, :user_token) == token
# 2FA session terminated
assert conn.resp_cookies["session_2fa"].max_age == 0
# Remember cookie cleared
@ -1846,7 +1856,8 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert get_session(conn)["current_user_id"] == user.id
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn)["user_token"] == token
# 2FA session terminated
assert conn.resp_cookies["session_2fa"].max_age == 0
end
@ -1877,7 +1888,7 @@ defmodule PlausibleWeb.AuthControllerTest do
500
)
assert get_session(response, :current_user_id) == nil
assert get_session(response, :user_token) == nil
# 2FA session terminated
assert response.resp_cookies["session_2fa"].max_age == 0
assert html_response(response, 429) =~ "Too many login attempts"
@ -1952,7 +1963,8 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert get_session(conn)["current_user_id"] == user.id
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn)["user_token"] == token
# 2FA session terminated
assert conn.resp_cookies["session_2fa"].max_age == 0
end
@ -2009,7 +2021,8 @@ defmodule PlausibleWeb.AuthControllerTest do
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert get_session(conn)["current_user_id"] == user.id
assert %{sessions: [%{token: token}]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert get_session(conn)["user_token"] == token
# 2FA session terminated
assert conn.resp_cookies["session_2fa"].max_age == 0
end
@ -2045,7 +2058,7 @@ defmodule PlausibleWeb.AuthControllerTest do
500
)
assert get_session(response, :current_user_id) == nil
assert get_session(response, :user_token) == nil
# 2FA session terminated
assert response.resp_cookies["session_2fa"].max_age == 0
assert html_response(response, 429) =~ "Too many login attempts"

View File

@ -4,15 +4,14 @@ defmodule PlausibleWeb.PageControllerTest do
setup {PlausibleWeb.FirstLaunchPlug.Test, :skip}
describe "GET /" do
test "shows landing page when user not authenticated", %{conn: conn} do
assert conn |> get("/") |> html_response(200) =~ "Welcome to Plausible!"
setup [:create_user, :log_in]
test "shows landing page when user not authenticated" do
assert build_conn() |> get("/") |> html_response(200) =~ "Welcome to Plausible!"
end
test "redirects to /sites if user is authenticated", %{conn: conn} do
user = insert(:user)
assert conn
|> init_test_session(%{current_user_id: user.id})
|> get("/")
|> redirected_to(302) == "/sites"
end

View File

@ -957,7 +957,7 @@ defmodule PlausibleWeb.SiteControllerTest do
describe "PUT /:website/settings/features/visibility/:setting" do
def query_conn_with_some_url(context) do
{:ok, Map.put(context, :conn, get(context.conn, "/some_parent_path"))}
{:ok, Map.put(context, :conn_with_url, get(context.conn, "/some_parent_path"))}
end
setup [:create_user, :log_in, :query_conn_with_some_url]
@ -969,7 +969,8 @@ defmodule PlausibleWeb.SiteControllerTest do
} do
test "can toggle #{title} with admin access", %{
user: user,
conn: conn0
conn: conn0,
conn_with_url: conn_with_url
} do
site =
insert(:site,
@ -982,7 +983,12 @@ defmodule PlausibleWeb.SiteControllerTest do
conn =
put(
conn0,
PlausibleWeb.Components.Site.Feature.target(site, unquote(setting), conn0, false)
PlausibleWeb.Components.Site.Feature.target(
site,
unquote(setting),
conn_with_url,
false
)
)
assert Phoenix.Flash.get(conn.assigns.flash, :success) ==
@ -995,7 +1001,12 @@ defmodule PlausibleWeb.SiteControllerTest do
conn =
put(
conn0,
PlausibleWeb.Components.Site.Feature.target(site, unquote(setting), conn0, true)
PlausibleWeb.Components.Site.Feature.target(
site,
unquote(setting),
conn_with_url,
true
)
)
assert Phoenix.Flash.get(conn.assigns.flash, :success) ==
@ -1014,7 +1025,8 @@ defmodule PlausibleWeb.SiteControllerTest do
} do
test "cannot toggle #{title} with viewer access", %{
user: user,
conn: conn0
conn: conn0,
conn_with_url: conn_with_url
} do
site = insert(:site)
insert(:site_membership, user: user, site: site, role: :viewer)
@ -1022,7 +1034,12 @@ defmodule PlausibleWeb.SiteControllerTest do
conn =
put(
conn0,
PlausibleWeb.Components.Site.Feature.target(site, unquote(setting), conn0, false)
PlausibleWeb.Components.Site.Feature.target(
site,
unquote(setting),
conn_with_url,
false
)
)
assert conn.status == 404
@ -1030,7 +1047,11 @@ defmodule PlausibleWeb.SiteControllerTest do
end
end
test "setting feature visibility is idempotent", %{user: user, conn: conn0} do
test "setting feature visibility is idempotent", %{
user: user,
conn: conn0,
conn_with_url: conn_with_url
} do
site = insert(:site)
insert(:site_membership, user: user, site: site, role: :admin)
@ -1039,7 +1060,7 @@ defmodule PlausibleWeb.SiteControllerTest do
conn =
put(
conn0,
PlausibleWeb.Components.Site.Feature.target(site, setting, conn0, false)
PlausibleWeb.Components.Site.Feature.target(site, setting, conn_with_url, false)
)
assert %{^setting => false} = Plausible.Sites.get_by_domain(site.domain)
@ -1048,7 +1069,7 @@ defmodule PlausibleWeb.SiteControllerTest do
conn =
put(
conn0,
PlausibleWeb.Components.Site.Feature.target(site, setting, conn0, false)
PlausibleWeb.Components.Site.Feature.target(site, setting, conn_with_url, false)
)
assert %{^setting => false} = Plausible.Sites.get_by_domain(site.domain)
@ -1135,11 +1156,11 @@ defmodule PlausibleWeb.SiteControllerTest do
site = insert(:site)
insert(:weekly_report, site: site, recipients: ["recipient@email.com"])
conn = delete(conn, "/sites/#{site.domain}/weekly-report/recipients/recipient@email.com")
assert conn.status == 404
conn1 = delete(conn, "/sites/#{site.domain}/weekly-report/recipients/recipient@email.com")
assert conn1.status == 404
conn = delete(conn, "/sites/#{site.domain}/weekly-report/recipients/recipient%40email.com")
assert conn.status == 404
conn2 = delete(conn, "/sites/#{site.domain}/weekly-report/recipients/recipient%40email.com")
assert conn2.status == 404
report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id)
assert [_] = report.recipients
@ -1218,11 +1239,13 @@ defmodule PlausibleWeb.SiteControllerTest do
site = insert(:site)
insert(:monthly_report, site: site, recipients: ["recipient@email.com"])
conn = delete(conn, "/sites/#{site.domain}/monthly-report/recipients/recipient@email.com")
assert conn.status == 404
conn1 = delete(conn, "/sites/#{site.domain}/monthly-report/recipients/recipient@email.com")
assert conn1.status == 404
conn = delete(conn, "/sites/#{site.domain}/monthly-report/recipients/recipient%40email.com")
assert conn.status == 404
conn2 =
delete(conn, "/sites/#{site.domain}/monthly-report/recipients/recipient%40email.com")
assert conn2.status == 404
report = Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id)
assert [_] = report.recipients

View File

@ -54,12 +54,16 @@ defmodule PlausibleWeb.Live.SentryContextTest do
assert_receive {:context, context}
assert context.request.headers["User-Agent"] == "Firefox"
end
end
test "user_id is included", %{conn: conn} do
context_hook(conn, %{"current_user_id" => 172})
describe "sentry context with logged in user" do
setup [:create_user, :log_in]
test "user_id is included", %{conn: conn, user: user} do
context_hook(conn)
assert_receive {:context, context}
assert context.user.id == 172
assert context.user.id == user.id
end
end

View File

@ -1,41 +1,40 @@
defmodule PlausibleWeb.AuthPlugTest do
use Plausible.DataCase, async: true
use Plug.Test
use PlausibleWeb.ConnCase, async: true
alias PlausibleWeb.AuthPlug
setup [:create_user, :log_in]
test "does nothing if user is not logged in" do
conn =
conn(:get, "/")
build_conn(:get, "/")
|> init_test_session(%{})
|> AuthPlug.call(%{})
assert is_nil(conn.assigns[:current_user])
end
test "looks up current user if they are logged in" do
user = insert(:user)
subscription = insert(:subscription, user: user)
test "looks up current user if they are logged in", %{conn: conn, user: user} do
subscription = insert(:subscription, user: user, inserted_at: Timex.now())
conn =
conn(:get, "/")
|> init_test_session(%{current_user_id: user.id})
conn
|> Plug.Adapters.Test.Conn.conn(:get, "/", %{})
|> AuthPlug.call(%{})
assert conn.assigns[:current_user].id == user.id
assert conn.assigns[:current_user].subscription.id == subscription.id
end
test "looks up the latest subscription" do
user = insert(:user)
test "looks up the latest subscription", %{conn: conn, user: user} do
_old_subscription =
insert(:subscription, user: user, inserted_at: Timex.now() |> Timex.shift(days: -1))
subscription = insert(:subscription, user: user, inserted_at: Timex.now())
conn =
conn(:get, "/")
|> init_test_session(%{current_user_id: user.id})
conn
|> Plug.Adapters.Test.Conn.conn(:get, "/", %{})
|> AuthPlug.call(%{})
assert conn.assigns[:current_user].id == user.id

View File

@ -1,9 +1,15 @@
defmodule PlausibleWeb.SessionTimeoutPlugTest do
use ExUnit.Case, async: true
use Plausible.DataCase, async: true
use Plug.Test
import Plausible.Factory
alias PlausibleWeb.SessionTimeoutPlug
@opts %{timeout_after_seconds: 10}
@moduletag :capture_log
test "does nothing if user is not logged in" do
conn =
conn(:get, "/")
@ -14,9 +20,11 @@ defmodule PlausibleWeb.SessionTimeoutPlugTest do
end
test "sets session timeout if user is logged in" do
user = insert(:user)
conn =
conn(:get, "/")
|> init_test_session(%{current_user_id: 1})
|> init_test_session(%{current_user_id: user.id})
|> SessionTimeoutPlug.call(@opts)
timeout = get_session(conn, :session_timeout_at)
@ -25,9 +33,11 @@ defmodule PlausibleWeb.SessionTimeoutPlugTest do
end
test "logs user out if timeout passed" do
user = insert(:user)
conn =
conn(:get, "/")
|> init_test_session(%{current_user_id: 1, session_timeout_at: 1})
|> init_test_session(%{current_user_id: user.id, session_timeout_at: 1})
|> SessionTimeoutPlug.call(@opts)
assert conn.private[:plug_session_info] == :renew

View File

@ -0,0 +1,57 @@
defmodule PlausibleWeb.Plugs.UserSessionTouchTest do
use PlausibleWeb.ConnCase, async: true
alias Plausible.Repo
alias PlausibleWeb.AuthPlug
alias PlausibleWeb.UserAuth
alias PlausibleWeb.Plugs.UserSessionTouch
setup [:create_user, :log_in]
@moduletag :capture_log
test "refreshes session", %{conn: conn, user: user} do
now = NaiveDateTime.utc_now(:second)
one_day_ago = NaiveDateTime.shift(now, day: -1)
%{sessions: [user_session]} = Repo.preload(user, :sessions)
UserAuth.touch_user_session(user_session, one_day_ago)
assert %{assigns: %{current_user_session: user_session}} =
conn
|> AuthPlug.call([])
|> UserSessionTouch.call([])
assert NaiveDateTime.compare(user_session.last_used_at, now) in [:gt, :eq]
assert NaiveDateTime.compare(user_session.timeout_at, user_session.last_used_at) == :gt
end
test "passes through when there's no authenticated session" do
conn =
build_conn()
|> init_session()
|> put_session(:login_dest, "/")
|> UserSessionTouch.call([])
refute conn.halted
assert get_session(conn, :login_dest) == "/"
refute get_session(conn, :current_user_id)
refute get_session(conn, :user_token)
end
test "converts legacy session when present", %{user: user} do
%{sessions: [other_session]} = Repo.preload(user, :sessions)
conn =
build_conn()
|> init_session()
|> put_session(:current_user_id, user.id)
|> AuthPlug.call([])
|> UserSessionTouch.call([])
refute get_session(conn, :current_user_id)
assert user_token = get_session(conn, :user_token)
assert conn.assigns.current_user_session.id
assert conn.assigns.current_user_session.id != other_session.id
assert conn.assigns.current_user_session.token == user_token
end
end

View File

@ -1,10 +1,17 @@
defmodule PlausibleWeb.UserAuthTest do
use PlausibleWeb.ConnCase, async: true
import Ecto.Query, only: [from: 2]
import ExUnit.CaptureLog
alias Plausible.Auth
alias Plausible.Repo
alias PlausibleWeb.UserAuth
alias PlausibleWeb.Router.Helpers, as: Routes
@moduletag capture_log: true
describe "log_in_user/2,3" do
setup [:create_user]
@ -14,10 +21,17 @@ defmodule PlausibleWeb.UserAuthTest do
|> init_session()
|> UserAuth.log_in_user(user)
now = NaiveDateTime.utc_now(:second)
assert %{sessions: [session]} = user |> Repo.reload!() |> Repo.preload(:sessions)
assert session.user_id == user.id
assert NaiveDateTime.compare(session.last_used_at, now) in [:eq, :gt]
assert NaiveDateTime.compare(session.timeout_at, session.last_used_at) == :gt
assert redirected_to(conn, 302) == Routes.site_path(conn, :index)
assert conn.private[:plug_session_info] == :renew
assert conn.resp_cookies["logged_in"].max_age > 0
assert get_session(conn, :current_user_id) == user.id
assert get_session(conn, :user_token) == session.token
assert get_session(conn, :login_dest) == nil
end
@ -52,15 +66,24 @@ defmodule PlausibleWeb.UserAuthTest do
end
describe "log_out_user/1" do
setup [:create_user, :log_in]
setup [:create_user]
test "logs user out", %{conn: conn, user: user} do
# another independent session for the same user
{:ok, conn: another_conn} = log_in(%{conn: conn, user: user})
another_session_token = get_session(another_conn, :user_token)
{:ok, conn: conn} = log_in(%{conn: conn, user: user})
test "logs user out", %{conn: conn} do
conn =
conn
|> init_session()
|> put_session("login_dest", "/ignored")
|> UserAuth.log_out_user()
# the other session remains intact
assert %{sessions: [another_session]} = Repo.preload(user, :sessions)
assert another_session.token == another_session_token
assert conn.private[:plug_session_info] == :renew
assert conn.resp_cookies["logged_in"].max_age == 0
assert get_session(conn, :current_user_id) == nil
@ -68,50 +91,6 @@ defmodule PlausibleWeb.UserAuthTest do
end
end
describe "get_user/1" do
setup [:create_user, :log_in]
test "gets user from session data in conn", %{conn: conn, user: user} do
assert {:ok, session_user} = UserAuth.get_user(conn)
assert session_user.id == user.id
end
test "gets user from session data map", %{user: user} do
assert {:ok, session_user} = UserAuth.get_user(%{"current_user_id" => user.id})
assert session_user.id == user.id
end
test "gets user from session schema", %{user: user} do
assert {:ok, session_user} =
UserAuth.get_user(%Plausible.Auth.UserSession{user_id: user.id})
assert session_user.id == user.id
end
test "returns error on invalid or missing session data" do
conn = init_session(build_conn())
assert {:error, :no_valid_token} = UserAuth.get_user(conn)
assert {:error, :no_valid_token} = UserAuth.get_user(%{})
end
test "returns error on missing user", %{conn: conn, user: user} do
Plausible.Repo.delete!(user)
assert {:error, :user_not_found} = UserAuth.get_user(conn)
assert {:error, :user_not_found} = UserAuth.get_user(%{"current_user_id" => user.id})
assert {:error, :user_not_found} =
UserAuth.get_user(%Plausible.Auth.UserSession{user_id: user.id})
end
test "returns error on missing session (new token scaffold; to be revised)" do
conn = build_conn() |> init_session() |> put_session(:user_token, "does_not_exist")
assert {:error, :session_not_found} = UserAuth.get_user(conn)
assert {:error, :session_not_found} = UserAuth.get_user(%{"user_token" => "does_not_exist"})
end
end
describe "get_user_session/1" do
setup [:create_user, :log_in]
@ -121,8 +100,18 @@ defmodule PlausibleWeb.UserAuthTest do
end
test "gets session from session data map", %{user: user} do
assert {:ok, user_session} = UserAuth.get_user_session(%{"current_user_id" => user.id})
assert user_session.user_id == user.id
user_id = user.id
%{sessions: [user_session]} = Repo.preload(user, :sessions)
assert {:ok, session_from_token} =
UserAuth.get_user_session(%{"user_token" => user_session.token})
assert session_from_token.id == user_session.id
capture_log(fn ->
assert {:ok, %Auth.UserSession{user_id: ^user_id, token: nil}} =
UserAuth.get_user_session(%{"current_user_id" => user.id})
end) =~ "Legacy user session detected"
end
test "returns error on invalid or missing session data" do
@ -131,13 +120,17 @@ defmodule PlausibleWeb.UserAuthTest do
assert {:error, :no_valid_token} = UserAuth.get_user_session(%{})
end
test "returns error on missing session (new token scaffold; to be revised)" do
conn = build_conn() |> init_session() |> put_session(:user_token, "does_not_exist")
test "returns error on missing session (new token scaffold; to be revised)", %{
conn: conn,
user: user
} do
%{sessions: [user_session]} = Repo.preload(user, :sessions)
Repo.delete!(user_session)
assert {:error, :session_not_found} = UserAuth.get_user_session(conn)
assert {:error, :session_not_found} =
UserAuth.get_user_session(%{"user_token" => "does_not_exist"})
UserAuth.get_user_session(%{"user_token" => user_session.token})
end
end
@ -150,4 +143,188 @@ defmodule PlausibleWeb.UserAuthTest do
assert cookie.value == "true"
end
end
describe "touch_user_session/1" do
setup [:create_user, :log_in]
test "refreshes user session timestamps", %{user: user} do
%{sessions: [user_session]} = Repo.preload(user, :sessions)
two_days_later =
NaiveDateTime.utc_now(:second)
|> NaiveDateTime.shift(day: 2)
assert refreshed_session =
%Auth.UserSession{} = UserAuth.touch_user_session(user_session, two_days_later)
assert refreshed_session.id == user_session.id
assert NaiveDateTime.compare(refreshed_session.last_used_at, two_days_later) == :eq
assert NaiveDateTime.compare(Repo.reload(user).last_seen, two_days_later) == :eq
assert NaiveDateTime.compare(refreshed_session.timeout_at, user_session.timeout_at) == :gt
end
test "does not refresh if timestamps were updated less than hour before", %{user: user} do
%{sessions: [user_session]} = Repo.preload(user, :sessions)
user_session = Repo.reload(user_session)
last_seen = Repo.reload(user).last_seen
fifty_minutes_later =
NaiveDateTime.utc_now(:second)
|> NaiveDateTime.shift(minute: 50)
assert refreshed_session1 =
%Auth.UserSession{} =
UserAuth.touch_user_session(user_session, fifty_minutes_later)
assert NaiveDateTime.compare(
refreshed_session1.last_used_at,
user_session.last_used_at
) == :eq
assert NaiveDateTime.compare(Repo.reload(user).last_seen, last_seen) == :eq
sixty_five_minutes_later =
NaiveDateTime.utc_now(:second)
|> NaiveDateTime.shift(minute: 65)
assert refreshed_session2 =
%Auth.UserSession{} =
UserAuth.touch_user_session(user_session, sixty_five_minutes_later)
assert NaiveDateTime.compare(
refreshed_session2.last_used_at,
sixty_five_minutes_later
) == :eq
assert NaiveDateTime.compare(Repo.reload(user).last_seen, sixty_five_minutes_later) == :eq
end
test "handles concurrent refresh gracefully", %{user: user} do
%{sessions: [user_session]} = Repo.preload(user, :sessions)
# concurrent update
now = NaiveDateTime.utc_now(:second)
two_days_later = NaiveDateTime.shift(now, day: 2)
Repo.update_all(
from(us in Auth.UserSession, where: us.token == ^user_session.token),
set: [timeout_at: two_days_later, last_used_at: now]
)
assert refreshed_session =
%Auth.UserSession{} = UserAuth.touch_user_session(user_session)
assert refreshed_session.id == user_session.id
assert Repo.reload(user_session)
end
test "handles deleted session case gracefully", %{user: user} do
%{sessions: [user_session]} = Repo.preload(user, :sessions)
Repo.delete!(user_session)
assert refreshed_session =
%Auth.UserSession{} = UserAuth.touch_user_session(user_session)
assert refreshed_session.id == user_session.id
refute Repo.reload(user_session)
end
test "skips refreshing legacy session", %{user: user} do
user_session = %Auth.UserSession{user_id: user.id}
assert UserAuth.touch_user_session(user_session) == user_session
end
end
describe "convert_legacy_session/1" do
setup [:create_user, :log_in]
test "does nothing when there's no authenticated session" do
conn =
build_conn()
|> init_session()
|> UserAuth.convert_legacy_session()
refute get_session(conn, :user_token)
refute get_session(conn, :live_socket_id)
refute conn.assigns[:current_user_session]
end
test "does nothing when there's a new token-based session already", %{conn: conn, user: user} do
%{sessions: [user_session]} = Repo.preload(user, :sessions)
conn =
conn
|> UserAuth.convert_legacy_session()
|> PlausibleWeb.AuthPlug.call([])
assert get_session(conn, :user_token) == user_session.token
assert get_session(conn, :live_socket_id) ==
"user_sessions:#{Base.url_encode64(user_session.token)}"
assert conn.assigns.current_user_session.id == user_session.id
end
test "converts legacy session to a new one", %{user: user} do
%{sessions: [existing_session]} = Repo.preload(user, :sessions)
conn =
build_conn()
|> init_session()
|> put_session(:current_user_id, user.id)
|> PlausibleWeb.AuthPlug.call([])
|> UserAuth.convert_legacy_session()
assert conn.assigns.current_user_session.id
assert conn.assigns.current_user_session.id != existing_session.id
assert conn.assigns.current_user_session.token != existing_session.token
assert conn.assigns.current_user_session.user_id == user.id
assert conn.assigns.current_user.id == user.id
refute get_session(conn, :current_user_id)
assert get_session(conn, :user_token) == conn.assigns.current_user_session.token
assert get_session(conn, :live_socket_id) ==
"user_sessions:#{Base.url_encode64(conn.assigns.current_user_session.token)}"
end
end
@user_agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
@user_agent_mobile "Mozilla/5.0 (Linux; Android 6.0; U007 Pro Build/MRA58K; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/44.0.2403.119 Mobile Safari/537.36"
@user_agent_tablet "Mozilla/5.0 (Linux; U; Android 4.2.2; it-it; Surfing TAB B 9.7 3G Build/JDQ39) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"
describe "device name detection" do
setup [:create_user]
test "detects browser and os when possible", %{conn: conn, user: user} do
assert login_device(conn, user, @user_agent) == "Chrome (Mac)"
assert login_device(conn, user, @user_agent_mobile) == "Mobile App (Android)"
assert login_device(conn, user, @user_agent_tablet) == "Android Browser (Android)"
end
test "falls back to unknown when can't detect browser", %{conn: conn, user: user} do
assert login_device(conn, user, nil) == "Unknown"
assert login_device(conn, user, "Bogus UA") == "Unknown"
end
test "skips os when can't detect it", %{conn: conn, user: user} do
assert login_device(conn, user, "Mozilla Firefox") == "Firefox"
end
end
defp login_device(conn, user, ua_string) do
conn =
if ua_string do
Plug.Conn.put_req_header(conn, "user-agent", ua_string)
else
conn
end
{:ok, conn: conn} = log_in(%{conn: conn, user: user})
{:ok, user_session} = conn |> UserAuth.get_user_session()
user_session.device
end
end

View File

@ -101,11 +101,11 @@ defmodule Plausible.TestUtils do
def log_in(%{user: user, conn: conn}) do
conn =
conn
|> PlausibleWeb.UserAuth.set_logged_in_cookie()
|> init_session()
|> PlausibleWeb.UserAuth.log_in_user(user)
|> Phoenix.ConnTest.recycle()
|> Map.put(:secret_key_base, secret_key_base())
|> init_session()
|> Plug.Conn.put_session(:current_user_id, user.id)
{:ok, conn: conn}
end

View File

@ -0,0 +1,34 @@
defmodule Plausible.Workers.CleanUserSessionsTest do
use Plausible.DataCase
alias Plausible.Auth.UserSession
alias Plausible.Workers.CleanUserSessions
test "cleans invitation that is more than timeout_at + grace_period days old" do
grace_cutoff =
NaiveDateTime.utc_now(:second)
|> NaiveDateTime.shift(Duration.negate(UserSession.timeout_duration()))
|> NaiveDateTime.shift(CleanUserSessions.grace_period_duration())
ten_days_after = NaiveDateTime.shift(grace_cutoff, day: 10)
one_day_after = NaiveDateTime.shift(grace_cutoff, day: 1)
one_day_before = NaiveDateTime.shift(grace_cutoff, day: -1)
session_to_clean = insert_session(one_day_before)
session_to_leave1 = insert_session(one_day_after)
session_to_leave2 = insert_session(ten_days_after)
CleanUserSessions.perform(nil)
refute Repo.reload(session_to_clean)
assert Repo.reload(session_to_leave1)
assert Repo.reload(session_to_leave2)
end
defp insert_session(now) do
user = insert(:user)
user
|> UserSession.new_session("Unknown", now)
|> Repo.insert!()
end
end