mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-18 15:01:01 +00:00
478 lines
18 KiB
Bash
Executable File
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
|