defmodule WhoNeedHelp.ProductionAndroidFCMSmoke do import Ecto.Query alias Oban.Job 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 Android FCM smoke action") defp verified_context(options) do run_id = required_option!(options, :run_id) expected_database = required_option!(options, :expected_database) device_id = required_option!(options, :device_id) manifest_path = required_option!(options, :manifest_path) unless Regex.match?(~r/^[a-z0-9-]+$/, run_id), do: raise("invalid run id") unless uuid?(device_id), do: raise("invalid device id") unless String.starts_with?(manifest_path, "/tmp/wnh-production-android-fcm-") 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, device_id: device_id, manifest_path: manifest_path, idempotency_key: "production-android-fcm-smoke:#{run_id}" } end defp prepare(context) do if File.exists?(context.manifest_path), do: raise("manifest already exists") device = Repo.get(PushDevice, context.device_id) unless match?( %PushDevice{platform: :android, provider: :fcm, disabled_at: nil}, device ) do raise "target is not an active Android FCM device" end 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: device.user_id, kind: :support_update, title: "Who Need Help notification check", body: "Android 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, job: job} end) manifest = %{ "schema_version" => 1, "run_id" => context.run_id, "database" => context.database, "idempotency_key" => context.idempotency_key, "notification_id" => fixture.notification.id, "device_id" => 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("android_fcm_smoke_prepared=true") IO.puts("job_id=#{fixture.job.id}") IO.puts("delivery_scope=one exact active Android FCM device") 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 match?(%PushDevice{platform: :android, provider: :fcm, disabled_at: nil}, device) and notification.user_id == device.user_id and notification.idempotency_key == context.idempotency_key and match?(%Job{state: "completed", attempt: 1}, job) and job.args["notification_id"] == notification.id and job.args["device_id"] == device.id do raise "Android FCM smoke has not completed successfully on the exact target" end IO.puts("android_fcm_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 Android FCM 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("android_fcm_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["device_id"] == context.device_id and manifest["idempotency_key"] == context.idempotency_key and uuid?(manifest["notification_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