mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-09-28 03:58:49 -05:00
Add recurring CLI scrape automation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
353
scripts/setup-cron.sh
Executable file
353
scripts/setup-cron.sh
Executable file
@@ -0,0 +1,353 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
|
||||
REPO_ROOT="${DCE_REPO_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd -P)}"
|
||||
COMPOSE_FILE="${DCE_COMPOSE_FILE:-$REPO_ROOT/docker-compose.yml}"
|
||||
ENV_FILE="${DCE_ENV_FILE:-$REPO_ROOT/scrape.env}"
|
||||
CONFIG_FILE="${DCE_CONFIG_FILE:-$REPO_ROOT/config/scrape-targets.json}"
|
||||
LOG_FILE="${DCE_LOG_FILE:-$REPO_ROOT/logs/discord-scrape.log}"
|
||||
JOB_NAME="discord-scrape"
|
||||
INTERVAL="monthly"
|
||||
RUN_AT="04:00"
|
||||
CRON_EXPRESSION=""
|
||||
DRY_RUN=0
|
||||
REMOVE=0
|
||||
SKIP_PREFLIGHT=0
|
||||
|
||||
TARGETS=()
|
||||
GUILDS=()
|
||||
CHANNELS=()
|
||||
|
||||
JQ_BIN="${DCE_JQ_BIN:-jq}"
|
||||
CRONTAB_BIN="${DCE_CRONTAB_BIN:-crontab}"
|
||||
DOCKER_BIN="${DCE_DOCKER_BIN:-docker}"
|
||||
COMPOSE_BIN="${DCE_COMPOSE_BIN:-}"
|
||||
DOCKER_BIN_OVERRIDDEN=0
|
||||
|
||||
if [[ -n "${DCE_DOCKER_BIN:-}" ]]; then
|
||||
DOCKER_BIN_OVERRIDDEN=1
|
||||
fi
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
$(basename "$0") [options]
|
||||
|
||||
Options:
|
||||
--target NAME Restrict the cron job to one configured target. Repeatable.
|
||||
--guild ID Narrow a selected target to one of its allowed guild IDs. Repeatable.
|
||||
--channel ID Narrow a selected target to one of its allowed channel IDs. Repeatable.
|
||||
--interval VALUE monthly, weekly, or daily. Default: monthly
|
||||
--at HH:MM Run time in 24-hour format. Default: 04:00
|
||||
--cron EXPR Use an explicit five-field cron expression instead of --interval/--at.
|
||||
--job-name NAME Marker name for the installed cron block. Default: discord-scrape
|
||||
--log-file PATH Cron log file. Default: $LOG_FILE
|
||||
--env-file PATH Compose env file. Default: $ENV_FILE
|
||||
--skip-preflight Install the cron job without running the authenticated container preflight.
|
||||
--dry-run Print the cron block instead of installing it.
|
||||
--remove Remove the managed cron block and exit.
|
||||
--help Show this help text.
|
||||
|
||||
Examples:
|
||||
$(basename "$0")
|
||||
$(basename "$0") --target discord_dms --interval weekly --at 02:30
|
||||
$(basename "$0") --target Cline --channel 123456789012345678 --channel 234567890123456789
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_program() {
|
||||
local program_path=$1
|
||||
command -v "$program_path" >/dev/null 2>&1 || die "Required command '$program_path' is missing."
|
||||
}
|
||||
|
||||
cron_from_schedule() {
|
||||
local interval=$1
|
||||
local run_at=$2
|
||||
local hour minute
|
||||
|
||||
[[ "$run_at" =~ ^([0-1][0-9]|2[0-3]):([0-5][0-9])$ ]] || die "--at must use HH:MM in 24-hour time."
|
||||
hour=${BASH_REMATCH[1]}
|
||||
minute=${BASH_REMATCH[2]}
|
||||
|
||||
case "$interval" in
|
||||
monthly) printf '%s %s 1 * *' "$minute" "$hour" ;;
|
||||
weekly) printf '%s %s * * 0' "$minute" "$hour" ;;
|
||||
daily) printf '%s %s * * *' "$minute" "$hour" ;;
|
||||
*) die "Unsupported --interval '$interval'. Use monthly, weekly, or daily." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
strip_existing_job() {
|
||||
local existing_crontab=$1
|
||||
local begin_marker=$2
|
||||
local end_marker=$3
|
||||
|
||||
awk -v begin="$begin_marker" -v end="$end_marker" '
|
||||
$0 == begin { skipping = 1; next }
|
||||
$0 == end { skipping = 0; next }
|
||||
!skipping { print }
|
||||
' <<<"$existing_crontab"
|
||||
}
|
||||
|
||||
build_compose_command() {
|
||||
local subcommand=$1
|
||||
local -a command_parts
|
||||
|
||||
if [[ -n "$COMPOSE_BIN" ]]; then
|
||||
command_parts=(
|
||||
"$COMPOSE_BIN"
|
||||
--env-file "$ENV_FILE"
|
||||
-f "$COMPOSE_FILE"
|
||||
run
|
||||
-T
|
||||
--rm
|
||||
discord-scraper
|
||||
"$subcommand"
|
||||
)
|
||||
elif (( DOCKER_BIN_OVERRIDDEN == 0 )) && command -v docker-compose >/dev/null 2>&1; then
|
||||
command_parts=(
|
||||
docker-compose
|
||||
--env-file "$ENV_FILE"
|
||||
-f "$COMPOSE_FILE"
|
||||
run
|
||||
-T
|
||||
--rm
|
||||
discord-scraper
|
||||
"$subcommand"
|
||||
)
|
||||
else
|
||||
command_parts=(
|
||||
"$DOCKER_BIN"
|
||||
compose
|
||||
--env-file "$ENV_FILE"
|
||||
-f "$COMPOSE_FILE"
|
||||
run
|
||||
-T
|
||||
--rm
|
||||
discord-scraper
|
||||
"$subcommand"
|
||||
)
|
||||
fi
|
||||
|
||||
local target
|
||||
for target in "${TARGETS[@]}"; do
|
||||
command_parts+=(--target "$target")
|
||||
done
|
||||
|
||||
local guild_id
|
||||
for guild_id in "${GUILDS[@]}"; do
|
||||
command_parts+=(--guild "$guild_id")
|
||||
done
|
||||
|
||||
local channel_id
|
||||
for channel_id in "${CHANNELS[@]}"; do
|
||||
command_parts+=(--channel "$channel_id")
|
||||
done
|
||||
|
||||
printf '%q ' "${command_parts[@]}"
|
||||
}
|
||||
|
||||
ensure_target_directories() {
|
||||
local selected_targets_json archive_root output_dir
|
||||
|
||||
archive_root=$("$JQ_BIN" -r '.archive_root // empty' "$CONFIG_FILE")
|
||||
[[ -n "$archive_root" ]] || die "Config is missing archive_root."
|
||||
mkdir -p "$archive_root"
|
||||
|
||||
selected_targets_json=$("$JQ_BIN" -cn '$ARGS.positional' --args "${TARGETS[@]}")
|
||||
|
||||
if (( ${#TARGETS[@]} == 0 )); then
|
||||
while IFS= read -r output_dir; do
|
||||
mkdir -p "$output_dir"
|
||||
done < <("$JQ_BIN" -r '.targets[] | select(.enabled != false) | .output_dir' "$CONFIG_FILE")
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r output_dir; do
|
||||
mkdir -p "$output_dir"
|
||||
done < <(
|
||||
"$JQ_BIN" -r \
|
||||
--argjson selected_targets "$selected_targets_json" \
|
||||
'.targets[]
|
||||
| select(.name as $name | $selected_targets | index($name))
|
||||
| .output_dir' \
|
||||
"$CONFIG_FILE"
|
||||
)
|
||||
}
|
||||
|
||||
validate_targets() {
|
||||
(( ${#TARGETS[@]} == 0 )) && return 0
|
||||
|
||||
local requested_targets_json resolved_count
|
||||
requested_targets_json=$("$JQ_BIN" -cn '$ARGS.positional' --args "${TARGETS[@]}")
|
||||
resolved_count=$(
|
||||
"$JQ_BIN" -r \
|
||||
--argjson requested_targets "$requested_targets_json" \
|
||||
'[.targets[] | select(.name as $name | $requested_targets | index($name))] | length' \
|
||||
"$CONFIG_FILE"
|
||||
)
|
||||
|
||||
[[ "$resolved_count" == "${#TARGETS[@]}" ]] || die "One or more --target values are missing from $CONFIG_FILE."
|
||||
}
|
||||
|
||||
run_preflight() {
|
||||
local preflight_command
|
||||
|
||||
[[ -f "$ENV_FILE" ]] || die "Missing env file: $ENV_FILE"
|
||||
preflight_command=$(build_compose_command preflight)
|
||||
eval "$preflight_command"
|
||||
}
|
||||
|
||||
main() {
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--target)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --target."
|
||||
TARGETS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--guild)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --guild."
|
||||
GUILDS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--channel)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --channel."
|
||||
CHANNELS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--interval)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --interval."
|
||||
INTERVAL=$2
|
||||
shift 2
|
||||
;;
|
||||
--at)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --at."
|
||||
RUN_AT=$2
|
||||
shift 2
|
||||
;;
|
||||
--cron)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --cron."
|
||||
CRON_EXPRESSION=$2
|
||||
shift 2
|
||||
;;
|
||||
--job-name)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --job-name."
|
||||
JOB_NAME=$2
|
||||
shift 2
|
||||
;;
|
||||
--log-file)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --log-file."
|
||||
LOG_FILE=$2
|
||||
shift 2
|
||||
;;
|
||||
--env-file)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --env-file."
|
||||
ENV_FILE=$2
|
||||
shift 2
|
||||
;;
|
||||
--skip-preflight)
|
||||
SKIP_PREFLIGHT=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--remove)
|
||||
REMOVE=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_program "$JQ_BIN"
|
||||
require_program "$CRONTAB_BIN"
|
||||
if [[ -n "$COMPOSE_BIN" ]]; then
|
||||
require_program "$COMPOSE_BIN"
|
||||
elif (( DOCKER_BIN_OVERRIDDEN == 0 )) && command -v docker-compose >/dev/null 2>&1; then
|
||||
:
|
||||
else
|
||||
require_program "$DOCKER_BIN"
|
||||
fi
|
||||
|
||||
[[ -f "$COMPOSE_FILE" ]] || die "Missing compose file: $COMPOSE_FILE"
|
||||
[[ -f "$CONFIG_FILE" ]] || die "Missing config file: $CONFIG_FILE"
|
||||
"$JQ_BIN" empty "$CONFIG_FILE" >/dev/null 2>&1 || die "Invalid JSON config: $CONFIG_FILE"
|
||||
|
||||
validate_targets
|
||||
|
||||
if (( (${#GUILDS[@]} > 0 || ${#CHANNELS[@]} > 0) && ${#TARGETS[@]} != 1 )); then
|
||||
die "--guild and --channel overrides require exactly one --target."
|
||||
fi
|
||||
|
||||
local cron_line
|
||||
if [[ -n "$CRON_EXPRESSION" ]]; then
|
||||
cron_line=$CRON_EXPRESSION
|
||||
else
|
||||
cron_line=$(cron_from_schedule "$INTERVAL" "$RUN_AT")
|
||||
fi
|
||||
|
||||
local begin_marker="# BEGIN ${JOB_NAME}"
|
||||
local end_marker="# END ${JOB_NAME}"
|
||||
local current_crontab cleaned_crontab compose_command job_line lock_prefix
|
||||
current_crontab=$("$CRONTAB_BIN" -l 2>/dev/null || true)
|
||||
cleaned_crontab=$(strip_existing_job "$current_crontab" "$begin_marker" "$end_marker")
|
||||
|
||||
if (( REMOVE == 1 )); then
|
||||
if (( DRY_RUN == 1 )); then
|
||||
printf '%s\n' "$cleaned_crontab"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "$cleaned_crontab" | "$CRONTAB_BIN" -
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
ensure_target_directories
|
||||
|
||||
if (( SKIP_PREFLIGHT == 0 )); then
|
||||
run_preflight
|
||||
fi
|
||||
|
||||
compose_command=$(build_compose_command scrape)
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
lock_prefix=$(printf '%q ' "$(command -v flock)" "-n" "/tmp/${JOB_NAME}.lock")
|
||||
else
|
||||
lock_prefix=""
|
||||
fi
|
||||
|
||||
job_line="$cron_line cd $(printf '%q' "$REPO_ROOT") && ${lock_prefix}${compose_command}>> $(printf '%q' "$LOG_FILE") 2>&1"
|
||||
|
||||
local cron_block
|
||||
cron_block=$(printf '%s\n%s\n%s\n' "$begin_marker" "$job_line" "$end_marker")
|
||||
|
||||
if (( DRY_RUN == 1 )); then
|
||||
printf '%s\n' "$cron_block"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
if [[ -n "$cleaned_crontab" ]]; then
|
||||
printf '%s\n\n' "$cleaned_crontab"
|
||||
fi
|
||||
printf '%s\n' "$cron_block"
|
||||
} | "$CRONTAB_BIN" -
|
||||
}
|
||||
|
||||
main "$@"
|
||||
25
scripts/tests/container-smoke.sh
Executable file
25
scripts/tests/container-smoke.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)
|
||||
TMP_ENV=$(mktemp "${TMPDIR:-/tmp}/dce-container-smoke.XXXXXX.env")
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_ENV"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cat >"$TMP_ENV" <<EOF
|
||||
DISCORD_TOKEN=dummy
|
||||
DCE_UID=$(id -u)
|
||||
DCE_GID=$(id -g)
|
||||
TZ=UTC
|
||||
EOF
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
docker compose --env-file "$TMP_ENV" build
|
||||
docker compose --env-file "$TMP_ENV" run --rm discord-scraper help >/dev/null
|
||||
docker compose --env-file "$TMP_ENV" run --rm discord-scraper list-targets >/dev/null
|
||||
|
||||
echo "container smoke test passed"
|
||||
107
scripts/tests/run-discord-scrape-smoke.sh
Executable file
107
scripts/tests/run-discord-scrape-smoke.sh
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)
|
||||
FIXTURE_DIR="$REPO_ROOT/scripts/tests/test-fixtures"
|
||||
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/dce-run-smoke.XXXXXX")
|
||||
ARCHIVE_ROOT="$TMP_DIR/archive"
|
||||
CONFIG_PATH="$TMP_DIR/config.json"
|
||||
FAKE_CLI="$TMP_DIR/fake-cli.sh"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cat >"$CONFIG_PATH" <<JSON
|
||||
{
|
||||
"archive_root": "$ARCHIVE_ROOT",
|
||||
"defaults": {
|
||||
"include_threads": "all",
|
||||
"include_voice_channels": false
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"name": "demo",
|
||||
"kind": "guild",
|
||||
"output_dir": "$ARCHIVE_ROOT/demo",
|
||||
"channel_ids": ["111"],
|
||||
"guild_ids": [],
|
||||
"guild_name_patterns": []
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
cat >"$FAKE_CLI" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
mode=${FAKE_DCE_MODE:?}
|
||||
fixture_dir=${FAKE_DCE_FIXTURE_DIR:?}
|
||||
subcommand=${1:?}
|
||||
shift || true
|
||||
|
||||
case "$subcommand" in
|
||||
export)
|
||||
output=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--output)
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
--channel|--format|--after)
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$mode" in
|
||||
initial) cp "$fixture_dir/append-existing.json" "$output" ;;
|
||||
append) cp "$fixture_dir/append-incremental.json" "$output" ;;
|
||||
wrong-channel) cp "$fixture_dir/wrong-channel.json" "$output" ;;
|
||||
*) echo "unexpected mode: $mode" >&2; exit 1 ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "unexpected subcommand: $subcommand" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$FAKE_CLI"
|
||||
|
||||
run_wrapper() {
|
||||
DISCORD_TOKEN=dummy \
|
||||
DCE_CLI_BIN="$FAKE_CLI" \
|
||||
DCE_PRIMARY_CONFIG="$CONFIG_PATH" \
|
||||
DCE_FALLBACK_CONFIG="$CONFIG_PATH" \
|
||||
FAKE_DCE_FIXTURE_DIR="$FIXTURE_DIR" \
|
||||
FAKE_DCE_MODE="$1" \
|
||||
"$REPO_ROOT/scripts/run-discord-scrape.sh" scrape --target demo
|
||||
}
|
||||
|
||||
run_wrapper initial
|
||||
|
||||
DEST="$ARCHIVE_ROOT/demo/channels/111.json"
|
||||
[[ -f "$DEST" ]] || { echo "expected destination archive missing" >&2; exit 1; }
|
||||
[[ "$(jq -r '.messages | length' "$DEST")" == "2" ]] || { echo "expected initial message count of 2" >&2; exit 1; }
|
||||
|
||||
run_wrapper append
|
||||
[[ "$(jq -r '.messages | length' "$DEST")" == "3" ]] || { echo "expected appended message count of 3" >&2; exit 1; }
|
||||
[[ "$(jq -r '.messages[-1].id' "$DEST")" == "3" ]] || { echo "expected last message id 3 after append" >&2; exit 1; }
|
||||
|
||||
before_checksum=$(sha256sum "$DEST" | awk '{print $1}')
|
||||
if run_wrapper wrong-channel; then
|
||||
echo "wrong-channel fixture should have failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
after_checksum=$(sha256sum "$DEST" | awk '{print $1}')
|
||||
[[ "$before_checksum" == "$after_checksum" ]] || { echo "destination archive changed after failed wrong-channel run" >&2; exit 1; }
|
||||
|
||||
echo "run-discord-scrape smoke test passed"
|
||||
100
scripts/tests/setup-cron-smoke.sh
Executable file
100
scripts/tests/setup-cron-smoke.sh
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)
|
||||
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/dce-cron-smoke.XXXXXX")
|
||||
ARCHIVE_ROOT="$TMP_DIR/archive"
|
||||
CONFIG_PATH="$TMP_DIR/config.json"
|
||||
ENV_PATH="$TMP_DIR/scrape.env"
|
||||
CRONTAB_FILE="$TMP_DIR/crontab.txt"
|
||||
DOCKER_LOG="$TMP_DIR/docker.log"
|
||||
FAKE_DOCKER="$TMP_DIR/docker"
|
||||
FAKE_CRONTAB="$TMP_DIR/crontab"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cat >"$CONFIG_PATH" <<JSON
|
||||
{
|
||||
"archive_root": "$ARCHIVE_ROOT",
|
||||
"defaults": {
|
||||
"include_threads": "all",
|
||||
"include_voice_channels": false
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"name": "demo",
|
||||
"kind": "guild",
|
||||
"output_dir": "$ARCHIVE_ROOT/demo",
|
||||
"channel_ids": ["111"],
|
||||
"guild_ids": [],
|
||||
"guild_name_patterns": []
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
cat >"$ENV_PATH" <<EOF
|
||||
DISCORD_TOKEN=dummy
|
||||
DCE_UID=1000
|
||||
DCE_GID=1000
|
||||
TZ=UTC
|
||||
EOF
|
||||
|
||||
cat >"$FAKE_DOCKER" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
printf '%s\n' "\$*" >>"$DOCKER_LOG"
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$FAKE_DOCKER"
|
||||
|
||||
cat >"$FAKE_CRONTAB" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
file="$CRONTAB_FILE"
|
||||
if [[ "\${1:-}" == "-l" ]]; then
|
||||
[[ -f "\$file" ]] || exit 1
|
||||
cat "\$file"
|
||||
elif [[ "\${1:-}" == "-" ]]; then
|
||||
cat >"\$file"
|
||||
else
|
||||
echo "unexpected crontab args: \$*" >&2
|
||||
exit 1
|
||||
fi
|
||||
EOF
|
||||
chmod +x "$FAKE_CRONTAB"
|
||||
|
||||
printf 'MAILTO=test@example.com\n' >"$CRONTAB_FILE"
|
||||
|
||||
run_setup() {
|
||||
DCE_CONFIG_FILE="$CONFIG_PATH" \
|
||||
DCE_ENV_FILE="$ENV_PATH" \
|
||||
DCE_CRONTAB_BIN="$FAKE_CRONTAB" \
|
||||
DCE_DOCKER_BIN="$FAKE_DOCKER" \
|
||||
DCE_JQ_BIN="$(command -v jq)" \
|
||||
DCE_REPO_ROOT="$REPO_ROOT" \
|
||||
DCE_LOG_FILE="$TMP_DIR/logs/discord-scrape.log" \
|
||||
"$REPO_ROOT/scripts/setup-cron.sh" --target demo "$@"
|
||||
}
|
||||
|
||||
run_setup
|
||||
grep -q '^MAILTO=test@example.com$' "$CRONTAB_FILE" || { echo "expected unrelated crontab line to remain" >&2; exit 1; }
|
||||
[[ "$(grep -c '^# BEGIN discord-scrape$' "$CRONTAB_FILE")" == "1" ]] || { echo "expected exactly one managed cron block after install" >&2; exit 1; }
|
||||
grep -q 'compose --env-file' "$DOCKER_LOG" || { echo "expected docker preflight to run during install" >&2; exit 1; }
|
||||
|
||||
run_setup
|
||||
[[ "$(grep -c '^# BEGIN discord-scrape$' "$CRONTAB_FILE")" == "1" ]] || { echo "expected exactly one managed cron block after reinstall" >&2; exit 1; }
|
||||
|
||||
preview_output=$(run_setup --dry-run)
|
||||
grep -q '^# BEGIN discord-scrape$' <<<"$preview_output" || { echo "expected dry-run preview to contain managed block" >&2; exit 1; }
|
||||
[[ "$(grep -c '^# BEGIN discord-scrape$' "$CRONTAB_FILE")" == "1" ]] || { echo "dry-run should not alter crontab state" >&2; exit 1; }
|
||||
|
||||
run_setup --remove
|
||||
grep -q '^MAILTO=test@example.com$' "$CRONTAB_FILE" || { echo "expected unrelated crontab line to remain after remove" >&2; exit 1; }
|
||||
! grep -q '^# BEGIN discord-scrape$' "$CRONTAB_FILE" || { echo "expected managed cron block to be removed" >&2; exit 1; }
|
||||
|
||||
echo "setup-cron smoke test passed"
|
||||
22
scripts/tests/test-fixtures/append-existing.json
Normal file
22
scripts/tests/test-fixtures/append-existing.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"channel": {
|
||||
"id": "111"
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"id": "1",
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"content": "first"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"timestamp": "2026-01-02T00:00:00Z",
|
||||
"content": "second"
|
||||
}
|
||||
],
|
||||
"dateRange": {
|
||||
"after": null,
|
||||
"before": null
|
||||
},
|
||||
"exportedAt": "2026-01-02T00:00:00Z"
|
||||
}
|
||||
22
scripts/tests/test-fixtures/append-incremental.json
Normal file
22
scripts/tests/test-fixtures/append-incremental.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"channel": {
|
||||
"id": "111"
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"id": "2",
|
||||
"timestamp": "2026-01-02T00:00:00Z",
|
||||
"content": "second"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"timestamp": "2026-01-03T00:00:00Z",
|
||||
"content": "third"
|
||||
}
|
||||
],
|
||||
"dateRange": {
|
||||
"after": "2026-01-02T00:00:00Z",
|
||||
"before": null
|
||||
},
|
||||
"exportedAt": "2026-01-03T00:00:00Z"
|
||||
}
|
||||
17
scripts/tests/test-fixtures/wrong-channel.json
Normal file
17
scripts/tests/test-fixtures/wrong-channel.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"channel": {
|
||||
"id": "999"
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"id": "4",
|
||||
"timestamp": "2026-01-04T00:00:00Z",
|
||||
"content": "wrong"
|
||||
}
|
||||
],
|
||||
"dateRange": {
|
||||
"after": "2026-01-03T00:00:00Z",
|
||||
"before": null
|
||||
},
|
||||
"exportedAt": "2026-01-04T00:00:00Z"
|
||||
}
|
||||
Reference in New Issue
Block a user