Skip to content

Commit 82c43c3

Browse files
committed
Allow HistoryManager to mock input library history
1 parent 60c09e5 commit 82c43c3

File tree

2 files changed

+73
-71
lines changed

2 files changed

+73
-71
lines changed

lib/rex/ui/text/shell/history_manager.rb

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,30 @@ def debug?
7373
@debug
7474
end
7575

76+
# A wrapper around mapping the input library to its history; this way we can mock the return value of this method.
77+
def map_library_to_history(input_library)
78+
case input_library
79+
when :readline
80+
::Readline::HISTORY
81+
when :reline
82+
::Reline::HISTORY
83+
else
84+
$stderr.puts("Unknown input library: #{input_library}") if debug?
85+
[]
86+
end
87+
end
88+
89+
def clear_library(input_library)
90+
case input_library
91+
when :readline
92+
clear_readline
93+
when :reline
94+
clear_reline
95+
else
96+
$stderr.puts("Unknown input library: #{input_library}") if debug?
97+
end
98+
end
99+
76100
def push_context(history_file: nil, name: nil, input_library: nil)
77101
$stderr.puts("Push context before\n#{JSON.pretty_generate(_contexts)}") if debug?
78102
new_context = { history_file: history_file, name: name, input_library: input_library || :readline }
@@ -122,11 +146,11 @@ def clear_reline
122146

123147
def load_history_file(context)
124148
history_file = context[:history_file]
125-
history = context[:input_library] == :reline ? ::Reline::HISTORY : ::Readline::HISTORY
149+
history = map_library_to_history(context[:input_library])
126150

127151
begin
128152
File.open(history_file, 'r') do |f|
129-
context[:input_library] == :reline ? clear_reline : clear_readline
153+
clear_library(context[:input_library])
130154
f.each do |line|
131155
chomped_line = line.chomp
132156
if context[:input_library] == :reline && history.last&.end_with?("\\")
@@ -144,7 +168,7 @@ def load_history_file(context)
144168

145169
def store_history_file(context)
146170
history_file = context[:history_file]
147-
history = context[:input_library] == :reline ? ::Reline::HISTORY : ::Readline::HISTORY
171+
history = map_library_to_history(context[:input_library])
148172

149173
history_diff = history.length < MAX_HISTORY ? history.length : MAX_HISTORY
150174

spec/lib/rex/ui/text/shell/history_manager_spec.rb

Lines changed: 46 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -131,105 +131,83 @@
131131
end
132132

133133
describe '#store_history_file' do
134-
context 'when storing above max history lines' do
135-
def clear_readline
136-
::Readline::HISTORY.pop until ::Readline::HISTORY.empty?
137-
end
138-
139-
def clear_reline
140-
::Reline::HISTORY.pop until ::Reline::HISTORY.empty?
141-
end
134+
let(:initial_history) { [] }
135+
let(:history_mock) { initial_history }
136+
let(:history_choices) { %w[sessions run query help] }
137+
let(:history_file) { ::Tempfile.new('history') }
142138

143-
before(:each) do
144-
@history_file = ::Tempfile.new('history')
145-
146-
# Store the current history & clear Readline && Reline
147-
@readline_history_before = ::Readline::HISTORY.to_a
148-
@reline_history_before = ::Reline::HISTORY.to_a
139+
after(:each) do
140+
# https://ruby-doc.org/stdlib-2.5.3/libdoc/tempfile/rdoc/Tempfile.html#class-Tempfile-label-Explicit+close
141+
history_file.unlink
142+
end
149143

150-
clear_readline
151-
clear_reline
152-
end
144+
[
145+
{ history_size: described_class::MAX_HISTORY + 10, expected_size: described_class::MAX_HISTORY },
146+
{ history_size: described_class::MAX_HISTORY, expected_size: described_class::MAX_HISTORY },
147+
{ history_size: described_class::MAX_HISTORY - 10, expected_size: described_class::MAX_HISTORY - 10 },
148+
].each do |test|
149+
context "when storing #{test[:history_size]} lines" do
150+
it "correctly stores #{test[:expected_size]} lines" do
151+
allow(subject).to receive(:_remaining_work).and_call_original
152+
allow(subject).to receive(:store_history_file).and_call_original
153+
allow(subject).to receive(:map_library_to_history).and_return(history_mock)
154+
155+
test[:history_size].times do
156+
# This imitates the user typing in a command and pressing the 'enter' key.
157+
history_mock << history_choices.sample
158+
end
153159

154-
after(:each) do
155-
clear_readline
156-
@readline_history_before.each { |line| ::Readline::HISTORY << line }
160+
context = { input_library: :readline, history_file: history_file.path, name: 'history'}
157161

158-
clear_reline
159-
@reline_history_before.each { |line| ::Reline::HISTORY << line }
160-
end
162+
subject.send(:store_history_file, context)
161163

162-
it 'truncates to max allowed history' do
163-
allow(subject).to receive(:_remaining_work).and_call_original
164-
allow(subject).to receive(:store_history_file).and_call_original
164+
sleep(0.1) until subject._remaining_work.empty?
165165

166-
history_choices = %w[sessions run query help]
167-
max_history = subject.class::MAX_HISTORY
168-
# Populate example history we want to store
169-
total_times = max_history + 10
170-
total_times.times do
171-
::Readline::HISTORY << history_choices[rand(history_choices.count)]
166+
expect(history_file.read.split("\n").count).to eq(test[:expected_size])
172167
end
173-
174-
context = { input_library: :readline, history_file: @history_file.path, name: 'history'}
175-
176-
subject.send(:store_history_file, context)
177-
178-
sleep(0.1) until subject._remaining_work.empty?
179-
180-
expect(@history_file.read.split("\n").count).to eq(max_history)
181168
end
182169
end
183170
end
184171

185172
describe '#load_history_file' do
186-
def clear_readline
187-
::Readline::HISTORY.pop until ::Readline::HISTORY.empty?
188-
end
189-
190-
def clear_reline
191-
::Reline::HISTORY.pop until ::Reline::HISTORY.empty?
192-
end
193-
194-
before(:each) do
195-
@history_file = ::Tempfile.new('history')
196-
197-
# Store the current history & clear Readline && Reline
198-
@readline_history_before = ::Readline::HISTORY.to_a
199-
@reline_history_before = ::Reline::HISTORY.to_a
200-
201-
clear_readline
202-
clear_reline
203-
end
173+
let(:initial_history) { [] }
174+
let(:history_mock) { initial_history }
175+
let(:history_choices) { %w[sessions run query help] }
176+
let(:history_file) { ::Tempfile.new('history') }
204177

205178
after(:each) do
206-
clear_readline
207-
@readline_history_before.each { |line| ::Readline::HISTORY << line }
208-
209-
clear_reline
210-
@reline_history_before.each { |line| ::Reline::HISTORY << line }
179+
history_file.unlink
211180
end
212181

213182
context 'when history file is not accessible' do
214183
it 'the library history remains unchanged' do
184+
allow(subject).to receive(:map_library_to_history).and_return(history_mock)
215185
history_file = ::File.join('does/not/exist/history')
216186
context = { input_library: :readline, history_file: history_file, name: 'history' }
217187

218188
subject.send(:load_history_file, context)
219-
expect(::Readline::HISTORY.to_a).to eq(@readline_history_before)
189+
expect(history_mock).to eq(initial_history)
220190
end
221191
end
222192

223193
context 'when history file is accessible' do
224194
it 'correctly loads the history' do
225-
history_file = ::File.join(Msf::Config.history_file)
226-
history_lines = ::File.read(history_file).split("\n")
195+
allow(subject).to receive(:map_library_to_history).and_return(history_mock)
227196

228-
context = { input_library: :readline, history_file: history_file, name: 'history' }
197+
# Populate our own history file with random entries.
198+
# Using this allows us to not have to worry about history files present/not present on disk.
199+
new_history = []
200+
50.times do
201+
new_history << history_choices.sample
202+
end
203+
history_file.puts new_history
204+
history_file.rewind
205+
206+
context = { input_library: :readline, history_file: history_file.path, name: 'history' }
229207

230208
subject.send(:load_history_file, context)
231209

232-
expect(::Readline::HISTORY.to_a).to eq(history_lines)
210+
expect(history_mock).to eq(new_history)
233211
end
234212
end
235213
end

0 commit comments

Comments
 (0)