Files
simplex-chat/scripts/ci/ios-sim/backend.sh
T

478 lines
18 KiB
Bash
Executable File

#!/bin/bash
# Test backend on the app runner: a chat relay and a directory service on the preset servers, with groups and
# channels registered and approved by a CLI peer, and a search that must find them; and for SimpleX names,
# a local SMP server with a name resolver, names claimed by channels and by contacts that accept connections.
# Usage:
# scripts/ci/ios-sim/backend.sh start <backend zip> <work dir> writes the directory's short address to <work dir>/directory-link,
# the names server's address to <work dir>/names-server
# and what was seeded to <work dir>/seed.md
# scripts/ci/ios-sim/backend.sh stop <work dir> stops everything it started, prints the logs
# scripts/ci/ios-sim/backend.sh directory-stop <work dir> stops the directory only
# scripts/ci/ios-sim/backend.sh directory-start <work dir> starts it again
# scripts/ci/ios-sim/backend.sh listings <work dir> prints the directory's listed groups and channels
# Everything it starts keeps running between start and stop.
set -euo pipefail
directory_name="SimpleX Directory"
admin=ci_admin
search_text="ci seed"
avatar_search_text="ci avatar"
long_name=ci_long_name_entry_for_layout_truncation_checks
names_resolver_port=18080
# Runs one chat command in a CLI client that stays online for <secs> after it; the output is in $work/out.
chat() {
local db=$1 secs=$2 cmd=$3
shift 3
local status=0
"$bin/simplex-chat" -d "$work/$db" -e "$cmd" -t "$secs" --execute-log all "$@" < /dev/null > "$work/out" 2>&1 || status=$?
cat "$work/out" >> "$work/$db-cli.log"
echo "::group::$db: $(cut -c1-200 <<< "$cmd")"
cat "$work/out"
echo "::endgroup::"
return "$status"
}
admin() {
chat "$admin" "$1" "$2" --user-display-name "$admin"
}
# A command that resolves a SimpleX name: given -s, the client uses that server alone, with every role, names included.
# -s also saves it among the user's servers, and later runs would put queues on it that the directory can't reach
# (it routes to unknown servers through a preset proxy), so it is then kept for names only.
named() {
local db=$1 user=$2 secs=$3 cmd=$4
local status=0
chat "$db" "$secs" "$cmd" --user-display-name "$user" -s "$names_server" || status=$?
db_query "$db" "UPDATE protocol_servers SET role_storage = 0, role_proxy = 0, role_names = 1 WHERE host = 'localhost'"
return "$status"
}
expect_out() {
if ! grep -qE "$1" "$work/out"; then
echo "error: \"$1\" not in the output"
return 1
fi
}
expect_no_error() {
if grep -qiE "error" "$work/out"; then
echo "error: the command failed"
return 1
fi
}
# Each run keeps the client online for a few seconds, which also lets the handshakes it waits for progress.
admin_until() {
local pattern=$1 secs=$2 cmd=$3
local deadline=$((SECONDS + secs))
until admin 5 "$cmd" && grep -qE "$pattern" "$work/out"; do
if [ "$SECONDS" -ge "$deadline" ]; then
echo "error: \"$pattern\" not shown by \"$cmd\" after $secs s"
return 1
fi
done
}
db_query() {
sqlite3 "$work/$1_chat.db" "$2"
}
listings_query="SELECT gp.display_name AS name, gp.short_descr AS description, g.summary_current_members_count AS members, length(gp.image) AS image_characters
FROM sx_directory_group_regs r
JOIN groups g ON g.group_id = r.group_id
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
WHERE r.group_reg_status = 'active'
ORDER BY g.summary_current_members_count DESC, r.group_id"
start_relay() {
nohup "$bin/simplex-chat" -d "$work/relay" --relay --headless --user-display-name ci_relay < /dev/null > "$work/relay.log" 2>&1 &
echo $! > "$work/relay.pid"
local deadline=$((SECONDS + 120))
until grep -qE '^https://[^ ]+/r#' "$work/relay.log"; do
if ! kill -0 "$(cat "$work/relay.pid")" 2> /dev/null || [ "$SECONDS" -ge "$deadline" ]; then
echo "error: the relay did not create its address"
cat "$work/relay.log"
return 1
fi
sleep 1
done
relay_link=$(grep -m 1 -oE '^https://[^ ]+/r#[^ ]+' "$work/relay.log")
echo "relay address: $relay_link"
}
# The address is created here, like the directory tests do, with auto-accept on before the service subscribes to it.
create_directory() {
chat directory 1 "/ad pq_ratchet=on" --create-bot-display-name "$directory_name"
directory_link=$(new_address)
chat directory 1 "/auto_accept on"
expect_out "^auto_accept on"
echo "$directory_link" > "$work/directory-link"
echo "directory address: $directory_link"
}
start_directory() {
# -y: the service adds its own tables to the database the CLI created, and would otherwise ask on stdin
nohup "$bin/simplex-directory-service" -d "$work/directory" -y --super-users "$1" < /dev/null >> "$work/directory.log" 2>&1 &
echo $! > "$work/directory.pid"
echo "$1" > "$work/super-user"
sleep 5
if ! kill -0 "$(cat "$work/directory.pid")" 2> /dev/null; then
echo "error: the directory service exited"
cat "$work/directory.log"
return 1
fi
}
stop_directory() {
local pid deadline
pid=$(cat "$work/directory.pid")
kill -INT "$pid" 2> /dev/null || true
deadline=$((SECONDS + 30))
while kill -0 "$pid" 2> /dev/null; do
if [ "$SECONDS" -ge "$deadline" ]; then
kill -KILL "$pid" 2> /dev/null || true
fi
sleep 1
done
}
# A super user is known by contact ID, which exists only after the admin connects, so the service starts
# with nobody and is restarted with the admin.
connect_admin() {
admin 10 "/c $directory_link"
expect_out "connection request sent!"
admin_until "Welcome to $directory_name" 180 "/tail @'$directory_name' 10"
local id
id=$(db_query directory "SELECT contact_id FROM contacts WHERE local_display_name = '$admin'")
stop_directory
start_directory "$id:$admin"
echo "directory restarted with super user $id:$admin"
}
# The directory sends each registration to super users as a message with the exact /approve command,
# and tells the owner "The group ID N (name) is approved".
approve() {
local name=$1
admin_until "/approve [0-9]+:$name [0-9]+" 300 "/tail @'$directory_name' 20"
local cmd
cmd=$(grep -oE "/approve [0-9]+:$name [0-9]+" "$work/out" | tail -n 1)
admin 5 "@'$directory_name' $cmd"
admin_until "\($name\) is approved" 120 "/tail @'$directory_name' 20"
}
# The profile the app sends to create a group or a channel (AddGroupView, AddChannelView), with an optional image file.
group_profile() {
local name=$1 descr=$2 prefs=$3 image_file=${4:-}
if [ -n "$image_file" ]; then
jq -cn --arg n "$name" --arg d "$descr" --argjson p "$prefs" --rawfile i "$image_file" '{displayName: $n, fullName: "", shortDescr: $d, image: $i, groupPreferences: $p}'
else
jq -cn --arg n "$name" --arg d "$descr" --argjson p "$prefs" '{displayName: $n, fullName: "", shortDescr: $d, groupPreferences: $p}'
fi
}
seed_group() {
local name=$1 descr=$2 image_file=${3:-}
admin 1 "/_group 1 $(group_profile "$name" "$descr" '{"history":{"enable":"on"}}' "$image_file")"
expect_out "group #$name .*is created"
admin 5 "/a $name '$directory_name' admin"
expect_out "invitation to join the group #$name sent to"
approve "$name"
}
setup_relay() {
admin 1 "/relays name=ci_relay $relay_link"
expect_out "^ok$"
relay_id=$(db_query "$admin" "SELECT chat_relay_id FROM chat_relays WHERE display_name = 'ci_relay' AND deleted = 0")
}
create_channel() {
local name=$1 descr=$2 image_file=${3:-}
admin 10 "/_public group 1 $relay_id $(group_profile "$name" "$descr" '{"history":{"enable":"on"},"support":{"enable":"off"}}' "$image_file")"
expect_out "group #$name .*is created"
local group_id
group_id=$(db_query "$admin" "SELECT group_id FROM groups WHERE local_display_name = '$name'")
admin_until "relay id [0-9]+: active" 180 "/_get relays #$group_id"
}
list_channel() {
local name=$1
admin 5 "/share chat #$name @'$directory_name'"
expect_out "link to join channel #$name"
approve "$name"
}
seed_channel() {
create_channel "$@"
list_channel "$1"
}
channel_link() {
admin 1 "/show link #$1" >&2
grep -m 1 -oE 'https://[^ ]+/c#[^ ]+' "$work/out"
}
# The short address that /ad printed.
new_address() {
local address
address=$(awk '/Your new chat address is created!/ {getline; getline; print; exit}' "$work/out")
case "$address" in
https://*/a#*) echo "$address" ;;
*)
echo "error: no short address in the output" >&2
return 1
;;
esac
}
# Avatars at the size the app makes them (avatar.swift), with different amounts of detail;
# and small ones for ci_group, ci_channel and ci_cards, so that several fit in one size-limited search response.
make_avatars() {
xcrun swiftc -O -o "$work/avatar" "$(dirname "$0")/avatar.swift"
local i
for i in 1 2 3 4 5 6; do
"$work/avatar" "$i" "0.$((i + 2))" "$work/avatar-$i.txt"
done
for i in 7 8 9; do
"$work/avatar" "$i" 0 "$work/avatar-$i.txt" 64
done
}
seed_avatars() {
local i
for i in 1 2 3; do
seed_group "ci_avatar_g$i" "CI avatar group $i" "$work/avatar-$i.txt"
seed_channel "ci_avatar_c$i" "CI avatar channel $i" "$work/avatar-$((i + 3)).txt"
done
}
# A local SMP server whose [NAMES] section points at names-resolver.py, which answers from names.json.
start_names() {
echo "{}" > "$work/names.json"
nohup python3 "$(dirname "$0")/names-resolver.py" "$names_resolver_port" "$work/names.json" < /dev/null > "$work/names-resolver.log" 2>&1 &
echo $! > "$work/names-resolver.pid"
export SMP_SERVER_CFG_PATH=$work/smp-server/etc SMP_SERVER_LOG_PATH=$work/smp-server/log
"$bin/smp-server" init -y --disable-store-log --no-password --fqdn localhost --disable-web > "$work/out" 2>&1 || {
cat "$work/out"
return 1
}
cat "$work/out"
names_server=$(grep -m 1 -oE 'smp://[^ ]+@localhost' "$work/out")
sed -i '' \
-e 's/^enable: off$/enable: on/' \
-e "s|^# resolver_endpoint: http://127.0.0.1:8000\$|resolver_endpoint: http://127.0.0.1:$names_resolver_port|" \
-e 's/^port = 5223,443$/port = 5223/' \
"$SMP_SERVER_CFG_PATH/smp-server.ini"
grep -E '^(enable: on|resolver_endpoint|port = )' "$SMP_SERVER_CFG_PATH/smp-server.ini"
nohup "$bin/smp-server" start < /dev/null > "$work/smp-server.log" 2>&1 &
echo $! > "$work/smp-server.pid"
local deadline=$((SECONDS + 60))
until nc -z 127.0.0.1 5223 2> /dev/null; do
if ! kill -0 "$(cat "$work/smp-server.pid")" 2> /dev/null || [ "$SECONDS" -ge "$deadline" ]; then
echo "error: the names server did not start"
cat "$work/smp-server.log" "$work/names-resolver.log"
return 1
fi
sleep 1
done
echo "$names_server" > "$work/names-server"
echo "names server: $names_server"
}
register_name() {
local name=$1 contact=$2 channel=$3
jq --arg n "$name" --arg ct "$contact" --arg ch "$channel" \
'.[$n] = {simplexContact: ([$ct] | map(select(. != ""))), simplexChannel: ([$ch] | map(select(. != "")))}' \
"$work/names.json" > "$work/names.json.new"
mv "$work/names.json.new" "$work/names.json"
}
# A contact that claims a name and accepts every connection; it keeps running, so the app can connect to it.
start_contact() {
local db=$1 display_name=$2 name=$3
chat "$db" 1 "/ad" --user-display-name "$display_name"
local address
address=$(new_address)
chat "$db" 1 "/auto_accept on" --user-display-name "$display_name"
expect_out "^auto_accept on"
register_name "$name" "$address" ""
named "$db" "$display_name" 5 "/_set domain 1 $name"
expect_no_error
# --headless is for relays only; a command with a long -t keeps the client online, accepting contacts, for 6 hours
nohup "$bin/simplex-chat" -d "$work/$db" -e "/auto_accept on" -t 21600 --user-display-name "$display_name" < /dev/null > "$work/$db.log" 2>&1 &
echo $! > "$work/$db.pid"
}
# A name is claimed before its channel is listed: a profile change after approval hides it until approved again.
seed_names() {
local address link
create_channel cinews "CI news channel"
link=$(channel_link cinews)
register_name cinews.testing "" "$link"
named "$admin" "$admin" 5 "/public group access #cinews domain=cinews.testing"
expect_out "updated public group access: domain=cinews.testing"
list_channel cinews
seed_group cinews_digest "CI news digest"
seed_group cinews_weekly "CI news weekly"
create_channel ciboth "CI both channel"
admin 1 "/ad"
address=$(new_address)
link=$(channel_link ciboth)
register_name ciboth.testing "$address" "$link"
named "$admin" "$admin" 5 "/_set domain 1 ciboth.testing"
expect_no_error
named "$admin" "$admin" 5 "/public group access #ciboth domain=ciboth.testing"
expect_out "updated public group access: domain=ciboth.testing"
start_contact ci_alice cialice cialice.testing
start_contact ci_robert Robert cibobby.testing
}
# What the app will resolve, from a client that knows none of the names' owners.
probe_names() {
local target pattern
for target in "#cinews.testing|ok to connect via relays" "@cialice.testing|contact address: ok to connect" "@cibobby.testing|contact address: ok to connect" "ciboth.testing|You can also connect to @ciboth"; do
pattern=${target#*|}
target=${target%%|*}
named probe ci_probe 5 "/_connect plan 1 $target"
expect_out "$pattern"
done
named probe ci_probe 5 "/_connect plan 1 #cinone.testing" || true
echo "names resolve: cinews, cialice, cibobby, ciboth; cinone is not registered"
}
# The directory's reply to a search starts with "Found N group(s)"; earlier messages name the same groups.
in_search_reply() {
local reply
reply=$(sed -n '/Found [0-9]* group(s)/,$p' "$work/out")
grep -qF "$1" <<< "$reply"
}
# A search sent as a message, which any client can send.
probe_search() {
admin 5 "@'$directory_name' $search_text"
local deadline=$((SECONDS + 120))
until admin 5 "/tail @'$directory_name' 10" && in_search_reply "ci_group (CI seed group)" && in_search_reply "ci_channel (CI seed channel)"; do
if [ "$SECONDS" -ge "$deadline" ]; then
echo "error: the directory's reply to \"$search_text\" does not list ci_group and ci_channel"
return 1
fi
done
echo "search for \"$search_text\" found ci_group and ci_channel"
}
write_seed() {
{
echo "# Test directory"
echo
echo "Directory address: $directory_link"
echo
echo "## Listed groups and channels"
echo
echo "| Search with | Entries | Seeded for |"
echo "|---|---|---|"
echo "| \`$search_text\` | ci_group (group), ci_channel (channel), each with a small avatar (64 px) | a group and a channel to find |"
echo "| \`ci cards\` | ci_cards (channel, with a small avatar) | a channel to join, so that the profile has a conversation |"
echo "| \`$avatar_search_text\` | ci_avatar_g1…3 (groups), ci_avatar_c1…3 (channels), each with an avatar the size the app makes | images at the size limit |"
echo "| \`ci long\` | $long_name (group): a long name and a two-line description | truncation |"
echo "| \`ci markdown\` | ci_markdown (group): markdown in the description | markdown shown literally |"
echo "| \`cinews\` | cinews (channel, with the name cinews.testing), cinews_digest and cinews_weekly (groups) | a search that matches a SimpleX name and listed entries |"
echo
sqlite3 -markdown "$work/directory_chat.db" "$listings_query"
echo
echo "## SimpleX names"
echo
echo "Names server, to add in the app for resolving names: \`$names_server\`"
echo
echo "| Name | Resolves to |"
echo "|---|---|"
echo "| cinews.testing | the channel cinews, listed in the directory (\"CI news channel\") |"
echo "| ciboth.testing | a contact address (ci_admin) and the channel ciboth, not listed |"
echo "| cialice.testing | the contact cialice, online and accepting connections |"
echo "| cibobby.testing | the contact Robert (display name without the name), online and accepting connections |"
echo "| any other, e.g. cinone.testing | not registered |"
echo
} > "$work/seed.md"
}
dump_directory() {
echo "::group::directory registrations"
db_query directory "SELECT group_id, group_reg_status FROM sx_directory_group_regs" || true
echo "::endgroup::"
}
start() {
bin=$work/bin
mkdir -p "$bin"
unzip -o -q "$1" -d "$bin"
"$bin/simplex-chat" --version
"$bin/simplex-directory-service" --version
"$bin/smp-server" --version
make_avatars
start_names
start_relay
create_directory
start_directory "0:nobody"
trap dump_directory EXIT
connect_admin
seed_group ci_group "CI seed group" "$work/avatar-7.txt"
setup_relay
seed_channel ci_channel "CI seed channel" "$work/avatar-8.txt"
seed_channel ci_cards "CI cards channel" "$work/avatar-9.txt"
seed_avatars
seed_group "$long_name" "CI long entry: a description long enough to take two lines in the directory results, to check how the row truncates"
seed_group ci_markdown "CI markdown *bold* _italic_ [x](https://example.com)"
seed_names
probe_search
probe_names
write_seed
}
stop() {
local p processes="directory relay ci_alice ci_robert smp-server names-resolver"
for p in $processes; do
if [ -f "$work/$p.pid" ]; then
kill -INT "$(cat "$work/$p.pid")" 2> /dev/null || true
fi
done
sleep 3
for p in $processes; do
echo "::group::$p log"
cat "$work/$p.log" 2> /dev/null || true
echo "::endgroup::"
done
}
case "${1:-}" in
start)
work=$3
mkdir -p "$work"
start "$2"
;;
stop)
work=$2
stop
;;
directory-stop)
work=$2
stop_directory
;;
directory-start)
work=$2
bin=$work/bin
start_directory "$(cat "$work/super-user")"
;;
listings)
work=$2
sqlite3 -markdown "$work/directory_chat.db" "$listings_query"
;;
*)
echo "usage: $0 start <backend zip> <work dir> | stop <work dir> | directory-stop <work dir> | directory-start <work dir> | listings <work dir>"
exit 1
;;
esac