booth 0.1.7 → 0.1.9
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/CHANGELOG.md +9 -0
- data/lib/booth/adminland/credentials/block.rb +38 -0
- data/lib/booth/adminland/credentials/unblock.rb +37 -0
- data/lib/booth/adminland.rb +8 -0
- data/lib/booth/testing/shortcuts.rb +5 -0
- data/lib/booth/testing/support/assert_logged_in.rb +42 -1
- data/lib/booth/testing/support/visit.rb +1 -1
- data/lib/booth/testing/support/visit_url.rb +56 -0
- data/lib/booth/testing/userland/login_remotely.rb +2 -1
- data/lib/booth/testing/userland/registration_without_passkey.rb +2 -1
- data/lib/booth/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db40cf227698f8a64fdb6f1071dd3ee00a1b5183dfc75d93ea240aeaee342ce6
|
|
4
|
+
data.tar.gz: 8bd55ef8c5f2c6f19fa37f8208a49cb8892e15b1e89c9198a9ca9c017e74287a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae2923901eb6bfad19ba323d1e1196e681fc13844bbed95df090a82a1ee328ef682a770e57aff8837a0376ee425a4079193240f3fcf8b804d8f4a61b55645865
|
|
7
|
+
data.tar.gz: ffbdf991361b877d58cca3499e1b6b1d61927bb942e2159cc676f7d1eb892a7cc85f63b71ba7667113e69c523dbd779d146299996615755cf1d637c9b96689a1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# main
|
|
2
2
|
|
|
3
|
+
# 0.1.9
|
|
4
|
+
|
|
5
|
+
* Test support: `AssertLoggedIn` now also waits for the page navigation that `booth/verification.js` triggers after a passkey ceremony, so the next `visit` of a scenario no longer races with it ("Navigation to ... is interrupted by another navigation")
|
|
6
|
+
* Test support: `Support::Visit` (used by `visit_namespaced`) and the new shortcut `visit_url` survive a navigation that collides with the self-reload of a polling page such as `remote_session_available` (`net::ERR_ABORTED`). The userland scenarios `LoginRemotely` and `RegistrationWithoutPasskey` use it
|
|
7
|
+
|
|
8
|
+
# 0.1.8
|
|
9
|
+
|
|
10
|
+
* Add Credential block and unblock to Adminland
|
|
11
|
+
|
|
3
12
|
# 0.1.7
|
|
4
13
|
|
|
5
14
|
* Fix time travel actually returning in test suite
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Booth
|
|
4
|
+
module Adminland
|
|
5
|
+
module Credentials
|
|
6
|
+
# Blocks a credential from authenticating.
|
|
7
|
+
class Block
|
|
8
|
+
include Calls
|
|
9
|
+
|
|
10
|
+
option :credential_id
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
return Tron.failure(:credential_not_found) unless credential
|
|
14
|
+
|
|
15
|
+
if credential.blocked?
|
|
16
|
+
return Tron.success(:already_blocked,
|
|
17
|
+
id: credential.id,
|
|
18
|
+
blocked_at: credential.blocked_at)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
blocked_at = Time.current
|
|
22
|
+
credential.update!(blocked_at:)
|
|
23
|
+
|
|
24
|
+
Tron.success(:credential_blocked, id: credential.id, blocked_at:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def credential
|
|
30
|
+
return @credential if defined?(@credential)
|
|
31
|
+
|
|
32
|
+
uuid = ::Booth::Syntaxes::Uuid.call(credential_id, raise_if_invalid: false).uuid
|
|
33
|
+
@credential = ::Booth::Models::Credential.find_by(id: uuid) if uuid
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Booth
|
|
4
|
+
module Adminland
|
|
5
|
+
module Credentials
|
|
6
|
+
# Allows a credential to authenticate again.
|
|
7
|
+
class Unblock
|
|
8
|
+
include Calls
|
|
9
|
+
|
|
10
|
+
option :credential_id
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
return Tron.failure(:credential_not_found) unless credential
|
|
14
|
+
|
|
15
|
+
unless credential.blocked?
|
|
16
|
+
return Tron.success(:already_unblocked,
|
|
17
|
+
id: credential.id,
|
|
18
|
+
blocked_at: credential.blocked_at)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
credential.update!(blocked_at: nil)
|
|
22
|
+
|
|
23
|
+
Tron.success(:credential_unblocked, id: credential.id, blocked_at: nil)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def credential
|
|
29
|
+
return @credential if defined?(@credential)
|
|
30
|
+
|
|
31
|
+
uuid = ::Booth::Syntaxes::Uuid.call(credential_id, raise_if_invalid: false).uuid
|
|
32
|
+
@credential = ::Booth::Models::Credential.find_by(id: uuid) if uuid
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/booth/adminland.rb
CHANGED
|
@@ -7,6 +7,14 @@ module Booth
|
|
|
7
7
|
# Credential Helpers
|
|
8
8
|
# ------------------
|
|
9
9
|
|
|
10
|
+
def self.block_credential(...)
|
|
11
|
+
::Booth::Adminland::Credentials::Block.call(...)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.unblock_credential(...)
|
|
15
|
+
::Booth::Adminland::Credentials::Unblock.call(...)
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
def self.create_credential(...)
|
|
11
19
|
::Booth::Adminland::Credentials::Create.call(...)
|
|
12
20
|
end
|
|
@@ -14,6 +14,11 @@ module Booth
|
|
|
14
14
|
::Booth::Testing::Support::Visit.call(routing_namespace:, controller:, action:, params:)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Like Capybara's `visit`, but survives a page that reloads itself at the same moment.
|
|
18
|
+
def visit_url(url)
|
|
19
|
+
::Booth::Testing::Support::VisitUrl.call(url:)
|
|
20
|
+
end
|
|
21
|
+
|
|
17
22
|
def create_and_onboard(username:)
|
|
18
23
|
::Booth::Testing::Support::Shortcuts::CreateAndOnboard.call(routing_namespace:,
|
|
19
24
|
scope:,
|
|
@@ -13,7 +13,26 @@ module Booth
|
|
|
13
13
|
option :credential, as: :manual_credential, optional: true
|
|
14
14
|
option :username, optional: true
|
|
15
15
|
|
|
16
|
+
# The Session cookie arrives with the 201 response of the passkey verification.
|
|
17
|
+
# Only afterwards does `booth/verification.js` navigate the page itself
|
|
18
|
+
# (`window.location.href = window.location.href`). Without waiting for that, the next
|
|
19
|
+
# `visit` of a scenario races with it and Playwright raises
|
|
20
|
+
# "Navigation to ... is interrupted by another navigation".
|
|
21
|
+
PASSKEY_FORM = '.js-booth-webauth-authentication, .js-booth-webauth-registration'
|
|
22
|
+
SETTLE_TIMEOUT = 5 # seconds; the wait ends silently when no navigation follows
|
|
23
|
+
|
|
16
24
|
def call
|
|
25
|
+
# Only a page that still shows a passkey form is about to navigate by itself.
|
|
26
|
+
document_before = document_started_at if passkey_form_present?
|
|
27
|
+
|
|
28
|
+
wait_for_session_cookie.tap do
|
|
29
|
+
wait_until_page_was_replaced(document_before) if document_before
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def wait_for_session_cookie
|
|
17
36
|
browser_session_id = nil
|
|
18
37
|
|
|
19
38
|
begin
|
|
@@ -37,7 +56,29 @@ module Booth
|
|
|
37
56
|
"but the Browser Cookie has #{browser_session_id} "
|
|
38
57
|
end
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
def passkey_form_present?
|
|
60
|
+
page.evaluate_script("document.querySelector(#{PASSKEY_FORM.to_json}) !== null")
|
|
61
|
+
rescue ::Playwright::Error
|
|
62
|
+
true # Mid-navigation: the page is replacing itself, so wait for the new document.
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Millisecond timestamp at which the current document started loading
|
|
66
|
+
# (nil while navigating).
|
|
67
|
+
def document_started_at
|
|
68
|
+
page.evaluate_script("document.readyState === 'complete' ? performance.timeOrigin : null")
|
|
69
|
+
rescue ::Playwright::Error
|
|
70
|
+
nil # The execution context was destroyed by a navigation: a new document is on its way.
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def wait_until_page_was_replaced(document_before)
|
|
74
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + SETTLE_TIMEOUT
|
|
75
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
76
|
+
current = document_started_at
|
|
77
|
+
return if current && current != document_before
|
|
78
|
+
|
|
79
|
+
sleep 0.05
|
|
80
|
+
end
|
|
81
|
+
end
|
|
41
82
|
|
|
42
83
|
# Playwright session handling causes ActiveRecord to aggressively cache queries.
|
|
43
84
|
# Randomizing the query seems to be the only reliable way to avoid this.
|
|
@@ -15,7 +15,7 @@ module Booth
|
|
|
15
15
|
|
|
16
16
|
def call
|
|
17
17
|
log { "-----> Visiting #{controller}/#{action} #{params.presence} on #{subdomain}.localhost" }
|
|
18
|
-
|
|
18
|
+
::Booth::Testing::Support::VisitUrl.call(url: ::Rails.application.routes.url_helpers.url_for(options))
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
private
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Booth
|
|
4
|
+
module Testing
|
|
5
|
+
module Support
|
|
6
|
+
# Visits a URL, surviving a navigation the page started by itself.
|
|
7
|
+
#
|
|
8
|
+
# Booth pages that wait for something on another device (e.g. `remote_session_available`)
|
|
9
|
+
# reload themselves every 5-7 seconds (`Gui.reloadSoon` in `booth/gui.js`).
|
|
10
|
+
# When such a page lives in another browser session while a scenario does other things,
|
|
11
|
+
# the scenario's next `visit` can collide with that reload. Playwright then raises
|
|
12
|
+
# `net::ERR_ABORTED` or "Navigation to ... is interrupted by another navigation".
|
|
13
|
+
# The reloaded page is stable for several seconds, so navigating again is safe.
|
|
14
|
+
# Any other error is raised as usual.
|
|
15
|
+
class VisitUrl
|
|
16
|
+
include ::Calls
|
|
17
|
+
include ::Capybara::DSL
|
|
18
|
+
include ::Booth::Logging
|
|
19
|
+
|
|
20
|
+
MAX_ATTEMPTS = 3
|
|
21
|
+
OVERTAKEN = /ERR_ABORTED|interrupted by another navigation/
|
|
22
|
+
|
|
23
|
+
option :url
|
|
24
|
+
|
|
25
|
+
def call
|
|
26
|
+
attempts = 0
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
attempts += 1
|
|
30
|
+
visit url
|
|
31
|
+
rescue ::Playwright::Error => e
|
|
32
|
+
raise unless e.message.match?(OVERTAKEN) && attempts < MAX_ATTEMPTS
|
|
33
|
+
|
|
34
|
+
log { "Navigation to #{url} was overtaken by the page itself, retrying..." }
|
|
35
|
+
wait_until_page_is_loaded
|
|
36
|
+
retry
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def wait_until_page_is_loaded
|
|
43
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) +
|
|
44
|
+
Capybara.default_max_wait_time
|
|
45
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
46
|
+
return if page.evaluate_script('document.readyState') == 'complete'
|
|
47
|
+
|
|
48
|
+
sleep 0.05
|
|
49
|
+
end
|
|
50
|
+
rescue ::Playwright::Error
|
|
51
|
+
sleep 0.1 # Still navigating, the retry itself will wait for the page.
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -49,7 +49,8 @@ module Booth
|
|
|
49
49
|
assert_userland_view controller: :remote_logins, step: :remote_solved
|
|
50
50
|
|
|
51
51
|
using_session(:other_device) do
|
|
52
|
-
|
|
52
|
+
# This page polls (reloads itself every 5-7s), which may overtake our navigation.
|
|
53
|
+
visit_url current_path
|
|
53
54
|
|
|
54
55
|
# ------------ SIGNIFICANT TEST --------------
|
|
55
56
|
# Webauth sudo is required after remote login.
|
|
@@ -57,7 +57,8 @@ module Booth
|
|
|
57
57
|
assert_userland_view controller: :remote_logins, step: :remote_solved
|
|
58
58
|
|
|
59
59
|
using_session(:other_device) do
|
|
60
|
-
|
|
60
|
+
# This page polls (reloads itself every 5-7s), which may overtake our navigation.
|
|
61
|
+
visit_url current_path
|
|
61
62
|
|
|
62
63
|
# -------------------------- SIGNIFICANT TEST ---------------------------
|
|
63
64
|
# You can remote-login even though you don't have any Authenticators yet.
|
data/lib/booth/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: booth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- halo
|
|
@@ -228,8 +228,10 @@ files:
|
|
|
228
228
|
- lib/booth.rb
|
|
229
229
|
- lib/booth/adminland.rb
|
|
230
230
|
- lib/booth/adminland/authenticators/exist.rb
|
|
231
|
+
- lib/booth/adminland/credentials/block.rb
|
|
231
232
|
- lib/booth/adminland/credentials/create.rb
|
|
232
233
|
- lib/booth/adminland/credentials/index.rb
|
|
234
|
+
- lib/booth/adminland/credentials/unblock.rb
|
|
233
235
|
- lib/booth/adminland/incognitos/consume_token.rb
|
|
234
236
|
- lib/booth/adminland/incognitos/generate_secret_key.rb
|
|
235
237
|
- lib/booth/adminland/incognitos/stop_incognito.rb
|
|
@@ -339,6 +341,7 @@ files:
|
|
|
339
341
|
- lib/booth/testing/support/virtual_authenticators/enable.rb
|
|
340
342
|
- lib/booth/testing/support/virtual_authenticators/load.rb
|
|
341
343
|
- lib/booth/testing/support/visit.rb
|
|
344
|
+
- lib/booth/testing/support/visit_url.rb
|
|
342
345
|
- lib/booth/testing/userland.rb
|
|
343
346
|
- lib/booth/testing/userland/login_remotely.rb
|
|
344
347
|
- lib/booth/testing/userland/onboarding_first_time.rb
|
|
@@ -417,7 +420,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
417
420
|
- !ruby/object:Gem::Version
|
|
418
421
|
version: '0'
|
|
419
422
|
requirements: []
|
|
420
|
-
rubygems_version:
|
|
423
|
+
rubygems_version: 4.0.20
|
|
421
424
|
specification_version: 4
|
|
422
425
|
summary: Opinionated authentication framework for Rails
|
|
423
426
|
test_files: []
|