diff --git a/.rubocop.yml b/.rubocop.yml index ab7c618..ae0bde2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,78 +1,37 @@ -require: rubocop-rspec +# The behavior of RuboCop can be controlled via the .rubocop.yml +# configuration file. It makes it possible to enable/disable +# certain cops (checks) and to alter their behavior if they accept +# any parameters. The file can be placed either in your home +# directory or in some project directory. +# +# RuboCop will start looking for the configuration file in the directory +# where the inspected file is and continue its way up to the root directory. +# +# See https://docs.rubocop.org/rubocop/configuration +require: + - rubocop-rspec -Gemspec/DateAssignment: # (new in 1.10) - Enabled: true -Layout/LineEndStringConcatenationIndentation: # (new in 1.18) - Enabled: true -Layout/SpaceBeforeBrackets: # (new in 1.7) - Enabled: true -Lint/AmbiguousAssignment: # (new in 1.7) - Enabled: true -Lint/DeprecatedConstants: # (new in 1.8) - Enabled: true -Lint/DuplicateBranch: # (new in 1.3) - Enabled: true -Lint/DuplicateRegexpCharacterClassElement: # (new in 1.1) - Enabled: true -Lint/EmptyBlock: # (new in 1.1) - Enabled: true -Lint/EmptyClass: # (new in 1.3) - Enabled: true -Lint/EmptyInPattern: # (new in 1.16) - Enabled: true -Lint/LambdaWithoutLiteralBlock: # (new in 1.8) - Enabled: true -Lint/NoReturnInBeginEndBlocks: # (new in 1.2) - Enabled: true -Lint/NumberedParameterAssignment: # (new in 1.9) - Enabled: true -Lint/OrAssignmentToConstant: # (new in 1.9) - Enabled: true -Lint/RedundantDirGlobSort: # (new in 1.8) - Enabled: true -Lint/SymbolConversion: # (new in 1.9) - Enabled: true -Lint/ToEnumArguments: # (new in 1.1) - Enabled: true -Lint/TripleQuotes: # (new in 1.9) - Enabled: true -Lint/UnexpectedBlockArity: # (new in 1.5) - Enabled: true -Lint/UnmodifiedReduceAccumulator: # (new in 1.1) - Enabled: true -Naming/InclusiveLanguage: # (new in 1.18) - Enabled: true -Style/ArgumentsForwarding: # (new in 1.1) - Enabled: true -Style/CollectionCompact: # (new in 1.2) - Enabled: true -Style/DocumentDynamicEvalDefinition: # (new in 1.1) - Enabled: true -Style/EndlessMethod: # (new in 1.8) - Enabled: true -Style/HashConversion: # (new in 1.10) - Enabled: true -Style/HashExcept: # (new in 1.7) - Enabled: true -Style/IfWithBooleanLiteralBranches: # (new in 1.9) - Enabled: true -Style/InPatternThen: # (new in 1.16) - Enabled: true -Style/MultilineInPatternThen: # (new in 1.16) - Enabled: true -Style/NegatedIfElseCondition: # (new in 1.2) - Enabled: true -Style/NilLambda: # (new in 1.3) - Enabled: true -Style/QuotedSymbols: # (new in 1.16) - Enabled: true -Style/RedundantArgument: # (new in 1.4) - Enabled: true -Style/StringChars: # (new in 1.12) - Enabled: true -Style/SwapValues: # (new in 1.1) - Enabled: true -RSpec/IdenticalEqualityAssertion: # (new in 2.4) - Enabled: true -RSpec/Rails/AvoidSetupHook: # (new in 2.4) - Enabled: true \ No newline at end of file +AllCops: + TargetRubyVersion: 2.7 + NewCops: enable + +Layout/LineLength: + Enabled: false + +Metrics/BlockLength: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Naming/AccessorMethodName: + Enabled: false + +RSpec/MultipleMemoizedHelpers: + Enabled: false + +RSpec/ExampleLength: + Enabled: false \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index ec9dee1..93a95f4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -62,7 +62,7 @@ GEM byebug (~> 11.0) pry (>= 0.13, < 0.15) rainbow (3.1.1) - rb-fsevent (0.11.1) + rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) regexp_parser (2.5.0) diff --git a/bin/datadog_backup b/bin/datadog_backup index f723943..b989b18 100755 --- a/bin/datadog_backup +++ b/bin/datadog_backup @@ -11,7 +11,6 @@ LOGGER.level = Logger::INFO require 'datadog_backup' - def fatal(message) LOGGER.fatal(message) exit 1 @@ -19,10 +18,10 @@ end def options_valid?(options) %w[backup diffs restore].include?(options[:action]) - %w[DD_API_KEY DD_APP_KEY].all? { |key| ENV[key] } + %w[DD_API_KEY DD_APP_KEY].all? { |key| ENV.fetch(key, nil) } end -def prereqs(defaults) +def prereqs(defaults) # rubocop:disable Metrics/AbcSize ARGV << '--help' if ARGV.empty? result = defaults.dup @@ -49,6 +48,9 @@ def prereqs(defaults) opts.on('--dashboards-only') do result[:resources] = [DatadogBackup::Dashboards] end + opts.on('--synthetics-only') do + result[:resources] = [DatadogBackup::Synthetics] + end opts.on( '--json', 'format backups as JSON instead of YAML. Does not impact `diffs` nor `restore`, but do not mix formats in the same backup-dir.' @@ -78,9 +80,9 @@ defaults = { action: nil, backup_dir: File.join(ENV.fetch('PWD'), 'backup'), diff_format: :color, - resources: [DatadogBackup::Dashboards, DatadogBackup::Monitors], + resources: [DatadogBackup::Dashboards, DatadogBackup::Monitors, DatadogBackup::Synthetics], output_format: :yaml, force_restore: false } -DatadogBackup::Cli.new(prereqs(defaults)).run! \ No newline at end of file +DatadogBackup::Cli.new(prereqs(defaults)).run! diff --git a/datadog_backup.gemspec b/datadog_backup.gemspec index cbde451..62f5b36 100644 --- a/datadog_backup.gemspec +++ b/datadog_backup.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - spec.required_ruby_version = ['>= 2.7'] + spec.required_ruby_version = '>= 2.7' spec.add_dependency 'amazing_print' spec.add_dependency 'concurrent-ruby' @@ -28,11 +28,10 @@ Gem::Specification.new do |spec| spec.add_dependency 'faraday' spec.add_dependency 'faraday-retry' - spec.add_development_dependency 'bundler' + spec.add_development_dependency 'guard-rspec' spec.add_development_dependency 'pry' spec.add_development_dependency 'pry-byebug' - spec.add_development_dependency 'guard-rspec' spec.add_development_dependency 'rspec' spec.add_development_dependency 'rubocop' spec.add_development_dependency 'rubocop-rspec' diff --git a/lib/datadog_backup.rb b/lib/datadog_backup.rb index c974964..c772b5e 100644 --- a/lib/datadog_backup.rb +++ b/lib/datadog_backup.rb @@ -8,12 +8,12 @@ require_relative 'datadog_backup/core' require_relative 'datadog_backup/dashboards' require_relative 'datadog_backup/monitors' +require_relative 'datadog_backup/synthetics' require_relative 'datadog_backup/thread_pool' require_relative 'datadog_backup/version' require_relative 'datadog_backup/deprecations' DatadogBackup::Deprecations.check - +# DatadogBackup is a gem for backing up and restoring Datadog monitors and dashboards. module DatadogBackup end - diff --git a/lib/datadog_backup/cli.rb b/lib/datadog_backup/cli.rb index f71bcce..d6a041a 100644 --- a/lib/datadog_backup/cli.rb +++ b/lib/datadog_backup/cli.rb @@ -4,6 +4,7 @@ require 'amazing_print' module DatadogBackup + # CLI is the command line interface for the datadog_backup gem. class Cli include ::DatadogBackup::Options @@ -11,9 +12,9 @@ def all_diff_futures LOGGER.info("Starting diffs on #{::DatadogBackup::ThreadPool::TPOOL.max_length} threads") any_resource_instance .all_file_ids_for_selected_resources - .map do |id| - Concurrent::Promises.future_on(::DatadogBackup::ThreadPool::TPOOL, id) do |id| - [id, getdiff(id)] + .map do |file_id| + Concurrent::Promises.future_on(::DatadogBackup::ThreadPool::TPOOL, file_id) do |fid| + [fid, getdiff(fid)] end end end @@ -32,32 +33,17 @@ def definitive_resource_instance(id) matching_resource_instance(any_resource_instance.class_from_id(id)) end - def diffs - futures = all_diff_futures - ::DatadogBackup::ThreadPool.watcher.join - - format_diff_output( - Concurrent::Promises - .zip(*futures) - .value! - .compact - ) - end - def getdiff(id) result = definitive_resource_instance(id).diff(id) case result - when '' - nil - when "\n" - nil - when '
' + when '---' || '' || "\n" || '
' nil else result end end + # rubocop:disable Style/StringConcatenation def format_diff_output(diff_output) case diff_format when nil, :color @@ -69,58 +55,31 @@ def format_diff_output(diff_output) Diffy::CSS + '' + diff_output.map do |id, diff| - "

---
id: #{id}
" + diff + "

---
id: #{id}
#{diff}" end.join('
') + '' else raise 'Unexpected diff_format.' end end + # rubocop:enable Style/StringConcatenation def initialize(options) @options = options end - def matching_resource_instance(klass) - resource_instances.select { |resource_instance| resource_instance.instance_of?(klass) }.first - end - - def resource_instances - @resource_instances ||= resources.map do |resource| - resource.new(@options) - end - end - def restore futures = all_diff_futures watcher = ::DatadogBackup::ThreadPool.watcher futures.each do |future| id, diff = *future.value! - next unless diff + next if diff.nil? || diff.empty? if @options[:force_restore] definitive_resource_instance(id).restore(id) else - puts '--------------------------------------------------------------------------------' - puts format_diff_output([id, diff]) - puts '(r)estore to Datadog, overwrite local changes and (d)ownload, (s)kip, or (q)uit?' - response = $stdin.gets.chomp - case response - when 'q' - exit - when 'r' - puts "Restoring #{id} to Datadog." - definitive_resource_instance(id).restore(id) - when 'd' - puts "Downloading #{id} from Datadog." - definitive_resource_instance(id).get_and_write_file(id) - when 's' - next - else - puts 'Invalid response, please try again.' - response = $stdin.gets.chomp - end + ask_to_restore(id, diff) end end watcher.join if watcher.status @@ -131,5 +90,42 @@ def run! rescue SystemExit, Interrupt ::DatadogBackup::ThreadPool.shutdown end + + private + + def ask_to_restore(id, diff) + puts '--------------------------------------------------------------------------------' + puts format_diff_output([id, diff]) + puts '(r)estore to Datadog, overwrite local changes and (d)ownload, (s)kip, or (q)uit?' + loop do + response = $stdin.gets.chomp + case response + when 'q' + exit + when 'r' + puts "Restoring #{id} to Datadog." + definitive_resource_instance(id).restore(id) + break + when 'd' + puts "Downloading #{id} from Datadog." + definitive_resource_instance(id).get_and_write_file(id) + break + when 's' + break + else + puts 'Invalid response, please try again.' + end + end + end + + def matching_resource_instance(klass) + resource_instances.select { |resource_instance| resource_instance.instance_of?(klass) }.first + end + + def resource_instances + @resource_instances ||= resources.map do |resource| + resource.new(@options) + end + end end end diff --git a/lib/datadog_backup/core.rb b/lib/datadog_backup/core.rb index 552173c..f5bf7ad 100644 --- a/lib/datadog_backup/core.rb +++ b/lib/datadog_backup/core.rb @@ -5,14 +5,14 @@ require 'faraday' require 'faraday/retry' - - module DatadogBackup + # The default options for backing up and restores. + # This base class is meant to be extended by specific resources, such as Dashboards, Monitors, and so on. class Core include ::DatadogBackup::LocalFilesystem include ::DatadogBackup::Options - @@retry_options = { + @retry_options = { max: 5, interval: 0.05, interval_randomness: 0.5, @@ -20,27 +20,27 @@ class Core } def api_service - conn ||= Faraday.new( - url: api_url, - headers: { - 'DD-API-KEY' => ENV.fetch('DD_API_KEY'), - 'DD-APPLICATION-KEY' => ENV.fetch('DD_APP_KEY') - } - ) do |faraday| - faraday.request :json - faraday.request :retry, @@retry_options - faraday.response(:logger, LOGGER, {headers: true, bodies: LOGGER.debug?, log_level: :debug}) do | logger | - logger.filter(/(DD-API-KEY:)([^&]+)/, '\1[REDACTED]') - logger.filter(/(DD-APPLICATION-KEY:)([^&]+)/, '\1[REDACTED]') - end - faraday.response :raise_error - faraday.response :json - faraday.adapter Faraday.default_adapter - end + @api_service ||= Faraday.new( + url: api_url, + headers: { + 'DD-API-KEY' => ENV.fetch('DD_API_KEY'), + 'DD-APPLICATION-KEY' => ENV.fetch('DD_APP_KEY') + } + ) do |faraday| + faraday.request :json + faraday.request :retry, @retry_options + faraday.response(:logger, LOGGER, { headers: true, bodies: LOGGER.debug?, log_level: :debug }) do |logger| + logger.filter(/(DD-API-KEY:)([^&]+)/, '\1[REDACTED]') + logger.filter(/(DD-APPLICATION-KEY:)([^&]+)/, '\1[REDACTED]') + end + faraday.response :raise_error + faraday.response :json + faraday.adapter Faraday.default_adapter + end end def api_url - ENV.fetch('DD_SITE_URL', "https://api.datadoghq.com/") + ENV.fetch('DD_SITE_URL', 'https://api.datadoghq.com/') end def api_version @@ -51,6 +51,11 @@ def api_resource_name raise 'subclass is expected to implement #api_resource_name' end + # Some resources have a different key for the id. + def id_keyname + 'id' + end + def backup raise 'subclass is expected to implement #backup' end @@ -61,8 +66,8 @@ def diff(id) current = except(get_by_id(id)).deep_sort.to_yaml filesystem = except(load_from_file_by_id(id)).deep_sort.to_yaml result = ::Diffy::Diff.new(current, filesystem, include_plus_and_minus_in_html: true).to_s(diff_format) - LOGGER.debug("Compared ID #{id} and found #{result}") - result + LOGGER.debug("Compared ID #{id} and found filesystem: #{filesystem} <=> current: #{current} == result: #{result}") + result.chomp end # Returns a hash with banlist elements removed @@ -74,6 +79,7 @@ def except(hash) end end + # Fetch the specified resource from Datadog def get(id) params = {} headers = {} @@ -81,18 +87,25 @@ def get(id) body_with_2xx(response) end + # Returns a list of all resources in Datadog + # Do not use directly, but use the child classes' #all method instead def get_all return @get_all if @get_all + params = {} headers = {} response = api_service.get("/api/#{api_version}/#{api_resource_name}", params, headers) @get_all = body_with_2xx(response) end + # Download the resource from Datadog and write it to a file def get_and_write_file(id) - write_file(dump(get_by_id(id)), filename(id)) + body = get_by_id(id) + write_file(dump(body), filename(id)) + body end + # Fetch the specified resource from Datadog and remove the banlist elements def get_by_id(id) except(get(id)) end @@ -107,43 +120,57 @@ def myclass self.class.to_s.split(':').last.downcase end - # Calls out to Datadog and checks for a '200' response + # Create a new resource in Datadog def create(body) headers = {} - response = api_service.post("/api/#{api_version}/#{api_resource_name}", body, headers) + response = api_service.post("/api/#{api_version}/#{api_resource_name}", body, headers) body = body_with_2xx(response) - LOGGER.warn "Successfully created #{body.fetch('id')} in datadog." + LOGGER.warn "Successfully created #{body.fetch(id_keyname)} in datadog." + LOGGER.info 'Invalidating cache' + @get_all = nil body end - # Calls out to Datadog and checks for a '200' response + # Update an existing resource in Datadog def update(id, body) headers = {} response = api_service.put("/api/#{api_version}/#{api_resource_name}/#{id}", body, headers) body = body_with_2xx(response) - LOGGER.warn 'Successfully restored #{id} to datadog.' + LOGGER.warn "Successfully restored #{id} to datadog." + LOGGER.info 'Invalidating cache' + @get_all = nil body end + # If the resource exists in Datadog, update it. Otherwise, create it. def restore(id) body = load_from_file_by_id(id) begin update(id, body) rescue RuntimeError => e - if e.message.include?('update failed with error 404') - new_id = create(body).fetch('id') + raise e.message unless e.message.include?('update failed with error 404') - FileUtils.rm(find_file_by_id(id)) - get_and_write_file(new_id) - else - raise e.message - end + create_newly(id, body) end end + # Return the Faraday body from a response with a 2xx status code, otherwise raise an error def body_with_2xx(response) - raise "#{caller_locations(1,1)[0].label} failed with error #{response.status}" unless response.status.to_s =~ /^2/ + unless response.status.to_s =~ /^2/ + raise "#{caller_locations(1, + 1)[0].label} failed with error #{response.status}" + end + response.body end + + private + + # Create a new resource in Datadog, then move the old file to the new resource's ID + def create_newly(file_id, body) + new_id = create(body).fetch(id_keyname) + FileUtils.rm(find_file_by_id(file_id)) + get_and_write_file(new_id) + end end end diff --git a/lib/datadog_backup/dashboards.rb b/lib/datadog_backup/dashboards.rb index 889d73d..9c44f0a 100644 --- a/lib/datadog_backup/dashboards.rb +++ b/lib/datadog_backup/dashboards.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true module DatadogBackup + # Dashboards specific overrides for backup and restore. class Dashboards < Core + def all + get_all.fetch('dashboards') + end def api_version 'v1' @@ -11,13 +15,15 @@ def api_resource_name 'dashboard' end + def id_keyname + 'id' + end + def backup LOGGER.info("Starting diffs on #{::DatadogBackup::ThreadPool::TPOOL.max_length} threads") - - dashboards = get_all.fetch('dashboards') - futures = dashboards.map do |board| - Concurrent::Promises.future_on(::DatadogBackup::ThreadPool::TPOOL, board) do |board| - id = board['id'] + futures = all.map do |dashboard| + Concurrent::Promises.future_on(::DatadogBackup::ThreadPool::TPOOL, dashboard) do |board| + id = board[id_keyname] get_and_write_file(id) end end diff --git a/lib/datadog_backup/deprecations.rb b/lib/datadog_backup/deprecations.rb index 09c4074..4a431cb 100644 --- a/lib/datadog_backup/deprecations.rb +++ b/lib/datadog_backup/deprecations.rb @@ -1,11 +1,10 @@ - +# frozen_string_literal: true module DatadogBackup + # Notify the user if they are using deprecated features. module Deprecations def self.check - if RUBY_VERSION < '2.7' - LOGGER.warn "ruby-#{RUBY_VERSION} is deprecated. Ruby 2.7 or higher will be required to use this gem after datadog_backup@v3" - end + LOGGER.warn "ruby-#{RUBY_VERSION} is deprecated. Ruby 2.7 or higher will be required to use this gem after datadog_backup@v3" if RUBY_VERSION < '2.7' end end -end \ No newline at end of file +end diff --git a/lib/datadog_backup/local_filesystem.rb b/lib/datadog_backup/local_filesystem.rb index 3a6d674..df3f30d 100644 --- a/lib/datadog_backup/local_filesystem.rb +++ b/lib/datadog_backup/local_filesystem.rb @@ -6,11 +6,10 @@ require 'deepsort' module DatadogBackup + ## + # Meant to be mixed into DatadogBackup::Core + # Relies on @options[:backup_dir] and @options[:output_format] module LocalFilesystem - ## - # Meant to be mixed into DatadogBackup::Core - # Relies on @options[:backup_dir] and @options[:output_format] - def all_files ::Dir.glob(::File.join(backup_dir, '**', '*')).select { |f| ::File.file?(f) } end @@ -46,7 +45,7 @@ def filename(id) end def file_type(filepath) - ::File.extname(filepath).strip.downcase[1..-1].to_sym + ::File.extname(filepath).strip.downcase[1..].to_sym end def find_file_by_id(id) diff --git a/lib/datadog_backup/monitors.rb b/lib/datadog_backup/monitors.rb index 3ecae48..a35dcba 100644 --- a/lib/datadog_backup/monitors.rb +++ b/lib/datadog_backup/monitors.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true module DatadogBackup + # Monitor specific overrides for backup and restore. class Monitors < Core + def all + get_all + end def api_version 'v1' @@ -12,14 +16,14 @@ def api_resource_name end def backup - get_all.map do |monitor| + all.map do |monitor| id = monitor['id'] write_file(dump(get_by_id(id)), filename(id)) end end def get_by_id(id) - monitor = get_all.select { |monitor| monitor['id'].to_s == id.to_s }.first + monitor = all.select { |m| m['id'].to_s == id.to_s }.first monitor.nil? ? {} : except(monitor) end diff --git a/lib/datadog_backup/options.rb b/lib/datadog_backup/options.rb index b769a51..6280da2 100644 --- a/lib/datadog_backup/options.rb +++ b/lib/datadog_backup/options.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module DatadogBackup + # Describes what the user wants to see done. module Options def action @options[:action] diff --git a/lib/datadog_backup/synthetics.rb b/lib/datadog_backup/synthetics.rb new file mode 100644 index 0000000..957a806 --- /dev/null +++ b/lib/datadog_backup/synthetics.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module DatadogBackup + # Synthetic specific overrides for backup and restore. + class Synthetics < Core + def all + get_all.fetch('tests') + end + + def api_version + 'v1' + end + + def api_resource_name(body = nil) + return 'synthetics/tests' if body.nil? + return 'synthetics/tests' if body['type'].nil? + return 'synthetics/tests/browser' if body['type'].to_s == 'browser' + return 'synthetics/tests/api' if body['type'].to_s == 'api' + + raise "Unknown type #{body['type']}" + end + + def id_keyname + 'public_id' + end + + def backup + all.map do |synthetic| + id = synthetic[id_keyname] + get_and_write_file(id) + end + end + + def get_by_id(id) + synthetic = all.select { |s| s[id_keyname].to_s == id.to_s }.first + synthetic.nil? ? {} : except(synthetic) + end + + def initialize(options) + super(options) + @banlist = %w[creator created_at modified_at monitor_id public_id].freeze + end + + def create(body) + create_api_resource_name = api_resource_name(body) + headers = {} + response = api_service.post("/api/#{api_version}/#{create_api_resource_name}", body, headers) + resbody = body_with_2xx(response) + LOGGER.warn "Successfully created #{resbody.fetch(id_keyname)} in datadog." + LOGGER.info 'Invalidating cache' + @get_all = nil + resbody + end + + def update(id, body) + update_api_resource_name = api_resource_name(body) + headers = {} + response = api_service.put("/api/#{api_version}/#{update_api_resource_name}/#{id}", body, headers) + resbody = body_with_2xx(response) + LOGGER.warn "Successfully restored #{id} to datadog." + LOGGER.info 'Invalidating cache' + @get_all = nil + resbody + end + end +end diff --git a/lib/datadog_backup/thread_pool.rb b/lib/datadog_backup/thread_pool.rb index 1a4228b..6d607d8 100644 --- a/lib/datadog_backup/thread_pool.rb +++ b/lib/datadog_backup/thread_pool.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module DatadogBackup + # Used by CLI and Dashboards to size thread pool according to available CPU cores. module ThreadPool TPOOL = ::Concurrent::ThreadPoolExecutor.new( min_threads: [2, Concurrent.processor_count].max, diff --git a/spec/datadog_backup/cli_spec.rb b/spec/datadog_backup/cli_spec.rb index 70044ac..5d7590e 100644 --- a/spec/datadog_backup/cli_spec.rb +++ b/spec/datadog_backup/cli_spec.rb @@ -47,8 +47,8 @@ dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/deleted.json") stubs.get('/api/v1/dashboard') { all_dashboards } - stubs.get('/api/v1/dashboard/stillthere') {[200, {}, {}]} - stubs.get('/api/v1/dashboard/alsostillthere') {[200, {}, {}]} + stubs.get('/api/v1/dashboard/stillthere') { respond_with200({}) } + stubs.get('/api/v1/dashboard/alsostillthere') { respond_with200({}) } end it 'deletes the file locally as well' do @@ -58,66 +58,81 @@ end end - describe '#diffs' do - subject { cli.diffs } - - before do - dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/diffs1.json") - dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/diffs2.json") - dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/diffs3.json") - allow(dashboards).to receive(:get_by_id).and_return({ 'text' => 'diff2' }) - end - - it { - expect(subject).to include( - " ---\n id: diffs1\n ---\n-text: diff2\n+text: diff\n", - " ---\n id: diffs3\n ---\n-text: diff2\n+text: diff\n", - " ---\n id: diffs2\n ---\n-text: diff2\n+text: diff\n" - ) - } - end - describe '#restore' do - subject { cli.restore } + subject(:restore) { cli.restore } before do dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/diffs1.json") allow(dashboards).to receive(:get_by_id).and_return({ 'text' => 'diff2' }) + allow(dashboards).to receive(:write_file) + allow(dashboards).to receive(:update) end example 'starts interactive restore' do allow($stdin).to receive(:gets).and_return('q') - expect { subject }.to( + expect { restore }.to( output(/\(r\)estore to Datadog, overwrite local changes and \(d\)ownload, \(s\)kip, or \(q\)uit\?/).to_stdout .and(raise_error(SystemExit)) ) end - example 'restore' do - allow($stdin).to receive(:gets).and_return('r') - expect(dashboards).to receive(:update).with('diffs1', { 'text' => 'diff' }) - subject + context 'when the user chooses to restore' do + before do + allow($stdin).to receive(:gets).and_return('r') + end + + example 'it restores from disk to server' do + restore + expect(dashboards).to have_received(:update).with('diffs1', { 'text' => 'diff' }) + end end - example 'download' do - allow($stdin).to receive(:gets).and_return('d') - expect(dashboards).to receive(:write_file).with(%({\n "text": "diff2"\n}), "#{tempdir}/dashboards/diffs1.json") - subject + context 'when the user chooses to download' do + before do + allow($stdin).to receive(:gets).and_return('d') + end + + example 'it writes from server to disk' do + restore + expect(dashboards).to have_received(:write_file).with(%({\n "text": "diff2"\n}), "#{tempdir}/dashboards/diffs1.json") + end end - example 'skip' do - allow($stdin).to receive(:gets).and_return('s') - expect(dashboards).not_to receive(:write_file) - expect(dashboards).not_to receive(:update) - subject + context 'when the user chooses to skip' do + before do + allow($stdin).to receive(:gets).and_return('s') + end + + example 'it does not write to disk' do + restore + expect(dashboards).not_to have_received(:write_file) + end + + example 'it does not update the server' do + restore + expect(dashboards).not_to have_received(:update) + end end - example 'quit' do - allow($stdin).to receive(:gets).and_return('q') - expect(dashboards).not_to receive(:write_file) - expect(dashboards).not_to receive(:update) - expect { subject }.to raise_error(SystemExit) + context 'when the user chooses to quit' do + before do + allow($stdin).to receive(:gets).and_return('q') + end + + example 'it exits' do + expect { restore }.to raise_error(SystemExit) + end + + example 'it does not write to disk' do + restore + expect(dashboards).not_to have_received(:write_file) + end + + example 'it does not update the server' do + restore + expect(dashboards).not_to have_received(:update) + end end end end diff --git a/spec/datadog_backup/core_spec.rb b/spec/datadog_backup/core_spec.rb index 947f228..5367ad6 100644 --- a/spec/datadog_backup/core_spec.rb +++ b/spec/datadog_backup/core_spec.rb @@ -18,11 +18,8 @@ return core end - - - describe '#diff' do - subject { core.diff('diff') } + subject(:diff) { core.diff('diff') } before do allow(core).to receive(:get_by_id).and_return({ 'text' => 'diff1', 'extra' => 'diff1' }) @@ -30,13 +27,14 @@ end it { - expect(subject).to eq <<~EOF + expect(diff).to eq(<<~EODIFF --- -extra: diff1 -text: diff1 +extra: diff2 +text: diff2 - EOF + EODIFF + .chomp) } end @@ -47,11 +45,13 @@ end describe '#initialize' do - subject { core } + subject(:mycore) { core } it 'makes the subdirectories' do - expect(FileUtils).to receive(:mkdir_p).with("#{tempdir}/core") - subject + fileutils = class_double(FileUtils).as_stubbed_const + allow(fileutils).to receive(:mkdir_p) + mycore + expect(fileutils).to have_received(:mkdir_p).with("#{tempdir}/core") end end @@ -62,25 +62,25 @@ end describe '#create' do - subject { core.create({ 'a' => 'b' }) } + subject(:create) { core.create({ 'a' => 'b' }) } example 'it will post /api/v1/dashboard' do allow(core).to receive(:api_version).and_return('v1') allow(core).to receive(:api_resource_name).and_return('dashboard') - stubs.post('/api/v1/dashboard', {'a' => 'b'}) {[200, {}, {'id' => 'whatever-id-abc' }]} - subject + stubs.post('/api/v1/dashboard', { 'a' => 'b' }) { respond_with200({ 'id' => 'whatever-id-abc' }) } + create stubs.verify_stubbed_calls end end describe '#update' do - subject { core.update('abc-123-def', { 'a' => 'b' }) } + subject(:update) { core.update('abc-123-def', { 'a' => 'b' }) } example 'it puts /api/v1/dashboard' do allow(core).to receive(:api_version).and_return('v1') allow(core).to receive(:api_resource_name).and_return('dashboard') - stubs.put('/api/v1/dashboard/abc-123-def', {'a' => 'b'}) {[200, {}, {'id' => 'whatever-id-abc' }]} - subject + stubs.put('/api/v1/dashboard/abc-123-def', { 'a' => 'b' }) { respond_with200({ 'id' => 'whatever-id-abc' }) } + update stubs.verify_stubbed_calls end @@ -88,10 +88,11 @@ before do allow(core).to receive(:api_version).and_return('v1') allow(core).to receive(:api_resource_name).and_return('dashboard') - stubs.put('/api/v1/dashboard/abc-123-def', {'a' => 'b'}) {[404, {}, {'id' => 'whatever-id-abc' }]} + stubs.put('/api/v1/dashboard/abc-123-def', { 'a' => 'b' }) { [404, {}, { 'id' => 'whatever-id-abc' }] } end + it 'raises an error' do - expect { subject }.to raise_error(RuntimeError, 'update failed with error 404') + expect { update }.to raise_error(RuntimeError, 'update failed with error 404') end end end @@ -100,35 +101,55 @@ before do allow(core).to receive(:api_version).and_return('api-version-string') allow(core).to receive(:api_resource_name).and_return('api-resource-name-string') - stubs.get('/api/api-version-string/api-resource-name-string/abc-123-def') {[200, {}, {'test' => 'ok' }]} - stubs.get('/api/api-version-string/api-resource-name-string/bad-123-id') {[404, {}, {'error' => 'blahblah_not_found' }]} + stubs.get('/api/api-version-string/api-resource-name-string/abc-123-def') { respond_with200({ 'test' => 'ok' }) } + stubs.get('/api/api-version-string/api-resource-name-string/bad-123-id') do + [404, {}, { 'error' => 'blahblah_not_found' }] + end allow(core).to receive(:load_from_file_by_id).and_return({ 'load' => 'ok' }) end context 'when id exists' do - subject { core.restore('abc-123-def') } + subject(:restore) { core.restore('abc-123-def') } example 'it calls out to update' do - expect(core).to receive(:update).with('abc-123-def', { 'load' => 'ok' }) - subject + allow(core).to receive(:update) + restore + expect(core).to have_received(:update).with('abc-123-def', { 'load' => 'ok' }) end end context 'when id does not exist on remote' do - subject { core.restore('bad-123-id') } + subject(:restore_newly) { core.restore('bad-123-id') } + + let(:fileutils) { class_double(FileUtils).as_stubbed_const } before do allow(core).to receive(:load_from_file_by_id).and_return({ 'load' => 'ok' }) - stubs.put('/api/api-version-string/api-resource-name-string/bad-123-id') {[404, {}, {'error' => 'id not found' }]} - stubs.post('/api/api-version-string/api-resource-name-string', {'load' => 'ok'}) {[200, {}, {'id' => 'my-new-id' }]} + stubs.put('/api/api-version-string/api-resource-name-string/bad-123-id') do + [404, {}, { 'error' => 'id not found' }] + end + stubs.post('/api/api-version-string/api-resource-name-string', { 'load' => 'ok' }) do + respond_with200({ 'id' => 'my-new-id' }) + end + allow(fileutils).to receive(:rm) + allow(core).to receive(:create).with({ 'load' => 'ok' }).and_return({ 'id' => 'my-new-id' }) + allow(core).to receive(:get_and_write_file) + allow(core).to receive(:find_file_by_id).with('bad-123-id').and_return('/path/to/bad-123-id.json') end - example 'it calls out to create then saves the new file and deletes the new file' do - expect(core).to receive(:create).with({ 'load' => 'ok' }).and_return({ 'id' => 'my-new-id' }) - expect(core).to receive(:get_and_write_file).with('my-new-id') - allow(core).to receive(:find_file_by_id).with('bad-123-id').and_return('/path/to/bad-123-id.json') - expect(FileUtils).to receive(:rm).with('/path/to/bad-123-id.json') - subject + example 'it calls out to create' do + restore_newly + expect(core).to have_received(:create).with({ 'load' => 'ok' }) + end + + example 'it saves the new file' do + restore_newly + expect(core).to have_received(:get_and_write_file).with('my-new-id') + end + + example 'it deletes the old file' do + restore_newly + expect(fileutils).to have_received(:rm).with('/path/to/bad-123-id.json') end end end diff --git a/spec/datadog_backup/dashboards_spec.rb b/spec/datadog_backup/dashboards_spec.rb index b8356c0..8a8fe5d 100644 --- a/spec/datadog_backup/dashboards_spec.rb +++ b/spec/datadog_backup/dashboards_spec.rb @@ -23,24 +23,6 @@ 'title' => 'foo' } end - let(:all_dashboards) do - [ - 200, - {}, - { - 'dashboards' => [ - dashboard_description - ] - } - ] - end - let(:example_dashboard) do - [ - 200, - {}, - board_abc_123_def - ] - end let(:board_abc_123_def) do { 'graphs' => [ @@ -61,6 +43,8 @@ 'title' => 'example dashboard' } end + let(:all_dashboards) { respond_with200({ 'dashboards' => [dashboard_description] }) } + let(:example_dashboard) { respond_with200(board_abc_123_def) } before do stubs.get('/api/v1/dashboard') { all_dashboards } @@ -71,20 +55,32 @@ subject { dashboards.backup } it 'is expected to create a file' do - file = double('file') + file = instance_double(File) allow(File).to receive(:open).with(dashboards.filename('abc-123-def'), 'w').and_return(file) - expect(file).to receive(:write).with(::JSON.pretty_generate(board_abc_123_def.deep_sort)) + allow(file).to receive(:write) allow(file).to receive(:close) dashboards.backup + expect(file).to have_received(:write).with(::JSON.pretty_generate(board_abc_123_def.deep_sort)) end end + describe '#filename' do + subject { dashboards.filename('abc-123-def') } + + it { is_expected.to eq("#{tempdir}/dashboards/abc-123-def.json") } + end + + describe '#get_by_id' do + subject { dashboards.get_by_id('abc-123-def') } + + it { is_expected.to eq board_abc_123_def } + end describe '#diff' do it 'calls the api only once' do dashboards.write_file('{"a":"b"}', dashboards.filename('abc-123-def')) - expect(dashboards.diff('abc-123-def')).to eq(<<~EOF + expect(dashboards.diff('abc-123-def')).to eq(<<~EODASH --- -description: example dashboard -graphs: @@ -96,8 +92,8 @@ - title: example graph -title: example dashboard +a: b - EOF - ) + EODASH + .chomp) end end @@ -106,10 +102,4 @@ it { is_expected.to eq({ a: :b }) } end - - describe '#get_by_id' do - subject { dashboards.get_by_id('abc-123-def') } - - it { is_expected.to eq board_abc_123_def } - end end diff --git a/spec/datadog_backup/deprecations_spec.rb b/spec/datadog_backup/deprecations_spec.rb index d080c75..b08e030 100644 --- a/spec/datadog_backup/deprecations_spec.rb +++ b/spec/datadog_backup/deprecations_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' describe DatadogBackup::Deprecations do - let(:logger) { double } + subject(:check) { described_class.check } + + let(:logger) { instance_double(Logger) } before do stub_const('LOGGER', logger) @@ -12,24 +14,20 @@ %w[2.4.10 2.5.9 2.6.8].each do |ruby_version| describe "#check#{ruby_version}" do - subject { described_class.check } - it 'does warn' do stub_const('RUBY_VERSION', ruby_version) - expect(logger).to receive(:warn).with(/ruby-#{ruby_version} is deprecated./) - subject + check + expect(logger).to have_received(:warn).with(/ruby-#{ruby_version} is deprecated./) end end end %w[2.7.4 3.0.4 3.1.2 3.2.0-preview1].each do |ruby_version| describe "#check#{ruby_version}" do - subject { described_class.check } - it 'does not warn' do stub_const('RUBY_VERSION', ruby_version) - expect(logger).to_not receive(:warn).with(/ruby-#{ruby_version} is deprecated./) - subject + check + expect(logger).not_to have_received(:warn).with(/ruby-#{ruby_version} is deprecated./) end end end diff --git a/spec/datadog_backup/local_filesystem_spec.rb b/spec/datadog_backup/local_filesystem_spec.rb index 2e93254..727de3e 100644 --- a/spec/datadog_backup/local_filesystem_spec.rb +++ b/spec/datadog_backup/local_filesystem_spec.rb @@ -68,13 +68,13 @@ end describe '#dump' do - context ':json' do + context 'when mode is :json' do subject { core.dump({ a: :b }) } it { is_expected.to eq(%({\n "a": "b"\n})) } end - context ':yaml' do + context 'when mode is :yaml' do subject { core_yaml.dump({ 'a' => 'b' }) } it { is_expected.to eq(%(---\na: b\n)) } @@ -82,13 +82,13 @@ end describe '#filename' do - context ':json' do + context 'when mode is :json' do subject { core.filename('abc-123-def') } it { is_expected.to eq("#{tempdir}/core/abc-123-def.json") } end - context ':yaml' do + context 'when mode is :yaml' do subject { core_yaml.filename('abc-123-def') } it { is_expected.to eq("#{tempdir}/core/abc-123-def.yaml") } @@ -124,13 +124,13 @@ end describe '#load_from_file' do - context ':json' do + context 'when mode is :json' do subject { core.load_from_file(%({\n "a": "b"\n}), :json) } it { is_expected.to eq('a' => 'b') } end - context ':yaml' do + context 'when mode is :yaml' do subject { core.load_from_file(%(---\na: b\n), :yaml) } it { is_expected.to eq('a' => 'b') } @@ -138,7 +138,7 @@ end describe '#load_from_file_by_id' do - context 'written in json read in yaml mode' do + context 'when the backup is in json but the mode is :yaml' do subject { core_yaml.load_from_file_by_id('abc-123-def') } before { core.write_file(%({"a": "b"}), "#{tempdir}/core/abc-123-def.json") } @@ -148,7 +148,7 @@ it { is_expected.to eq('a' => 'b') } end - context 'written in yaml read in json mode' do + context 'when the backup is in yaml but the mode is :json' do subject { core.load_from_file_by_id('abc-123-def') } before { core.write_file(%(---\na: b), "#{tempdir}/core/abc-123-def.yaml") } @@ -158,7 +158,7 @@ it { is_expected.to eq('a' => 'b') } end - context 'Integer as parameter' do + context 'with Integer as parameter' do subject { core.load_from_file_by_id(12_345) } before { core.write_file(%(---\na: b), "#{tempdir}/core/12345.yaml") } @@ -170,9 +170,9 @@ end describe '#write_file' do - subject { core.write_file('abc123', "#{tempdir}/core/abc-123-def.json") } + subject(:write_file) { core.write_file('abc123', "#{tempdir}/core/abc-123-def.json") } - let(:file_like_object) { double } + let(:file_like_object) { instance_double(File) } it 'writes a file to abc-123-def.json' do allow(File).to receive(:open).and_call_original @@ -180,7 +180,7 @@ allow(file_like_object).to receive(:write) allow(file_like_object).to receive(:close) - subject + write_file expect(file_like_object).to have_received(:write).with('abc123') end diff --git a/spec/datadog_backup/monitors_spec.rb b/spec/datadog_backup/monitors_spec.rb index eb026ba..090e057 100644 --- a/spec/datadog_backup/monitors_spec.rb +++ b/spec/datadog_backup/monitors_spec.rb @@ -16,7 +16,6 @@ allow(monitors).to receive(:api_service).and_return(api_client_double) return monitors end - let(:monitor_description) do { 'query' => 'bar', @@ -35,22 +34,8 @@ 'query' => 'bar' } end - let(:all_monitors) do - [ - 200, - {}, - [ - monitor_description - ] - ] - end - let(:example_monitor) do - [ - 200, - {}, - monitor_description - ] - end + let(:all_monitors) { respond_with200([monitor_description]) } + let(:example_monitor) { respond_with200(monitor_description) } before do stubs.get('/api/v1/monitor') { all_monitors } @@ -67,22 +52,21 @@ subject { monitors.backup } it 'is expected to create a file' do - file = double('file') + file = instance_double(File) allow(File).to receive(:open).with(monitors.filename(123_455), 'w').and_return(file) - expect(file).to receive(:write).with(::JSON.pretty_generate(clean_monitor_description)) + allow(file).to receive(:write) allow(file).to receive(:close) monitors.backup + expect(file).to have_received(:write).with(::JSON.pretty_generate(clean_monitor_description)) end end describe '#diff and #except' do example 'it ignores `overall_state` and `overall_state_modified`' do monitors.write_file(monitors.dump(monitor_description), monitors.filename(123_455)) - stubs.get('/api/v1/dashboard/123455') { - [ - 200, - {}, + stubs.get('/api/v1/dashboard/123455') do + respond_with200( [ { 'query' => 'bar', @@ -93,8 +77,8 @@ 'overall_state_modified' => '9999-07-27T22:55:55+00:00' } ] - ] - } + ) + end expect(monitors.diff(123_455)).to eq '' @@ -109,13 +93,13 @@ end describe '#get_by_id' do - context 'Integer' do + context 'when Integer' do subject { monitors.get_by_id(123_455) } it { is_expected.to eq monitor_description } end - context 'String' do + context 'when String' do subject { monitors.get_by_id('123455') } it { is_expected.to eq monitor_description } diff --git a/spec/datadog_backup/synthetics_spec.rb b/spec/datadog_backup/synthetics_spec.rb new file mode 100644 index 0000000..c1e4d82 --- /dev/null +++ b/spec/datadog_backup/synthetics_spec.rb @@ -0,0 +1,258 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DatadogBackup::Synthetics do + let(:stubs) { Faraday::Adapter::Test::Stubs.new } + let(:api_client_double) { Faraday.new { |f| f.adapter :test, stubs } } + let(:tempdir) { Dir.mktmpdir } # TODO: delete afterward + let(:synthetics) do + synthetics = described_class.new( + action: 'backup', + backup_dir: tempdir, + output_format: :json, + resources: [] + ) + allow(synthetics).to receive(:api_service).and_return(api_client_double) + return synthetics + end + let(:api_test) do + { 'config' => { 'assertions' => [{ 'operator' => 'contains', 'property' => 'set-cookie', 'target' => '_user_id', 'type' => 'header' }, + { 'operator' => 'contains', 'target' => 'body message', 'type' => 'body' }, + { 'operator' => 'is', 'property' => 'content-type', 'target' => 'text/html; charset=utf-8', 'type' => 'header' }, + { 'operator' => 'is', 'target' => 200, 'type' => 'statusCode' }, + { 'operator' => 'lessThan', 'target' => 5000, 'type' => 'responseTime' }], + 'request' => { 'headers' => { 'User-Agent' => 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0', + 'cookie' => '_a=12345; _example_session=abc123' }, + 'method' => 'GET', + 'url' => 'https://www.example.com/' } }, + 'creator' => { 'email' => 'user@example.com', 'handle' => 'user@example.com', 'name' => 'Hugh Zer' }, + 'locations' => ['aws:ap-northeast-1', 'aws:eu-central-1', 'aws:eu-west-2', 'aws:us-west-2'], + 'message' => 'TEST: This is a test', + 'monitor_id' => 12_345, + 'name' => 'TEST: This is a test', + 'options' => { 'follow_redirects' => true, + 'httpVersion' => 'http1', + 'min_failure_duration' => 120, + 'min_location_failed' => 2, + 'monitor_options' => { 'renotify_interval' => 0 }, + 'monitor_priority' => 1, + 'retry' => { 'count' => 1, 'interval' => 500 }, + 'tick_every' => 120 }, + 'public_id' => 'abc-123-def', + 'status' => 'live', + 'subtype' => 'http', + 'tags' => ['env:test'], + 'type' => 'api' } + end + let(:browser_test) do + { 'config' => { 'assertions' => [], + 'configVariables' => [], + 'request' => { 'headers' => {}, 'method' => 'GET', 'url' => 'https://www.example.com' }, + 'setCookie' => nil, + 'variables' => [] }, + 'creator' => { 'email' => 'user@example.com', + 'handle' => 'user@example.com', + 'name' => 'Hugh Zer' }, + 'locations' => ['aws:us-east-2'], + 'message' => 'Test message', + 'monitor_id' => 12_345, + 'name' => 'www.example.com', + 'options' => { 'ci' => { 'executionRule' => 'non_blocking' }, + 'device_ids' => ['chrome.laptop_large', 'chrome.mobile_small'], + 'disableCors' => false, + 'disableCsp' => false, + 'ignoreServerCertificateError' => false, + 'min_failure_duration' => 300, + 'min_location_failed' => 1, + 'monitor_options' => { 'renotify_interval' => 0 }, + 'noScreenshot' => false, + 'retry' => { 'count' => 0, 'interval' => 1000 }, + 'tick_every' => 900 }, + 'public_id' => '456-ghi-789', + 'status' => 'live', + 'tags' => ['env:test'], + 'type' => 'browser' } + end + let(:all_synthetics) { respond_with200({ 'tests' => [api_test, browser_test] }) } + let(:api_synthetic) { respond_with200(api_test) } + let(:browser_synthetic) { respond_with200(browser_test) } + + before do + stubs.get('/api/v1/synthetics/tests') { all_synthetics } + stubs.get('/api/v1/synthetics/tests/api/abc-123-def') { api_synthetic } + stubs.get('/api/v1/synthetics/tests/browser/456-ghi-789') { browser_synthetic } + end + + describe '#all' do + subject { synthetics.all } + + it { is_expected.to contain_exactly(api_test, browser_test) } + end + + describe '#backup' do + subject(:backup) { synthetics.backup } + + let(:apifile) { instance_double(File) } + let(:browserfile) { instance_double(File) } + + before do + allow(File).to receive(:open).with(synthetics.filename('abc-123-def'), 'w').and_return(apifile) + allow(File).to receive(:open).with(synthetics.filename('456-ghi-789'), 'w').and_return(browserfile) + allow(apifile).to receive(:write) + allow(apifile).to receive(:close) + allow(browserfile).to receive(:write) + allow(browserfile).to receive(:close) + end + + it 'is expected to write the API test' do + backup + expect(apifile).to have_received(:write).with(::JSON.pretty_generate(api_test)) + end + + it 'is expected to write the browser test' do + backup + expect(browserfile).to have_received(:write).with(::JSON.pretty_generate(browser_test)) + end + end + + describe '#filename' do + subject { synthetics.filename('abc-123-def') } + + it { is_expected.to eq("#{tempdir}/synthetics/abc-123-def.json") } + end + + describe '#get_by_id' do + context 'when the type is api' do + subject { synthetics.get_by_id('abc-123-def') } + + it { is_expected.to eq api_test } + end + + context 'when the type is browser' do + subject { synthetics.get_by_id('456-ghi-789') } + + it { is_expected.to eq browser_test } + end + end + + describe '#diff' do # TODO: migrate to core_spec.rb, since #diff is not defined here. + subject { synthetics.diff('abc-123-def') } + + before do + synthetics.write_file(synthetics.dump(api_test), synthetics.filename('abc-123-def')) + end + + context 'when the test is identical' do + it { is_expected.to be_empty } + end + + context 'when the remote is not found' do + subject(:invalid_diff) { synthetics.diff('invalid-id') } + + before do + synthetics.write_file(synthetics.dump({ 'name' => 'invalid-diff' }), synthetics.filename('invalid-id')) + end + + it { + expect(invalid_diff).to eq(%(---- {}\n+---\n+name: invalid-diff)) + } + end + + context 'when there is a local update' do + before do + different_test = api_test.dup + different_test['message'] = 'Different message' + synthetics.write_file(synthetics.dump(different_test), synthetics.filename('abc-123-def')) + end + + it { is_expected.to include(%(-message: 'TEST: This is a test'\n+message: Different message)) } + end + end + + describe '#create' do + context 'when the type is api' do + subject(:create) { synthetics.create({ 'type' => 'api' }) } + + before do + stubs.post('/api/v1/synthetics/tests/api') { respond_with200({ 'public_id' => 'api-create-abc' }) } + end + + it { is_expected.to eq({ 'public_id' => 'api-create-abc' }) } + end + + context 'when the type is browser' do + subject(:create) { synthetics.create({ 'type' => 'browser' }) } + + before do + stubs.post('/api/v1/synthetics/tests/browser') { respond_with200({ 'public_id' => 'browser-create-abc' }) } + end + + it { is_expected.to eq({ 'public_id' => 'browser-create-abc' }) } + end + end + + describe '#update' do + context 'when the type is api' do + subject(:update) { synthetics.update('api-update-abc', { 'type' => 'api' }) } + + before do + stubs.put('/api/v1/synthetics/tests/api/api-update-abc') { respond_with200({ 'public_id' => 'api-update-abc' }) } + end + + it { is_expected.to eq({ 'public_id' => 'api-update-abc' }) } + end + + context 'when the type is browser' do + subject(:update) { synthetics.update('browser-update-abc', { 'type' => 'browser' }) } + + before do + stubs.put('/api/v1/synthetics/tests/browser/browser-update-abc') { respond_with200({ 'public_id' => 'browser-update-abc' }) } + end + + it { is_expected.to eq({ 'public_id' => 'browser-update-abc' }) } + end + end + + describe '#restore' do + context 'when the id exists' do + subject { synthetics.restore('abc-123-def') } + + before do + synthetics.write_file(synthetics.dump({ 'name' => 'restore-valid-id', 'type' => 'api' }), synthetics.filename('abc-123-def')) + stubs.put('/api/v1/synthetics/tests/api/abc-123-def') { respond_with200({ 'public_id' => 'abc-123-def', 'type' => 'api' }) } + end + + it { is_expected.to eq({ 'public_id' => 'abc-123-def', 'type' => 'api' }) } + end + + context 'when the id does not exist' do + subject(:restore) { synthetics.restore('restore-invalid-id') } + + before do + synthetics.write_file(synthetics.dump({ 'name' => 'restore-invalid-id', 'type' => 'api' }), synthetics.filename('restore-invalid-id')) + stubs.put('/api/v1/synthetics/tests/api/restore-invalid-id') { [404, {}, ''] } + stubs.post('/api/v1/synthetics/tests/api') { respond_with200({ 'public_id' => 'restore-valid-id' }) } + allow(synthetics).to receive(:create).and_call_original + allow(synthetics).to receive(:all).and_return([api_test, browser_test, { 'public_id' => 'restore-valid-id', 'type' => 'api' }]) + end + + it { is_expected.to eq({ 'type' => 'api' }) } + + it 'calls create with the contents of the original file' do + restore + expect(synthetics).to have_received(:create).with({ 'name' => 'restore-invalid-id', 'type' => 'api' }) + end + + it 'deletes the original file' do + restore + expect(File.exist?(synthetics.filename('restore-invalid-id'))).to be false + end + + it 'creates a new file with the restored contents' do + restore + expect(File.exist?(synthetics.filename('restore-valid-id'))).to be true + end + end + end +end diff --git a/spec/datadog_backup_bin_spec.rb b/spec/datadog_backup_bin_spec.rb index 9360033..44ae87f 100644 --- a/spec/datadog_backup_bin_spec.rb +++ b/spec/datadog_backup_bin_spec.rb @@ -3,13 +3,13 @@ require 'open3' require 'timeout' -describe 'bin/datadog_backup' do +describe 'bin/datadog_backup' do # rubocop:disable RSpec/DescribeClass # Contract Or[nil,String] => self def run_bin(env = {}, args = '') status = nil output = '' cmd = "bin/datadog_backup #{args}" - Open3.popen2e(env, cmd) do |i, oe, t| + Open3.popen2e(env, cmd) do |_i, oe, t| pid = t.pid Timeout.timeout(4.0) do @@ -44,9 +44,17 @@ def run_bin(env = {}, args = '') end end - it 'supplies help' do - out_err, status = run_bin(env, '--help') - expect(out_err).to match(/Usage: DD_API_KEY=/) - expect(status).to be_success + describe 'help' do + subject(:bin) { run_bin(env, '--help') } + + it 'prints usage' do + out_err, _status = bin + expect(out_err).to match(/Usage: DD_API_KEY=/) + end + + it 'exits cleanly' do + _out_err, status = bin + expect(status).to be_success + end end end diff --git a/spec/datadog_backup_spec.rb b/spec/datadog_backup_spec.rb deleted file mode 100644 index e08c368..0000000 --- a/spec/datadog_backup_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe DatadogBackup do -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4b2c71c..e784e1e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,8 +11,6 @@ require 'tmpdir' require 'datadog_backup' - - SPEC_ROOT = __dir__ WORK_ROOT = File.expand_path(File.join(SPEC_ROOT, '..')) @@ -38,3 +36,7 @@ Kernel.srand config.seed end + +def respond_with200(body) + [200, {}, body] +end