Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion lib/bolt/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def initialize(msg, kind, details = nil, issue_code = nil)
super(msg)
@kind = kind
@issue_code = issue_code
@details = details || {}
@details = flatten_errors(details || {})
@error_code ||= 1
end

Expand Down Expand Up @@ -38,6 +38,26 @@ def to_puppet_error
Puppet::DataTypes::Error.from_asserted_hash(to_h)
end

private

# Recursively convert nested Error objects to plain hashes at
# construction time, preventing infinite recursion in Puppet's
# type inference and JSON serialization (see #3373).
def flatten_errors(obj)
case obj
when Bolt::Error
obj.to_h
when Hash
obj.transform_values { |v| flatten_errors(v) }
when Array
obj.map { |v| flatten_errors(v) }
else
obj
end
end

public

def self.unknown_task(task)
command = Bolt::Util.powershell? ? "Get-BoltTask" : "bolt task show"
new(
Expand Down
71 changes: 71 additions & 0 deletions spec/unit/error_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

require 'spec_helper'
require 'bolt/error'

describe Bolt::Error do
describe '#to_h' do
it 'returns a hash with kind, msg, and details' do
error = Bolt::Error.new('test message', 'bolt/test-error', { 'key' => 'value' })
h = error.to_h
expect(h['kind']).to eq('bolt/test-error')
expect(h['msg']).to eq('test message')
expect(h['details']).to eq({ 'key' => 'value' })
end

it 'includes issue_code when present' do
error = Bolt::Error.new('msg', 'bolt/test', {}, 'ISSUE_1')
expect(error.to_h['issue_code']).to eq('ISSUE_1')
end
end

describe '#to_json' do
it 'returns valid JSON' do
error = Bolt::Error.new('msg', 'bolt/test', { 'a' => 1 })
parsed = JSON.parse(error.to_json)
expect(parsed['kind']).to eq('bolt/test')
expect(parsed['msg']).to eq('msg')
end
end

describe 'nested Error objects in details' do
it 'flattens a nested Bolt::Error to a hash' do
inner = Bolt::Error.new('inner message', 'bolt/inner-error', { 'inner_key' => 'inner_value' })
outer = Bolt::Error.new('outer message', 'bolt/outer-error', { 'error' => inner })

expect(outer.details['error']).to be_a(Hash)
expect(outer.details['error']['kind']).to eq('bolt/inner-error')
expect(outer.details['error']['msg']).to eq('inner message')
expect(outer.details['error']['details']).to eq({ 'inner_key' => 'inner_value' })
end

it 'flattens deeply nested Bolt::Error objects' do
innermost = Bolt::Error.new('deep', 'bolt/deep', {})
middle = Bolt::Error.new('mid', 'bolt/mid', { 'cause' => innermost })
outer = Bolt::Error.new('top', 'bolt/top', { 'wrapped' => middle })

expect(outer.details['wrapped']).to be_a(Hash)
expect(outer.details['wrapped']['details']['cause']).to be_a(Hash)
expect(outer.details['wrapped']['details']['cause']['msg']).to eq('deep')
end

it 'flattens Bolt::Error inside an array in details' do
inner = Bolt::Error.new('arr error', 'bolt/arr', {})
outer = Bolt::Error.new('top', 'bolt/top', { 'errors' => [inner] })

expect(outer.details['errors']).to be_an(Array)
expect(outer.details['errors'][0]).to be_a(Hash)
expect(outer.details['errors'][0]['msg']).to eq('arr error')
end

it 'serializes to JSON without infinite recursion' do
inner = Bolt::Error.new('inner', 'bolt/inner', { 'x' => 1 })
outer = Bolt::Error.new('outer', 'bolt/outer', { 'error' => inner })

json = nil
expect { json = outer.to_json }.not_to raise_error
parsed = JSON.parse(json)
expect(parsed['details']['error']['kind']).to eq('bolt/inner')
end
end
end