Prepare reproducible Play location video fixture
This commit is contained in:
parent
95caa5d38e
commit
16956c62e5
|
|
@ -152,6 +152,7 @@ COPY .dialyzer_ignore.exs .formatter.exs ./
|
||||||
COPY priv priv
|
COPY priv priv
|
||||||
COPY lib lib
|
COPY lib lib
|
||||||
COPY test test
|
COPY test test
|
||||||
|
COPY scripts/production-play-physical-fixture.exs scripts/production-play-physical-fixture.exs
|
||||||
|
|
||||||
RUN mix compile
|
RUN mix compile
|
||||||
|
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -588,6 +588,24 @@ run-scoped loopback SSH tunnel, and executes fixture preparation, verification,
|
||||||
and exact cleanup next to the remote test database. It does not delete
|
and exact cleanup next to the remote test database. It does not delete
|
||||||
unrelated records.
|
unrelated records.
|
||||||
|
|
||||||
|
The exact production foreground-service recording flow has a separate
|
||||||
|
run-scoped operator fixture. It creates one temporary browser account and one
|
||||||
|
matched request for an already confirmed physical-device helper, retains its
|
||||||
|
exact IDs in ignored mode-`0600` state, verifies the active and stopped location
|
||||||
|
states, and performs exact cleanup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run only from /srv/who_need_help-production after reading the Play declaration.
|
||||||
|
./scripts/production-play-physical-fixture.sh \
|
||||||
|
plan HELPER_EMAIL --check-only whoneedhelp.com .env
|
||||||
|
```
|
||||||
|
|
||||||
|
The mutating `prepare`, `verify-active`, `verify-stopped`, and `cleanup`
|
||||||
|
commands are documented in
|
||||||
|
`android/play-store/location-and-fgs-declaration.md`. The wrapper refuses the
|
||||||
|
independent hackathon test checkout and any unexpected production root, origin,
|
||||||
|
Compose project, image, health state, or database.
|
||||||
|
|
||||||
## First administrator
|
## First administrator
|
||||||
|
|
||||||
Register and confirm the first account, then explicitly bootstrap it:
|
Register and confirm the first account, then explicitly bootstrap it:
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,59 @@ permission or disclosure screen.
|
||||||
Do not use a real home address, real medical information, chat text, email,
|
Do not use a real home address, real medical information, chat text, email,
|
||||||
handover code, access token, or another person's location in the recording.
|
handover code, access token, or another person's location in the recording.
|
||||||
|
|
||||||
|
### Reproducible production fixture
|
||||||
|
|
||||||
|
The production operator script creates one run-scoped requester with a temporary
|
||||||
|
password, one synthetic matched medicine request, and one accepted assignment
|
||||||
|
for an existing confirmed helper. It refuses any root, Compose project, public
|
||||||
|
origin, image, health state, or database other than the explicitly verified
|
||||||
|
production values. It stores the exact IDs and temporary credentials only in
|
||||||
|
ignored mode-`0600` runtime files so cleanup can be resumed after a container
|
||||||
|
restart.
|
||||||
|
|
||||||
|
Run the read-only plan first from `/srv/who_need_help-production`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/production-play-physical-fixture.sh \
|
||||||
|
plan HELPER_EMAIL --check-only whoneedhelp.com .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Prepare the recording fixture only when the helper is signed into the exact
|
||||||
|
Play-delivered build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/production-play-physical-fixture.sh \
|
||||||
|
prepare HELPER_EMAIL --confirm whoneedhelp.com .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the printed temporary requester email and password in the recipient browser.
|
||||||
|
Do not copy those credentials into documentation, Play Console, chat, email, or
|
||||||
|
the recording. After location sharing starts, verify the server-side active
|
||||||
|
state; after using the notification Stop action, verify deletion:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/production-play-physical-fixture.sh \
|
||||||
|
verify-active --confirm whoneedhelp.com .env
|
||||||
|
|
||||||
|
./scripts/production-play-physical-fixture.sh \
|
||||||
|
verify-stopped --confirm whoneedhelp.com .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Always remove the fixture immediately after the recording, including after an
|
||||||
|
aborted take:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/production-play-physical-fixture.sh \
|
||||||
|
cleanup --confirm whoneedhelp.com .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Cleanup stops an exact still-active fixture session before deleting its current
|
||||||
|
raw position, tracking session, messages, notifications/jobs, assignment,
|
||||||
|
request, temporary tokens/rate-limit buckets, and requester. It then verifies
|
||||||
|
that the three primary fixture records were deleted and removes the local
|
||||||
|
runtime state. Never delete the runtime manifest manually while its fixture may
|
||||||
|
still exist.
|
||||||
|
|
||||||
## Pre-submission evidence
|
## Pre-submission evidence
|
||||||
|
|
||||||
- Run `./scripts/android-play-policy-check.sh`.
|
- Run `./scripts/android-play-policy-check.sh`.
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ defmodule WhoNeedHelp.ProductionPlayPhysicalFixture do
|
||||||
|
|
||||||
now = DateTime.utc_now(:second)
|
now = DateTime.utc_now(:second)
|
||||||
category = Repo.get_by!(Category, slug: "medicine-pickup", active: true)
|
category = Repo.get_by!(Category, slug: "medicine-pickup", active: true)
|
||||||
|
requester_password = temporary_password()
|
||||||
|
|
||||||
{:ok, fixture} =
|
{:ok, fixture} =
|
||||||
Repo.transaction(fn ->
|
Repo.transaction(fn ->
|
||||||
|
|
@ -82,6 +83,8 @@ defmodule WhoNeedHelp.ProductionPlayPhysicalFixture do
|
||||||
})
|
})
|
||||||
|> Ecto.Changeset.put_change(:confirmed_at, now)
|
|> Ecto.Changeset.put_change(:confirmed_at, now)
|
||||||
|> Repo.insert!()
|
|> Repo.insert!()
|
||||||
|
|> User.password_changeset(%{"password" => requester_password})
|
||||||
|
|> Repo.update!()
|
||||||
|
|
||||||
request =
|
request =
|
||||||
%HelpRequest{requester_id: requester.id}
|
%HelpRequest{requester_id: requester.id}
|
||||||
|
|
@ -117,10 +120,14 @@ defmodule WhoNeedHelp.ProductionPlayPhysicalFixture do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
manifest = %{
|
manifest = %{
|
||||||
"schema_version" => 1,
|
"schema_version" => 2,
|
||||||
"run_id" => context.run_id,
|
"run_id" => context.run_id,
|
||||||
"database" => context.database,
|
"database" => context.database,
|
||||||
"requester" => %{"id" => fixture.requester.id, "email" => fixture.requester.email},
|
"requester" => %{
|
||||||
|
"id" => fixture.requester.id,
|
||||||
|
"email" => fixture.requester.email,
|
||||||
|
"password" => requester_password
|
||||||
|
},
|
||||||
"helper" => %{"id" => fixture.helper.id, "email" => fixture.helper.email},
|
"helper" => %{"id" => fixture.helper.id, "email" => fixture.helper.email},
|
||||||
"request" => %{
|
"request" => %{
|
||||||
"id" => fixture.request.id,
|
"id" => fixture.request.id,
|
||||||
|
|
@ -133,6 +140,9 @@ defmodule WhoNeedHelp.ProductionPlayPhysicalFixture do
|
||||||
File.chmod!(context.manifest_path, 0o600)
|
File.chmod!(context.manifest_path, 0o600)
|
||||||
IO.puts("fixture_prepared=true")
|
IO.puts("fixture_prepared=true")
|
||||||
IO.puts("request_path=#{manifest["request"]["path"]}")
|
IO.puts("request_path=#{manifest["request"]["path"]}")
|
||||||
|
IO.puts("requester_email=#{fixture.requester.email}")
|
||||||
|
IO.puts("requester_password=#{requester_password}")
|
||||||
|
IO.puts("credentials_are_temporary=true")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp verify_active(context) do
|
defp verify_active(context) do
|
||||||
|
|
@ -254,10 +264,11 @@ defmodule WhoNeedHelp.ProductionPlayPhysicalFixture do
|
||||||
manifest = context.manifest_path |> File.read!() |> Jason.decode!()
|
manifest = context.manifest_path |> File.read!() |> Jason.decode!()
|
||||||
|
|
||||||
valid? =
|
valid? =
|
||||||
manifest["schema_version"] == 1 and manifest["run_id"] == context.run_id and
|
manifest["schema_version"] == 2 and manifest["run_id"] == context.run_id and
|
||||||
manifest["database"] == context.database and
|
manifest["database"] == context.database and
|
||||||
manifest["requester"]["email"] == context.requester_email and
|
manifest["requester"]["email"] == context.requester_email and
|
||||||
manifest["helper"]["email"] == context.helper_email and
|
manifest["helper"]["email"] == context.helper_email and
|
||||||
|
temporary_password?(manifest["requester"]["password"]) and
|
||||||
uuid?(manifest["requester"]["id"]) and uuid?(manifest["helper"]["id"]) and
|
uuid?(manifest["requester"]["id"]) and uuid?(manifest["helper"]["id"]) and
|
||||||
uuid?(manifest["request"]["id"]) and uuid?(manifest["assignment"]["id"]) and
|
uuid?(manifest["request"]["id"]) and uuid?(manifest["assignment"]["id"]) and
|
||||||
manifest["request"]["path"] == "/requests/#{manifest["request"]["id"]}"
|
manifest["request"]["path"] == "/requests/#{manifest["request"]["id"]}"
|
||||||
|
|
@ -276,6 +287,7 @@ defmodule WhoNeedHelp.ProductionPlayPhysicalFixture do
|
||||||
match?(%User{}, requester) and match?(%User{}, helper) and
|
match?(%User{}, requester) and match?(%User{}, helper) and
|
||||||
match?(%HelpRequest{}, request) and match?(%Assignment{}, assignment) and
|
match?(%HelpRequest{}, request) and match?(%Assignment{}, assignment) and
|
||||||
requester.email == manifest["requester"]["email"] and
|
requester.email == manifest["requester"]["email"] and
|
||||||
|
User.valid_password?(requester, manifest["requester"]["password"]) and
|
||||||
helper.email == manifest["helper"]["email"] and request.requester_id == requester.id and
|
helper.email == manifest["helper"]["email"] and request.requester_id == requester.id and
|
||||||
assignment.request_id == request.id and assignment.helper_id == helper.id and
|
assignment.request_id == request.id and assignment.helper_id == helper.id and
|
||||||
assignment.status in [:accepted, :in_progress] and assignment.active
|
assignment.status in [:accepted, :in_progress] and assignment.active
|
||||||
|
|
@ -351,6 +363,17 @@ defmodule WhoNeedHelp.ProductionPlayPhysicalFixture do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp temporary_password do
|
||||||
|
32
|
||||||
|
|> :crypto.strong_rand_bytes()
|
||||||
|
|> Base.url_encode64(padding: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp temporary_password?(password) when is_binary(password),
|
||||||
|
do: byte_size(password) >= 32 and byte_size(password) <= 72
|
||||||
|
|
||||||
|
defp temporary_password?(_password), do: false
|
||||||
|
|
||||||
defp uuid?(value) when is_binary(value), do: match?({:ok, _}, Ecto.UUID.cast(value))
|
defp uuid?(value) when is_binary(value), do: match?({:ok, _}, Ecto.UUID.cast(value))
|
||||||
defp uuid?(_), do: false
|
defp uuid?(_), do: false
|
||||||
end
|
end
|
||||||
|
|
|
||||||
301
scripts/production-play-physical-fixture.sh
Executable file
301
scripts/production-play-physical-fixture.sh
Executable file
|
|
@ -0,0 +1,301 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
EXPECTED_ROOT=/srv/who_need_help-production
|
||||||
|
EXPECTED_PROJECT=who_need_help_production
|
||||||
|
EXPECTED_ORIGIN=https://whoneedhelp.com
|
||||||
|
STATE_FILE="$ROOT/output/runtime/production-play-physical.env"
|
||||||
|
HOST_MANIFEST="$ROOT/output/runtime/production-play-physical-manifest.json"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/production-play-physical-fixture.sh plan HELPER_EMAIL --check-only whoneedhelp.com ENV_FILE
|
||||||
|
./scripts/production-play-physical-fixture.sh prepare HELPER_EMAIL --confirm whoneedhelp.com ENV_FILE
|
||||||
|
./scripts/production-play-physical-fixture.sh verify-active --confirm whoneedhelp.com ENV_FILE
|
||||||
|
./scripts/production-play-physical-fixture.sh verify-stopped --confirm whoneedhelp.com ENV_FILE
|
||||||
|
./scripts/production-play-physical-fixture.sh cleanup --confirm whoneedhelp.com ENV_FILE
|
||||||
|
|
||||||
|
prepare creates one run-scoped requester, request and accepted assignment. The
|
||||||
|
other actions use the ignored mode-0600 state created by prepare. cleanup is
|
||||||
|
the only supported way to remove the exact fixture after recording.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
read_env() {
|
||||||
|
local file=$1
|
||||||
|
local key=$2
|
||||||
|
|
||||||
|
awk -F= -v key="$key" '
|
||||||
|
$1 == key {
|
||||||
|
value = substr($0, index($0, "=") + 1)
|
||||||
|
sub(/\r$/, "", value)
|
||||||
|
|
||||||
|
if ((value ~ /^".*"$/) || (value ~ /^\047.*\047$/)) {
|
||||||
|
value = substr(value, 2, length(value) - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
if (count == 1) print value
|
||||||
|
else exit 1
|
||||||
|
}
|
||||||
|
' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
read_state() {
|
||||||
|
local key=$1
|
||||||
|
read_env "$STATE_FILE" "$key"
|
||||||
|
}
|
||||||
|
|
||||||
|
encode() {
|
||||||
|
printf %s "$1" | base64 | tr -d '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_into_container() {
|
||||||
|
local source=$1
|
||||||
|
local destination=$2
|
||||||
|
|
||||||
|
docker exec -i "$CONTAINER" sh -c \
|
||||||
|
'umask 077; cat >"$1"' sh "$destination" <"$source"
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_from_container() {
|
||||||
|
local source=$1
|
||||||
|
local destination=$2
|
||||||
|
|
||||||
|
docker exec "$CONTAINER" cat "$source" >"$destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $# -lt 4 || $# -gt 5 ]]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
ACTION=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
plan | prepare)
|
||||||
|
[[ $# -eq 4 ]] || usage
|
||||||
|
HELPER_EMAIL=${1,,}
|
||||||
|
CONFIRMATION=$2
|
||||||
|
HOST=$3
|
||||||
|
ENV_FILE=$4
|
||||||
|
;;
|
||||||
|
verify-active | verify-stopped | cleanup)
|
||||||
|
[[ $# -eq 3 ]] || usage
|
||||||
|
HELPER_EMAIL=
|
||||||
|
CONFIRMATION=$1
|
||||||
|
HOST=$2
|
||||||
|
ENV_FILE=$3
|
||||||
|
;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "$ACTION" == plan ]]; then
|
||||||
|
[[ "$CONFIRMATION" == --check-only ]] || usage
|
||||||
|
else
|
||||||
|
[[ "$CONFIRMATION" == --confirm ]] || usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$HOST" == whoneedhelp.com ]] || usage
|
||||||
|
|
||||||
|
if [[ "$(realpath --canonicalize-existing "$ROOT")" != "$EXPECTED_ROOT" ]]; then
|
||||||
|
echo "Physical Play fixtures may only run from $EXPECTED_ROOT." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ENV_FILE" != /* ]]; then
|
||||||
|
ENV_FILE="$ROOT/$ENV_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "Environment file does not exist: $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(read_env "$ENV_FILE" DEPLOYMENT_ENV)" != production ]]; then
|
||||||
|
echo "Physical Play fixtures require DEPLOYMENT_ENV=production." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(read_env "$ENV_FILE" DEPLOYMENT_TARGET)" != compose ]]; then
|
||||||
|
echo "Physical Play fixtures require DEPLOYMENT_TARGET=compose." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT=$(read_env "$ENV_FILE" COMPOSE_PROJECT_NAME)
|
||||||
|
if [[ "$PROJECT" != "$EXPECTED_PROJECT" ]]; then
|
||||||
|
echo "Unexpected production Compose project: $PROJECT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(read_env "$ENV_FILE" WNH_BASE_URL)" != "$EXPECTED_ORIGIN" ]]; then
|
||||||
|
echo "Production origin must be $EXPECTED_ORIGIN." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXPECTED_DATABASE=$(read_env "$ENV_FILE" POSTGRES_DB)
|
||||||
|
if [[ -z "$EXPECTED_DATABASE" ]]; then
|
||||||
|
echo "POSTGRES_DB must identify the expected production database." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONTAINER=$("$ROOT/scripts/compose.sh" "$ENV_FILE" ps -q app | head -n 1)
|
||||||
|
if [[ -z "$CONTAINER" ]]; then
|
||||||
|
CONTAINER=$("$ROOT/scripts/compose.sh" "$ENV_FILE" ps -q web | head -n 1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$CONTAINER" ]]; then
|
||||||
|
echo "No running production app or web container was found for $PROJECT." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXPECTED_IMAGE=$(read_env "$ENV_FILE" APP_IMAGE)
|
||||||
|
OBSERVED_IMAGE=$(docker inspect --format '{{.Config.Image}}' "$CONTAINER")
|
||||||
|
CONTAINER_STATE=$(docker inspect --format '{{.State.Status}}' "$CONTAINER")
|
||||||
|
CONTAINER_HEALTH=$(docker inspect \
|
||||||
|
--format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CONTAINER")
|
||||||
|
|
||||||
|
if [[ "$OBSERVED_IMAGE" != "$EXPECTED_IMAGE" ]]; then
|
||||||
|
echo "Running container image does not match APP_IMAGE." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$CONTAINER_STATE" != running || "$CONTAINER_HEALTH" != healthy ]]; then
|
||||||
|
echo "Production application container is not running and healthy." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ACTUAL_DATABASE=$(docker exec "$CONTAINER" /app/bin/who_need_help rpc \
|
||||||
|
'%Postgrex.Result{rows: [[database]]} = WhoNeedHelp.Repo.query!("SELECT current_database()", [], log: false); IO.puts(database)' |
|
||||||
|
tail -n 1)
|
||||||
|
|
||||||
|
if [[ "$ACTUAL_DATABASE" != "$EXPECTED_DATABASE" ]]; then
|
||||||
|
echo "Database identity mismatch: expected $EXPECTED_DATABASE, observed $ACTUAL_DATABASE." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ACTION" == plan ]]; then
|
||||||
|
if [[ ! "$HELPER_EMAIL" =~ ^[^[:space:]@]+@[^[:space:]@]+$ ]]; then
|
||||||
|
echo "HELPER_EMAIL is invalid." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e "$STATE_FILE" || -e "$HOST_MANIFEST" ]]; then
|
||||||
|
echo "A physical Play fixture state already exists; inspect and clean it first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HELPER=$(encode "$HELPER_EMAIL")
|
||||||
|
docker exec "$CONTAINER" /app/bin/who_need_help rpc \
|
||||||
|
"require Ecto.Query; email = Base.decode64!(\"$HELPER\"); user = WhoNeedHelp.Repo.get_by(WhoNeedHelp.Accounts.User, email: email); unless match?(%WhoNeedHelp.Accounts.User{confirmed_at: %DateTime{}, moderation_status: :active}, user), do: raise(\"helper is missing, unconfirmed, or inactive\"); active_query = Ecto.Query.from(s in WhoNeedHelp.Tracking.TrackingSession, where: s.user_id == ^user.id and s.active); if WhoNeedHelp.Repo.exists?(active_query), do: raise(\"helper already has an active tracking session\"); IO.puts(\"helper_ready=true\")"
|
||||||
|
|
||||||
|
printf 'scope=one temporary requester, one matched request, one accepted assignment\n'
|
||||||
|
printf 'database=%s\nimage=%s\ncontainer=%s\n' \
|
||||||
|
"$ACTUAL_DATABASE" "$OBSERVED_IMAGE" "$CONTAINER"
|
||||||
|
printf 'cleanup=exact run-scoped IDs retained in mode-0600 state\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$ROOT/output/runtime"
|
||||||
|
chmod 700 "$ROOT/output/runtime"
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
if [[ "$ACTION" == prepare ]]; then
|
||||||
|
if [[ ! "$HELPER_EMAIL" =~ ^[^[:space:]@]+@[^[:space:]@]+$ ]]; then
|
||||||
|
echo "HELPER_EMAIL is invalid." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e "$STATE_FILE" || -e "$HOST_MANIFEST" ]]; then
|
||||||
|
echo "A physical Play fixture state already exists; cleanup is required first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN_ID="$(date -u +%Y%m%d%H%M%S)-$(tr -d - </proc/sys/kernel/random/uuid | cut -c1-12)"
|
||||||
|
CONTAINER_MANIFEST="/tmp/wnh-play-physical-$RUN_ID.json"
|
||||||
|
CONTAINER_SCRIPT="/tmp/wnh-play-physical-$RUN_ID.exs"
|
||||||
|
|
||||||
|
cat >"$STATE_FILE" <<EOF
|
||||||
|
schema_version=1
|
||||||
|
run_id=$RUN_ID
|
||||||
|
expected_database=$EXPECTED_DATABASE
|
||||||
|
helper_email=$HELPER_EMAIL
|
||||||
|
container_manifest=$CONTAINER_MANIFEST
|
||||||
|
container_script=$CONTAINER_SCRIPT
|
||||||
|
image=$OBSERVED_IMAGE
|
||||||
|
EOF
|
||||||
|
chmod 600 "$STATE_FILE"
|
||||||
|
else
|
||||||
|
if [[ ! -f "$STATE_FILE" ]]; then
|
||||||
|
echo "No physical Play fixture state exists." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(read_state schema_version)" != 1 ]]; then
|
||||||
|
echo "Unsupported physical Play fixture state version." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN_ID=$(read_state run_id)
|
||||||
|
EXPECTED_DATABASE_FROM_STATE=$(read_state expected_database)
|
||||||
|
HELPER_EMAIL=$(read_state helper_email)
|
||||||
|
CONTAINER_MANIFEST=$(read_state container_manifest)
|
||||||
|
CONTAINER_SCRIPT=$(read_state container_script)
|
||||||
|
STATE_IMAGE=$(read_state image)
|
||||||
|
|
||||||
|
if [[ "$EXPECTED_DATABASE_FROM_STATE" != "$EXPECTED_DATABASE" ||
|
||||||
|
"$STATE_IMAGE" != "$OBSERVED_IMAGE" ]]; then
|
||||||
|
echo "Current production database or image does not match the recorded fixture state." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! copy_into_container \
|
||||||
|
"$ROOT/scripts/production-play-physical-fixture.exs" "$CONTAINER_SCRIPT"; then
|
||||||
|
if [[ "$ACTION" == prepare ]]; then
|
||||||
|
rm -f "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Could not copy the operator script into the container tmpfs." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ACTION" != prepare ]]; then
|
||||||
|
if [[ ! -f "$HOST_MANIFEST" ]]; then
|
||||||
|
echo "The mode-0600 host manifest is missing; refusing an unverifiable action." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
copy_into_container "$HOST_MANIFEST" "$CONTAINER_MANIFEST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN=$(encode "$RUN_ID")
|
||||||
|
DATABASE=$(encode "$EXPECTED_DATABASE")
|
||||||
|
HELPER=$(encode "$HELPER_EMAIL")
|
||||||
|
MANIFEST=$(encode "$CONTAINER_MANIFEST")
|
||||||
|
|
||||||
|
EXPRESSION="Code.require_file(\"$CONTAINER_SCRIPT\"); WhoNeedHelp.ProductionPlayPhysicalFixture.run(\"$ACTION\", %{run_id: Base.decode64!(\"$RUN\"), expected_database: Base.decode64!(\"$DATABASE\"), helper_email: Base.decode64!(\"$HELPER\"), manifest_path: Base.decode64!(\"$MANIFEST\")})"
|
||||||
|
|
||||||
|
if ! docker exec "$CONTAINER" /app/bin/who_need_help rpc "$EXPRESSION"; then
|
||||||
|
echo "Fixture action failed. State was retained for inspection and exact cleanup." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ACTION" == prepare ]]; then
|
||||||
|
copy_from_container "$CONTAINER_MANIFEST" "$HOST_MANIFEST.tmp"
|
||||||
|
chmod 600 "$HOST_MANIFEST.tmp"
|
||||||
|
mv "$HOST_MANIFEST.tmp" "$HOST_MANIFEST"
|
||||||
|
echo "host_state=$STATE_FILE"
|
||||||
|
echo "host_manifest=$HOST_MANIFEST"
|
||||||
|
echo "Record the video now; do not leave the fixture active after recording."
|
||||||
|
elif [[ "$ACTION" == cleanup ]]; then
|
||||||
|
docker exec "$CONTAINER" rm -f "$CONTAINER_SCRIPT" "$CONTAINER_MANIFEST"
|
||||||
|
rm -f "$HOST_MANIFEST" "$STATE_FILE"
|
||||||
|
echo "host_fixture_state_removed=true"
|
||||||
|
fi
|
||||||
55
test/who_need_help/production_play_physical_fixture_test.exs
Normal file
55
test/who_need_help/production_play_physical_fixture_test.exs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
defmodule WhoNeedHelp.ProductionPlayPhysicalFixtureTest do
|
||||||
|
use WhoNeedHelp.DataCase, async: false
|
||||||
|
|
||||||
|
import ExUnit.CaptureIO
|
||||||
|
import WhoNeedHelp.AccountsFixtures
|
||||||
|
|
||||||
|
alias WhoNeedHelp.Accounts.User
|
||||||
|
alias WhoNeedHelp.Catalog
|
||||||
|
alias WhoNeedHelp.Repo
|
||||||
|
|
||||||
|
Code.require_file("scripts/production-play-physical-fixture.exs")
|
||||||
|
|
||||||
|
test "prepares temporary browser credentials and removes their complete fixture" do
|
||||||
|
Catalog.seed_defaults()
|
||||||
|
helper = user_fixture(display_name: "Physical Play helper")
|
||||||
|
run_id = "fixture-#{System.unique_integer([:positive])}"
|
||||||
|
manifest_path = "/tmp/wnh-play-physical-#{run_id}.json"
|
||||||
|
|
||||||
|
on_exit(fn -> File.rm(manifest_path) end)
|
||||||
|
|
||||||
|
%Postgrex.Result{rows: [[database]]} =
|
||||||
|
Repo.query!("SELECT current_database()", [], log: false)
|
||||||
|
|
||||||
|
options = %{
|
||||||
|
run_id: run_id,
|
||||||
|
expected_database: database,
|
||||||
|
helper_email: helper.email,
|
||||||
|
manifest_path: manifest_path
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_output =
|
||||||
|
capture_io(fn ->
|
||||||
|
WhoNeedHelp.ProductionPlayPhysicalFixture.run("prepare", options)
|
||||||
|
end)
|
||||||
|
|
||||||
|
manifest = manifest_path |> File.read!() |> Jason.decode!()
|
||||||
|
requester = Repo.get!(User, manifest["requester"]["id"])
|
||||||
|
|
||||||
|
assert prepare_output =~ "fixture_prepared=true"
|
||||||
|
assert prepare_output =~ "credentials_are_temporary=true"
|
||||||
|
assert manifest["schema_version"] == 2
|
||||||
|
assert manifest["request"]["path"] =~ "/requests/"
|
||||||
|
assert byte_size(manifest["requester"]["password"]) >= 32
|
||||||
|
assert User.valid_password?(requester, manifest["requester"]["password"])
|
||||||
|
|
||||||
|
cleanup_output =
|
||||||
|
capture_io(fn ->
|
||||||
|
WhoNeedHelp.ProductionPlayPhysicalFixture.run("cleanup", options)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert cleanup_output =~ "fixture_cleanup_verified=true"
|
||||||
|
refute File.exists?(manifest_path)
|
||||||
|
refute Repo.get(User, requester.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user