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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 117283879ce936895cf4def7b4d6e7780af5739434078bc91f7c462c6145526a
4
- data.tar.gz: ad8f7c4d0fcdeba1987d965eb503f68ccf0daac32328c8a1332977191bbc289c
3
+ metadata.gz: db40cf227698f8a64fdb6f1071dd3ee00a1b5183dfc75d93ea240aeaee342ce6
4
+ data.tar.gz: 8bd55ef8c5f2c6f19fa37f8208a49cb8892e15b1e89c9198a9ca9c017e74287a
5
5
  SHA512:
6
- metadata.gz: b35328cca811b4a19136a819ab05c0e894cd4fa65d1a6195c8abd96fc49d0fa14894aed1bb746293034728e48ae421a07f758f2734fb9d2b19c13f0ebb1e1c7f
7
- data.tar.gz: 6a15f32a17273e19164f108ce483a0eea5ebfe5a516a12218a3877c2170b32d2ad6dd4efe73a5988218f48b33b52d8e795dcd7f1a726f629a389dc5539424ee5
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
@@ -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
- private
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
- visit ::Rails.application.routes.url_helpers.url_for(options)
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
- visit current_path
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
- visit current_path
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Booth
4
- VERSION = '0.1.7'
4
+ VERSION = '0.1.9'
5
5
  end
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.7
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: 3.6.9
423
+ rubygems_version: 4.0.20
421
424
  specification_version: 4
422
425
  summary: Opinionated authentication framework for Rails
423
426
  test_files: []