who_need_help/scripts/production-web-push-smoke.exs

217 lines
7.2 KiB
Elixir

defmodule WhoNeedHelp.ProductionWebPushSmoke do
import Ecto.Query
alias Oban.Job
alias WhoNeedHelp.Accounts.User
alias WhoNeedHelp.Notifications.{Notification, PushDevice}
alias WhoNeedHelp.Push.DeviceDeliveryWorker
alias WhoNeedHelp.Repo
@allowed_actions ~w(prepare verify cleanup)
def run(action, options) when action in @allowed_actions and is_map(options) do
context = verified_context(options)
case action do
"prepare" -> prepare(context)
"verify" -> verify(context)
"cleanup" -> cleanup(context)
end
end
def run(_action, _options), do: raise("unsupported Web Push smoke action")
defp verified_context(options) do
run_id = required_option!(options, :run_id)
expected_database = required_option!(options, :expected_database)
user_email = required_option!(options, :user_email) |> String.downcase()
manifest_path = required_option!(options, :manifest_path)
unless Regex.match?(~r/^[a-z0-9-]+$/, run_id), do: raise("invalid run id")
unless String.starts_with?(manifest_path, "/tmp/wnh-production-web-push-") do
raise "invalid manifest path"
end
%Postgrex.Result{rows: [[actual_database]]} =
Repo.query!("SELECT current_database()", [], log: false)
unless actual_database == expected_database, do: raise("database identity mismatch")
%{
run_id: run_id,
database: actual_database,
user_email: user_email,
manifest_path: manifest_path,
idempotency_key: "production-web-push-smoke:#{run_id}"
}
end
defp prepare(context) do
if File.exists?(context.manifest_path), do: raise("manifest already exists")
user = Repo.get_by(User, email: context.user_email)
unless match?(%User{confirmed_at: %DateTime{}, moderation_status: :active}, user) do
raise "target user is missing, unconfirmed, or inactive"
end
device =
PushDevice
|> where(
[device],
device.user_id == ^user.id and device.platform == :web and
device.provider == :web_push and is_nil(device.disabled_at)
)
|> order_by([device], desc: device.last_seen_at, desc: device.inserted_at)
|> limit(1)
|> Repo.one()
unless match?(%PushDevice{}, device), do: raise("no active Web Push device exists")
if Repo.exists?(
from(notification in Notification,
where: notification.idempotency_key == ^context.idempotency_key
)
) do
raise "run-scoped notification already exists"
end
{:ok, fixture} =
Repo.transaction(fn ->
notification =
%Notification{}
|> Notification.changeset(%{
user_id: user.id,
kind: :support_update,
title: "Who Need Help notification check",
body: "Production browser notifications are working.",
path: "/notifications",
data: %{"run_id" => context.run_id, "synthetic" => true},
idempotency_key: context.idempotency_key
})
|> Repo.insert!()
job =
%{"notification_id" => notification.id, "device_id" => device.id}
|> DeviceDeliveryWorker.new()
|> Oban.insert!()
%{notification: notification, device: device, job: job}
end)
manifest = %{
"schema_version" => 1,
"run_id" => context.run_id,
"database" => context.database,
"user_email" => context.user_email,
"idempotency_key" => context.idempotency_key,
"notification_id" => fixture.notification.id,
"device_id" => fixture.device.id,
"job_id" => fixture.job.id
}
File.write!(context.manifest_path, Jason.encode_to_iodata!(manifest, pretty: true))
File.chmod!(context.manifest_path, 0o600)
IO.puts("web_push_smoke_prepared=true")
IO.puts("job_id=#{fixture.job.id}")
IO.puts("delivery_scope=latest active Web Push device only")
IO.puts("email_enqueued=false")
end
defp verify(context) do
fixture = load_and_validate_manifest!(context)
notification = Repo.get(Notification, fixture["notification_id"])
device = Repo.get(PushDevice, fixture["device_id"])
job = Repo.get(Job, fixture["job_id"])
unless match?(%Notification{}, notification) and
notification.user_id == device.user_id and
notification.idempotency_key == context.idempotency_key and
match?(%PushDevice{platform: :web, provider: :web_push, disabled_at: nil}, device) and
match?(%Job{state: "completed", attempt: 1}, job) and
job.args["notification_id"] == notification.id and
job.args["device_id"] == device.id do
raise "Web Push smoke has not completed successfully on the exact target"
end
IO.puts("web_push_provider_delivery_verified=true")
IO.puts("job_state=#{job.state}")
IO.puts("job_attempt=#{job.attempt}")
IO.puts("device_still_active=true")
end
defp cleanup(context) do
fixture = load_and_validate_manifest!(context)
job = Repo.get(Job, fixture["job_id"])
notification = Repo.get(Notification, fixture["notification_id"])
unless match?(%Job{}, job) and match?(%Notification{}, notification) and
notification.idempotency_key == context.idempotency_key and
job.args["notification_id"] == notification.id and
job.args["device_id"] == fixture["device_id"] do
raise "run-scoped records no longer match the manifest"
end
{:ok, deleted} =
Repo.transaction(fn ->
{jobs, _} = Repo.delete_all(from(candidate in Job, where: candidate.id == ^job.id))
{notifications, _} =
Repo.delete_all(
from(candidate in Notification,
where:
candidate.id == ^notification.id and
candidate.idempotency_key == ^context.idempotency_key
)
)
%{jobs: jobs, notifications: notifications}
end)
unless deleted == %{jobs: 1, notifications: 1} do
raise "exact Web Push smoke cleanup failed"
end
File.rm!(context.manifest_path)
if Repo.exists?(
from(candidate in Notification,
where: candidate.idempotency_key == ^context.idempotency_key
)
) do
raise "run-scoped notification remains after cleanup"
end
IO.puts("web_push_smoke_cleanup_verified=true")
IO.puts("deleted_jobs=1")
IO.puts("deleted_notifications=1")
end
defp load_and_validate_manifest!(context) do
manifest = context.manifest_path |> File.read!() |> Jason.decode!()
valid? =
manifest["schema_version"] == 1 and manifest["run_id"] == context.run_id and
manifest["database"] == context.database and
manifest["user_email"] == context.user_email and
manifest["idempotency_key"] == context.idempotency_key and
uuid?(manifest["notification_id"]) and uuid?(manifest["device_id"]) and
is_integer(manifest["job_id"])
unless valid?, do: raise("manifest does not match this exact run")
manifest
end
defp required_option!(options, name) do
case Map.get(options, name) do
value when is_binary(value) and value != "" -> value
_missing -> raise("#{name} is required")
end
end
defp uuid?(value) when is_binary(value), do: match?({:ok, _}, Ecto.UUID.cast(value))
defp uuid?(_value), do: false
end