🎉 Sync 2026-09-27 21:03:26

This commit is contained in:
github-actions[bot] committed 2026-09-27 21:03:26 +08:00
1 parent f4559bbed1
commit 065763f7fd
673 files changed
+261285 -34160

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
# shellcheck shell=sh
Describe 'SafeShield blocklist behavior'
It 'normalizes, merges, samples, and verifies dnsmasq rules'
When call ss_case_blocklist_format
The status should be success
The output should equal ''
The error should equal ''
End
It 'resolves and downloads multiple Hub artifact sources'
When call ss_case_multi_artifact
The status should be success
The output should equal ''
The error should equal ''
End
It 'retries artifact downloads and enforces size and checksum integrity'
When call ss_case_artifact_integrity
The status should be success
The output should equal ''
The error should equal ''
End
It 'stops retries when the Hub requires a SafeShield upgrade'
When call ss_case_upgrade_required
The status should be success
The output should equal ''
The error should equal ''
End
It 'keeps optimized merge and statistics serialization paths'
When call ss_case_performance_paths
The status should be success
The output should equal ''
The error should equal ''
End
End
+59
View File
@@ -0,0 +1,59 @@
# shellcheck shell=sh
Describe 'SafeShield core shell behavior'
It 'validates utility helpers'
When call ss_case_utils
The status should be success
The output should equal ''
The error should equal ''
End
It 'validates configuration defaults and boundaries'
When call ss_case_config_validation
The status should be success
The output should equal ''
The error should equal ''
End
It 'validates supported dnsmasq versions'
When call ss_case_dnsmasq_version
The status should be success
The output should equal ''
The error should equal ''
End
It 'validates device identity behavior'
When call ss_case_identity
The status should be success
The output should equal ''
The error should equal ''
End
It 'validates Hub resolve payload metadata'
When call ss_case_resolve_payload
The status should be success
The output should equal ''
The error should equal ''
End
It 'validates rpcd and ACL contracts'
When call ss_case_rpcd_contract
The status should be success
The output should equal ''
The error should equal ''
End
It 'validates status state transitions and blocklist restore'
When call ss_case_status_state
The status should be success
The output should equal ''
The error should equal ''
End
It 'keeps package and runtime versions synchronized'
When call ss_case_status_version
The status should be success
The output should equal ''
The error should equal ''
End
End
+31
View File
@@ -0,0 +1,31 @@
# shellcheck shell=sh
Describe 'SafeShield lifecycle and refresh state machines'
It 'rolls back full refresh failures while preserving the active blocklist for upgrade-required responses'
When call ss_case_refresh_state_machine
The status should be success
The output should equal ''
The error should equal ''
End
It 'applies cached local rules idempotently and rolls back failed fast applies'
When call ss_case_local_apply_state_machine
The status should be success
The output should equal ''
The error should equal ''
End
It 'serializes full refresh and local apply workers with a termination-aware lock'
When call ss_case_refresh_locking
The status should be success
The output should equal ''
The error should equal ''
End
It 'starts and stops SafeShield safely across disabled, statistics, and migration paths'
When call ss_case_service_lifecycle
The status should be success
The output should equal ''
The error should equal ''
End
End
+59
View File
@@ -0,0 +1,59 @@
# shellcheck shell=sh
Describe 'SafeShield runtime process behavior'
It 'interrupts refreshd long sleeps without one-second polling'
When call ss_case_refreshd_wait
The status should be success
The output should equal ''
The error should equal ''
End
It 'schedules refreshes without retry storms and handles boot, WAN, and disabled states'
When call ss_case_refreshd_scheduler
The status should be success
The output should equal ''
The error should equal ''
End
It 'starts and stops the statistics collector cleanly'
When call ss_case_statistics_collector
The status should be success
The output should equal ''
The error should equal ''
End
It 'uploads statistics idempotently and reconciles after failures'
When call ss_case_statistics_uploader
The status should be success
The output should equal ''
The error should equal ''
End
It 'stores Hub statistics upload credentials only in the runtime cache'
When call ss_case_statistics_upload_credentials
The status should be success
The output should equal ''
The error should equal ''
End
It 'sends the Hub statistics bearer credential as an HTTP authorization header'
When call ss_case_statistics_http_auth
The status should be success
The output should equal ''
The error should equal ''
End
It 'uses authenticated uclient-fetch safely on OpenWrt'
When call ss_case_http_uclient_transport
The status should be success
The output should equal ''
The error should equal ''
End
It 'reconciles statistics runtime without disturbing refreshd'
When call ss_case_statistics_reconcile
The status should be success
The output should equal ''
The error should equal ''
End
End
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
# shellcheck shell=sh
set -eu
spec_helper_precheck() {
minimum_version "0.28.1"
}
SS_SPEC_ROOT="${SHELLSPEC_PROJECT_ROOT:-$(pwd)}"
export SS_SPEC_ROOT
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/spec/support/common.sh"
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/spec/support/core_cases.sh"
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/spec/support/lifecycle_cases.sh"
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/spec/support/blocklist_cases.sh"
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/spec/support/runtime_cases.sh"
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/spec/support/statistics_cases.sh"
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/spec/support/ucode_cases.sh"
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/spec/support/tooling_cases.sh"
+129
View File
@@ -0,0 +1,129 @@
# shellcheck shell=sh
Describe 'SafeShield statistics behavior'
It 'collects global and per-device DNS statistics'
When call ss_case_statistics
The status should be success
The output should equal ''
The error should equal ''
End
It 'calculates deltas from dnsmasq cumulative UBus counters'
When call ss_case_statistics_ubus_source
The status should be success
The output should equal ''
The error should equal ''
End
It 'restores persistent state and handles volatile mode metadata'
When call ss_case_statistics_persistence
The status should be success
The output should equal ''
The error should equal ''
End
It 'replays, compacts, and validates the statistics journal'
When call ss_case_statistics_journal
The status should be success
The output should equal ''
The error should equal ''
End
It 'does not replay journal transactions older than the persistent base'
When call ss_case_statistics_stale_journal
The status should be success
The output should equal ''
The error should equal ''
End
It 'excludes internal loopback DNS health checks from statistics'
When call ss_case_statistics_internal_queries
The status should be success
The output should equal ''
The error should equal ''
End
It 'keeps a generation stable while collector sessions restart'
When call ss_case_statistics_generation
The status should be success
The output should equal ''
The error should equal ''
End
It 'keeps snapshot sequences monotonic across restart and persistent recovery'
When call ss_case_statistics_snapshot_sequence
The status should be success
The output should equal ''
The error should equal ''
End
It 'uses snapshot sequence instead of wall clock to choose recovery state'
When call ss_case_statistics_snapshot_freshness
The status should be success
The output should equal ''
The error should equal ''
End
It 'replays newer journal transactions by snapshot sequence across NTP rollback'
When call ss_case_statistics_journal_sequence_freshness
The status should be success
The output should equal ''
The error should equal ''
End
It 'does not compare snapshot sequence numbers across different generations'
When call ss_case_statistics_generation_sequence_scope
The status should be success
The output should equal ''
The error should equal ''
End
It 'keeps unresolved IPv6 clients as explicit temporary identities'
When call ss_case_statistics_ipv6_identity
The status should be success
The output should equal ''
The error should equal ''
End
It 'merges IPv6 privacy addresses through the kernel neighbor identity'
When call ss_case_statistics_ipv6_neighbor_identity
The status should be success
The output should equal ''
The error should equal ''
End
It 'merges private MAC rotation by DHCP client identifier'
When call ss_case_statistics_dhcp_client_id_identity
The status should be success
The output should equal ''
The error should equal ''
End
It 'merges IPv6 private MAC rotation by DHCPv6 DUID'
When call ss_case_statistics_ipv6_duid_identity
The status should be success
The output should equal ''
The error should equal ''
End
It 'uses conservative hostname fallback for historical MAC identities'
When call ss_case_statistics_hostname_identity
The status should be success
The output should equal ''
The error should equal ''
End
It 'projects a recent upload window with internally consistent totals'
When call ss_case_statistics_upload_projection
The status should be success
The output should equal ''
The error should equal ''
End
It 'loads the statistics collector from focused awk modules'
When call ss_case_statistics_modules
The status should be success
The output should equal ''
The error should equal ''
End
End
+445
View File
@@ -0,0 +1,445 @@
#!/bin/sh
# shellcheck shell=sh
ss_case_blocklist_format() (
set -eu
TMP_DIR="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
SS_TMP_DIR="$TMP_DIR/tmp"
SS_DNSMASQ_DIR="$TMP_DIR/dnsmasq.d"
SS_BLOCKLIST_FILE="$SS_DNSMASQ_DIR/safeshield.blocklist"
ss_min_valid_line_count=0
ss_max_blocklist_file_size_kb=1024
ss_dnsmasq_sanity_check=0
ss_valid_line_count=0
mkdir -p "$SS_TMP_DIR" "$SS_DNSMASQ_DIR"
ss_should_stop() { return 1; }
ss_status_set() { :; }
ss_status_add_error() { :; }
log_info() { :; }
log_error() { :; }
log_warn() { :; }
ss_sync_path() { :; }
ss_blocklist_tmp_path() { printf '%s/.safeshield.blocklist.tmp.%s\n' "$SS_DNSMASQ_DIR" "$$"; }
ss_install_blocklist_atomic() { mv -f "$1" "$2"; }
DNSMASQ_PATCHED=1
ss_dnsmasq_safeshield_block_supported() { [ "$DNSMASQ_PATCHED" = '1' ]; }
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
ss_spec_assert_file_not_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh" 'smartsafehub-block='
normalized="$(printf '%s\n' \
'address=/ads.example/#' \
'address=/legacy-v4.example/0.0.0.0' \
'address=/legacy-v6.example/::' \
'safeshield-block=/native.example/' | ss_normalize_domains)"
expected='ads.example
legacy-v4.example
legacy-v6.example
native.example'
ss_spec_assert_eq "$normalized" "$expected"
cat >"$SS_TMP_DIR/api.0.block.txt" <<'DATA'
ads.example
tracker.example
ads.example
DATA
cat >"$SS_TMP_DIR/api.1.block.txt" <<'DATA'
phishing.example
tracker.example
DATA
cat >"$SS_TMP_DIR/api.2.allow.txt" <<'DATA'
tracker.example
remote-allow.example
DATA
cat >"$SS_TMP_DIR/local.block.txt" <<'DATA'
remote-allow.example
local-block.example
DATA
cat >"$SS_TMP_DIR/local.allow.txt" <<'DATA'
local-block.example
DATA
ss_merge_lists
expected='safeshield-block=/ads.example/
safeshield-block=/phishing.example/
safeshield-block=/remote-allow.example/
server=/local-block.example/#
server=/tracker.example/#'
ss_spec_assert_eq "$(cat "$SS_BLOCKLIST_FILE")" "$expected"
ss_spec_assert_eq "$(grep -c . "$SS_BLOCKLIST_FILE")" '5'
check_blocklist_rule_present 'ads.example'
ss_spec_assert_eq "$(find_test_domains 1)" 'ads.example'
rm -f "$SS_TMP_DIR"/api.*.block.txt "$SS_TMP_DIR"/api.*.allow.txt
printf '%s\n' 'example.com' >"$SS_TMP_DIR/api.0.block.txt"
printf '%s\n' 'good.example.com' >"$SS_TMP_DIR/api.1.allow.txt"
printf '%s\n' 'blocked.good.example.com' >"$SS_TMP_DIR/local.block.txt"
printf '%s\n' 'allowed.blocked.good.example.com' >"$SS_TMP_DIR/local.allow.txt"
ss_merge_lists
expected='safeshield-block=/blocked.good.example.com/
safeshield-block=/example.com/
server=/allowed.blocked.good.example.com/#
server=/good.example.com/#'
ss_spec_assert_eq "$(cat "$SS_BLOCKLIST_FILE")" "$expected"
cat >"$SS_BLOCKLIST_FILE" <<'DATA'
address=/migrate-one.example/#
address=/migrate-two.example/0.0.0.0
server=/allowed.example/#
DATA
ss_reconcile_blocklist_file_for_dnsmasq "$SS_BLOCKLIST_FILE"
expected='safeshield-block=/migrate-one.example/
safeshield-block=/migrate-two.example/
server=/allowed.example/#'
ss_spec_assert_eq "$(cat "$SS_BLOCKLIST_FILE")" "$expected"
DNSMASQ_PATCHED=0
ss_reconcile_blocklist_file_for_dnsmasq "$SS_BLOCKLIST_FILE"
expected='address=/migrate-one.example/#
address=/migrate-two.example/#
server=/allowed.example/#'
ss_spec_assert_eq "$(cat "$SS_BLOCKLIST_FILE")" "$expected"
rm -f \
"$SS_TMP_DIR"/api.*.block.txt \
"$SS_TMP_DIR"/api.*.allow.txt \
"$SS_TMP_DIR"/local.block.txt \
"$SS_TMP_DIR"/local.allow.txt
printf '%s\n' 'fallback.example' >"$SS_TMP_DIR/api.0.block.txt"
ss_merge_lists
ss_spec_assert_eq "$(cat "$SS_BLOCKLIST_FILE")" 'address=/fallback.example/#'
DNSMASQ_PATCHED=1
cat >"$SS_BLOCKLIST_FILE" <<'DATA'
address=/legacy.example/0.0.0.0
address=/legacy.example/::
DATA
check_blocklist_rule_present 'legacy.example'
cat >"$SS_BLOCKLIST_FILE" <<'DATA'
address=/sample-one.example/#
address=/sample-two.example/#
address=/sample-three.example/#
DATA
sampled="$(find_test_domains 2)"
expected_sample='sample-one.example
sample-two.example'
ss_spec_assert_eq "$sampled" "$expected_sample"
RULE_CHECK_COUNT=0
DNS_CHECK_COUNT=0
check_blocklist_rule_present() {
RULE_CHECK_COUNT=$((RULE_CHECK_COUNT + 1))
return 1
}
check_domain_blocked() {
DNS_CHECK_COUNT=$((DNS_CHECK_COUNT + 1))
return 0
}
check_blocklist_applied_multi_with_stats 2 2
ss_spec_assert_eq "$RULE_CHECK_COUNT" '0'
ss_spec_assert_eq "$DNS_CHECK_COUNT" '2'
)
ss_case_multi_artifact() (
set -eu
TMP_DIR="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
SS_TMP_DIR="$TMP_DIR/tmp"
SS_API_PAYLOAD="$SS_TMP_DIR/resolve-request.json"
SS_API_RESPONSE="$SS_TMP_DIR/resolve-response.json"
SS_RESOLVED_SOURCES="$SS_TMP_DIR/resolved-sources.tsv"
SS_ARTIFACT_CACHE_STATE="$SS_TMP_DIR/artifact-sources.state"
SS_MAX_ARTIFACT_SOURCES=16
ss_license_key='test-license'
ss_download_retry=1
ss_download_timeout=10
ss_max_blocklist_file_size_kb=1024
mkdir -p "$SS_TMP_DIR"
ss_should_stop() { return 1; }
ss_status_set() { :; }
ss_status_add_error() { :; }
log_info() { :; }
log_error() { :; }
log_ok() { :; }
command_exists() { command -v "$1" >/dev/null 2>&1; }
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
ss_write_resolve_payload() { printf '{}\n' >"$1"; }
ss_http_post_json() { printf '{}\n' >"$3"; }
ss_json_get_file() {
case "$2" in
'@.artifact.sources[0].download_url') printf '%s\n' 'https://example.invalid/hagezi.txt' ;;
'@.artifact.sources[0].action') printf '%s\n' 'block' ;;
'@.artifact.sources[0].id') printf '%s\n' 'hagezi-pro' ;;
'@.artifact.sources[0].sha256') printf '%s\n' "$HAGEZI_SHA" ;;
'@.artifact.sources[1].download_url') printf '%s\n' 'https://example.invalid/smartsafehub-block.txt' ;;
'@.artifact.sources[1].action') printf '%s\n' 'block' ;;
'@.artifact.sources[1].id') printf '%s\n' 'smartsafehub-pro' ;;
'@.artifact.sources[1].sha256') printf '%s\n' "$SMART_BLOCK_SHA" ;;
'@.artifact.sources[2].download_url') printf '%s\n' 'https://example.invalid/smartsafehub-allow.txt' ;;
'@.artifact.sources[2].action') printf '%s\n' 'allow' ;;
'@.artifact.sources[2].id') printf '%s\n' 'smartsafehub-allow' ;;
'@.artifact.sources[2].sha256') printf '%s\n' "$SMART_ALLOW_SHA" ;;
'@.artifact.sources[3].download_url' | '@.artifact.sources[16].download_url' | '@.artifact.sha256') : ;;
'@.artifact.tier') printf '%s\n' 'pro' ;;
'@.artifact.version') printf '%s\n' '20260830T120000Z' ;;
'@.artifact.unique_domains' | '@.artifact.rules') printf '%s\n' '4' ;;
'@.license.plan') printf '%s\n' 'pro' ;;
'@.license.status') printf '%s\n' 'active' ;;
'@.device.profile') printf '%s\n' 'test-profile' ;;
*) : ;;
esac
}
cat >"$TMP_DIR/hagezi.txt" <<'DATA'
address=/ads.example/#
address=/shared.example/#
DATA
cat >"$TMP_DIR/smartsafehub-block.txt" <<'DATA'
phishing.example
shared.example
DATA
printf '%s\n' 'shared.example' >"$TMP_DIR/smartsafehub-allow.txt"
HAGEZI_SHA="$(sha256sum "$TMP_DIR/hagezi.txt" | awk '{print $1}')"
SMART_BLOCK_SHA="$(sha256sum "$TMP_DIR/smartsafehub-block.txt" | awk '{print $1}')"
SMART_ALLOW_SHA="$(sha256sum "$TMP_DIR/smartsafehub-allow.txt" | awk '{print $1}')"
export HAGEZI_SHA SMART_BLOCK_SHA SMART_ALLOW_SHA
ss_resolve_artifact
expected_sources="0|block|hagezi-pro|https://example.invalid/hagezi.txt|$HAGEZI_SHA
1|block|smartsafehub-pro|https://example.invalid/smartsafehub-block.txt|$SMART_BLOCK_SHA
2|allow|smartsafehub-allow|https://example.invalid/smartsafehub-allow.txt|$SMART_ALLOW_SHA"
ss_spec_assert_eq "$(cat "$SS_RESOLVED_SOURCES")" "$expected_sources"
ss_http_get_file() {
case "$1" in
'https://example.invalid/hagezi.txt') cp "$TMP_DIR/hagezi.txt" "$2" ;;
'https://example.invalid/smartsafehub-block.txt') cp "$TMP_DIR/smartsafehub-block.txt" "$2" ;;
'https://example.invalid/smartsafehub-allow.txt') cp "$TMP_DIR/smartsafehub-allow.txt" "$2" ;;
*) return 1 ;;
esac
}
ss_download_api_artifacts
ss_cached_api_sources_available
ss_spec_assert_eq "$(cat "$SS_TMP_DIR/api.0.block.txt")" 'ads.example
shared.example'
ss_spec_assert_eq "$(cat "$SS_TMP_DIR/api.1.block.txt")" 'phishing.example
shared.example'
ss_spec_assert_eq "$(cat "$SS_TMP_DIR/api.2.allow.txt")" 'shared.example'
ss_json_get_file() {
case "$2" in
'@.artifact.sources[0].download_url') : ;;
'@.artifact.download_url') printf '%s\n' 'https://example.invalid/legacy.txt' ;;
'@.artifact.sha256') printf '%s\n' 'legacy-sha' ;;
*) : ;;
esac
}
ss_resolve_artifact_sources "$SS_API_RESPONSE"
ss_spec_assert_eq "$(cat "$SS_RESOLVED_SOURCES")" '0|block|legacy|https://example.invalid/legacy.txt|legacy-sha'
)
ss_case_performance_paths() (
set -eu
TMP="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
SS_TMP_DIR="$TMP"
export SS_TMP_DIR
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
cat >"$TMP/api.0.block.txt" <<'EOF_BLOCK_0'
alpha.example
shared.example
EOF_BLOCK_0
cat >"$TMP/api.1.block.txt" <<'EOF_BLOCK_1'
beta.example
shared.example
EOF_BLOCK_1
ss_merge_sorted_api_sources block "$TMP/merged-blocks.txt"
expected='alpha.example
beta.example
shared.example'
ss_spec_assert_eq "$(cat "$TMP/merged-blocks.txt")" "$expected"
rm -f "$TMP"/api.*.block.txt
ss_merge_sorted_api_sources block "$TMP/empty-blocks.txt"
[ ! -s "$TMP/empty-blocks.txt" ]
BLOCKLIST="$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
STATISTICS_AGGREGATE="$SS_SPEC_ROOT/files/usr/lib/safeshield/statistics/30-aggregate.awk"
STATISTICS_OUTPUT="$SS_SPEC_ROOT/files/usr/lib/safeshield/statistics/50-output.awk"
ss_spec_assert_file_contains "$BLOCKLIST" 'sort -m -u "$@" >"$out"'
ss_spec_assert_file_contains "$STATISTICS_AGGREGATE" 'delete device_first_bucket[key]'
ss_spec_assert_file_contains "$STATISTICS_OUTPUT" 'device_first_hour = device_first_bucket[key] + 0'
ss_spec_assert_file_contains "$STATISTICS_OUTPUT" 'for (bucket = device_first_hour; bucket <= device_last_hour; bucket += 3600)'
)
ss_case_upgrade_required() (
set -eu
TMP_DIR="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
SS_TMP_DIR="$TMP_DIR/tmp"
SS_API_PAYLOAD="$SS_TMP_DIR/resolve-request.json"
SS_API_RESPONSE="$SS_TMP_DIR/resolve-response.json"
SS_RESOLVED_SOURCES="$SS_TMP_DIR/resolved-sources.tsv"
SS_ARTIFACT_CACHE_STATE="$SS_TMP_DIR/artifact-sources.state"
ss_license_key='test-license'
ss_download_retry=3
ss_download_timeout=10
ss_max_blocklist_file_size_kb=1024
mkdir -p "$SS_TMP_DIR"
ss_should_stop() { return 1; }
ss_status_set() { printf '%s=%s\n' "$1" "$2" >>"$TMP_DIR/status"; }
ss_status_add_error() { :; }
ss_status_add_warning() { :; }
log_info() { :; }
log_error() { :; }
log_ok() { :; }
command_exists() { return 1; }
sleep() { printf 'sleep\n' >>"$TMP_DIR/sleep"; }
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
ss_write_resolve_payload() { printf '{}\n' >"$1"; }
printf '%s\n' 'HTTP error 426' >"$TMP_DIR/uclient.log"
ss_spec_assert_eq "$(ss_http_status_from_uclient_log "$TMP_DIR/uclient.log")" '426'
command_exists() { [ "$1" = 'curl' ]; }
curl() {
out=''
while [ "$#" -gt 0 ]; do
if [ "$1" = '-o' ]; then
shift
out="$1"
fi
shift
done
[ -z "$out" ] || printf '%s\n' '{"code":"safeshield_upgrade_required"}' >"$out"
printf '%s' '426'
return 0
}
if ss_http_post_json 'https://example.invalid/resolve' "$SS_API_PAYLOAD" "$SS_API_RESPONSE"; then
return 1
fi
ss_spec_assert_eq "$SS_HTTP_STATUS" '426'
if ss_http_get_file 'https://example.invalid/artifact' "$SS_TMP_DIR/artifact.raw"; then
return 1
fi
ss_spec_assert_eq "$SS_HTTP_STATUS" '426'
command_exists() { return 1; }
RESOLVE_CALLS=0
ss_http_post_json() {
RESOLVE_CALLS=$((RESOLVE_CALLS + 1))
SS_HTTP_STATUS='426'
return 1
}
if ss_resolve_artifact; then
return 1
fi
ss_spec_assert_eq "$RESOLVE_CALLS" '1'
ss_spec_assert_eq "$SS_RESOLVE_ERROR_CODE" 'safeshield_upgrade_required'
ss_spec_assert_file_contains "$TMP_DIR/status" 'health_api_resolve=0'
ss_spec_assert_not_exists "$TMP_DIR/sleep"
printf '%s\n' '0|block|test-source|https://example.invalid/artifact.txt|' >"$SS_RESOLVED_SOURCES"
DOWNLOAD_CALLS=0
ss_http_get_file() {
DOWNLOAD_CALLS=$((DOWNLOAD_CALLS + 1))
SS_HTTP_STATUS='426'
return 1
}
if ss_download_api_artifacts; then
return 1
fi
ss_spec_assert_eq "$DOWNLOAD_CALLS" '1'
ss_spec_assert_eq "$SS_ARTIFACT_DOWNLOAD_ERROR_CODE" 'safeshield_upgrade_required'
ss_spec_assert_file_contains "$TMP_DIR/status" 'health_artifact_download=0'
ss_spec_assert_not_exists "$TMP_DIR/sleep"
)
ss_case_artifact_integrity() (
set -eu
TMP="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
SS_TMP_DIR="$TMP/runtime"
SS_RESOLVED_SOURCES="$SS_TMP_DIR/resolved-sources.tsv"
SS_ARTIFACT_CACHE_STATE="$SS_TMP_DIR/artifact-sources.state"
ss_download_retry=2
ss_download_timeout=10
ss_max_blocklist_file_size_kb=1024
mkdir -p "$SS_TMP_DIR"
STATUS="$TMP/status"
: >"$STATUS"
ss_should_stop() { return 1; }
ss_status_set() { printf 'set %s %s\n' "$1" "${2:-}" >>"$STATUS"; }
ss_status_add_error() { printf 'error %s\n' "$1" >>"$STATUS"; }
ss_status_add_warning() { printf 'warning %s\n' "$1" >>"$STATUS"; }
log_info() { :; }
log_error() { :; }
log_ok() { :; }
sleep() { :; }
command_exists() { command -v "$1" >/dev/null 2>&1; }
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
printf '%s\n' 'ads.example' 'shared.example' >"$TMP/source0.txt"
printf '%s\n' 'shared.example' >"$TMP/source1.txt"
SOURCE0_SHA="$(sha256sum "$TMP/source0.txt" | awk '{print $1}')"
printf '0|block|source-0|https://example.invalid/source0|%s\n' "$SOURCE0_SHA" >"$SS_RESOLVED_SOURCES"
printf '%s\n' '1|allow|source-1|https://example.invalid/source1|' >>"$SS_RESOLVED_SOURCES"
GET_CALLS=0
ss_http_get_file() {
GET_CALLS=$((GET_CALLS + 1))
SS_HTTP_STATUS=''
case "$1" in
https://example.invalid/source0)
if [ "$GET_CALLS" -eq 1 ]; then
return 1
fi
cp "$TMP/source0.txt" "$2"
;;
https://example.invalid/source1)
cp "$TMP/source1.txt" "$2"
;;
*) return 1 ;;
esac
}
ss_download_api_artifacts
ss_spec_assert_eq "$GET_CALLS" '3'
ss_cached_api_sources_available
ss_spec_assert_file_line "$STATUS" 'set health_artifact_download 1'
ss_spec_assert_file_line "$STATUS" 'set health_artifact_sha256 '
ss_spec_assert_file_line "$STATUS" 'warning artifact_sha256_partial'
ss_spec_assert_eq "$(cat "$SS_TMP_DIR/api.0.block.txt")" 'ads.example
shared.example'
ss_spec_assert_eq "$(cat "$SS_TMP_DIR/api.1.allow.txt")" 'shared.example'
: >"$STATUS"
printf '%s\n' '0|block|bad-sha|https://example.invalid/source0|0000000000000000000000000000000000000000000000000000000000000000' >"$SS_RESOLVED_SOURCES"
GET_CALLS=0
ss_http_get_file() {
GET_CALLS=$((GET_CALLS + 1))
SS_HTTP_STATUS=''
cp "$TMP/source0.txt" "$2"
}
! ss_download_api_artifacts
ss_spec_assert_file_line "$STATUS" 'set health_artifact_sha256 0'
ss_spec_assert_file_line "$STATUS" 'error artifact_sha256_mismatch'
ss_spec_assert_not_exists "$SS_ARTIFACT_CACHE_STATE"
: >"$STATUS"
command_exists() { return 1; }
! ss_verify_artifact_sha256 "$TMP/source0.txt" "$SOURCE0_SHA"
ss_spec_assert_file_line "$STATUS" 'set health_artifact_sha256 0'
ss_spec_assert_file_line "$STATUS" 'error sha256sum_not_found'
command_exists() { command -v "$1" >/dev/null 2>&1; }
: >"$STATUS"
printf '%s\n' '0|block|too-large|https://example.invalid/source0|' >"$SS_RESOLVED_SOURCES"
ss_max_blocklist_file_size_kb=1
ss_http_get_file() {
SS_HTTP_STATUS=''
cp "$TMP/source0.txt" "$2"
}
du() { printf '2\t%s\n' "$2"; }
! ss_download_api_artifacts
ss_spec_assert_file_line "$STATUS" 'set health_artifact_download 0'
ss_spec_assert_file_line "$STATUS" 'error artifact_too_large'
ss_spec_assert_not_exists "$SS_ARTIFACT_CACHE_STATE"
)
+40
View File
@@ -0,0 +1,40 @@
#!/bin/sh
# shellcheck shell=sh
SS_SPEC_ROOT="${SS_SPEC_ROOT:-$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)}"
ss_spec_tmpdir() {
mktemp -d
}
ss_spec_assert_eq() {
[ "$1" = "$2" ]
}
ss_spec_assert_ne() {
[ "$1" != "$2" ]
}
ss_spec_assert_file_contains() {
grep -Fq -- "$2" "$1"
}
ss_spec_assert_file_not_contains() {
! grep -Fq -- "$2" "$1"
}
ss_spec_assert_file_line() {
grep -Fxq -- "$2" "$1"
}
ss_spec_assert_exists() {
[ -e "$1" ]
}
ss_spec_assert_not_exists() {
[ ! -e "$1" ]
}
ss_spec_assert_nonempty() {
[ -s "$1" ]
}
+449
View File
@@ -0,0 +1,449 @@
#!/bin/sh
# shellcheck shell=sh
ss_case_utils() (
set -eu
PKG_NAME='safeshield-test'
export PKG_NAME
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/utils.sh"
command_exists sh
! command_exists safeshield-command-that-does-not-exist
ss_spec_assert_eq "$(ss_mask_secret '')" ''
ss_spec_assert_eq "$(ss_mask_secret '12345678')" '********'
ss_spec_assert_eq "$(ss_mask_secret 'abcd1234wxyz')" 'abcd****wxyz'
is_valid_integer 0
is_valid_integer 42
for value in '' -1 1.5 12x; do
! is_valid_integer "$value"
done
is_greater_equal 2.90 2.90
is_greater_equal 2.91 2.90
! is_greater_equal 2.79 2.80
)
ss_case_config_validation() (
set -eu
PKG_NAME='safeshield-test'
export PKG_NAME
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/utils.sh"
WARNINGS=''
CONFIG_GET_VALUE=''
log_warn() { :; }
ss_status_add_warning() {
WARNINGS="${WARNINGS}${WARNINGS:+ }$1"
}
config_get() {
eval "$1='${CONFIG_GET_VALUE}'"
}
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/config.sh"
CONFIG_GET_VALUE='configured'
ss_spec_assert_eq "$(ss_config_get config enabled default)" 'configured'
CONFIG_GET_VALUE=''
ss_spec_assert_eq "$(ss_config_get config enabled default)" 'default'
ss_download_timeout='invalid'
ss_download_retry='-1'
ss_max_blocklist_file_size_kb='1.5'
ss_min_valid_line_count=''
ss_pause_timeout='invalid'
ss_boot_start_delay_s='-5'
ss_statistics_snapshot_interval_s='9'
ss_statistics_retention_hours='169'
ss_compress_blocklist='2'
ss_initial_dnsmasq_restart='yes'
ss_dnsmasq_sanity_check='no'
ss_apply_local_overrides='2'
ss_statistics_enabled='off'
WARNINGS=''
ss_validate_config
ss_spec_assert_eq "$ss_download_timeout" '10'
ss_spec_assert_eq "$ss_download_retry" '3'
ss_spec_assert_eq "$ss_max_blocklist_file_size_kb" '30000'
ss_spec_assert_eq "$ss_min_valid_line_count" '3000'
ss_spec_assert_eq "$ss_pause_timeout" '20'
ss_spec_assert_eq "$ss_boot_start_delay_s" '30'
ss_spec_assert_eq "$ss_statistics_snapshot_interval_s" '60'
ss_spec_assert_eq "$ss_statistics_retention_hours" '168'
ss_spec_assert_eq "$ss_compress_blocklist" '0'
ss_spec_assert_eq "$ss_initial_dnsmasq_restart" '0'
ss_spec_assert_eq "$ss_dnsmasq_sanity_check" '1'
ss_spec_assert_eq "$ss_apply_local_overrides" '1'
ss_spec_assert_eq "$ss_statistics_enabled" '1'
for code in \
invalid_download_timeout \
invalid_download_retry \
invalid_max_blocklist_file_size_kb \
invalid_min_valid_line_count \
invalid_pause_timeout \
invalid_boot_start_delay_s \
invalid_statistics_snapshot_interval_s \
invalid_statistics_retention_hours \
invalid_compress_blocklist \
invalid_initial_dnsmasq_restart \
invalid_dnsmasq_sanity_check \
invalid_apply_local_overrides \
invalid_statistics_enabled; do
case " $WARNINGS " in
*" $code "*) ;;
*) return 1 ;;
esac
done
ss_download_timeout='1'
ss_download_retry='0'
ss_max_blocklist_file_size_kb='1'
ss_min_valid_line_count='0'
ss_pause_timeout='0'
ss_boot_start_delay_s='0'
ss_statistics_snapshot_interval_s='10'
ss_statistics_retention_hours='168'
ss_compress_blocklist='1'
ss_initial_dnsmasq_restart='1'
ss_dnsmasq_sanity_check='0'
ss_apply_local_overrides='0'
ss_statistics_enabled='0'
WARNINGS=''
ss_validate_config
[ -z "$WARNINGS" ]
ss_spec_assert_eq "$ss_statistics_snapshot_interval_s" '10'
ss_spec_assert_eq "$ss_statistics_retention_hours" '168'
)
ss_case_dnsmasq_version() (
set -eu
TMP_DIR="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
PKG_NAME='safeshield'
export PKG_NAME
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/utils.sh"
DNSMASQ_FEATURE_HEALTH=''
ss_status_set() {
[ "$1" = 'health_dnsmasq_features' ] && DNSMASQ_FEATURE_HEALTH="$2"
return 0
}
log_error() { :; }
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/dns.sh"
ss_spec_assert_file_not_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/dns.sh" 'smartsafehub'
make_dnsmasq() {
version="$1"
feature_rc="${2:-0}"
cat >"$TMP_DIR/dnsmasq" <<SCRIPT
#!/bin/sh
case "\${1:-}" in
--version)
printf '%s\n' 'Dnsmasq version ${version} Copyright (c) 2000-2026 Simon Kelley'
exit 0
;;
--test)
exit ${feature_rc}
;;
esac
exit 0
SCRIPT
chmod +x "$TMP_DIR/dnsmasq"
}
PATH="$TMP_DIR:$PATH"
export PATH
make_dnsmasq '2.93'
ss_spec_assert_eq "$(ss_dnsmasq_version)" '2.93'
ss_require_supported_dnsmasq
make_dnsmasq '2.94'
ss_require_supported_dnsmasq
make_dnsmasq '2.93rc1'
ss_spec_assert_eq "$(ss_dnsmasq_version)" '2.93'
ss_require_supported_dnsmasq
make_dnsmasq '2.92'
! ss_require_supported_dnsmasq
ss_spec_assert_eq "$(ss_dnsmasq_check_error)" 'dnsmasq_version_unsupported'
make_dnsmasq '2.93' 1
ss_require_supported_dnsmasq
ss_spec_assert_eq "$(ss_dnsmasq_check_error)" ''
ss_spec_assert_eq "$DNSMASQ_FEATURE_HEALTH" '0'
cat >"$TMP_DIR/dnsmasq" <<'SCRIPT'
#!/bin/sh
printf '%s\n' 'unexpected version output'
SCRIPT
chmod +x "$TMP_DIR/dnsmasq"
! ss_require_supported_dnsmasq
ss_spec_assert_eq "$(ss_dnsmasq_check_error)" 'dnsmasq_version_unknown'
rm -f "$TMP_DIR/dnsmasq"
ORIGINAL_PATH="$PATH"
PATH="$TMP_DIR"
! ss_require_supported_dnsmasq
PATH="$ORIGINAL_PATH"
ss_spec_assert_eq "$(ss_dnsmasq_check_error)" 'dnsmasq_binary_not_found'
)
ss_case_identity() (
set -eu
TMP_DIR="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
SS_IDENTITY_DIR="$TMP_DIR/identity"
SS_IDENTITY_FILE="$SS_IDENTITY_DIR/identity.env"
export SS_IDENTITY_DIR SS_IDENTITY_FILE
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/identity.sh"
ss_spec_assert_eq "$(ss_identity_normalize_mac 'MAC=AA:BB:CC:DD:EE:FF')" 'aa:bb:cc:dd:ee:ff'
ss_identity_valid_mac 'aa:bb:cc:dd:ee:ff'
for mac in '' '00:00:00:00:00:00' 'ff:ff:ff:ff:ff:ff' 'aa:bb:cc:dd:ee'; do
! ss_identity_valid_mac "$mac"
done
ss_spec_assert_eq "$(ss_identity_hex_to_mac 'AABBCCDDEEFF')" 'aa:bb:cc:dd:ee:ff'
ss_spec_assert_eq "$(ss_identity_profile_code 'iptime,ax3000sm')" 'iptime_ax3000sm'
ss_spec_assert_eq "$(ss_identity_profile_code 'glinet,gl-mt300n-v2')" 'gl_mt300n_v2'
ss_spec_assert_eq "$(ss_identity_profile_code 'xiaomi,mi-router-ax3000t')" 'xiaomi_mi_router_ax3000t'
ss_spec_assert_eq "$(ss_identity_profile_code 'smartsafehub,router')" 'smartsafehub'
ss_spec_assert_eq "$(ss_identity_device_code_from_board 'iptime,ax3000sm')" 'iptime-ax3000sm'
ss_spec_assert_eq "$(ss_identity_device_code_from_board 'glinet,gl-mt300n-v2')" 'gl-mt300n-v2'
ss_spec_assert_eq "$(ss_identity_device_code_from_board 'xiaomi,mi-router-ax3000t')" 'xiaomi-ax3000t'
ss_spec_assert_eq "$(ss_identity_device_code_from_board 'xiaomi,mi-router-ax3000t-ubootmod')" 'xiaomi-ax3000t'
! ss_identity_device_code_from_board 'vendor,unsupported-router'
ss_identity_valid_device_code 'xiaomi-ax3000t'
! ss_identity_valid_device_code 'Xiaomi AX3000T'
SS_SMARTSAFEHUB_FIRMWARE_JSON="$TMP_DIR/firmware.json"
export SS_SMARTSAFEHUB_FIRMWARE_JSON
printf '%s\n' '{"device_code":"xiaomi-ax3000t"}' >"$SS_SMARTSAFEHUB_FIRMWARE_JSON"
MOCK_DEVICE_CODE='xiaomi-ax3000t'
jsonfilter() { printf '%s\n' "$MOCK_DEVICE_CODE"; }
ss_identity_board_name() { printf '%s' 'iptime,ax3000sm'; }
ss_identity_refresh_device_code
ss_spec_assert_eq "$SS_DEVICE_CODE" 'xiaomi-ax3000t'
ss_spec_assert_eq "$SS_DEVICE_CODE_SOURCE" 'smartsafehub_firmware'
MOCK_DEVICE_CODE='invalid device code'
ss_identity_refresh_device_code
ss_spec_assert_eq "$SS_DEVICE_CODE" 'iptime-ax3000sm'
ss_spec_assert_eq "$SS_DEVICE_CODE_SOURCE" 'board'
rm -f "$SS_SMARTSAFEHUB_FIRMWARE_JSON"
ss_identity_board_name() { printf '%s' 'vendor,unsupported-router'; }
ss_identity_refresh_device_code
ss_spec_assert_eq "$SS_DEVICE_CODE" ''
ss_spec_assert_eq "$SS_DEVICE_CODE_SOURCE" 'unknown'
ss_identity_board_name() { printf '%s' 'iptime,ax3000sm'; }
ss_identity_try_mtd_factory_mac() { printf '%s' 'aa:bb:cc:dd:ee:ff|mtd:Factory:0x4:mac'; }
ss_identity_detect_primary_mac() { return 1; }
ss_identity_compute_physical 'ipTIME AX3000SM' 'aarch64'
ss_spec_assert_eq "$SS_IDENTITY_PROVIDER" 'factory_mac'
ss_spec_assert_eq "$SS_IDENTITY_SOURCE" 'mtd:Factory:0x4:mac'
ss_spec_assert_eq "$SS_IDENTITY_STRENGTH" 'hardware_soft'
ss_spec_assert_eq "$SS_IDENTITY_PROFILE" 'iptime_ax3000sm'
ss_identity_valid_sha256 "$SS_IDENTITY_VALUE_SHA256"
ss_identity_valid_sha256 "$SS_PHYSICAL_FINGERPRINT"
SS_INSTALLATION_ID='11111111-2222-3333-4444-555555555555'
SS_INSTALLATION_SECRET='0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
SS_IDENTITY_CREATED_AT='1788090000'
SS_IDENTITY_UPDATED_AT='1788090001'
ss_identity_write_env
ss_spec_assert_nonempty "$SS_IDENTITY_FILE"
identity_mode="$(LC_ALL=C ls -ld "$SS_IDENTITY_FILE" | awk '{ print substr($1, 2, 9) }')"
ss_spec_assert_eq "$identity_mode" 'rw-------'
expected_fingerprint="$SS_PHYSICAL_FINGERPRINT"
SS_PHYSICAL_FINGERPRINT=''
SS_IDENTITY_PROVIDER=''
SS_IDENTITY_SOURCE=''
SS_IDENTITY_STRENGTH=''
SS_IDENTITY_PROFILE=''
SS_INSTALLATION_ID=''
ss_identity_load
ss_spec_assert_eq "$SS_PHYSICAL_FINGERPRINT" "$expected_fingerprint"
ss_spec_assert_eq "$SS_IDENTITY_PROVIDER" 'factory_mac'
ss_spec_assert_eq "$SS_INSTALLATION_ID" '11111111-2222-3333-4444-555555555555'
SYNCED=0
STATUS_SYNCED=0
ss_identity_sync_uci() { SYNCED=1; }
ss_identity_status_set() { STATUS_SYNCED=1; }
ss_identity_create() { return 97; }
ss_identity_ensure 'ipTIME AX3000SM' 'aarch64'
ss_spec_assert_eq "$SYNCED" '1'
ss_spec_assert_eq "$STATUS_SYNCED" '1'
)
ss_case_resolve_payload() (
set -eu
TMP_DIR="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
ss_detect_device_model() { printf '%s' 'Test Router'; }
ss_detect_device_vendor() { printf '%s' 'TestVendor'; }
ss_detect_device_arch() { printf '%s' 'test_arch'; }
ss_detect_device_memory_mb() { printf '%s' '128'; }
SS_PHYSICAL_FINGERPRINT='test-fingerprint'
SS_FINGERPRINT_VERSION='1'
SS_IDENTITY_PROVIDER='factory_mac'
SS_IDENTITY_SOURCE='test-source'
SS_IDENTITY_STRENGTH='hardware_soft'
SS_IDENTITY_PROFILE='test-profile'
SS_DEVICE_CODE='test-router'
SS_DEVICE_CODE_SOURCE='board'
SS_INSTALLATION_ID='test-installation'
ss_identity_ensure() { return 0; }
is_valid_integer() {
case "$1" in
'' | *[!0-9]*) return 1 ;;
*) return 0 ;;
esac
}
ss_status_set() { :; }
ss_license_key='test-license'
SS_VERSION_FILE="$TMP_DIR/version"
export SS_VERSION_FILE
printf '%s\n' '0.3.18-r1' >"$SS_VERSION_FILE"
payload="$TMP_DIR/resolve-request.json"
ss_write_resolve_payload "$payload"
ss_spec_assert_file_contains "$payload" ' "safeshield_version": "0.3.18-r1"'
! grep -Eq '^ "safeshield_version"' "$payload"
ss_spec_assert_file_contains "$payload" '"license_key": "test-license"'
ss_spec_assert_file_contains "$payload" '"physical_fingerprint": "test-fingerprint"'
ss_spec_assert_file_contains "$payload" '"fingerprint_version": 1'
ss_spec_assert_file_contains "$payload" '"identity_provider": "factory_mac"'
ss_spec_assert_file_contains "$payload" '"identity_source": "test-source"'
ss_spec_assert_file_contains "$payload" '"identity_strength": "hardware_soft"'
ss_spec_assert_file_contains "$payload" '"identity_profile": "test-profile"'
ss_spec_assert_file_contains "$payload" '"device_code": "test-router"'
ss_spec_assert_file_contains "$payload" '"device_code_source": "board"'
ss_spec_assert_file_contains "$payload" '"installation_id": "test-installation"'
ss_spec_assert_file_contains "$payload" '"vendor": "TestVendor"'
ss_spec_assert_file_contains "$payload" '"model": "Test Router"'
ss_spec_assert_file_contains "$payload" '"arch": "test_arch"'
ss_spec_assert_file_contains "$payload" '"memory_mb": 128'
ss_detect_device_memory_mb() { printf '%s' 'invalid-memory'; }
invalid_memory_payload="$TMP_DIR/resolve-request-invalid-memory.json"
ss_write_resolve_payload "$invalid_memory_payload"
ss_spec_assert_file_contains "$invalid_memory_payload" '"memory_mb": 0'
SS_VERSION_FILE="$TMP_DIR/missing-version"
export SS_VERSION_FILE
fallback_payload="$TMP_DIR/resolve-request-fallback.json"
ss_write_resolve_payload "$fallback_payload"
ss_spec_assert_file_contains "$fallback_payload" ' "safeshield_version": "unknown"'
! grep -Eq '^ "safeshield_version"' "$fallback_payload"
)
ss_case_rpcd_contract() (
set -eu
ENTRY="$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield.uc"
ACL="$SS_SPEC_ROOT/files/usr/share/rpcd/acl.d/safeshield.json"
MAKEFILE="$SS_SPEC_ROOT/Makefile"
STATISTICS_MODULE="$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield/statistics.uc"
[ -f "$ENTRY" ]
[ -f "$ACL" ]
ss_spec_assert_file_contains "$ENTRY" "unshift(REQUIRE_SEARCH_PATH, '/usr/share/rpcd/ucode/safeshield/*.uc');"
for module in status config refresh rules license statistics; do
ss_spec_assert_file_contains "$ENTRY" "let $module = require('$module');"
done
for method in status config statistics config_update set_enabled refresh rules_list rule_add rule_delete license_get license_update; do
ss_spec_assert_file_contains "$ENTRY" " $method: {"
ss_spec_assert_file_contains "$ACL" "\"$method\""
done
ss_spec_assert_file_contains "$MAKEFILE" './files/usr/share/rpcd/ucode/safeshield/*.uc'
ss_spec_assert_file_contains "$MAKEFILE" './files/usr/share/rpcd/acl.d/safeshield.json'
ss_spec_assert_file_contains "$STATISTICS_MODULE" 'effective_snapshot_interval_s: to_int(data.snapshot_interval_s, snapshot_interval_s)'
)
ss_case_status_state() (
set -eu
TMP_DIR="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
RUNNING_STATUS_FILE="$TMP_DIR/status.json"
SS_DNSMASQ_DIR="$TMP_DIR/dnsmasq.d"
SS_BLOCKLIST_FILE="$SS_DNSMASQ_DIR/safeshield.blocklist"
SS_PREV_BLOCKLIST_GZ="$TMP_DIR/previous.gz"
export RUNNING_STATUS_FILE SS_DNSMASQ_DIR SS_BLOCKLIST_FILE SS_PREV_BLOCKLIST_GZ
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh"
CALLS="$TMP_DIR/calls"
: >"$CALLS"
record() { printf '%s\t%s\t%s\n' "$1" "${2:-}" "${3:-}" >>"$CALLS"; }
ss_status_set() { record set "$1" "$2"; }
ss_status_add_error() { record error "$1"; }
ss_status_apply() {
record apply "$1" "${2:-}"
return 0
}
ss_now() { printf '%s' '1000'; }
is_valid_integer() {
case "$1" in '' | *[!0-9]*) return 1 ;; esac
[ "$1" -ge 0 ] 2>/dev/null
}
ss_status_mark_failure 'artifact_download_failed'
grep -F "$(printf 'set\tstatus\terror')" "$CALLS" >/dev/null
grep -F "$(printf 'set\tlast_failure\t1000')" "$CALLS" >/dev/null
grep -F "$(printf 'set\tlast_result\terror')" "$CALLS" >/dev/null
grep -F "$(printf 'set\tlast_error_code\tartifact_download_failed')" "$CALLS" >/dev/null
grep -F "$(printf 'error\tartifact_download_failed')" "$CALLS" >/dev/null
: >"$CALLS"
ss_status_mark_success 300
grep -F "$(printf 'apply\tclear_messages')" "$CALLS" >/dev/null
grep -F "$(printf 'set\tstatus\tready')" "$CALLS" >/dev/null
grep -F "$(printf 'set\tstage\tdone')" "$CALLS" >/dev/null
grep -F "$(printf 'set\tlast_success\t1000')" "$CALLS" >/dev/null
grep -F "$(printf 'set\tnext_refresh_at\t1300')" "$CALLS" >/dev/null
: >"$CALLS"
ss_status_mark_success invalid
! grep -F "$(printf 'set\tnext_refresh_at')" "$CALLS" >/dev/null
: >"$CALLS"
ss_status_reset_health_fields
for key in health_dnsmasq_binary health_dns_runtime health_artifact_download dnsmasq_min_version; do
grep -F "$(printf 'set\t%s\t' "$key")" "$CALLS" >/dev/null
done
: >"$CALLS"
ss_status_reset_blocklist_fields
for key in blocklist_installed blocklist_file_size_kb blocklist_verification_ok blocklist_backup_available; do
grep -F "$(printf 'set\t%s\t' "$key")" "$CALLS" >/dev/null
done
: >"$CALLS"
ss_status_reset_artifact_fields
for key in license_plan artifact_tier artifact_source_count artifact_allow_source_count; do
grep -F "$(printf 'set\t%s\t' "$key")" "$CALLS" >/dev/null
done
mkdir -p "$SS_DNSMASQ_DIR"
printf '%s\n' 'address=/ads.example/#' >"$TMP_DIR/new.blocklist"
ss_sync_path() { :; }
ss_install_blocklist_atomic "$TMP_DIR/new.blocklist" "$SS_BLOCKLIST_FILE"
ss_spec_assert_eq "$(cat "$SS_BLOCKLIST_FILE")" 'address=/ads.example/#'
command_exists() { command -v "$1" >/dev/null 2>&1; }
ss_export_existing_blocklist
ss_spec_assert_nonempty "$SS_PREV_BLOCKLIST_GZ"
rm -f "$SS_BLOCKLIST_FILE"
ss_restore_previous_blocklist
ss_spec_assert_eq "$(cat "$SS_BLOCKLIST_FILE")" 'address=/ads.example/#'
)
ss_case_status_version() (
set -eu
make_version="$(sed -n 's/^PKG_VERSION:=//p' "$SS_SPEC_ROOT/Makefile")"
make_release="$(sed -n 's/^PKG_RELEASE:=//p' "$SS_SPEC_ROOT/Makefile")"
init_version="$(sed -n "s/^readonly PKG_VERSION='\([^']*\)'/\1/p" "$SS_SPEC_ROOT/files/etc/init.d/safeshield")"
[ -n "$make_version" ]
[ -n "$make_release" ]
[ -n "$init_version" ]
ss_spec_assert_eq "$init_version" "${make_version}-r${make_release}"
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/etc/init.d/safeshield" "printf '%s\\n' \"\$PKG_VERSION\""
ss_spec_assert_file_contains "$SS_SPEC_ROOT/Makefile" "echo '\$(PKG_VERSION)-r\$(PKG_RELEASE)' > \$(1)/usr/lib/safeshield/version"
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield/core.uc" 'fs.readfile(`/usr/lib/${PKG_NAME}/version`)'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield/status.uc" 'version: PKG_VERSION,'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'safeshield_upgrade_required'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'failure_code="$(ss_resolve_error_code)"'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'failure_code="$(ss_artifact_download_error_code)"'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" 'if [ "$failure_code" != "safeshield_upgrade_required" ]; then'
)
+535
View File
@@ -0,0 +1,535 @@
#!/bin/sh
# shellcheck shell=sh
ss_case_refresh_state_machine() (
set -eu
TMP="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
CORE="$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh"
HARNESS="$TMP/core-state-machine.sh"
sed -n '/^safeshield_apply_local_rules() {/,${p;}' "$CORE" >"$HARNESS"
# shellcheck disable=SC1090
. "$HARNESS"
SS_TMP_DIR="$TMP/runtime"
SS_DNSMASQ_DIR="$TMP/dnsmasq.d"
SS_BLOCKLIST_FILE="$SS_DNSMASQ_DIR/safeshield.blocklist"
SS_INACTIVE_BLOCKLIST_FILE="$SS_TMP_DIR/inactive.blocklist"
SS_PREV_BLOCKLIST_GZ="$TMP/previous.gz"
mkdir -p "$SS_TMP_DIR" "$SS_DNSMASQ_DIR"
CALLS="$TMP/calls"
: >"$CALLS"
record() { printf '%s\n' "$*" >>"$CALLS"; }
log_info() { :; }
log_warn() { :; }
log_error() { :; }
log_ok() { :; }
ss_status_prepare_refresh() { record status_prepare_refresh; }
ss_status_set() { record "status_set $1 ${2:-}"; }
ss_status_set_now() { record "status_set_now $1"; }
ss_status_add_error() { record "status_error $1"; }
ss_status_mark_failure() { record "failure $1"; }
ss_status_mark_success() { record "success $1"; }
ss_load_config() {
ss_enabled=1
export ss_initial_dnsmasq_restart=0
ss_apply_local_overrides=1
return 0
}
ss_config_get() {
case "$2" in
refresh_interval_s) printf '%s\n' '300' ;;
*) printf '%s\n' "${3:-}" ;;
esac
}
is_valid_integer() { case "$1" in '' | *[!0-9]*) return 1 ;; *) return 0 ;; esac }
ss_sync_detected_device_config() { record sync_device_config; }
ss_identity_ensure() {
record identity_ensure
[ "${FAIL_STAGE:-}" != 'identity' ]
}
ss_detect_device_model() { printf '%s\n' 'Test Router'; }
ss_detect_device_arch() { printf '%s\n' 'test-arch'; }
ss_should_stop() { return 1; }
ss_mkdirs() {
record mkdirs
[ "${FAIL_STAGE:-}" != 'mkdir' ]
}
ss_clean_tmp() { record clean_tmp; }
ss_require_supported_dnsmasq() {
record require_dnsmasq
[ "${FAIL_STAGE:-}" != 'dnsmasq_version' ]
}
ss_dnsmasq_check_error() { printf '%s\n' 'dnsmasq_version_unsupported'; }
ss_ensure_dnsmasq_confdir() {
record ensure_confdir
[ "${FAIL_STAGE:-}" != 'confdir' ]
}
ss_export_existing_blocklist() {
record export_blocklist
printf '%s\n' 'backup' >"$SS_PREV_BLOCKLIST_GZ"
return 0
}
DNSMASQ_CALL=0
dnsmasq_restart() {
DNSMASQ_CALL=$((DNSMASQ_CALL + 1))
record "dnsmasq_restart $DNSMASQ_CALL"
[ "${FAIL_STAGE:-}" != 'restart' ]
}
ss_resolve_artifact() {
record resolve_artifact
[ "${FAIL_STAGE:-}" != 'resolve' ]
}
ss_resolve_error_code() { printf '%s\n' "${RESOLVE_ERROR_CODE:-artifact_resolve_failed}"; }
ss_download_api_artifacts() {
record download_artifacts
[ "${FAIL_STAGE:-}" != 'download' ]
}
ss_artifact_download_error_code() { printf '%s\n' "${DOWNLOAD_ERROR_CODE:-artifact_download_failed}"; }
ss_prepare_local_rule_files() {
record prepare_local_rules
[ "${FAIL_STAGE:-}" != 'local_rules' ]
}
ss_merge_lists() {
record merge_lists
[ "${FAIL_STAGE:-}" != 'merge' ]
}
ss_install_blocklist() {
record install_blocklist
[ "${FAIL_STAGE:-}" != 'install' ]
}
check_dns_runtime() {
record check_dns_runtime
[ "${FAIL_STAGE:-}" != 'runtime' ]
}
check_blocklist_applied_multi_with_stats() {
record "verify_blocklist $*"
[ "${FAIL_STAGE:-}" != 'verify' ]
}
ss_restore_and_restart() { record restore_and_restart; }
ss_refresh_lock_open() {
record lock_open
[ "${LOCK_AVAILABLE:-1}" = '1' ]
}
ss_refresh_lock_close() { record lock_close; }
ss_local_rules_fingerprint_from_tmp() { printf '%s\n' 'fingerprint-new'; }
ss_local_rules_state_write() { record "local_state_write $1"; }
ss_abort_refresh() {
record abort_refresh
return 130
}
assert_refresh_failure() {
stage="$1"
code="$2"
expect_restore="$3"
FAIL_STAGE="$stage"
RESOLVE_ERROR_CODE='artifact_resolve_failed'
DOWNLOAD_ERROR_CODE='artifact_download_failed'
DNSMASQ_CALL=0
: >"$CALLS"
if safeshield_force_download; then
return 1
fi
ss_spec_assert_file_line "$CALLS" "failure $code"
ss_spec_assert_file_line "$CALLS" 'lock_close'
if [ "$expect_restore" = '1' ]; then
ss_spec_assert_file_line "$CALLS" 'restore_and_restart'
else
! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null
fi
}
FAIL_STAGE=''
LOCK_AVAILABLE=1
: >"$CALLS"
safeshield_force_download
ss_spec_assert_file_line "$CALLS" 'status_set stage resolve_api'
ss_spec_assert_file_line "$CALLS" 'status_set stage download_artifact'
ss_spec_assert_file_line "$CALLS" 'status_set stage merge'
ss_spec_assert_file_line "$CALLS" 'status_set stage install'
ss_spec_assert_file_line "$CALLS" 'status_set stage restart_dnsmasq'
ss_spec_assert_file_line "$CALLS" 'status_set stage runtime_check'
ss_spec_assert_file_line "$CALLS" 'status_set stage blocklist_verify'
ss_spec_assert_file_line "$CALLS" 'success 300'
ss_spec_assert_file_line "$CALLS" 'local_state_write fingerprint-new'
ss_spec_assert_file_line "$CALLS" 'lock_close'
! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null
assert_refresh_failure merge merge_failed 1
assert_refresh_failure install install_failed 1
assert_refresh_failure restart dnsmasq_restart_failed 1
assert_refresh_failure runtime dns_runtime_check_failed 1
assert_refresh_failure verify blocklist_verification_failed 1
FAIL_STAGE='resolve'
RESOLVE_ERROR_CODE='safeshield_upgrade_required'
: >"$CALLS"
! safeshield_force_download
ss_spec_assert_file_line "$CALLS" 'failure safeshield_upgrade_required'
! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null
FAIL_STAGE='download'
DOWNLOAD_ERROR_CODE='safeshield_upgrade_required'
: >"$CALLS"
! safeshield_force_download
ss_spec_assert_file_line "$CALLS" 'failure safeshield_upgrade_required'
! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null
FAIL_STAGE=''
LOCK_AVAILABLE=0
: >"$CALLS"
safeshield_force_download
ss_spec_assert_file_line "$CALLS" 'lock_open'
! grep -F 'status_prepare_refresh' "$CALLS" >/dev/null
)
ss_case_local_apply_state_machine() (
set -eu
TMP="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
CORE="$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh"
HARNESS="$TMP/local-apply.sh"
sed -n '/^safeshield_apply_local_rules() {/,/^safeshield_force_download() {/p' "$CORE" | sed '$d' >"$HARNESS"
# shellcheck disable=SC1090
. "$HARNESS"
SS_TMP_DIR="$TMP/runtime"
SS_DNSMASQ_DIR="$TMP/dnsmasq.d"
SS_BLOCKLIST_FILE="$SS_DNSMASQ_DIR/safeshield.blocklist"
SS_PREV_BLOCKLIST_GZ="$TMP/previous.gz"
SS_INACTIVE_BLOCKLIST_FILE="$SS_TMP_DIR/inactive.blocklist"
mkdir -p "$SS_TMP_DIR" "$SS_DNSMASQ_DIR"
CALLS="$TMP/calls"
: >"$CALLS"
record() { printf '%s\n' "$*" >>"$CALLS"; }
log_info() { :; }
log_warn() { :; }
log_error() { :; }
log_ok() { :; }
sleep() { :; }
ss_load_config() {
ss_enabled="${MOCK_ENABLED:-1}"
export ss_apply_local_overrides="${MOCK_OVERRIDES:-1}"
return 0
}
ss_mkdirs() { return 0; }
ss_refresh_lock_open_wait() {
record "lock_wait $1"
return "${LOCK_RC:-0}"
}
ss_refresh_lock_close() { record lock_close; }
ss_cached_api_sources_available() { [ "${CACHE_AVAILABLE:-1}" = '1' ]; }
ss_status_set() { record "status_set $1 ${2:-}"; }
ss_status_set_now() { record "status_set_now $1"; }
ss_status_add_error() { record "status_error $1"; }
ss_prepare_local_rule_files() {
record prepare_local_rules
[ "${LOCAL_FAIL:-}" != 'local_rules' ]
}
ss_local_rules_fingerprint_from_tmp() { printf '%s\n' "${NEW_FINGERPRINT:-fingerprint-new}"; }
ss_local_rules_state_read() { [ -n "${APPLIED_FINGERPRINT:-}" ] && printf '%s\n' "$APPLIED_FINGERPRINT"; }
ss_local_rules_state_write() { record "local_state_write $1"; }
ss_require_supported_dnsmasq() { [ "${LOCAL_FAIL:-}" != 'dnsmasq_version' ]; }
ss_dnsmasq_check_error() { printf '%s\n' 'dnsmasq_version_unsupported'; }
ss_ensure_dnsmasq_confdir() {
record ensure_confdir
[ "${LOCAL_FAIL:-}" != 'confdir' ]
}
ss_export_existing_blocklist() {
printf '%s\n' backup >"$SS_PREV_BLOCKLIST_GZ"
return 0
}
ss_merge_lists() {
record merge_lists
[ "${LOCAL_FAIL:-}" != 'merge' ]
}
dnsmasq_restart() {
record dnsmasq_restart
[ "${LOCAL_FAIL:-}" != 'restart' ]
}
check_dns_runtime() {
record check_dns_runtime
[ "${LOCAL_FAIL:-}" != 'runtime' ]
}
check_blocklist_applied_multi_with_stats() {
record "verify_blocklist $*"
[ "${LOCAL_FAIL:-}" != 'verify' ]
}
ss_restore_and_restart() { record restore_and_restart; }
ss_mark_local_apply_failure() { record "local_failure $1"; }
ss_mark_local_apply_success() { record local_success; }
FALLBACK_RC=0
safeshield_force_download() {
record fallback_refresh
return "$FALLBACK_RC"
}
CACHE_AVAILABLE=0
LOCK_RC=0
LOCAL_FAIL=''
: >"$CALLS"
safeshield_apply_local_rules
ss_spec_assert_file_line "$CALLS" 'fallback_refresh'
ss_spec_assert_file_line "$CALLS" 'lock_close'
CACHE_AVAILABLE=1
LOCK_RC=1
: >"$CALLS"
! safeshield_apply_local_rules
ss_spec_assert_file_line "$CALLS" 'local_failure local_apply_lock_timeout'
LOCK_RC=130
: >"$CALLS"
set +e
safeshield_apply_local_rules
rc=$?
set -e
ss_spec_assert_eq "$rc" '130'
! grep -F 'local_failure' "$CALLS" >/dev/null
LOCK_RC=0
APPLIED_FINGERPRINT='fingerprint-new'
NEW_FINGERPRINT='fingerprint-new'
printf '%s\n' 'safeshield-block=/cached.example/' >"$SS_BLOCKLIST_FILE"
: >"$CALLS"
safeshield_apply_local_rules
ss_spec_assert_file_line "$CALLS" 'status_set stage done'
! grep -Fx 'merge_lists' "$CALLS" >/dev/null
! grep -Fx 'dnsmasq_restart' "$CALLS" >/dev/null
APPLIED_FINGERPRINT='fingerprint-old'
NEW_FINGERPRINT='fingerprint-new'
for scenario in merge restart runtime verify; do
LOCAL_FAIL="$scenario"
: >"$CALLS"
! safeshield_apply_local_rules
case "$scenario" in
merge) code='local_merge_failed' ;;
restart) code='local_dnsmasq_restart_failed' ;;
runtime) code='local_dns_runtime_check_failed' ;;
verify) code='local_blocklist_verification_failed' ;;
esac
ss_spec_assert_file_line "$CALLS" "local_failure $code"
ss_spec_assert_file_line "$CALLS" 'restore_and_restart'
ss_spec_assert_file_line "$CALLS" 'lock_close'
done
LOCAL_FAIL=''
: >"$CALLS"
safeshield_apply_local_rules
ss_spec_assert_file_line "$CALLS" 'merge_lists'
ss_spec_assert_file_line "$CALLS" 'dnsmasq_restart'
ss_spec_assert_file_line "$CALLS" 'check_dns_runtime'
ss_spec_assert_file_line "$CALLS" 'verify_blocklist 5 3'
ss_spec_assert_file_line "$CALLS" 'local_state_write fingerprint-new'
ss_spec_assert_file_line "$CALLS" 'local_success'
! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null
)
ss_case_refresh_locking() (
set -eu
TMP="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
RUNNING_STATUS_FILE="$TMP/status.json"
SS_REFRESH_LOCK="$TMP/safeshield-refresh.lock"
SS_REFRESH_LOCK_FD=9
MOCK_FLOCK_DIR="$TMP/mock-flock.lock"
# Keep this regression test portable on development hosts such as macOS,
# where flock(1) is not installed by default. The production code still
# invokes flock -n/-u; this shim emulates those operations with atomic
# mkdir/rmdir so contention, release, timeout, and termination paths are
# exercised without depending on a host-specific flock binary.
flock() {
case "${1:-}" in
-n)
mkdir "$MOCK_FLOCK_DIR" 2>/dev/null
;;
-u)
rmdir "$MOCK_FLOCK_DIR" 2>/dev/null || true
return 0
;;
*)
return 2
;;
esac
}
# shellcheck disable=SC1091
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh"
is_valid_integer() { case "$1" in '' | *[!0-9]*) return 1 ;; *) return 0 ;; esac }
ss_should_stop() { return 1; }
mkdir "$MOCK_FLOCK_DIR"
! ss_refresh_lock_open
! ss_refresh_lock_open_wait 0
rmdir "$MOCK_FLOCK_DIR"
ss_refresh_lock_open
ss_spec_assert_exists "$MOCK_FLOCK_DIR"
ss_refresh_lock_close
ss_spec_assert_not_exists "$MOCK_FLOCK_DIR"
mkdir "$MOCK_FLOCK_DIR"
ss_should_stop() { return 0; }
set +e
ss_refresh_lock_open_wait 10
rc=$?
set -e
rmdir "$MOCK_FLOCK_DIR"
ss_spec_assert_eq "$rc" '130'
)
ss_case_service_lifecycle() (
set -eu
TMP="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
INIT="$SS_SPEC_ROOT/files/etc/init.d/safeshield"
HARNESS="$TMP/init-lifecycle.sh"
sed -n '/^start_service() {/,/^reconcile_statistics() {/p' "$INIT" | sed '$d' >"$HARNESS"
# shellcheck disable=SC1090
. "$HARNESS"
export PKG_NAME='safeshield'
SS_BLOCKLIST_FILE="$TMP/dnsmasq.d/safeshield.blocklist"
export SS_INACTIVE_BLOCKLIST_FILE="$TMP/inactive.blocklist"
mkdir -p "${SS_BLOCKLIST_FILE%/*}"
CALLS="$TMP/calls"
: >"$CALLS"
record() { printf '%s\n' "$*" >>"$CALLS"; }
log_error() { :; }
log_warn() { :; }
ss_load_config() {
export ss_enabled="${MOCK_ENABLED:-1}"
export ss_statistics_enabled="${MOCK_STATISTICS:-1}"
return "${LOAD_RC:-0}"
}
ss_identity_ensure() {
record identity_ensure
return 0
}
ss_detect_device_model() { printf '%s\n' 'Test Router'; }
ss_detect_device_arch() { printf '%s\n' 'test-arch'; }
ss_mkdirs() {
record mkdirs
return 0
}
ss_statistics_request_rebaseline() {
record request_rebaseline
return 0
}
ss_statistics_configure_dnsmasq() {
record "statistics_configure $1"
printf '%s\n' "${STATISTICS_CHANGED:-0}"
}
ss_runtime_deactivate_blocklist() {
record deactivate_blocklist
return "${DEACTIVATE_RC:-0}"
}
ss_runtime_activate_blocklist() {
record activate_blocklist
return "${ACTIVATE_RC:-0}"
}
ss_status_set() { record "status_set $1 ${2:-}"; }
ss_status_mark_failure() { record "failure $1"; }
ss_require_supported_dnsmasq() {
record require_dnsmasq
return 0
}
ss_dnsmasq_check_error() { printf '%s\n' 'dnsmasq_version_unsupported'; }
ss_statistics_disable_if_dnsmasq_unpatched() {
record statistics_guard
return 0
}
ss_reconcile_installed_blocklists_for_dnsmasq() {
record reconcile_blocklists
return "${MIGRATION_RC:-0}"
}
ss_ensure_dnsmasq_confdir() {
record ensure_confdir
return 0
}
dnsmasq_restart() {
record dnsmasq_restart
return "${DNSMASQ_RC:-0}"
}
procd_open_instance() { record procd_open_instance; }
procd_set_param() { record "procd_set_param $*"; }
procd_close_instance() { record procd_close_instance; }
ss_statistics_add_procd_instance() { record add_statistics_instance; }
ss_statistics_add_upload_procd_instance() { record add_statistics_upload_instance; }
MOCK_ENABLED=0
MOCK_STATISTICS=1
STATISTICS_CHANGED=1
MIGRATION_RC=0
DNSMASQ_RC=0
: >"$CALLS"
start_service
ss_spec_assert_file_line "$CALLS" 'request_rebaseline'
ss_spec_assert_file_line "$CALLS" 'statistics_configure 0'
ss_spec_assert_file_line "$CALLS" 'deactivate_blocklist'
ss_spec_assert_file_line "$CALLS" 'dnsmasq_restart'
ss_spec_assert_file_line "$CALLS" 'status_set stage disabled'
! grep -Fx 'procd_open_instance' "$CALLS" >/dev/null
printf '%s\n' 'safeshield-block=/active.example/' >"$SS_BLOCKLIST_FILE"
: >"$CALLS"
start_service
ss_spec_assert_file_line "$CALLS" 'deactivate_blocklist'
! grep -Fx 'dnsmasq_restart' "$CALLS" >/dev/null
rm -f "$SS_BLOCKLIST_FILE"
MOCK_ENABLED=1
MOCK_STATISTICS=1
STATISTICS_CHANGED=0
MIGRATION_RC=0
: >"$CALLS"
start_service
ss_spec_assert_file_line "$CALLS" 'require_dnsmasq'
ss_spec_assert_file_line "$CALLS" 'statistics_guard'
ss_spec_assert_file_line "$CALLS" 'reconcile_blocklists'
ss_spec_assert_file_line "$CALLS" 'statistics_configure 1'
ss_spec_assert_file_line "$CALLS" 'activate_blocklist'
ss_spec_assert_file_line "$CALLS" 'procd_set_param command /usr/libexec/safeshield-refreshd'
ss_spec_assert_file_line "$CALLS" 'add_statistics_instance'
ss_spec_assert_file_line "$CALLS" 'add_statistics_upload_instance'
MIGRATION_RC=1
: >"$CALLS"
! start_service
ss_spec_assert_file_line "$CALLS" 'failure blocklist_directive_migration_failed'
! grep -Fx 'activate_blocklist' "$CALLS" >/dev/null
MIGRATION_RC=0
MOCK_ENABLED=1
STATISTICS_CHANGED=1
rm -f "$SS_BLOCKLIST_FILE"
: >"$CALLS"
stop_service
ss_spec_assert_file_line "$CALLS" 'statistics_configure 0'
ss_spec_assert_file_line "$CALLS" 'deactivate_blocklist'
ss_spec_assert_file_line "$CALLS" 'dnsmasq_restart'
ss_spec_assert_file_line "$CALLS" 'status_set stage stopped'
printf '%s\n' 'safeshield-block=/active.example/' >"$SS_BLOCKLIST_FILE"
: >"$CALLS"
stop_service
ss_spec_assert_file_line "$CALLS" 'deactivate_blocklist'
! grep -Fx 'dnsmasq_restart' "$CALLS" >/dev/null
rm -f "$SS_BLOCKLIST_FILE"
MOCK_ENABLED=0
STATISTICS_CHANGED=0
: >"$CALLS"
stop_service
ss_spec_assert_file_line "$CALLS" 'status_set status disabled'
ss_spec_assert_file_line "$CALLS" 'status_set last_result disabled'
)
reload_service() {
restart
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+72
View File
@@ -0,0 +1,72 @@
#!/bin/sh
# shellcheck shell=sh
ss_case_tooling() (
set -eu
[ -f "$SS_SPEC_ROOT/.pre-commit-config.yaml" ]
[ -f "$SS_SPEC_ROOT/.shellspec" ]
[ -f "$SS_SPEC_ROOT/spec/spec_helper.sh" ]
[ -f "$SS_SPEC_ROOT/spec/core_spec.sh" ]
[ -f "$SS_SPEC_ROOT/spec/blocklist_spec.sh" ]
[ -f "$SS_SPEC_ROOT/spec/runtime_spec.sh" ]
[ -f "$SS_SPEC_ROOT/spec/statistics_spec.sh" ]
[ -f "$SS_SPEC_ROOT/spec/ucode_spec.sh" ]
[ -f "$SS_SPEC_ROOT/spec/tooling_spec.sh" ]
[ -f "$SS_SPEC_ROOT/scripts/lint.sh" ]
[ -f "$SS_SPEC_ROOT/.github/workflows/lint-shell.yml" ]
ss_spec_assert_file_contains "$SS_SPEC_ROOT/.pre-commit-config.yaml" 'entry: sh scripts/lint.sh'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/.pre-commit-config.yaml" 'always_run: true'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/.github/workflows/lint-shell.yml" 'sh scripts/lint.sh'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/.github/workflows/lint-shell.yml" 'SHELLSPEC_VERSION: 0.28.1'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/.github/workflows/lint-shell.yml" 'run: REQUIRE_UCODE=1 shellspec'
[ ! -e "$SS_SPEC_ROOT/tests/run.sh" ]
ss_spec_assert_file_contains "$SS_SPEC_ROOT/scripts/lint.sh" 'spec/*_spec.sh)'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/scripts/lint.sh" 'shfmt -d -ci'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/scripts/lint.sh" 'shellcheck -x -S warning'
ss_spec_assert_file_contains "$SS_SPEC_ROOT/scripts/lint.sh" 'sh -n "$file"'
[ ! -e "$SS_SPEC_ROOT/spec/legacy_tests_spec.sh" ]
)
ss_case_dead_code_contract() (
set -eu
UTILS="$SS_SPEC_ROOT/files/usr/lib/safeshield/utils.sh"
STATUS="$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh"
LOG="$SS_SPEC_ROOT/files/usr/lib/safeshield/log.sh"
DNS="$SS_SPEC_ROOT/files/usr/lib/safeshield/dns.sh"
BLOCKLIST="$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
CONFIG="$SS_SPEC_ROOT/files/usr/lib/safeshield/config.sh"
INIT="$SS_SPEC_ROOT/files/etc/init.d/safeshield"
for symbol in \
str_contains \
str_contains_word \
str_first_word \
str_to_lower \
str_to_upper \
str_replace \
is_greater; do
! grep -Eq "^${symbol}\\(\\)[[:space:]]*\\{" "$UTILS"
done
for symbol in is_enabled is_active; do
! grep -Eq "^${symbol}\\(\\)[[:space:]]*\\{" "$STATUS"
done
for symbol in log_fail log_debug; do
! grep -Eq "^${symbol}\\(\\)[[:space:]]*\\{" "$LOG"
done
for symbol in ss_clear_active_blocklist dnsmasq_kill; do
! grep -Eq "^${symbol}\\(\\)[[:space:]]*\\{" "$DNS"
done
! grep -Eq '^ss_detect_primary_mac\\(\\)[[:space:]]*\\{' "$BLOCKLIST"
! grep -F 'ss_debug=' "$CONFIG"
! grep -F '/lib/functions/network.sh' "$INIT"
# Keep the replacement/runtime contracts that made the retired helpers obsolete.
ss_spec_assert_file_contains "$BLOCKLIST" 'ss_identity_ensure "$model" "$arch"'
ss_spec_assert_file_contains "$DNS" '/etc/init.d/dnsmasq restart'
ss_spec_assert_file_contains "$INIT" "procd_add_reload_interface_trigger 'wan'"
)
+68
View File
@@ -0,0 +1,68 @@
#!/bin/sh
# shellcheck shell=sh
ss_case_ucode() (
set -eu
MODULE_DIR="$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield"
ENTRY="$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield.uc"
STATUS_STORE="$SS_SPEC_ROOT/files/usr/lib/safeshield/status-store.uc"
UCODE_BIN="${UCODE:-ucode}"
if ! command -v "$UCODE_BIN" >/dev/null 2>&1; then
[ "${REQUIRE_UCODE:-0}" != '1' ]
return 0
fi
TMPDIR="$(ss_spec_tmpdir)"
trap 'rm -rf "$TMPDIR"' EXIT HUP INT TERM
compile_ucode() {
file="$1"
output="$TMPDIR/$(basename -- "$file").ucb"
"$UCODE_BIN" -c -o "$output" "$file" >/dev/null
}
compile_ucode "$ENTRY"
compile_ucode "$STATUS_STORE"
for file in "$MODULE_DIR"/*.uc; do
compile_ucode "$file"
done
run_ucode_test() {
name="$1"
mock_dir="$SS_SPEC_ROOT/tests/ucode/mocks/$name"
test_file="$SS_SPEC_ROOT/tests/ucode/test_$name.uc"
test_tmp="$TMPDIR/$name"
test_modules="$TMPDIR/modules/$name"
mkdir -p "$test_tmp" "$test_modules"
cp "$MODULE_DIR/$name.uc" "$test_modules/$name.uc"
cp "$mock_dir"/*.uc "$test_modules/"
"$UCODE_BIN" -L "$test_modules" -D "TEST_TMP=\"$test_tmp\"" "$test_file" >/dev/null
}
for name in core config license refresh rules statistics status runtime; do
run_ucode_test "$name"
done
status_file="$TMPDIR/status-store.json"
printf '%s\n' 'not-json' >"$status_file"
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" dump)"
printf '%s\n' "$out" | grep -F '"data":{}' >/dev/null
printf '%s\n' "$out" | grep -F '"warnings":[]' >/dev/null
printf '%s\n' "$out" | grep -F '"errors":[]' >/dev/null
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" set stage running)"
printf '%s\n' "$out" >"$status_file"
printf '%s\n' "$out" | grep -F '"stage":"running"' >/dev/null
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" add_warning dnsmasq_warning)"
printf '%s\n' "$out" >"$status_file"
printf '%s\n' "$out" | grep -F '"code":"dnsmasq_warning"' >/dev/null
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" add_error artifact_download_failed)"
printf '%s\n' "$out" >"$status_file"
printf '%s\n' "$out" | grep -F '"code":"artifact_download_failed"' >/dev/null
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" clear_messages)"
printf '%s\n' "$out" | grep -F '"warnings":[]' >/dev/null
printf '%s\n' "$out" | grep -F '"errors":[]' >/dev/null
out="$("$UCODE_BIN" "$STATUS_STORE" "$status_file" reset)"
printf '%s\n' "$out" | grep -F '"data":{}' >/dev/null
if "$UCODE_BIN" "$STATUS_STORE" "$status_file" set >/dev/null 2>&1; then
return 1
fi
if "$UCODE_BIN" "$STATUS_STORE" "$status_file" unknown >/dev/null 2>&1; then
return 1
fi
)
+17
View File
@@ -0,0 +1,17 @@
# shellcheck shell=sh
Describe 'SafeShield test and lint integration'
It 'uses native ShellSpec discovery and the shared lint entrypoint'
When call ss_case_tooling
The status should be success
The output should equal ''
The error should equal ''
End
It 'keeps retired shell helpers and unused runtime sources removed'
When call ss_case_dead_code_contract
The status should be success
The output should equal ''
The error should equal ''
End
End
+10
View File
@@ -0,0 +1,10 @@
# shellcheck shell=sh
Describe 'SafeShield ucode modules'
It 'compiles modules and runs the ucode regression suite when available'
When call ss_case_ucode
The status should be success
The output should equal ''
The error should equal ''
End
End