From 765ffd2394f1209e823c5f7f9950e658e6c3cd5d Mon Sep 17 00:00:00 2001 From: jared Date: Thu, 7 May 2026 11:19:37 -0400 Subject: [PATCH] feat(backup-monitor): implement BackupNotifier and CLI entry point --- scripts/notify_backup.rb | 43 ++++++++++++++++++++++++++++- scripts/notify_backup_test.rb | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/scripts/notify_backup.rb b/scripts/notify_backup.rb index 4208159..da95126 100644 --- a/scripts/notify_backup.rb +++ b/scripts/notify_backup.rb @@ -115,8 +115,49 @@ class WebhookClient end class BackupNotifier + def initialize(options:, client:) + @options = options + @client = client + end + + def self.from_env(options) + url = ENV.fetch('N8N_SWANSONCLOUD_URL') + token = ENV.fetch('N8N_SWANSONCLOUD_BACKUP_AUTH_TOKEN') + new(options: options, client: WebhookClient.new(base_url: url, auth_token: token)) + end + + def notify + payload = PayloadBuilder.new(@options).build + @client.post(payload) + end end if __FILE__ == $PROGRAM_NAME - # CLI entry point — implemented in Task 5 + options = {} + + OptionParser.new do |opts| + opts.banner = 'Usage: notify_backup.rb [options]' + opts.separator '' + opts.separator 'Required:' + opts.on('--machine NAME', 'Host identifier (e.g. desktop, vps-ovh-prod-01)') { |v| options[:machine] = v } + opts.on('--destination NAME', 'Backup destination (e.g. synology, backblaze-b2)') { |v| options[:destination] = v } + opts.on('--status STATUS', 'success | failure | skipped') { |v| options[:status] = v } + opts.separator '' + opts.separator 'Optional:' + opts.on('--bytes N', Integer, 'Raw bytes transferred (formatted before sending)') { |v| options[:bytes] = v } + opts.on('--snapshot ID', 'Snapshot ID (e.g. restic snapshot hash)') { |v| options[:snapshot] = v } + opts.on('--log-path PATH', 'Path to the backup log for this run') { |v| options[:log_path] = v } + opts.on('--notes TEXT', 'Free-form output: restic tail, skip reason, etc.') { |v| options[:notes] = v } + end.parse! + + %i[machine destination status].each do |required| + abort "Error: --#{required} is required. Run with --help for usage." unless options[required] + end + + begin + BackupNotifier.from_env(options).notify + rescue => e + warn "notify_backup: failed to send webhook: #{e.message}" + exit 0 + end end diff --git a/scripts/notify_backup_test.rb b/scripts/notify_backup_test.rb index 76dfca4..30e340b 100644 --- a/scripts/notify_backup_test.rb +++ b/scripts/notify_backup_test.rb @@ -136,3 +136,54 @@ class WebhookClientTest < Minitest::Test assert_match(/HTTP 500/, err.message) end end + +class FakeClient + def initialize + @posted = false + @payload = nil + end + + def post(payload) + @posted = true + @payload = payload + end + + def posted_payload + @payload + end + + def was_called? + @posted + end +end + +class BackupNotifierTest < Minitest::Test + def base_options + { machine: 'desktop', destination: 'synology', status: 'success' } + end + + def test_notify_calls_client_with_a_hash_payload + fake_client = FakeClient.new + + BackupNotifier.new(options: base_options, client: fake_client).notify + + assert fake_client.was_called? + assert_instance_of Hash, fake_client.posted_payload + end + + def test_from_env_returns_a_backup_notifier + ENV['N8N_SWANSONCLOUD_URL'] = 'https://n8n.example.com' + ENV['N8N_SWANSONCLOUD_BACKUP_AUTH_TOKEN'] = 'test-token' + + notifier = BackupNotifier.from_env(base_options) + assert_instance_of BackupNotifier, notifier + ensure + ENV.delete('N8N_SWANSONCLOUD_URL') + ENV.delete('N8N_SWANSONCLOUD_BACKUP_AUTH_TOKEN') + end + + def test_from_env_raises_when_env_var_missing + ENV.delete('N8N_SWANSONCLOUD_URL') + assert_raises(KeyError) { BackupNotifier.from_env(base_options) } + end +end