Files
NeoTachyon/tests/subscription_cache_state.sh
T

448 lines
18 KiB
Bash

#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CACHE_UC="$ROOT_DIR/podkop/files/usr/lib/subscription/cache.uc"
PODKOP_LIB="$ROOT_DIR/podkop/files/usr/lib"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_eq() {
local expected="$1"
local actual="$2"
local label="$3"
[ "$actual" = "$expected" ] || fail "$label: expected '$expected', got '$actual'"
}
cache_ucode() {
ucode -L "$PODKOP_LIB" "$CACHE_UC" "$@"
}
assert_eq "first second" \
"$(cache_ucode append-state-list-once "first" second)" \
"append new state item"
assert_eq "first second" \
"$(cache_ucode append-state-list-once "first second" second)" \
"do not append duplicate state item"
assert_eq "first second" \
"$(cache_ucode append-state-list-once " first second " "")" \
"empty state item keeps normalized list"
cache_ucode state-list-contains "first second" second >/dev/null ||
fail "state list should contain second"
if cache_ucode state-list-contains "first second" third >/dev/null 2>&1; then
fail "state list should not contain third"
fi
SUBSCRIPTION_RUNTIME_SH="$ROOT_DIR/podkop/files/usr/lib/subscription_runtime.sh"
UPDATES_RUNTIME_SH="$ROOT_DIR/podkop/files/usr/lib/updates_runtime.sh"
[ ! -e "$SUBSCRIPTION_RUNTIME_SH" ] ||
fail "subscription_runtime.sh shell owner must be removed"
[ ! -e "$UPDATES_RUNTIME_SH" ] ||
fail "updates_runtime.sh shell owner must be removed"
if grep -R -n "subscription_runtime.sh" "$ROOT_DIR/podkop/files" >/dev/null 2>&1; then
fail "runtime files must not reference subscription_runtime.sh"
fi
subscription_runtime_shell_symbols='subscription_runtime_|prepare_subscription_caches_for_startup|prepare_subscription_caches_for_runtime_generation|run_deferred_subscription_bootstrap|stop_deferred_subscription_bootstrap_retry_worker'
if grep -R -n -E "$subscription_runtime_shell_symbols" "$ROOT_DIR/podkop/files/usr/bin/podkop" "$ROOT_DIR/podkop/files/usr/lib" --include='*.sh' >/dev/null 2>&1; then
fail "subscription_runtime shell symbols must not remain in runtime shell"
fi
if grep -R -n -E 'get_subscription_metadata_path|get_outbound_metadata_path' "$ROOT_DIR/podkop/files/usr/bin/podkop" "$ROOT_DIR/podkop/files/usr/lib" --include='*.sh' >/dev/null 2>&1; then
fail "subscription metadata path helpers must be owned by subscription/cache.uc"
fi
if grep -n -E 'require\("uci"\)\.cursor|uci -q' "$CACHE_UC" >/dev/null 2>&1; then
fail "subscription/cache.uc must use core.uci instead of owning direct UCI cursor or CLI calls"
fi
grep -q 'mode == "ensure-runtime-dirs"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose ensure-runtime-dirs owner mode"
grep -q 'mode == "ensure-runtime-cache-format"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose ensure-runtime-cache-format owner mode"
grep -q 'mode == "prepare-caches"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose prepare-caches owner mode"
grep -q 'mode == "update-source"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose update-source owner mode"
grep -q 'mode == "update-request"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose update-request owner mode"
grep -q 'mode == "update-section"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose update-section owner mode"
grep -q 'mode == "section-is-subscription-proxy"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose subscription proxy predicate mode"
grep -q 'mode == "subscription-metadata-path"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose subscription metadata path mode"
grep -q 'mode == "outbound-metadata-path"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose outbound metadata path mode"
grep -q 'mode == "run-deferred-bootstrap"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose deferred bootstrap owner mode"
grep -q 'mode == "stop-deferred-bootstrap-worker"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose deferred worker stop owner mode"
assert_eq "proxy-subscription-2" \
"$(cache_ucode source-id proxy 2)" \
"source id mode"
assert_eq "/var/run/podkop-plus/subscription-metadata/proxy.json" \
"$(cache_ucode subscription-metadata-path proxy)" \
"subscription metadata path mode"
assert_eq "/var/run/podkop-plus/outbound-metadata/proxy.json" \
"$(cache_ucode outbound-metadata-path proxy)" \
"outbound metadata path mode"
if cache_ucode subscription-metadata-path '../bad' >/dev/null 2>&1; then
fail "subscription metadata path mode should reject unsafe section names"
fi
cat >"$WORK_DIR/normalized-subscription.json" <<'JSON'
{
"version": 1,
"format": "uri-list",
"skipped": 2,
"outbounds": [
{ "type": "vless", "tag": "one" },
{ "type": "trojan", "tag": "two" }
]
}
JSON
assert_eq "2 proxy entries, 2 skipped entries" \
"$(cache_ucode subscription-import-stats "$WORK_DIR/normalized-subscription.json")" \
"subscription import stats"
assert_eq "Subscription source 1 for rule 'proxy' imported: 2 proxy entries, 2 skipped entries" \
"$(cache_ucode subscription-source-summary proxy 1 "$WORK_DIR/normalized-subscription.json" imported)" \
"subscription source imported summary"
assert_eq "Subscription source 1 for rule 'proxy' is unchanged: 2 proxy entries, 2 skipped entries" \
"$(cache_ucode subscription-source-summary proxy 1 "$WORK_DIR/normalized-subscription.json" unchanged)" \
"subscription source unchanged summary"
runtime_env() {
TMP_SING_BOX_FOLDER="$WORK_DIR/runtime/tmp-sing-box" \
TMP_RULESET_FOLDER="$WORK_DIR/runtime/tmp-sing-box/rulesets" \
TMP_SUBSCRIPTION_FOLDER="$WORK_DIR/runtime/tmp-sing-box/subscriptions" \
PODKOP_RUNTIME_STATE_DIR="$WORK_DIR/runtime/run" \
PODKOP_SUBSCRIPTION_UPDATE_STATE_DIR="$WORK_DIR/runtime/run/subscription-update" \
PODKOP_SUBSCRIPTION_LINKS_DIR="$WORK_DIR/runtime/run/subscription-links" \
PODKOP_SUBSCRIPTION_METADATA_DIR="$WORK_DIR/runtime/run/subscription-metadata" \
PODKOP_OUTBOUND_METADATA_DIR="$WORK_DIR/runtime/run/outbound-metadata" \
PODKOP_SECTION_CACHE_DIR="$WORK_DIR/runtime/run/section-cache" \
PODKOP_RUNTIME_CACHE_FORMAT_FILE="$WORK_DIR/runtime/run/cache-format" \
PODKOP_PERSISTENT_SUBSCRIPTION_CACHE_DIR="$WORK_DIR/runtime/persistent" \
PODKOP_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE="$WORK_DIR/runtime/persistent/cache-format" \
PODKOP_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE="$WORK_DIR/runtime/run/bootstrap.pid" \
"$@"
}
runtime_env cache_ucode ensure-runtime-dirs
for dir in \
"$WORK_DIR/runtime/tmp-sing-box" \
"$WORK_DIR/runtime/tmp-sing-box/rulesets" \
"$WORK_DIR/runtime/tmp-sing-box/subscriptions" \
"$WORK_DIR/runtime/run" \
"$WORK_DIR/runtime/run/subscription-update" \
"$WORK_DIR/runtime/run/subscription-links" \
"$WORK_DIR/runtime/run/subscription-metadata" \
"$WORK_DIR/runtime/run/outbound-metadata" \
"$WORK_DIR/runtime/run/section-cache"; do
[ -d "$dir" ] || fail "ensure-runtime-dirs should create $dir"
done
printf 'old\n' >"$WORK_DIR/runtime/run/cache-format"
printf 'stale\n' >"$WORK_DIR/runtime/run/section-cache/stale.json"
runtime_env cache_ucode ensure-runtime-cache-format
assert_eq "7" \
"$(sed -n '1p' "$WORK_DIR/runtime/run/cache-format")" \
"runtime cache format"
[ ! -e "$WORK_DIR/runtime/run/section-cache/stale.json" ] ||
fail "ensure-runtime-cache-format should clear old runtime section cache"
assert_eq "7" \
"$(sed -n '1p' "$WORK_DIR/runtime/persistent/cache-format")" \
"persistent cache format"
runtime_env cache_ucode run-deferred-bootstrap ""
runtime_env cache_ucode stop-deferred-bootstrap-worker
mkdir -p \
"$WORK_DIR/tmp" \
"$WORK_DIR/persistent" \
"$WORK_DIR/sections" \
"$WORK_DIR/links" \
"$WORK_DIR/metadata" \
"$WORK_DIR/outbound"
cat >"$WORK_DIR/cache-maintenance.json" <<'JSON'
{
"section": [
{
".name": "proxy",
"enabled": "1",
"action": "proxy",
"subscription_urls": [
"https://example.com/one.txt",
"https://example.com/two.txt"
]
},
{
".name": "direct",
"enabled": "1",
"action": "bypass"
},
{
".name": "disabled",
"enabled": "0",
"action": "proxy",
"subscription_urls": [
"https://example.com/disabled.txt"
]
},
{
".name": "bad/name",
"enabled": "1",
"action": "proxy",
"subscription_urls": [
"https://example.com/bad.txt"
]
}
]
}
JSON
assert_eq $'sections\tproxy direct disabled\nsources\tproxy-subscription-1 proxy-subscription-2 disabled-subscription-1\nall\tproxy proxy-subscription-1 proxy-subscription-2 direct disabled disabled-subscription-1\nmissing\t1' \
"$(cache_ucode maintenance-plan-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections")" \
"cache maintenance plan with missing proxy section cache"
touch "$WORK_DIR/sections/proxy.json"
assert_eq $'sections\tproxy direct disabled\nsources\tproxy-subscription-1 proxy-subscription-2 disabled-subscription-1\nall\tproxy proxy-subscription-1 proxy-subscription-2 direct disabled disabled-subscription-1\nmissing\t0' \
"$(cache_ucode maintenance-plan-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections")" \
"cache maintenance plan with available proxy section cache"
cache_ucode runtime-cache-needs-rebuild-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections" >/dev/null &&
fail "runtime cache should not need rebuild after proxy cache exists"
cache_ucode prepared-runtime-cache-should-skip-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections" runtime 1 >/dev/null ||
fail "prepared runtime cache should skip only when section cache exists"
rm -f "$WORK_DIR/sections/proxy.json"
cache_ucode runtime-cache-needs-rebuild-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections" >/dev/null ||
fail "runtime cache should need rebuild when proxy cache is missing"
if cache_ucode prepared-runtime-cache-should-skip-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections" runtime 1 >/dev/null 2>&1; then
fail "prepared runtime cache must not skip when section cache is missing"
fi
touch \
"$WORK_DIR/tmp/proxy-subscription-1.json" \
"$WORK_DIR/tmp/stale-subscription-1.json" \
"$WORK_DIR/persistent/proxy-subscription-2.url" \
"$WORK_DIR/persistent/stale-subscription-2.user_agent" \
"$WORK_DIR/sections/proxy.json" \
"$WORK_DIR/sections/proxy-subscription-1.json" \
"$WORK_DIR/sections/stale.json" \
"$WORK_DIR/links/proxy.json" \
"$WORK_DIR/links/proxy-subscription-1.json" \
"$WORK_DIR/metadata/direct.metadata.json" \
"$WORK_DIR/outbound/stale.json" \
"$WORK_DIR/outbound/cache-format"
candidate_paths="$(
printf '%s\n' \
"$WORK_DIR/tmp/proxy-subscription-1.json" \
"$WORK_DIR/tmp/stale-subscription-1.json" \
"$WORK_DIR/persistent/proxy-subscription-2.url" \
"$WORK_DIR/persistent/stale-subscription-2.user_agent" \
"$WORK_DIR/sections/proxy.json" \
"$WORK_DIR/sections/proxy-subscription-1.json" \
"$WORK_DIR/sections/stale.json" \
"$WORK_DIR/links/proxy.json" \
"$WORK_DIR/links/proxy-subscription-1.json" \
"$WORK_DIR/metadata/direct.metadata.json" \
"$WORK_DIR/outbound/stale.json" \
"$WORK_DIR/outbound/cache-format"
)"
assert_eq "$(
printf '%s\n' \
"$WORK_DIR/tmp/stale-subscription-1.json" \
"$WORK_DIR/persistent/stale-subscription-2.user_agent" \
"$WORK_DIR/sections/stale.json" \
"$WORK_DIR/links/proxy-subscription-1.json" \
"$WORK_DIR/outbound/stale.json"
)" \
"$(printf '%s\n' "$candidate_paths" | cache_ucode stale-cache-delete-paths-fixture \
"$WORK_DIR/cache-maintenance.json" \
"$WORK_DIR/tmp" \
"$WORK_DIR/persistent" \
"$WORK_DIR/sections" \
"$WORK_DIR/links" \
"$WORK_DIR/metadata" \
"$WORK_DIR/outbound")" \
"stale cache delete path plan"
mkdir -p "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache"
cat >"$WORK_DIR/cache-current.json" <<'JSON'
{
"section": [
{
".name": "proxy",
"subscription_urls": [
"https://example.com/one.txt",
"https://example.com/two.txt"
],
"subscription_url_settings": "{\"https://example.com/one.txt\":{\"user_agent\":\"v2rayN\"}}"
},
{
".name": "manual",
"selector_proxy_links": "vless://manual"
}
]
}
JSON
cat >"$WORK_DIR/runtime-cache/proxy-subscription-1.json" <<'JSON'
{"outbounds":[{"type":"direct","tag":"one"}]}
JSON
printf '%s' 'https://example.com/one.txt' >"$WORK_DIR/runtime-cache/proxy-subscription-1.url"
printf '%s' 'v2rayN' >"$WORK_DIR/runtime-cache/proxy-subscription-1.user_agent"
cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-current.json" proxy "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null ||
fail "runtime cache should be current and usable"
printf '%s' 'stale-hwid' >"$WORK_DIR/runtime-cache/proxy-subscription-1.hwid"
if cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-current.json" proxy "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >"$WORK_DIR/hwid.out" 2>"$WORK_DIR/hwid.err"; then
fail "mismatched generated HWID cache should not be current"
fi
if grep -Fq "left-hand side is not a function" "$WORK_DIR/hwid.err"; then
fail "mismatched generated HWID cache must not crash"
fi
rm -f "$WORK_DIR/runtime-cache/proxy-subscription-1.hwid"
printf '%s' 'https://example.com/stale.txt' >"$WORK_DIR/runtime-cache/proxy-subscription-1.url"
if cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-current.json" proxy "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null 2>&1; then
fail "stale runtime cache URL should not be current"
fi
rm -f "$WORK_DIR/runtime-cache/proxy-subscription-1.json" "$WORK_DIR/runtime-cache/proxy-subscription-1.url" "$WORK_DIR/runtime-cache/proxy-subscription-1.user_agent"
cat >"$WORK_DIR/persistent-cache/proxy-subscription-2.json" <<'JSON'
{"outbounds":[{"type":"direct","tag":"two"}]}
JSON
printf '%s' 'https://example.com/two.txt' >"$WORK_DIR/persistent-cache/proxy-subscription-2.url"
printf '%s' 'sing-box/default' >"$WORK_DIR/persistent-cache/proxy-subscription-2.user_agent"
cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-current.json" proxy "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null ||
fail "persistent cache should restore and satisfy current cache"
test -s "$WORK_DIR/runtime-cache/proxy-subscription-2.json" ||
fail "persistent cache restore should write runtime json"
assert_eq 'https://example.com/two.txt' \
"$(cat "$WORK_DIR/runtime-cache/proxy-subscription-2.url")" \
"restored subscription URL"
if cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-current.json" manual "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null 2>&1; then
fail "section without subscription URLs should not have usable subscription cache"
fi
mkdir -p "$WORK_DIR/link-cache" "$WORK_DIR/link-subscriptions"
cat >"$WORK_DIR/link-cache/proxy.json" <<'JSON'
{
"links": {},
"linkRefs": {
"vless-encrypted": {
"sourceSection": "proxy-subscription-1",
"sourceIndex": 1
},
"vless-none": {
"sourceSection": "proxy-subscription-1",
"sourceIndex": 2
}
}
}
JSON
cat >"$WORK_DIR/link-subscriptions/proxy-subscription-1.json" <<'JSON'
{
"outbounds": [
{
"type": "vless",
"tag": "vless-encrypted",
"server": "example.com",
"server_port": 443,
"uuid": "00000000-0000-4000-8000-000000000001",
"encryption": "mlkem768x25519plus.native.test"
},
{
"type": "vless",
"tag": "vless-none",
"server": "example.com",
"server_port": 443,
"uuid": "00000000-0000-4000-8000-000000000001",
"encryption": "none"
}
]
}
JSON
encrypted_link="$(cache_ucode get-link "$WORK_DIR/link-cache" "$WORK_DIR/link-subscriptions" proxy vless-encrypted "")"
JSON_VALUE="$encrypted_link" node - <<'NODE'
const value = JSON.parse(process.env.JSON_VALUE);
if (!value.link.includes("encryption=mlkem768x25519plus.native.test")) {
console.error(`expected VLESS encryption in serialized link, got ${value.link}`);
process.exit(1);
}
NODE
none_link="$(cache_ucode get-link "$WORK_DIR/link-cache" "$WORK_DIR/link-subscriptions" proxy vless-none "")"
JSON_VALUE="$none_link" node - <<'NODE'
const value = JSON.parse(process.env.JSON_VALUE);
if (value.link.includes("encryption=none")) {
console.error(`did not expect encryption=none in serialized link, got ${value.link}`);
process.exit(1);
}
NODE
mkdir -p "$WORK_DIR/persist-runtime" "$WORK_DIR/persist-empty" "$WORK_DIR/persist-fresh"
cat >"$WORK_DIR/persist-runtime/proxy-subscription-1.json" <<'JSON'
{"outbounds":[{"type":"direct","tag":"one"}]}
JSON
cat >"$WORK_DIR/runtime/persistent/proxy-subscription-1.metadata.json" <<'JSON'
{"version":1,"title":"WolfPN"}
JSON
printf '%s' 'https://example.com/one.txt' >"$WORK_DIR/runtime/persistent/proxy-subscription-1.url"
printf '%s' 'v2rayN' >"$WORK_DIR/runtime/persistent/proxy-subscription-1.user_agent"
cat >"$WORK_DIR/persist-empty/empty.metadata.json" <<'JSON'
{}
JSON
runtime_env cache_ucode persist-source-cache \
proxy-subscription-1 \
"$WORK_DIR/persist-runtime/proxy-subscription-1.json" \
'https://example.com/one.txt' \
'v2rayN' \
"$WORK_DIR/persist-empty/empty.metadata.json"
test -s "$WORK_DIR/runtime/persistent/proxy-subscription-1.metadata.json" ||
fail "same subscription source with empty fresh metadata must keep previous metadata"
node - "$WORK_DIR/runtime/persistent/proxy-subscription-1.metadata.json" <<'JS'
const fs = require("fs");
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (value.title !== "WolfPN") {
console.error("previous metadata was not preserved");
process.exit(1);
}
JS
runtime_env cache_ucode persist-source-cache \
proxy-subscription-1 \
"$WORK_DIR/persist-runtime/proxy-subscription-1.json" \
'https://example.com/changed.txt' \
'v2rayN' \
"$WORK_DIR/persist-empty/empty.metadata.json"
[ ! -e "$WORK_DIR/runtime/persistent/proxy-subscription-1.metadata.json" ] ||
fail "changed subscription URL with empty metadata must drop stale metadata"
printf 'subscription cache state checks passed\n'