Files
NeoTachyon/tests/updates_due.sh
T

302 lines
11 KiB
Bash

#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
UPDATES_UC="$ROOT_DIR/tachyon/files/usr/lib/components/updates.uc"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_status() {
local expected="$1"
local label="$2"
shift 2
local status=0
updates_ucode update-is-due "$@" >/dev/null 2>&1 || status="$?"
[ "$status" = "$expected" ] ||
fail "$label: expected exit $expected, got $status"
}
updates_ucode() {
ucode -L "$TACHYON_LIB" "$UPDATES_UC" "$@"
}
if grep -n -E 'require\("uci"\)\.cursor|uci -q' "$UPDATES_UC" >/dev/null 2>&1; then
fail "components/updates.uc must use core.uci instead of owning direct UCI cursor or CLI calls"
fi
assert_updates_status() {
local expected="$1"
local label="$2"
shift 2
local status=0
updates_ucode "$@" >/dev/null 2>&1 || status="$?"
[ "$status" = "$expected" ] ||
fail "$label: expected exit $expected, got $status"
}
assert_status 0 "never-run update should be due" 100 0 60
assert_status 0 "expired update should be due" 100 30 60
assert_status 1 "fresh update should not be due" 100 50 60
assert_status 1 "future timestamp should not be due" 100 130 60
assert_status 0 "invalid last-run timestamp should be treated as never-run" 100 invalid 60
assert_status 2 "invalid current timestamp should fail" invalid 0 60
assert_status 2 "invalid interval should fail" 100 0 invalid
assert_status 2 "zero interval should fail" 100 0 0
assert_eq() {
local expected="$1"
local actual="$2"
local label="$3"
[ "$actual" = "$expected" ] || fail "$label: expected '$expected', got '$actual'"
}
assert_eq 180 \
"$(updates_ucode duration-to-seconds 3m)" \
"minutes duration"
assert_eq 5400 \
"$(updates_ucode duration-to-seconds 1.5h)" \
"decimal duration"
if updates_ucode duration-to-seconds invalid >/dev/null 2>&1; then
fail "invalid duration should fail"
fi
assert_eq "* * * * *" \
"$(updates_ucode due-check-cron-schedule 60)" \
"minute cron schedule"
assert_eq "0 * * * *" \
"$(updates_ucode due-check-cron-schedule 3600)" \
"hour cron schedule"
assert_eq "0 */2 * * *" \
"$(updates_ucode due-check-cron-schedule 7200)" \
"two-hour cron schedule"
assert_eq "*/30 * * * *" \
"$(updates_ucode due-check-cron-schedule 1800)" \
"half-hour cron schedule"
assert_eq "0 0 * * *" \
"$(updates_ucode due-check-cron-schedule 86400)" \
"daily cron schedule"
assert_eq "*/30 * * * * /usr/bin/tachyon list_update_if_due # list" \
"$(updates_ucode list-update-cron-job 30m /usr/bin/tachyon '# list')" \
"list update cron job"
assert_eq $'https://raw.githubusercontent.com/itdoginfo/allow-domains/main/Subnets/IPv4/telegram.lst\nhttps://raw.githubusercontent.com/itdoginfo/allow-domains/main/Subnets/IPv6/telegram.lst' \
"$(updates_ucode builtin-subnet-urls telegram)" \
"Telegram built-in subnet families"
assert_eq 'https://raw.githubusercontent.com/itdoginfo/allow-domains/main/Subnets/IPv4/roblox.lst' \
"$(updates_ucode builtin-subnet-urls roblox)" \
"Roblox available subnet family"
assert_eq "0 */2 * * * /usr/bin/tachyon subscription_update_if_due # subscription" \
"$(updates_ucode subscription-update-cron-job 7200 /usr/bin/tachyon '# subscription')" \
"subscription update cron job"
assert_eq $'error\trule3\tinvalid\nmin\t1800' \
"$(printf 'rule1\t2h\nrule2\t30m\nrule3\tinvalid\nrule4\t\n' | updates_ucode subscription-update-interval-plan)" \
"subscription interval plan"
assert_eq $'min\t0' \
"$(printf 'rule1\t\n' | updates_ucode subscription-update-interval-plan)" \
"empty subscription interval plan"
if updates_ucode list-update-cron-job invalid /usr/bin/tachyon '# list' >/dev/null 2>&1; then
fail "invalid list update interval should fail"
fi
if updates_ucode subscription-update-cron-job 0 /usr/bin/tachyon '# subscription' >/dev/null 2>&1; then
fail "invalid subscription update interval should fail"
fi
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
cat >"$WORK_DIR/cron-plan.json" <<'JSON'
{
"settings": {
"list_update_enabled": "1",
"update_interval": "30m",
"component_update_check_enabled": "1",
"component_update_check_interval": "2h"
},
"section": [
{
".name": "list_rule",
"enabled": "1",
"action": "proxy",
"community_lists": [ "telegram" ],
"subscription_urls": [ "https://example.com/sub.txt" ],
"subscription_update_interval": "2h"
},
{
".name": "fast_sub",
"enabled": "1",
"action": "proxy",
"subscription_urls": [ "https://example.com/fast.txt" ],
"subscription_update_interval": "45m"
},
{
".name": "bad_sub",
"enabled": "1",
"action": "proxy",
"subscription_urls": [ "https://example.com/bad.txt" ],
"subscription_update_interval": "bad"
},
{
".name": "disabled",
"enabled": "0",
"action": "proxy",
"remote_domain_lists": [ "https://example.com/disabled.lst" ],
"subscription_urls": [ "https://example.com/disabled.txt" ]
}
]
}
JSON
assert_eq $'list\t*/30 * * * * /usr/bin/tachyon list_update_if_due # list\nsubscription-error\tbad_sub\tbad\nsubscription\t*/45 * * * * /usr/bin/tachyon subscription_update_if_due # subscription\ncomponent\t0 */2 * * * /usr/bin/tachyon component_updates_if_due # component' \
"$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan.json" /usr/bin/tachyon '# list' '# subscription' '# component')" \
"cron refresh plan"
grep -Fq 'fs.writefile(tmp, as_string(text)) == null' "$UPDATES_UC" ||
fail "empty crontab writes must not be treated as fs.writefile failure"
cat >"$WORK_DIR/cron-plan-all-presets.json" <<'JSON'
{
"settings": {
"list_update_enabled": "1",
"update_interval": "1h",
"download_all_presets": "1"
},
"section": []
}
JSON
assert_eq $'list\t0 * * * * /usr/bin/tachyon list_update_if_due # list' \
"$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-all-presets.json" /usr/bin/tachyon '# list' '# sub' '# comp')" \
"cron refresh plan with download_all_presets"
cat >"$WORK_DIR/cron-plan-list-disabled.json" <<'JSON'
{
"settings": {
"list_update_enabled": "0",
"update_interval": "30m"
},
"section": [
{
".name": "list_rule",
"enabled": "1",
"action": "proxy",
"remote_domain_lists": [ "https://example.com/list.txt" ]
}
]
}
JSON
assert_eq "list-disabled" \
"$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-list-disabled.json" /usr/bin/tachyon '# list' '# subscription' '# component')" \
"disabled list cron plan"
cat >"$WORK_DIR/cron-plan-invalid-list.json" <<'JSON'
{
"settings": {
"list_update_enabled": "1",
"update_interval": "bad"
},
"section": [
{
".name": "list_rule",
"enabled": "1",
"action": "proxy",
"remote_domain_lists": [ "https://example.com/list.txt" ]
}
]
}
JSON
status=0
invalid_plan="$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-invalid-list.json" /usr/bin/tachyon '# list' '# subscription' '# component')" || status="$?"
assert_eq 1 "$status" "invalid list cron plan status"
assert_eq $'list-error\tbad' "$invalid_plan" "invalid list cron plan output"
cat >"$WORK_DIR/existing.cron" <<'CRON'
0 1 * * * /bin/true # keep
* * * * * /usr/bin/tachyon list_update_if_due # list
* * * * * /usr/bin/tachyon subscription_update_if_due # subscription
* * * * * /usr/bin/tachyon component_updates_if_due # component
CRON
updates_ucode refresh-cron-fixture "$WORK_DIR/cron-plan.json" "$WORK_DIR/existing.cron" /usr/bin/tachyon '# list' '# subscription' '# component' >"$WORK_DIR/cron-apply.json"
node - "$WORK_DIR/cron-apply.json" <<'JS'
const fs = require("fs");
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const expected = [
"0 1 * * * /bin/true # keep",
"*/30 * * * * /usr/bin/tachyon list_update_if_due # list",
"*/45 * * * * /usr/bin/tachyon subscription_update_if_due # subscription",
"0 */2 * * * /usr/bin/tachyon component_updates_if_due # component",
""
].join("\n");
if (value.crontab !== expected) {
console.error("unexpected applied crontab", JSON.stringify(value.crontab));
process.exit(1);
}
const messages = value.logs.map(item => item.message);
if (!messages.includes("The cron job removed") ||
!messages.includes("The cron job has been created: */30 * * * * /usr/bin/tachyon list_update_if_due # list") ||
!messages.includes("The subscription cron job has been created: */45 * * * * /usr/bin/tachyon subscription_update_if_due # subscription") ||
!messages.includes("The component update check cron job has been created: 0 */2 * * * /usr/bin/tachyon component_updates_if_due # component")) {
console.error("unexpected cron apply logs", JSON.stringify(value.logs));
process.exit(1);
}
JS
status=0
updates_ucode refresh-cron-fixture "$WORK_DIR/cron-plan-invalid-list.json" "$WORK_DIR/existing.cron" /usr/bin/tachyon '# list' '# subscription' '# component' >"$WORK_DIR/cron-apply-invalid.json" || status="$?"
assert_eq 1 "$status" "invalid cron apply status"
node - "$WORK_DIR/cron-apply-invalid.json" <<'JS'
const fs = require("fs");
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (value.crontab !== "0 1 * * * /bin/true # keep\n") {
console.error("invalid plan should remove old tachyon cron jobs only", JSON.stringify(value.crontab));
process.exit(1);
}
if (!value.logs.some(item => item.level === "error" && item.message === "Invalid update_interval value: bad")) {
console.error("invalid plan error log missing", JSON.stringify(value.logs));
process.exit(1);
}
JS
LIST_TIMESTAMP="$WORK_DIR/list.timestamp"
SUB_TIMESTAMP="$WORK_DIR/sub.timestamp"
rm -f "$LIST_TIMESTAMP" "$SUB_TIMESTAMP"
assert_updates_status 0 "list update should be due without timestamp" \
list-update-due-status-fixture "$WORK_DIR/cron-plan.json" "$LIST_TIMESTAMP" 1000
printf '900\n' >"$LIST_TIMESTAMP"
assert_updates_status 1 "list update should not be due before interval" \
list-update-due-status-fixture "$WORK_DIR/cron-plan.json" "$LIST_TIMESTAMP" 1000
assert_updates_status 1 "disabled list update should not be due" \
list-update-due-status-fixture "$WORK_DIR/cron-plan-list-disabled.json" "$LIST_TIMESTAMP" 1000
status=0
invalid_due="$(updates_ucode list-update-due-status-fixture "$WORK_DIR/cron-plan-invalid-list.json" "$LIST_TIMESTAMP" 1000)" || status="$?"
assert_eq 2 "$status" "invalid list due status"
assert_eq $'error\tbad' "$invalid_due" "invalid list due output"
rm -f "$SUB_TIMESTAMP"
assert_updates_status 0 "subscription update should be due without timestamp" \
subscription-update-section-due-status-fixture "$WORK_DIR/cron-plan.json" fast_sub "$SUB_TIMESTAMP" 1000
printf '900\n' >"$SUB_TIMESTAMP"
assert_updates_status 1 "subscription update should not be due before interval" \
subscription-update-section-due-status-fixture "$WORK_DIR/cron-plan.json" fast_sub "$SUB_TIMESTAMP" 1000
assert_updates_status 1 "missing subscription section should not be due" \
subscription-update-section-due-status-fixture "$WORK_DIR/cron-plan.json" missing "$SUB_TIMESTAMP" 1000
status=0
invalid_due="$(updates_ucode subscription-update-section-due-status-fixture "$WORK_DIR/cron-plan.json" bad_sub "$SUB_TIMESTAMP" 1000)" || status="$?"
assert_eq 2 "$status" "invalid subscription due status"
assert_eq $'error\tbad' "$invalid_due" "invalid subscription due output"
printf 'updates due checks passed\n'