ruby_raider 3.10.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/reek.yml +5 -1
- data/.github/workflows/release.yml +2 -1
- data/.github/workflows/system_tests.yml +1 -1
- data/.reek.yml +13 -0
- data/README.md +44 -4
- data/lib/commands/utility_commands.rb +1 -1
- data/lib/generators/automation/automation_generator.rb +15 -10
- data/lib/generators/automation/templates/appium_caps.tt +2 -4
- data/lib/generators/automation/templates/home.tt +1 -2
- data/lib/generators/automation/templates/login.tt +6 -6
- data/lib/generators/automation/templates/page.tt +10 -3
- data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -6
- data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -6
- data/lib/generators/automation/templates/pdp.tt +3 -2
- data/lib/generators/cucumber/cucumber_generator.rb +15 -2
- data/lib/generators/cucumber/templates/feature.tt +5 -0
- data/lib/generators/cucumber/templates/partials/driver_world.tt +9 -0
- data/lib/generators/cucumber/templates/partials/mobile_steps.tt +1 -1
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +20 -0
- data/lib/generators/cucumber/templates/partials/watir_env.tt +16 -0
- data/lib/generators/cucumber/templates/partials/web_steps.tt +8 -0
- data/lib/generators/generator.rb +15 -4
- data/lib/generators/helper_generator.rb +15 -0
- data/lib/generators/invoke_generators.rb +23 -0
- data/lib/generators/menu_generator.rb +26 -14
- data/lib/generators/rspec/rspec_generator.rb +10 -3
- data/lib/generators/rspec/templates/spec.tt +27 -1
- data/lib/generators/template_renderer/template_error.rb +8 -8
- data/lib/generators/templates/common/git_ignore.tt +4 -1
- data/lib/generators/templates/common/partials/mobile_config.tt +1 -0
- data/lib/generators/templates/common/partials/web_config.tt +5 -2
- data/lib/generators/templates/common/read_me.tt +508 -0
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +1 -3
- data/lib/generators/templates/helpers/raider_element.tt +159 -0
- data/lib/generators/templates/helpers/session_state.tt +246 -0
- data/lib/generators/templates/helpers/spec_helper.tt +28 -0
- data/lib/generators/templates/helpers/wait_helper.tt +470 -0
- data/lib/ruby_raider/ffmpeg.rb +7 -7
- data/lib/ruby_raider.rb +2 -0
- data/lib/scaffolding/project_detector.rb +6 -0
- data/lib/scaffolding/scaffolding.rb +4 -0
- data/lib/scaffolding/templates/component.tt +4 -0
- data/lib/scaffolding/templates/page_object.tt +8 -0
- data/lib/version +1 -1
- data/ruby_raider.gemspec +1 -0
- data/sig/generators/generator.rbs +2 -0
- data/sig/scaffolding/project_detector.rbs +1 -0
- data/sig/scaffolding/scaffolding.rbs +1 -0
- data/spec/commands/raider_commands_spec.rb +24 -0
- data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +175 -0
- data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +24 -0
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +39 -0
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +33 -0
- data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +174 -0
- data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +39 -0
- data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +33 -0
- data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +49 -0
- data/spec/integration/commands/scaffolding_commands_spec.rb +49 -0
- data/spec/integration/commands/utility_commands_spec.rb +8 -0
- data/spec/integration/content/azure_content_spec.rb +4 -0
- data/spec/integration/content/ci_content_spec.rb +11 -0
- data/spec/integration/content/common_content_spec.rb +27 -0
- data/spec/integration/content/config_content_spec.rb +62 -0
- data/spec/integration/content/cucumber_reporter_content_spec.rb +25 -0
- data/spec/integration/content/helper_content_spec.rb +111 -0
- data/spec/integration/content/page_content_spec.rb +75 -4
- data/spec/integration/content/readme_content_spec.rb +149 -0
- data/spec/integration/content/syntax_validation_spec.rb +38 -0
- data/spec/integration/content/test_content_spec.rb +82 -0
- data/spec/integration/end_to_end_spec.rb +73 -28
- data/spec/integration/generators/automation_generator_spec.rb +50 -0
- data/spec/integration/generators/common_generator_spec.rb +20 -0
- data/spec/integration/generators/config_features_spec.rb +2 -2
- data/spec/integration/generators/cucumber_generator_spec.rb +36 -0
- data/spec/integration/generators/helpers_generator_spec.rb +60 -0
- data/spec/integration/generators/invoke_generators_spec.rb +70 -0
- data/spec/integration/generators/rspec_generator_spec.rb +20 -0
- data/spec/integration/helpers/session_state_spec.rb +474 -0
- data/spec/integration/helpers/wait_helper_mobile_spec.rb +281 -0
- data/spec/integration/helpers/wait_helper_spec.rb +496 -0
- data/spec/integration/helpers/wait_helper_support.rb +183 -0
- data/spec/integration/settings_helper.rb +4 -1
- data/spec/integration/spec_helper.rb +2 -2
- data/spec/menus/menu_generator_spec.rb +64 -2
- data/spec/scaffolding/scaffold_project_detector_spec.rb +12 -0
- metadata +31 -2
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'timeout'
|
|
6
|
+
require 'selenium-webdriver'
|
|
7
|
+
require 'time'
|
|
8
|
+
require 'tmpdir'
|
|
9
|
+
require 'yaml'
|
|
10
|
+
|
|
11
|
+
module SessionStateSpec
|
|
12
|
+
TEMPLATE = File.expand_path('../../../lib/generators/templates/helpers/session_state.tt', __dir__)
|
|
13
|
+
|
|
14
|
+
# Renders the template and loads it from a temp file that is removed straight away.
|
|
15
|
+
def self.load_template
|
|
16
|
+
Dir.mktmpdir('session_state_tt') do |dir|
|
|
17
|
+
file = File.join(dir, 'session_state.rb')
|
|
18
|
+
File.write(file, ERB.new(File.read(TEMPLATE), trim_mode: '-').result)
|
|
19
|
+
load file
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
SessionStateSpec.load_template
|
|
24
|
+
|
|
25
|
+
module SessionStateSpec
|
|
26
|
+
# Implements exactly the driver calls SessionState uses and records them in order.
|
|
27
|
+
class FakeDriver
|
|
28
|
+
ORIGIN_URL = 'https://raider-test-site.onrender.com/account'
|
|
29
|
+
|
|
30
|
+
def initialize(cookies: [], local: {}, session: {}, reject: nil, url: ORIGIN_URL)
|
|
31
|
+
@reject = reject
|
|
32
|
+
@page = { cookies:, url: }
|
|
33
|
+
@storage = { 'localStorage' => local, 'sessionStorage' => session }
|
|
34
|
+
@log = { calls: [], added: [] }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def calls = @log[:calls]
|
|
38
|
+
def added = @log[:added]
|
|
39
|
+
def local = @storage['localStorage']
|
|
40
|
+
def session = @storage['sessionStorage']
|
|
41
|
+
|
|
42
|
+
def manage = self
|
|
43
|
+
def navigate = self
|
|
44
|
+
def all_cookies = @page[:cookies]
|
|
45
|
+
def current_url = @page[:url]
|
|
46
|
+
def refresh = calls << :refresh
|
|
47
|
+
def delete_all_cookies = calls << :delete_all_cookies
|
|
48
|
+
|
|
49
|
+
def add_cookie(cookie)
|
|
50
|
+
raise 'rejected' if @reject&.call(cookie)
|
|
51
|
+
|
|
52
|
+
calls << :add_cookie
|
|
53
|
+
added << cookie
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to(url)
|
|
57
|
+
@page[:url] = url
|
|
58
|
+
calls << [:to, url]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def execute_script(_script, *args)
|
|
62
|
+
kind, values = args
|
|
63
|
+
return @storage[kind] if values.nil?
|
|
64
|
+
|
|
65
|
+
calls << [:write, kind]
|
|
66
|
+
@storage[kind] = values
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The browser end of a real Selenium::WebDriver::Manager: records the wire payloads it receives.
|
|
71
|
+
class FakeBridge
|
|
72
|
+
def initialize(log, reject)
|
|
73
|
+
@log = log
|
|
74
|
+
@reject = reject
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def add_cookie(cookie)
|
|
78
|
+
raise Selenium::WebDriver::Error::InvalidArgumentError, 'rejected' if @reject&.call(cookie)
|
|
79
|
+
|
|
80
|
+
@log[:calls] << :add_cookie
|
|
81
|
+
@log[:added] << cookie
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def delete_all_cookies = @log[:calls] << :delete_all_cookies
|
|
85
|
+
def cookies = []
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A FakeDriver whose manage is the real Selenium Manager, so its key translation is under test.
|
|
89
|
+
class ManagedDriver < FakeDriver
|
|
90
|
+
def manage
|
|
91
|
+
@manage ||= Selenium::WebDriver::Manager.new(FakeBridge.new(@log, @reject))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe 'SessionState' do
|
|
97
|
+
let(:url) { 'https://raider-test-site.onrender.com/' }
|
|
98
|
+
let(:driver) { SessionStateSpec::FakeDriver.new(cookies:, local: { 'token' => 'abc' }, session: { 'tab' => '1' }) }
|
|
99
|
+
let(:state) { SessionState.new(driver) }
|
|
100
|
+
let(:config) { { 'url' => url } }
|
|
101
|
+
|
|
102
|
+
def origin = 'https://raider-test-site.onrender.com'
|
|
103
|
+
def state_file = File.join('.raider', 'auth', 'user.json')
|
|
104
|
+
|
|
105
|
+
def cookies
|
|
106
|
+
[{ name: 'sid', value: 's3cr3t', domain: 'raider-test-site.onrender.com', path: '/', expires: nil,
|
|
107
|
+
secure: true, http_only: true, same_site: 'Lax' }]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
around do |example|
|
|
111
|
+
Dir.mktmpdir do |dir|
|
|
112
|
+
Dir.chdir(dir) do
|
|
113
|
+
FileUtils.mkdir_p('config')
|
|
114
|
+
example.run
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
before do
|
|
120
|
+
File.write('config/config.yml', YAML.dump(config))
|
|
121
|
+
stub_const('ENV', ENV.to_h.merge('RAIDER_LOGIN_STATE' => nil))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def write_state(overrides = {})
|
|
125
|
+
FileUtils.mkdir_p(File.dirname(state_file))
|
|
126
|
+
data = { 'format' => 1, 'name' => 'user', 'origin' => origin, 'saved_at' => (Time.now.utc - 10).iso8601,
|
|
127
|
+
'cookies' => [], 'local_storage' => {}, 'session_storage' => {} }.merge(overrides)
|
|
128
|
+
File.write(state_file, JSON.generate(data))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def saved
|
|
132
|
+
JSON.parse(File.read(state_file))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def saved_cookie(name, **attrs)
|
|
136
|
+
{ 'name' => name, 'value' => 'v', 'domain' => 'raider-test-site.onrender.com', 'path' => '/',
|
|
137
|
+
'expires' => nil, 'secure' => false, 'http_only' => false, 'same_site' => nil }.merge(attrs.transform_keys(&:to_s))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'rejects a name outside [a-z0-9_-]' do
|
|
141
|
+
expect { SessionState.new(driver, name: '../x') }.to raise_error(ArgumentError)
|
|
142
|
+
expect { SessionState.new(driver, name: 'Admin') }.to raise_error(ArgumentError)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'saves format 1 with origin, saved_at, cookies and both storages' do
|
|
146
|
+
state.save
|
|
147
|
+
expect(saved).to include('format' => 1, 'name' => 'user', 'origin' => origin,
|
|
148
|
+
'local_storage' => { 'token' => 'abc' }, 'session_storage' => { 'tab' => '1' })
|
|
149
|
+
expect { Time.iso8601(saved['saved_at']) }.not_to raise_error
|
|
150
|
+
expect(saved['cookies'].first).to include('name' => 'sid', 'http_only' => true, 'secure' => true)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'writes the file 0600 in a 0700 directory, atomically' do
|
|
154
|
+
state.save
|
|
155
|
+
expect(File.stat(state_file).mode & 0o777).to eq(0o600)
|
|
156
|
+
expect(File.stat(File.dirname(state_file)).mode & 0o777).to eq(0o700)
|
|
157
|
+
expect(Dir.glob('.raider/auth/*.tmp')).to be_empty
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
context 'with a url that has a port and a path' do
|
|
161
|
+
let(:url) { 'https://x.test:8443/app/' }
|
|
162
|
+
|
|
163
|
+
it 'derives the origin without path or trailing slash' do
|
|
164
|
+
state.save
|
|
165
|
+
expect(saved['origin']).to eq('https://x.test:8443')
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
describe '#save on another origin' do
|
|
170
|
+
it 'goes back to the origin before reading when the login ended elsewhere' do
|
|
171
|
+
sso = SessionStateSpec::FakeDriver.new(url: 'https://sso.other.test/x')
|
|
172
|
+
SessionState.new(sso).save
|
|
173
|
+
expect(sso.calls).to eq([[:to, "#{origin}/"]])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'stays put when the login ended on the origin' do
|
|
177
|
+
state.save
|
|
178
|
+
expect(driver.calls).to be_empty
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe '#fresh?' do
|
|
183
|
+
it 'is false when the file is missing' do
|
|
184
|
+
expect(state.fresh?).to be(false)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'is true for a recent file of the same format and origin' do
|
|
188
|
+
write_state
|
|
189
|
+
expect(state.fresh?).to be(true)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'is false for another format, another origin or an old file' do
|
|
193
|
+
write_state('format' => 2)
|
|
194
|
+
expect(state.fresh?).to be(false)
|
|
195
|
+
write_state('origin' => 'https://other.test')
|
|
196
|
+
expect(state.fresh?).to be(false)
|
|
197
|
+
write_state('saved_at' => (Time.now.utc - 3601).iso8601)
|
|
198
|
+
expect(state.fresh?).to be(false)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
context 'with login_state max_age 30' do
|
|
202
|
+
let(:config) { { 'url' => url, 'login_state' => { 'max_age' => 30 } } }
|
|
203
|
+
|
|
204
|
+
it 'is false for a file older than max_age' do
|
|
205
|
+
write_state('saved_at' => (Time.now.utc - 59).iso8601)
|
|
206
|
+
expect(state.fresh?).to be(false)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
describe '#restore' do
|
|
212
|
+
let(:driver) { SessionStateSpec::ManagedDriver.new }
|
|
213
|
+
|
|
214
|
+
it 'restores live cookies for the origin and skips expired and foreign ones' do
|
|
215
|
+
write_state('cookies' => [
|
|
216
|
+
saved_cookie('session'),
|
|
217
|
+
saved_cookie('future', expires: (Time.now.utc + 86_400).iso8601),
|
|
218
|
+
saved_cookie('past', expires: (Time.now.utc - 1).iso8601),
|
|
219
|
+
saved_cookie('dotted', domain: '.raider-test-site.onrender.com'),
|
|
220
|
+
saved_cookie('foreign', domain: 'other.com')
|
|
221
|
+
], 'local_storage' => { 'a' => 'b' })
|
|
222
|
+
warning = "[login_state] 2 cookies skipped (expired or another domain) from #{state_file}\n"
|
|
223
|
+
expect { expect(state.restore).to be(true) }.to output(warning).to_stderr
|
|
224
|
+
expect(driver.added.map { |c| c[:name] }).to eq(%w[session future dotted])
|
|
225
|
+
expect(driver.calls).to eq([[:to, "#{origin}/"], :delete_all_cookies, :add_cookie, :add_cookie, :add_cookie,
|
|
226
|
+
[:write, 'localStorage'], [:write, 'sessionStorage'], :refresh])
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'hands Selenium the cookie keys it translates to the wire format' do
|
|
230
|
+
write_state('cookies' => [saved_cookie('session', same_site: 'Lax', http_only: true),
|
|
231
|
+
saved_cookie('future', expires: (Time.now.utc + 86_400).iso8601)])
|
|
232
|
+
expect(state.restore).to be(true)
|
|
233
|
+
expect(driver.added.first).to include(sameSite: 'Lax', httpOnly: true, secure: false)
|
|
234
|
+
expect(driver.added.flat_map(&:keys)).not_to include(:same_site, :http_only, :expires)
|
|
235
|
+
expect(driver.added.last[:expiry]).to be_a(Integer)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'returns false and warns for corrupt JSON' do
|
|
239
|
+
FileUtils.mkdir_p(File.dirname(state_file))
|
|
240
|
+
File.write(state_file, '{"format":')
|
|
241
|
+
expect { expect(state.restore).to be(false) }.to output(/\[login_state\].*user\.json/).to_stderr
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it 'never prints cookie or storage values' do
|
|
246
|
+
leak = /s3cr3t|abc/
|
|
247
|
+
expect do
|
|
248
|
+
expect do
|
|
249
|
+
state.save
|
|
250
|
+
state.restore
|
|
251
|
+
write_state('cookies' => [saved_cookie('x', value: 's3cr3t')], 'local_storage' => { 'k' => 'abc' })
|
|
252
|
+
state.reuse(valid: -> { false }) { nil }
|
|
253
|
+
end.not_to output(leak).to_stdout
|
|
254
|
+
end.not_to output(leak).to_stderr
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
describe 'unsupported or unusable files' do
|
|
258
|
+
it 'warns once, naming the file, for another format' do
|
|
259
|
+
write_state('format' => 2)
|
|
260
|
+
expect { expect(state.restore).to be(false) }.to output(/\A\[login_state\] #{Regexp.escape(state_file)} has an unsupported format.*\n\z/).to_stderr
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it 'stays silent for a missing, expired or other-origin file' do
|
|
264
|
+
expect do
|
|
265
|
+
state.fresh?
|
|
266
|
+
write_state('origin' => 'https://other.test')
|
|
267
|
+
state.fresh?
|
|
268
|
+
write_state('saved_at' => (Time.now.utc - 7200).iso8601)
|
|
269
|
+
state.fresh?
|
|
270
|
+
end.not_to output.to_stderr
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
it 'retries a cookie the browser rejects without sameSite' do
|
|
274
|
+
rejecting = SessionStateSpec::ManagedDriver.new(reject: ->(cookie) { cookie.key?(:sameSite) })
|
|
275
|
+
write_state('cookies' => [saved_cookie('a', same_site: 'None')])
|
|
276
|
+
expect(SessionState.new(rejecting).restore).to be(true)
|
|
277
|
+
expect(rejecting.added.map { |c| [c[:name], c.key?(:sameSite)] }).to eq([['a', false]])
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
describe '#reuse' do
|
|
282
|
+
def run_reuse(**options)
|
|
283
|
+
count = 0
|
|
284
|
+
silence { state.reuse(**options) { count += 1 } }
|
|
285
|
+
count
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def silence
|
|
289
|
+
original = $stderr
|
|
290
|
+
$stderr = StringIO.new
|
|
291
|
+
yield
|
|
292
|
+
ensure
|
|
293
|
+
$stderr = original
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def warnings
|
|
297
|
+
original = $stderr
|
|
298
|
+
$stderr = StringIO.new
|
|
299
|
+
yield
|
|
300
|
+
$stderr.string
|
|
301
|
+
ensure
|
|
302
|
+
$stderr = original
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it 'logs in and saves when there is no file' do
|
|
306
|
+
expect(run_reuse).to eq(1)
|
|
307
|
+
expect(File).to exist(state_file)
|
|
308
|
+
expect(driver.calls).not_to include(:delete_all_cookies)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it 'clears the restored cookies and storage before the fallback login' do
|
|
312
|
+
managed = SessionStateSpec::ManagedDriver.new
|
|
313
|
+
write_state('cookies' => [saved_cookie('a')], 'local_storage' => { 'k' => 'v' })
|
|
314
|
+
silence { SessionState.new(managed).reuse(valid: -> { false }) { managed.calls << :login } }
|
|
315
|
+
after_refresh = managed.calls.drop(managed.calls.index(:refresh) + 1)
|
|
316
|
+
expect(after_refresh).to eq([:delete_all_cookies, [:write, 'localStorage'], [:write, 'sessionStorage'], :login])
|
|
317
|
+
expect(managed.local).to eq({})
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it 'clears a half-applied restore before the fallback login' do
|
|
321
|
+
broken = SessionStateSpec::ManagedDriver.new(reject: ->(_cookie) { true })
|
|
322
|
+
write_state('cookies' => [saved_cookie('a')])
|
|
323
|
+
silence { SessionState.new(broken).reuse { broken.calls << :login } }
|
|
324
|
+
expect(broken.calls.last(4)).to eq([:delete_all_cookies, [:write, 'localStorage'], [:write, 'sessionStorage'],
|
|
325
|
+
:login])
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it 'logs in without saving, with one warning, when the auth directory cannot be created' do
|
|
329
|
+
FileUtils.mkdir_p('.raider')
|
|
330
|
+
File.chmod(0o500, '.raider')
|
|
331
|
+
count = 0
|
|
332
|
+
output = warnings { expect(state.reuse { count += 1 }).to be(true) }
|
|
333
|
+
expect(count).to eq(1)
|
|
334
|
+
expect(output.lines).to match([start_with('[login_state] ')])
|
|
335
|
+
ensure
|
|
336
|
+
File.chmod(0o700, '.raider')
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it 'logs in without saving, with one warning, when the lock cannot be opened' do
|
|
340
|
+
FileUtils.mkdir_p(File.join('.raider', 'auth', 'user.lock'))
|
|
341
|
+
count = 0
|
|
342
|
+
output = warnings { expect(state.reuse { count += 1 }).to be(true) }
|
|
343
|
+
expect(count).to eq(1)
|
|
344
|
+
expect(output.lines).to match([start_with('[login_state] ')])
|
|
345
|
+
expect(File).not_to exist(state_file)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it 'keeps the login, with one warning naming the file, when saving after it fails' do
|
|
349
|
+
allow(driver).to receive(:execute_script).and_raise(Selenium::WebDriver::Error::JavascriptError, 'abc')
|
|
350
|
+
count = 0
|
|
351
|
+
output = warnings { expect(state.reuse { count += 1 }).to be(true) }
|
|
352
|
+
expect(count).to eq(1)
|
|
353
|
+
expect(output.lines).to match([include(state_file, 'JavascriptError')])
|
|
354
|
+
expect(output).not_to include('abc')
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it 'restores and skips the login when fresh and valid' do
|
|
358
|
+
write_state
|
|
359
|
+
expect(run_reuse(valid: -> { true })).to eq(0)
|
|
360
|
+
expect(driver.calls).to include(:refresh)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it 'falls back once when valid returns false' do
|
|
364
|
+
write_state('saved_at' => (Time.now.utc - 100).iso8601)
|
|
365
|
+
before = Time.iso8601(saved['saved_at'])
|
|
366
|
+
expect(run_reuse(valid: -> { false })).to eq(1)
|
|
367
|
+
expect(Time.iso8601(saved['saved_at'])).to be > before
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it 'falls back when the file is corrupt' do
|
|
371
|
+
FileUtils.mkdir_p(File.dirname(state_file))
|
|
372
|
+
File.write(state_file, '{"format":')
|
|
373
|
+
expect(run_reuse).to eq(1)
|
|
374
|
+
expect(saved['format']).to eq(1)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
it 'falls back once, with one warning, when valid raises' do
|
|
378
|
+
write_state
|
|
379
|
+
count = 0
|
|
380
|
+
expect { state.reuse(valid: -> { raise 'boom' }) { count += 1 } }.to output(/\A\[login_state\] validity check failed \(RuntimeError\), logging in again\n\z/).to_stderr
|
|
381
|
+
expect(count).to eq(1)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it 'falls back once, with one warning, when restore raises' do
|
|
385
|
+
write_state('cookies' => [saved_cookie('a')])
|
|
386
|
+
broken = SessionStateSpec::FakeDriver.new(reject: ->(_cookie) { true })
|
|
387
|
+
count = 0
|
|
388
|
+
expect { SessionState.new(broken).reuse { count += 1 } }.to output(/\A\[login_state\] could not restore.*\n\z/).to_stderr
|
|
389
|
+
expect(count).to eq(1)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
it 'warns once for another format and logs in' do
|
|
393
|
+
write_state('format' => 2)
|
|
394
|
+
count = 0
|
|
395
|
+
expect { state.reuse { count += 1 } }.to output(/\A\[login_state\] .*unsupported format.*\n\z/).to_stderr
|
|
396
|
+
expect(count).to eq(1)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
it 'RAIDER_LOGIN_STATE=off logs in and does not touch the disk' do
|
|
400
|
+
stub_const('ENV', ENV.to_h.merge('RAIDER_LOGIN_STATE' => 'off'))
|
|
401
|
+
expect(run_reuse).to eq(1)
|
|
402
|
+
expect(File).not_to exist('.raider')
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
it 'RAIDER_LOGIN_STATE=refresh logs in even when fresh' do
|
|
406
|
+
write_state('saved_at' => (Time.now.utc - 100).iso8601)
|
|
407
|
+
before = Time.iso8601(saved['saved_at'])
|
|
408
|
+
stub_const('ENV', ENV.to_h.merge('RAIDER_LOGIN_STATE' => 'refresh'))
|
|
409
|
+
expect(run_reuse).to eq(1)
|
|
410
|
+
expect(Time.iso8601(saved['saved_at'])).to be > before
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
it 'lets concurrent callers restore a fresh login at the same time' do
|
|
414
|
+
write_state('cookies' => [saved_cookie('session')])
|
|
415
|
+
arrived = Queue.new
|
|
416
|
+
checked = Queue.new
|
|
417
|
+
threads = Array.new(2) do
|
|
418
|
+
Thread.new do
|
|
419
|
+
valid = lambda do
|
|
420
|
+
arrived << 1
|
|
421
|
+
Timeout.timeout(2) { sleep 0.01 until arrived.size == 2 }
|
|
422
|
+
checked << 1
|
|
423
|
+
end
|
|
424
|
+
SessionState.new(SessionStateSpec::FakeDriver.new(cookies:)).reuse(valid:) { raise 'logged in' }
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
threads.each(&:join)
|
|
428
|
+
expect(checked.size).to eq(2)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
it 'restores a login another worker saved meanwhile, without a fallback warning' do
|
|
432
|
+
write_state('cookies' => [saved_cookie('session')])
|
|
433
|
+
checks = 0
|
|
434
|
+
valid = lambda do
|
|
435
|
+
checks += 1
|
|
436
|
+
write_state('cookies' => [saved_cookie('session')], 'saved_at' => Time.now.utc.iso8601) if checks == 1
|
|
437
|
+
checks > 1
|
|
438
|
+
end
|
|
439
|
+
logins = 0
|
|
440
|
+
expect { state.reuse(valid:) { logins += 1 } }.not_to output.to_stderr
|
|
441
|
+
expect([logins, checks]).to eq([0, 2])
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
it 'warns once when the second restore after a peer save is also invalid' do
|
|
445
|
+
write_state('cookies' => [saved_cookie('session')])
|
|
446
|
+
checks = 0
|
|
447
|
+
valid = lambda do
|
|
448
|
+
checks += 1
|
|
449
|
+
write_state('cookies' => [saved_cookie('session')], 'saved_at' => Time.now.utc.iso8601) if checks == 1
|
|
450
|
+
false
|
|
451
|
+
end
|
|
452
|
+
logins = 0
|
|
453
|
+
expect { state.reuse(valid:) { logins += 1 } }
|
|
454
|
+
.to output(/\A\[login_state\] login restored from \S+ is no longer valid, logging in again\n\z/).to_stderr
|
|
455
|
+
expect(logins).to eq(1)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
it 'serializes concurrent callers on the lock' do
|
|
459
|
+
count = 0
|
|
460
|
+
drivers = Array.new(2) { SessionStateSpec::FakeDriver.new(cookies:) }
|
|
461
|
+
threads = drivers.map do |fake|
|
|
462
|
+
Thread.new do
|
|
463
|
+
SessionState.new(fake).reuse do
|
|
464
|
+
sleep 0.2
|
|
465
|
+
count += 1
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
threads.each(&:join)
|
|
470
|
+
expect(count).to eq(1)
|
|
471
|
+
expect(drivers.count { |fake| fake.calls.include?(:refresh) }).to eq(1)
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|