From f0512fab8460980eed79246fa0857ec80819991b Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Thu, 23 Jul 2026 02:34:45 +0300 Subject: [PATCH] fix: close failed location sessions and harden upgrade rehearsal --- assets/js/hooks.js | 6 +- .../live/request_live/show.ex | 32 ++++++++--- scripts/upgrade-rehearsal-compose.sh | 57 +++++++++++++------ .../live/mutual_aid_live_test.exs | 53 +++++++++++++++++ 4 files changed, 123 insertions(+), 25 deletions(-) diff --git a/assets/js/hooks.js b/assets/js/hooks.js index d7dfcc4..30df5c0 100644 --- a/assets/js/hooks.js +++ b/assets/js/hooks.js @@ -1630,12 +1630,16 @@ export const Hooks = { if (typeof window.WhoNeedHelpAndroid?.postMessage === "function") { this.nativeError = () => { this.pushEvent("location-error", {}) - this.pushEvent("stop-tracking", {}) } window.addEventListener("wnh:native-tracking-error", this.nativeError) return } + if (!navigator.geolocation) { + this.pushEvent("location-error", {}) + return + } + this.watchId = navigator.geolocation.watchPosition( position => this.queueLocation({ latitude: position.coords.latitude, diff --git a/lib/who_need_help_web/live/request_live/show.ex b/lib/who_need_help_web/live/request_live/show.ex index 073291c..785f513 100644 --- a/lib/who_need_help_web/live/request_live/show.ex +++ b/lib/who_need_help_web/live/request_live/show.ex @@ -288,18 +288,19 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do end def handle_event("location-error", _, socket) do + socket = + case stop_tracking(socket) do + {:ok, stopped_socket} -> stopped_socket + {:error, _reason} -> socket + end + {:noreply, put_flash(socket, :error, gettext("The browser could not share your location."))} end def handle_event("stop-tracking", _, socket) do - case Tracking.stop_session(socket.assigns.current_scope, socket.assigns.assignment) do - {:ok, _} -> - {:noreply, - socket - |> assign(:tracking_active, false) - |> assign(:tracking_session_id, nil) - |> maybe_untrack_browser_presence() - |> maybe_stop_native_tracking()} + case stop_tracking(socket) do + {:ok, stopped_socket} -> + {:noreply, stopped_socket} {:error, reason} -> {:noreply, put_flash(socket, :error, message(reason))} @@ -470,6 +471,21 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do defp maybe_untrack_browser_presence(socket), do: socket + defp stop_tracking(socket) do + case Tracking.stop_session(socket.assigns.current_scope, socket.assigns.assignment) do + {:ok, _} -> + {:ok, + socket + |> assign(:tracking_active, false) + |> assign(:tracking_session_id, nil) + |> maybe_untrack_browser_presence() + |> maybe_stop_native_tracking()} + + {:error, reason} -> + {:error, reason} + end + end + defp transition(socket, fun, success) do case fun.(socket.assigns.current_scope, socket.assigns.assignment.id) do {:ok, _} -> {:noreply, put_flash(socket, :info, success)} diff --git a/scripts/upgrade-rehearsal-compose.sh b/scripts/upgrade-rehearsal-compose.sh index 8941282..42238f4 100755 --- a/scripts/upgrade-rehearsal-compose.sh +++ b/scripts/upgrade-rehearsal-compose.sh @@ -50,6 +50,15 @@ rehearsal_map_tile_url=$( '. "$1"; printf %s "${MAP_TILE_URL:-https://tile.openstreetmap.org/{z}/{x}/{y}.png}"' \ sh "$PUBLIC_ENV_FILE" ) +rehearsal_postgis_image=$( + sh -c '. "$1"; printf %s "${POSTGIS_IMAGE:?POSTGIS_IMAGE is missing from .env}"' \ + sh "$PUBLIC_ENV_FILE" +) +rehearsal_socket_proxy_image=$( + sh -c \ + '. "$1"; printf %s "${SOCKET_PROXY_IMAGE:?SOCKET_PROXY_IMAGE is missing from .env}"' \ + sh "$PUBLIC_ENV_FILE" +) set -a # shellcheck source=/dev/null @@ -112,11 +121,17 @@ export PHX_HOST="$rehearsal_phx_host" export PHX_SCHEME="$rehearsal_phx_scheme" export PHX_URL_PORT="$rehearsal_phx_url_port" export MAP_TILE_URL="$rehearsal_map_tile_url" +export POSTGIS_IMAGE="$rehearsal_postgis_image" +export SOCKET_PROXY_IMAGE="$rehearsal_socket_proxy_image" export TRAEFIK_TRUSTED_IPS=127.0.0.1/32 export TRAEFIK_PROJECT_CONSTRAINT="$project" export TRAEFIK_APP_NAME="wnh-upgrade-rehearsal-$compact_id" export TRAEFIK_DOCKER_NETWORK="${project}_ingress" export REHEARSAL_IMAGE="who-need-help:upgrade-rehearsal-$run_id" +# compose.yaml validates APP_IMAGE before applying the rehearsal override. +# Point both variables at the same unique image so config validation, the +# build, and cleanup all address only this rehearsal run. +export APP_IMAGE="$REHEARSAL_IMAGE" compose() { docker compose \ @@ -173,24 +188,29 @@ cleanup() { } trap cleanup EXIT HUP INT TERM +database_table_list() { + target=$1 + + compose exec -T db \ + psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \ + --tuples-only --no-align \ + --command " + SELECT tablename + FROM pg_tables + WHERE schemaname = 'public' + AND tablename NOT IN ('oban_jobs', 'schema_migrations', 'spatial_ref_sys') + ORDER BY tablename; + " >"$target" +} + database_snapshot() { target=$1 - tables=$( - compose exec -T db \ - psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \ - --tuples-only --no-align \ - --command " - SELECT tablename - FROM pg_tables - WHERE schemaname = 'public' - AND tablename NOT IN ('oban_jobs', 'schema_migrations', 'spatial_ref_sys') - ORDER BY tablename; - " - ) + table_list=$2 : >"$target" - for table in $tables; do + while IFS= read -r table; do + [ -n "$table" ] || continue row_count=$( compose exec -T db \ psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \ @@ -198,7 +218,7 @@ database_snapshot() { --command "SELECT count(*) FROM \"$table\";" ) printf '%s %s\n' "$table" "$row_count" >>"$target" - done + done <"$table_list" } compose config --quiet @@ -221,7 +241,10 @@ compose exec -T db \ compose exec -T db pg_restore --list <"$dump" >"$output_dir/pg-restore-list.txt" -database_snapshot "$output_dir/database-before.txt" +database_table_list "$output_dir/database-tables-before.txt" +database_snapshot \ + "$output_dir/database-before.txt" \ + "$output_dir/database-tables-before.txt" compose exec -T db \ psql --username "$POSTGRES_USER" --dbname "$rehearsal_db" \ --tuples-only --no-align \ @@ -230,7 +253,9 @@ compose exec -T db \ compose up --detach --wait mailpit migrate proxy web worker -database_snapshot "$output_dir/database-after.txt" +database_snapshot \ + "$output_dir/database-after.txt" \ + "$output_dir/database-tables-before.txt" diff -u "$output_dir/database-before.txt" "$output_dir/database-after.txt" \ >"$output_dir/database-diff.txt" diff --git a/test/who_need_help_web/live/mutual_aid_live_test.exs b/test/who_need_help_web/live/mutual_aid_live_test.exs index 80c8cec..18f00d5 100644 --- a/test/who_need_help_web/live/mutual_aid_live_test.exs +++ b/test/who_need_help_web/live/mutual_aid_live_test.exs @@ -924,6 +924,59 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do stop_live_view(helper_view) end + test "browser location errors stop the empty tracking session", _context do + category = Catalog.seed_defaults() + requester = user_fixture(display_name: "Location requester") + helper = user_fixture(display_name: "Location helper") + + {:ok, request} = + Help.create_request(user_scope_fixture(requester), request_attrs(category)) + + {:ok, assignment} = Help.accept_request(user_scope_fixture(helper), request.id) + + helper_conn = + build_conn() + |> log_in_user(helper) + + {:ok, helper_view, _html} = live(helper_conn, ~p"/requests/#{request.id}") + + helper_view + |> element("button[phx-click='start-tracking']") + |> render_click() + + assert Repo.get_by!(WhoNeedHelp.Tracking.TrackingSession, + assignment_id: assignment.id, + user_id: helper.id, + active: true + ) + + html = render_hook(helper_view, "location-error", %{}) + + assert html =~ "The browser could not share your location." + assert has_element?(helper_view, "button[phx-click='start-tracking']") + refute has_element?(helper_view, "button[phx-click='stop-tracking']") + + refute Repo.get_by(WhoNeedHelp.Tracking.TrackingSession, + assignment_id: assignment.id, + user_id: helper.id, + active: true + ) + + stopped_session = + Repo.get_by!(WhoNeedHelp.Tracking.TrackingSession, + assignment_id: assignment.id, + user_id: helper.id + ) + + assert stopped_session.ended_at + assert stopped_session.sample_count == 0 + refute Repo.get_by(WhoNeedHelp.Tracking.Position, + tracking_session_id: stopped_session.id + ) + + stop_live_view(helper_view) + end + test "regular users cannot enter moderation", %{conn: conn} do assert {:error, {:redirect, %{to: "/requests"}}} = live(conn, ~p"/moderation") end