test: add scoped public staging handover E2E
This commit is contained in:
parent
5b723831af
commit
378d0b4563
|
|
@ -20,6 +20,12 @@ export type AuthenticatedBrowser = {
|
|||
};
|
||||
|
||||
export function projectEmail(localPart: string, projectName: string): string {
|
||||
const runID = process.env.E2E_RUN_ID;
|
||||
|
||||
if (runID) {
|
||||
return `wnh-staging-e2e-${runID}-${localPart}@example.invalid`;
|
||||
}
|
||||
|
||||
const scope = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
||||
return `${localPart}+${scope}@example.invalid`;
|
||||
}
|
||||
|
|
@ -32,6 +38,12 @@ export async function newIsolatedContext(browser: Browser): Promise<BrowserConte
|
|||
return browser.newContext({ serviceWorkers: "block" });
|
||||
}
|
||||
|
||||
export async function waitForLiveViewConnected(page: Page): Promise<void> {
|
||||
await expect(page.locator("[data-phx-main].phx-connected")).toHaveCount(1, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForMapReady(page: Page): Promise<void> {
|
||||
const map = page.locator(".aid-map");
|
||||
|
||||
|
|
@ -70,13 +82,18 @@ export function captureBrowserFailures(page: Page): () => void {
|
|||
|
||||
page.on("requestfailed", (request) => {
|
||||
const failure = request.failure()?.errorText ?? "";
|
||||
|
||||
if (
|
||||
request.url().includes("/__e2e__/map-tile.png") &&
|
||||
(failure.includes("Load request cancelled") ||
|
||||
const cancelled =
|
||||
failure.includes("Load request cancelled") ||
|
||||
failure.includes("ERR_ABORTED") ||
|
||||
failure.includes("NS_BINDING_ABORTED"))
|
||||
) {
|
||||
failure.includes("NS_BINDING_ABORTED");
|
||||
const rasterTile =
|
||||
request.url().includes("/__e2e__/map-tile.png") ||
|
||||
/\/\d+\/\d+\/\d+\.png(?:\?|$)/.test(request.url());
|
||||
|
||||
// MapLibre cancels in-flight raster tiles when a LiveView navigation
|
||||
// destroys or re-centres a map. Actual tile/network failures retain their
|
||||
// engine error and remain test failures.
|
||||
if (rasterTile && cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -202,11 +219,30 @@ export async function loginWithMagicLink(
|
|||
return { context, page, email };
|
||||
}
|
||||
|
||||
export async function loginWithPassword(
|
||||
browser: Browser,
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<AuthenticatedBrowser> {
|
||||
const context = await newIsolatedContext(browser);
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto("/users/log-in");
|
||||
const form = page.locator("#login_form_password");
|
||||
await form.getByLabel("Email").fill(email);
|
||||
await form.getByLabel("Password").fill(password);
|
||||
await form.getByRole("button", { name: "Log in only this time" }).click();
|
||||
await expect(page.getByText(email, { exact: true })).toBeVisible();
|
||||
|
||||
return { context, page, email };
|
||||
}
|
||||
|
||||
export async function selectOptionContaining(
|
||||
page: Page,
|
||||
label: string,
|
||||
expectedText: string,
|
||||
): Promise<void> {
|
||||
await waitForLiveViewConnected(page);
|
||||
const select = page.getByLabel(label);
|
||||
const options = await select.locator("option").evaluateAll((nodes) =>
|
||||
nodes.map((node) => ({
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
captureBrowserFailures,
|
||||
loginWithPassword,
|
||||
projectEmail,
|
||||
projectText,
|
||||
registerAndConfirm,
|
||||
selectOptionContaining,
|
||||
waitForLiveViewConnected,
|
||||
waitForMapReady,
|
||||
} from "./helpers";
|
||||
|
||||
|
|
@ -12,19 +14,22 @@ test("two users complete a medicine handover with realtime chat and blind review
|
|||
browser,
|
||||
request,
|
||||
}, testInfo) => {
|
||||
const requestTitle = projectText("E2E medicine pickup", testInfo.project.name);
|
||||
const requester = await registerAndConfirm(
|
||||
browser,
|
||||
request,
|
||||
projectEmail("requester", testInfo.project.name),
|
||||
"E2E Requester",
|
||||
);
|
||||
const helper = await registerAndConfirm(
|
||||
browser,
|
||||
request,
|
||||
projectEmail("helper", testInfo.project.name),
|
||||
"E2E Helper",
|
||||
const runID = process.env.E2E_RUN_ID;
|
||||
const fixturePassword = process.env.E2E_FIXTURE_PASSWORD;
|
||||
const requestTitle = projectText(
|
||||
runID ? `Staging E2E medicine pickup ${runID}` : "E2E medicine pickup",
|
||||
testInfo.project.name,
|
||||
);
|
||||
const requesterEmail = projectEmail("requester", testInfo.project.name);
|
||||
const helperEmail = projectEmail("helper", testInfo.project.name);
|
||||
const requester =
|
||||
runID && fixturePassword
|
||||
? await loginWithPassword(browser, requesterEmail, fixturePassword)
|
||||
: await registerAndConfirm(browser, request, requesterEmail, "E2E Requester");
|
||||
const helper =
|
||||
runID && fixturePassword
|
||||
? await loginWithPassword(browser, helperEmail, fixturePassword)
|
||||
: await registerAndConfirm(browser, request, helperEmail, "E2E Helper");
|
||||
const assertRequesterClean = captureBrowserFailures(requester.page);
|
||||
const assertHelperClean = captureBrowserFailures(helper.page);
|
||||
|
||||
|
|
@ -51,6 +56,7 @@ test("two users complete a medicine handover with realtime chat and blind review
|
|||
await expect(requester.page.getByRole("heading", { name: requestTitle })).toBeVisible();
|
||||
|
||||
await helper.page.goto(requestURL);
|
||||
await waitForLiveViewConnected(helper.page);
|
||||
await helper.page.getByRole("button", { name: "I can help" }).click();
|
||||
await expect(helper.page.getByRole("heading", { name: "Private match chat" })).toBeVisible();
|
||||
await expect(requester.page.getByRole("heading", { name: "Private match chat" })).toBeVisible();
|
||||
|
|
@ -94,6 +100,7 @@ test("two users complete a medicine handover with realtime chat and blind review
|
|||
await expect(requester.page.getByText("None revealed yet.")).toBeVisible();
|
||||
await expect(requester.page.getByText("Clear and safe coordination.")).toHaveCount(0);
|
||||
await requester.page.goto(requestURL);
|
||||
await waitForLiveViewConnected(requester.page);
|
||||
|
||||
await requester.page.getByLabel("Rating").selectOption("5");
|
||||
await requester.page.getByLabel("Comment (optional)").fill("Reliable volunteer.");
|
||||
|
|
|
|||
328
lib/mix/tasks/wnh.staging_e2e.ex
Normal file
328
lib/mix/tasks/wnh.staging_e2e.ex
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
defmodule Mix.Tasks.Wnh.StagingE2e do
|
||||
use Mix.Task
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias WhoNeedHelp.Accounts.User
|
||||
alias WhoNeedHelp.Activities.Activity
|
||||
alias WhoNeedHelp.Catalog.CategoryProposal
|
||||
alias WhoNeedHelp.Help.{Assignment, HelpRequest}
|
||||
alias WhoNeedHelp.Messaging.Message
|
||||
alias WhoNeedHelp.Repo
|
||||
alias WhoNeedHelp.Tracking.{Position, TrackingSession}
|
||||
alias WhoNeedHelp.Trust.{AbuseSignal, AuditEvent, Report, Review}
|
||||
|
||||
@shortdoc "Prepares or removes an exact public-staging browser fixture"
|
||||
@confirmation "public-staging-e2e"
|
||||
|
||||
@impl Mix.Task
|
||||
def run([action]) when action in ["prepare", "cleanup"] do
|
||||
Mix.Task.run("app.start")
|
||||
context = verified_context()
|
||||
|
||||
case action do
|
||||
"prepare" -> prepare(context)
|
||||
"cleanup" -> cleanup(context)
|
||||
end
|
||||
end
|
||||
|
||||
def run(_args) do
|
||||
Mix.raise("usage: mix wnh.staging_e2e prepare|cleanup")
|
||||
end
|
||||
|
||||
defp verified_context do
|
||||
run_id = required_env!("WNH_STAGING_E2E_RUN_ID")
|
||||
expected_database = required_env!("WNH_STAGING_E2E_EXPECTED_DATABASE")
|
||||
manifest_path = required_env!("WNH_STAGING_E2E_MANIFEST_PATH") |> Path.expand()
|
||||
|
||||
unless Regex.match?(~r/^[a-z0-9-]+$/, run_id) do
|
||||
Mix.raise("WNH_STAGING_E2E_RUN_ID may contain only lowercase letters, numbers, and dash")
|
||||
end
|
||||
|
||||
unless System.get_env("WNH_STAGING_E2E_CONFIRM") == @confirmation do
|
||||
Mix.raise("WNH_STAGING_E2E_CONFIRM must equal #{@confirmation}")
|
||||
end
|
||||
|
||||
unless String.starts_with?(manifest_path, "/output/") do
|
||||
Mix.raise("WNH_STAGING_E2E_MANIFEST_PATH must resolve below /output")
|
||||
end
|
||||
|
||||
%Postgrex.Result{rows: [[actual_database]]} =
|
||||
Repo.query!("SELECT current_database()", [], log: false)
|
||||
|
||||
unless actual_database == expected_database do
|
||||
Mix.raise(
|
||||
"refusing staging E2E mutation: expected database #{inspect(expected_database)}, " <>
|
||||
"observed #{inspect(actual_database)}"
|
||||
)
|
||||
end
|
||||
|
||||
%{run_id: run_id, database: actual_database, manifest_path: manifest_path}
|
||||
end
|
||||
|
||||
defp prepare(context) do
|
||||
password = required_env!("WNH_STAGING_E2E_PASSWORD")
|
||||
|
||||
unless byte_size(password) in 12..72 do
|
||||
Mix.raise("WNH_STAGING_E2E_PASSWORD must contain between 12 and 72 bytes")
|
||||
end
|
||||
|
||||
emails = emails(context.run_id)
|
||||
|
||||
if Repo.exists?(from(user in User, where: user.email in ^Map.values(emails))) do
|
||||
Mix.raise("staging E2E users already exist for #{inspect(context.run_id)}")
|
||||
end
|
||||
|
||||
password_hash = Bcrypt.hash_pwd_salt(password)
|
||||
now = DateTime.utc_now(:second)
|
||||
|
||||
{:ok, users} =
|
||||
Repo.transaction(fn ->
|
||||
%{
|
||||
requester: insert_user!(emails.requester, "Staging E2E Requester", password_hash, now),
|
||||
helper: insert_user!(emails.helper, "Staging E2E Helper", password_hash, now)
|
||||
}
|
||||
end)
|
||||
|
||||
manifest = %{
|
||||
"schema_version" => 1,
|
||||
"run_id" => context.run_id,
|
||||
"database" => context.database,
|
||||
"requester_id" => users.requester.id,
|
||||
"requester_email" => users.requester.email,
|
||||
"helper_id" => users.helper.id,
|
||||
"helper_email" => users.helper.email
|
||||
}
|
||||
|
||||
context.manifest_path |> Path.dirname() |> File.mkdir_p!()
|
||||
File.write!(context.manifest_path, Jason.encode_to_iodata!(manifest, pretty: true))
|
||||
File.chmod!(context.manifest_path, 0o600)
|
||||
|
||||
Mix.shell().info("prepared exact staging E2E users for #{context.run_id}")
|
||||
end
|
||||
|
||||
defp cleanup(context) do
|
||||
manifest = context.manifest_path |> File.read!() |> Jason.decode!()
|
||||
validate_manifest!(context, manifest)
|
||||
|
||||
requester_id = manifest["requester_id"]
|
||||
helper_id = manifest["helper_id"]
|
||||
user_ids = [requester_id, helper_id]
|
||||
|
||||
request_ids =
|
||||
HelpRequest
|
||||
|> where([request], request.requester_id == ^requester_id)
|
||||
|> select([request], request.id)
|
||||
|> Repo.all()
|
||||
|
||||
assignment_ids =
|
||||
Assignment
|
||||
|> where([assignment], assignment.request_id in ^request_ids)
|
||||
|> select([assignment], assignment.id)
|
||||
|> Repo.all()
|
||||
|
||||
validate_owned_domain!(user_ids, request_ids, assignment_ids, helper_id)
|
||||
|
||||
{:ok, deleted} =
|
||||
Repo.transaction(fn ->
|
||||
tracking_session_ids =
|
||||
TrackingSession
|
||||
|> where([session], session.assignment_id in ^assignment_ids)
|
||||
|> select([session], session.id)
|
||||
|> Repo.all()
|
||||
|
||||
message_ids =
|
||||
Message
|
||||
|> where([message], message.assignment_id in ^assignment_ids)
|
||||
|> select([message], message.id)
|
||||
|> Repo.all()
|
||||
|
||||
reports =
|
||||
Report
|
||||
|> where(
|
||||
[report],
|
||||
report.reporter_id in ^user_ids or report.request_id in ^request_ids or
|
||||
report.assignment_id in ^assignment_ids or report.message_id in ^message_ids
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
|> elem(0)
|
||||
|
||||
audits =
|
||||
AuditEvent
|
||||
|> where(
|
||||
[event],
|
||||
event.actor_id in ^user_ids or
|
||||
(event.target_type == "request" and event.target_id in ^request_ids) or
|
||||
(event.target_type == "assignment" and event.target_id in ^assignment_ids)
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
|> elem(0)
|
||||
|
||||
positions =
|
||||
Position
|
||||
|> where([position], position.tracking_session_id in ^tracking_session_ids)
|
||||
|> Repo.delete_all()
|
||||
|> elem(0)
|
||||
|
||||
sessions =
|
||||
TrackingSession
|
||||
|> where([session], session.id in ^tracking_session_ids)
|
||||
|> Repo.delete_all()
|
||||
|> elem(0)
|
||||
|
||||
messages =
|
||||
Message
|
||||
|> where([message], message.assignment_id in ^assignment_ids)
|
||||
|> Repo.delete_all()
|
||||
|> elem(0)
|
||||
|
||||
abuse_signals =
|
||||
AbuseSignal
|
||||
|> where(
|
||||
[signal],
|
||||
signal.subject_id in ^user_ids or signal.assignment_id in ^assignment_ids
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
|> elem(0)
|
||||
|
||||
reviews =
|
||||
Review
|
||||
|> where(
|
||||
[review],
|
||||
review.assignment_id in ^assignment_ids or review.reviewer_id in ^user_ids or
|
||||
review.reviewee_id in ^user_ids
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
|> elem(0)
|
||||
|
||||
assignments =
|
||||
Assignment
|
||||
|> where([assignment], assignment.id in ^assignment_ids)
|
||||
|> Repo.delete_all()
|
||||
|> elem(0)
|
||||
|
||||
requests =
|
||||
HelpRequest
|
||||
|> where([request], request.id in ^request_ids)
|
||||
|> Repo.delete_all()
|
||||
|> elem(0)
|
||||
|
||||
users =
|
||||
User
|
||||
|> where([user], user.id in ^user_ids)
|
||||
|> Repo.delete_all()
|
||||
|> elem(0)
|
||||
|
||||
%{
|
||||
reports: reports,
|
||||
audit_events: audits,
|
||||
tracking_positions: positions,
|
||||
tracking_sessions: sessions,
|
||||
messages: messages,
|
||||
abuse_signals: abuse_signals,
|
||||
reviews: reviews,
|
||||
assignments: assignments,
|
||||
requests: requests,
|
||||
users: users
|
||||
}
|
||||
end)
|
||||
|
||||
unless deleted.users == 2 do
|
||||
Mix.raise("cleanup removed #{deleted.users} users instead of exactly 2")
|
||||
end
|
||||
|
||||
Mix.shell().info("removed exact staging E2E fixture: #{inspect(deleted)}")
|
||||
end
|
||||
|
||||
defp validate_manifest!(context, manifest) do
|
||||
expected_emails = emails(context.run_id)
|
||||
|
||||
unless manifest["schema_version"] == 1 and manifest["run_id"] == context.run_id and
|
||||
manifest["database"] == context.database and
|
||||
manifest["requester_email"] == expected_emails.requester and
|
||||
manifest["helper_email"] == expected_emails.helper and
|
||||
uuid?(manifest["requester_id"]) and uuid?(manifest["helper_id"]) do
|
||||
Mix.raise("staging E2E manifest does not match the requested run and database")
|
||||
end
|
||||
|
||||
expected =
|
||||
MapSet.new([
|
||||
{manifest["requester_id"], manifest["requester_email"]},
|
||||
{manifest["helper_id"], manifest["helper_email"]}
|
||||
])
|
||||
|
||||
observed =
|
||||
User
|
||||
|> where([user], user.id in ^[manifest["requester_id"], manifest["helper_id"]])
|
||||
|> select([user], {user.id, user.email})
|
||||
|> Repo.all()
|
||||
|> MapSet.new()
|
||||
|
||||
unless observed == expected do
|
||||
Mix.raise("staging E2E user ownership does not match the manifest")
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_owned_domain!(user_ids, request_ids, assignment_ids, helper_id) do
|
||||
unexpected_request? =
|
||||
Repo.exists?(
|
||||
from(request in HelpRequest,
|
||||
where: request.requester_id in ^user_ids and request.id not in ^request_ids
|
||||
)
|
||||
)
|
||||
|
||||
unexpected_assignment? =
|
||||
Repo.exists?(
|
||||
from(assignment in Assignment,
|
||||
where:
|
||||
assignment.helper_id in ^user_ids and
|
||||
(assignment.id not in ^assignment_ids or assignment.helper_id != ^helper_id)
|
||||
)
|
||||
)
|
||||
|
||||
unexpected_activity? =
|
||||
Repo.exists?(from(activity in Activity, where: activity.creator_id in ^user_ids))
|
||||
|
||||
unexpected_proposal? =
|
||||
Repo.exists?(from(proposal in CategoryProposal, where: proposal.proposer_id in ^user_ids))
|
||||
|
||||
unless length(request_ids) <= 1 and length(assignment_ids) <= 1 and
|
||||
not unexpected_request? and not unexpected_assignment? and
|
||||
not unexpected_activity? and not unexpected_proposal? do
|
||||
Mix.raise("staging E2E users own records outside the exact medicine-flow scope")
|
||||
end
|
||||
end
|
||||
|
||||
defp insert_user!(email, display_name, password_hash, now) do
|
||||
%User{}
|
||||
|> User.registration_changeset(%{
|
||||
"email" => email,
|
||||
"display_name" => display_name,
|
||||
"locale" => "en",
|
||||
"terms_accepted" => true
|
||||
})
|
||||
|> Ecto.Changeset.put_change(:hashed_password, password_hash)
|
||||
|> Ecto.Changeset.put_change(:confirmed_at, now)
|
||||
|> Repo.insert!()
|
||||
end
|
||||
|
||||
defp emails(run_id) do
|
||||
%{
|
||||
requester: "wnh-staging-e2e-#{run_id}-requester@example.invalid",
|
||||
helper: "wnh-staging-e2e-#{run_id}-helper@example.invalid"
|
||||
}
|
||||
end
|
||||
|
||||
defp required_env!(name) do
|
||||
case System.get_env(name) do
|
||||
value when is_binary(value) and value != "" -> value
|
||||
_ -> Mix.raise("#{name} is required")
|
||||
end
|
||||
end
|
||||
|
||||
defp uuid?(value) when is_binary(value) do
|
||||
match?({:ok, _}, Ecto.UUID.cast(value))
|
||||
end
|
||||
|
||||
defp uuid?(_value), do: false
|
||||
end
|
||||
160
scripts/staging-e2e-run.sh
Executable file
160
scripts/staging-e2e-run.sh
Executable file
|
|
@ -0,0 +1,160 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$ROOT"
|
||||
|
||||
ENV_FILE="$ROOT/.env"
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "Missing $ENV_FILE." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
: "${POSTGRES_DB:?POSTGRES_DB is missing from .env}"
|
||||
: "${PHX_HOST:?PHX_HOST is missing from .env}"
|
||||
: "${PHX_SCHEME:?PHX_SCHEME is missing from .env}"
|
||||
: "${PHX_URL_PORT:?PHX_URL_PORT is missing from .env}"
|
||||
|
||||
if [[ "$PHX_SCHEME" != https ]]; then
|
||||
echo "Public staging E2E requires PHX_SCHEME=https." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_id="$(date -u +%Y%m%d%H%M%S)-$$"
|
||||
output_dir="$ROOT/output/staging-e2e/$run_id"
|
||||
manifest="$output_dir/fixture.json"
|
||||
base_url="${PHX_SCHEME}://${PHX_HOST}"
|
||||
|
||||
if [[ "$PHX_URL_PORT" != 443 ]]; then
|
||||
base_url="${base_url}:${PHX_URL_PORT}"
|
||||
fi
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
chmod 700 "$ROOT/output" "$ROOT/output/staging-e2e" "$output_dir"
|
||||
|
||||
fixture_password=$(openssl rand -base64 36 | tr -d '\n')
|
||||
prepared=0
|
||||
|
||||
db_container=$(docker compose ps -q db)
|
||||
|
||||
if [[ -z "$db_container" ]]; then
|
||||
echo "The ordinary Compose database container is not running." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
internal_network_id=$(
|
||||
docker inspect "$db_container" |
|
||||
jq -r '.[0].NetworkSettings.Networks | to_entries[] | select(.key | endswith("_internal")) | .value.NetworkID' |
|
||||
head -n 1
|
||||
)
|
||||
|
||||
if [[ -z "$internal_network_id" ]]; then
|
||||
echo "Could not identify the ordinary Compose internal network." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
snapshot_database() {
|
||||
local destination=$1
|
||||
|
||||
docker compose exec -T db sh -c \
|
||||
'psql --no-psqlrc --set ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB"' \
|
||||
>"$destination" <<'SQL'
|
||||
BEGIN READ ONLY;
|
||||
SELECT 'users' AS table_name, count(*) AS row_count FROM users
|
||||
UNION ALL SELECT 'users_tokens', count(*) FROM users_tokens
|
||||
UNION ALL SELECT 'help_requests', count(*) FROM help_requests
|
||||
UNION ALL SELECT 'messages', count(*) FROM messages
|
||||
UNION ALL SELECT 'help_assignments', count(*) FROM help_assignments
|
||||
UNION ALL SELECT 'reports', count(*) FROM reports
|
||||
UNION ALL SELECT 'reviews', count(*) FROM reviews
|
||||
UNION ALL SELECT 'audit_events', count(*) FROM audit_events
|
||||
UNION ALL SELECT 'abuse_signals', count(*) FROM abuse_signals
|
||||
UNION ALL SELECT 'tracking_sessions', count(*) FROM tracking_sessions
|
||||
UNION ALL SELECT 'tracking_positions', count(*) FROM tracking_positions
|
||||
UNION ALL SELECT 'activities', count(*) FROM activities
|
||||
UNION ALL SELECT 'activity_participants', count(*) FROM activity_participants
|
||||
UNION ALL SELECT 'activity_messages', count(*) FROM activity_messages
|
||||
UNION ALL SELECT 'category_proposals', count(*) FROM category_proposals
|
||||
UNION ALL SELECT 'category_votes', count(*) FROM category_votes
|
||||
UNION ALL SELECT 'blocks', count(*) FROM blocks
|
||||
UNION ALL SELECT 'social_identities', count(*) FROM social_identities
|
||||
ORDER BY table_name;
|
||||
COMMIT;
|
||||
SQL
|
||||
}
|
||||
|
||||
run_fixture_tool() {
|
||||
local action=$1
|
||||
|
||||
docker run --rm \
|
||||
--network "$internal_network_id" \
|
||||
--env-file "$ENV_FILE" \
|
||||
--env APP_ROLE=migrate \
|
||||
--env "WNH_STAGING_E2E_EXPECTED_DATABASE=$POSTGRES_DB" \
|
||||
--env WNH_STAGING_E2E_CONFIRM=public-staging-e2e \
|
||||
--env "WNH_STAGING_E2E_RUN_ID=$run_id" \
|
||||
--env "WNH_STAGING_E2E_PASSWORD=$fixture_password" \
|
||||
--env WNH_STAGING_E2E_MANIFEST_PATH=/output/fixture.json \
|
||||
--volume "$output_dir:/output" \
|
||||
who-need-help:staging-e2e-tools \
|
||||
mix wnh.staging_e2e "$action"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local status=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
|
||||
if [[ "$prepared" -eq 1 ]]; then
|
||||
if ! run_fixture_tool cleanup >"$output_dir/fixture-cleanup.log" 2>&1; then
|
||||
echo "Exact staging E2E cleanup failed; inspect $output_dir." >&2
|
||||
status=1
|
||||
else
|
||||
snapshot_database "$output_dir/database-after.txt"
|
||||
|
||||
if ! diff -u \
|
||||
"$output_dir/database-before.txt" \
|
||||
"$output_dir/database-after.txt" \
|
||||
>"$output_dir/database-cleanup.diff"; then
|
||||
echo "Staging E2E cleanup did not restore application table counts." >&2
|
||||
status=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
unset fixture_password
|
||||
exit "$status"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
curl --fail --silent --show-error "$base_url/healthz/ready" >"$output_dir/public-ready.txt"
|
||||
snapshot_database "$output_dir/database-before.txt"
|
||||
|
||||
docker build --target load_tools --tag who-need-help:staging-e2e-tools . \
|
||||
>"$output_dir/tools-build.log"
|
||||
docker build --tag who-need-help-e2e-tests:local e2e \
|
||||
>"$output_dir/browser-build.log"
|
||||
|
||||
run_fixture_tool prepare >"$output_dir/fixture-prepare.log"
|
||||
prepared=1
|
||||
|
||||
docker run --rm \
|
||||
--network host \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--env "BASE_URL=$base_url" \
|
||||
--env MAILPIT_URL=http://127.0.0.1:8027 \
|
||||
--env "E2E_RUN_ID=$run_id" \
|
||||
--env "E2E_FIXTURE_PASSWORD=$fixture_password" \
|
||||
--env HOME=/tmp \
|
||||
--volume "$output_dir:/work/output" \
|
||||
who-need-help-e2e-tests:local \
|
||||
npx playwright test --project=chromium tests/mutual-aid.spec.ts
|
||||
|
||||
echo "Public staging medicine E2E passed: $base_url"
|
||||
echo "Evidence: $output_dir"
|
||||
Loading…
Reference in New Issue
Block a user