booth 0.0.3 → 0.0.4

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/app/assets/images/booth/fido/passkey_mark_b_reverse.svg +55 -0
  4. data/config/locales/de.yml +5 -5
  5. data/lib/booth/core/sessions/to_passport.rb +11 -1
  6. data/lib/booth/core/webauth/authentication_verification.rb +5 -5
  7. data/lib/booth/models/authenticator.rb +11 -0
  8. data/lib/booth/passport.rb +15 -0
  9. data/lib/booth/requests/authentication.rb +1 -1
  10. data/lib/booth/requests/storages/login.rb +4 -1
  11. data/lib/booth/requests/sudo.rb +12 -2
  12. data/lib/booth/test.rb +10 -17
  13. data/lib/booth/testing/incorporation_test_case.rb +3 -1
  14. data/lib/booth/testing/shortcuts.rb +8 -17
  15. data/lib/booth/testing/support/assert_logged_in.rb +26 -27
  16. data/lib/booth/testing/support/assert_logged_out.rb +15 -11
  17. data/lib/booth/testing/support/assert_partial.rb +20 -29
  18. data/lib/booth/testing/support/capybara_step_logger.rb +22 -0
  19. data/lib/booth/testing/support/{soft_reset_session.rb → clear_cookies.rb} +5 -6
  20. data/lib/booth/testing/support/cookie_data_from_browser.rb +38 -0
  21. data/lib/booth/testing/support/shortcuts/create_and_onboard.rb +4 -0
  22. data/lib/booth/testing/support/shortcuts/login_with_passkey.rb +6 -4
  23. data/lib/booth/testing/support/shortcuts/register_new_passkey.rb +6 -2
  24. data/lib/booth/testing/support/virtual_authenticator.rb +196 -0
  25. data/lib/booth/testing/support/virtual_authenticators/create.rb +22 -7
  26. data/lib/booth/testing/support/virtual_authenticators/destroy.rb +14 -9
  27. data/lib/booth/testing/support/virtual_authenticators/enable.rb +14 -2
  28. data/lib/booth/testing/support/virtual_authenticators/load.rb +7 -19
  29. data/lib/booth/testing/support/virtual_authenticators.rb +106 -0
  30. data/lib/booth/testing/support/visit.rb +1 -0
  31. data/lib/booth/testing/userland/login_remotely.rb +2 -2
  32. data/lib/booth/testing/userland/onboarding_first_time.rb +5 -4
  33. data/lib/booth/testing/userland/onboarding_to_reset_passkeys.rb +3 -3
  34. data/lib/booth/testing/userland/registration_with_passkey.rb +9 -6
  35. data/lib/booth/testing/userland/registration_without_passkey.rb +11 -7
  36. data/lib/booth/testing/userland/sessions_manage_behavior.rb +14 -3
  37. data/lib/booth/userland/webauths/index.rb +9 -3
  38. data/lib/booth/userland/webauths/new.rb +10 -2
  39. data/lib/booth/userland/webauths/transitions/create/choose_nickname.rb +1 -1
  40. data/lib/booth/userland/webauths/transitions/sudo/authentication_initiation.rb +5 -0
  41. data/lib/booth/userland/webauths/transitions/sudo/authentication_verification.rb +1 -1
  42. data/lib/booth/version.rb +1 -1
  43. metadata +8 -4
  44. data/lib/booth/testing/support/get_session_value.rb +0 -37
  45. data/lib/booth/testing/support/virtual_authenticators/manager.rb +0 -124
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75776c21f5e995703cc155d9689c504d9f5158c90b1e122c71c0cae6c7ec145f
4
- data.tar.gz: 0f3ef4e56515e035e2d7f48244cacc4d66609972e8d00087ea8967124a995369
3
+ metadata.gz: edffbca58e6e40fa8b572564a2fdefd1243becc6bc64247765adae3c2bc4cfe0
4
+ data.tar.gz: 9ca0edd3a0fd70873623f5cb7f109bab2082f4c3fd5546056dfdd4d0d29b54e1
5
5
  SHA512:
6
- metadata.gz: cfc9d3c104de49717ee87ea1cfa49e3b85ee9a452817fb7eb9b3526c4529831280be737ab23397b58de7c43d422a4bb0cd337b065a342402266a928864b24065
7
- data.tar.gz: ff800424fa6697a691f884c078892a742ac20fff360859facd027557d8c1917562425bba657161fb7131606675d6a88d8b44dde6ea8954c67850228472eea6f7
6
+ metadata.gz: cb38a8a59631b6590de981e724c3dce086f15f70e6d579b4948a5b8051af01b85321c58d8a92a9e7742da39f75f32c3c3e424d6e55c9ffd2b470411864fa7b8b
7
+ data.tar.gz: 8d2e7119be3efe18a9ff5ef840d75e332220f33ec876f1ef46b87a7ccebca508b39e35443d873f4283ee62d0725840d391ef1ee3011dba22ec4152fdb9b113f5
data/CHANGELOG.md CHANGED
@@ -1,4 +1,7 @@
1
1
  # main
2
+ # 0.0.4
3
+
4
+ - Expose Credential domain in Passport
2
5
 
3
6
  # 0.0.3
4
7
 
@@ -0,0 +1,55 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
+
4
+ <svg
5
+ width="512"
6
+ height="512"
7
+ viewBox="0 0 135.46666 135.46667"
8
+ version="1.1"
9
+ id="svg1"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ xmlns:svg="http://www.w3.org/2000/svg">
12
+ <defs
13
+ id="defs1" />
14
+ <g
15
+ id="layer1">
16
+ <g
17
+ transform="matrix(0.87804943,0,0,0.87804943,-217.61517,-58.068321)"
18
+ id="g6">
19
+ <g
20
+ transform="translate(221.49,33.864)"
21
+ fill="#ffbf3b"
22
+ id="g2">
23
+ <path
24
+ class="st1"
25
+ d="M 129.2,33.23 H 77.78 c -27.26,0 -49.36,22.43 -49.36,50.09 v 52.18 c 0,27.66 22.1,50.09 49.36,50.09 h 51.42 c 27.26,0 49.36,-22.43 49.36,-50.09 V 83.32 c 0,-27.67 -22.1,-50.09 -49.36,-50.09 z"
26
+ fill="#ffbf3b"
27
+ id="path2" />
28
+ </g>
29
+ <path
30
+ class="st2"
31
+ d="m 377.05,136.85 c 0,9.8 -6.03,18.13 -14.42,21.17 l 5.08,8.41 -7.51,9.24 7.51,9.03 -12.12,16.26 -8.54,-9.11 V 157.4 c -7.59,-3.45 -12.91,-11.35 -12.91,-20.55 0,-12.37 9.61,-22.4 21.45,-22.4 11.85,0.01 21.46,10.03 21.46,22.4 z m -21.46,3.44 c 2.86,0 5.18,-2.42 5.18,-5.41 0,-2.99 -2.32,-5.41 -5.18,-5.41 -2.86,0 -5.18,2.42 -5.18,5.41 0,2.99 2.32,5.41 5.18,5.41 z"
32
+ clip-rule="evenodd"
33
+ fill="#353535"
34
+ fill-rule="evenodd"
35
+ id="path3" />
36
+ <path
37
+ class="st3"
38
+ d="m 377.11,136.92 c 0,9.68 -5.87,17.93 -14.09,21.09 l 4.68,8.42 -6.92,9.24 6.92,9.03 -12.11,16.39 v -60.81 c 2.86,0 5.18,-2.42 5.18,-5.41 0,-2.99 -2.32,-5.41 -5.18,-5.41 v -15 c 11.89,0 21.52,10.05 21.52,22.46 z"
39
+ clip-rule="evenodd"
40
+ fill-rule="evenodd"
41
+ id="path4" />
42
+ <path
43
+ class="st3"
44
+ d="m 340.02,161.48 c -6.93,-5.69 -11.58,-14.43 -12.22,-24.36 h -37.14 c -7.79,0 -14.1,6.41 -14.1,14.31 v 17.89 c 0,3.95 3.16,7.16 7.05,7.16 h 49.36 c 3.89,0 7.05,-3.2 7.05,-7.16 z"
45
+ clip-rule="evenodd"
46
+ fill-rule="evenodd"
47
+ id="path5" />
48
+ <path
49
+ class="st4"
50
+ d="m 306.56,132.83 c -1.72,-0.33 -3.43,-0.64 -5.05,-1.32 -6.15,-2.58 -9.73,-7.34 -10.89,-14.06 -0.8,-4.6 -0.42,-9.15 1.44,-13.45 2.64,-6.11 7.39,-9.43 13.61,-10.55 3.72,-0.67 7.43,-0.52 11.02,0.82 5.4,2.01 9.01,5.87 10.69,11.55 1.69,5.72 1.44,11.45 -1.11,16.86 -2.64,5.66 -7.26,8.69 -13.1,9.88 -0.49,0.1 -0.97,0.2 -1.46,0.29 -1.71,-0.02 -3.43,-0.02 -5.15,-0.02 z"
51
+ fill="#141313"
52
+ id="path6" />
53
+ </g>
54
+ </g>
55
+ </svg>
@@ -17,7 +17,7 @@ de:
17
17
  blank_nickname: Please provide a name for your device.
18
18
  blank_remote_code: You did not enter a code.
19
19
  blank_secret_key: The provided secret key is empty. Is the URL correct?
20
- blank_username: Trage bitte deinen Benutzernamen ein.
20
+ blank_username: Tragen Sie bitte Ihren Benutzernamen ein.
21
21
  domain_mismatch: Dieser Benutzername kann sich nicht hier einloggen.
22
22
  email_too_long: The provided email is too long. It must be less at most %{maximum} characters long.
23
23
  email_too_short: The provided email is too short. It should be at least %{minimum} characters long.
@@ -29,7 +29,7 @@ de:
29
29
  invalid_secret_key_format: The provided secret key contains invalid characters. It should be from the Base58 alphabet.
30
30
  invalid_username_format: Der eingegebene Benutzername enthält ungültige Zeichen. Nur Buchstaben und Zahlen sind erlaubt.
31
31
  last_attempt: This is your last attempt and you will have to contact customer service if you fail.
32
- login_timeout: Du hattest %{lifespan_minutes} Minuten um den Login abzuschließen, es hat aber länger gedauert. Bitte beginne erneut.
32
+ login_timeout: Sie hatten %{lifespan_minutes} Minuten um den Login abzuschließen, es hat aber länger gedauert. Bitte beginnen Sie erneut.
33
33
  missing_secret_key: Missing the secret key parameter. Is the URL correct?
34
34
  mode_username_and_webauth_description: Auch bekannt als WebAuthentication (WebAuthn), FIDO2, CTAP2, Apple Passkey (Touch ID, Face ID).
35
35
  mode_username_and_webauth_title: Hardwareschlüssel (empfohlen)
@@ -40,14 +40,14 @@ de:
40
40
  remote_timed_out: You had %{lifespan_minutes} minutes to enter the response code, but it took too long. Please start again.
41
41
  session_revoked: Das Gerät mit der IP %{ip} wurde erfolgreich ausgeloggt.
42
42
  some_authenticator_removed: Hardware key successfully deleted.
43
- successfully_logged_out: Du hast dich erfolgreich ausgeloggt.
44
- try_again_cooldown: Du kannst es in %{distance_of_time_until_cooldown} erneut probieren.
43
+ successfully_logged_out: Sie haben sich erfolgreich ausgeloggt.
44
+ try_again_cooldown: Sie können es es in %{distance_of_time_until_cooldown} erneut probieren.
45
45
  uninitialized_credential: This account has not been initialized yet. Please contact support.
46
46
  unknown_email: We don't know that email.
47
47
  unknown_secret_key: The provided secret key is unknown.
48
48
  unknown_username: Dieser Benutzername existiert nicht.
49
49
  unknown_webauth_device: Sorry, We don't recognize that device. Have you registered it before?
50
- username_already_exists: This username already exists. Try to login instead?
50
+ username_already_exists: Dieser Benutzername ist leider nicht verfügbar.
51
51
  username_must_start_with_letter: Der Benutzername muss mit einem Buchstaben beginnen.
52
52
  username_too_long: The provided username is too long. It must be less at most %{maximum} characters long.
53
53
  username_too_short: The provided username is too short. It should be at least %{minimum} characters long.
@@ -12,7 +12,8 @@ module Booth
12
12
  param :session
13
13
 
14
14
  def call
15
- ::Booth::ToStruct.call(attributes)
15
+ # ::Booth::ToStruct.call(attributes)
16
+ ::Booth::Passport.new(**attributes)
16
17
  end
17
18
 
18
19
  private
@@ -24,7 +25,16 @@ module Booth
24
25
  username: credential.username,
25
26
  session_id: session.id,
26
27
  credential_id: credential.id,
28
+ domain: credential.domain,
29
+ scope: credential.scope.to_sym,
27
30
  incognito_credential_id: session.incognito_credential_id,
31
+
32
+ # A container wher the Rails developer may store information.
33
+ # Injectable using `passport.with(context: ...)`
34
+ context: nil,
35
+
36
+ # These are to be processed by the Warden AfterFetch hook.
37
+ # Normally the Rails developer doesn't need them.
28
38
  blocked: credential.blocked?,
29
39
  revoked: session.revoked_at.present?,
30
40
  }
@@ -17,12 +17,13 @@ module Booth
17
17
  end
18
18
 
19
19
  log do
20
- "Verifying using challenge #{challenge.inspect} and public key #{authenticator.public_key.inspect} and sign count #{authenticator.sign_count.inspect}"
20
+ "Verifying using challenge #{challenge.inspect} and public key #{authenticator.public_key.inspect} and expecting sign count #{authenticator.sign_count.inspect}"
21
21
  end
22
+
22
23
  webauth.verify(
23
- challenge,
24
- public_key: authenticator.public_key,
25
- sign_count: authenticator.sign_count,
24
+ challenge,
25
+ public_key: authenticator.public_key,
26
+ sign_count: authenticator.sign_count,
26
27
  )
27
28
  log { 'Response successfully verified' }
28
29
 
@@ -38,7 +39,6 @@ module Booth
38
39
  # TODO: Audit
39
40
  Tron.failure :webauth_failed, public_json: { public_message: 'Passkey Sign count mismatch.' },
40
41
  public_message: "Verification failed: #{e.message}",
41
- # expected_sign_count: authenticator.sign_count,
42
42
  http_status: :unprocessable_entity
43
43
  rescue WebAuthn::Error => e
44
44
  log { "Response verification failed: #{e.message}" }
@@ -26,6 +26,17 @@ module Booth
26
26
  confirmed_at.present?
27
27
  end
28
28
 
29
+ def provider_attributes
30
+ return {} unless aaguid
31
+
32
+ provider = ::Booth::Core::Webauth::Provider.find(aaguid)
33
+ return {} unless provider
34
+
35
+ { provider_name: provider.name,
36
+ provider_icon_dark: provider.icon_dark,
37
+ provider_icon_light: provider.icon_light }
38
+ end
39
+
29
40
  def step
30
41
  ::Booth::Core::Authenticators::Step.call(self)
31
42
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booth
4
+ Passport = Data.define(
5
+ :username,
6
+ :session_id,
7
+ :credential_id,
8
+ :domain,
9
+ :scope,
10
+ :incognito_credential_id,
11
+ :context,
12
+ :blocked,
13
+ :revoked,
14
+ )
15
+ end
@@ -25,7 +25,7 @@ module Booth
25
25
  end
26
26
 
27
27
  def logged_in?
28
- log { "Checking whether logged in in scope #{scope}" }
28
+ log { "Checking whether logged in in scope #{scope} - #{warden.authenticated?(scope)}" }
29
29
  warden.authenticated?(scope)
30
30
  end
31
31
 
@@ -63,7 +63,10 @@ module Booth
63
63
 
64
64
  # I think this could happen if the end-user has multiple browser windows open
65
65
  # and registers for different companies, finding race conditions.
66
- raise "Scope mismatch #{new_credential.scope} != #{scope}" if new_credential.scope.to_sym != scope.to_sym
66
+ #
67
+ # Problem: When you register a taken username and it is persisted in the cookie,
68
+ # you should not raise if the username was merely taken in another scope.
69
+ # raise "Scope mismatch #{new_credential.scope} != #{scope}" if new_credential.scope.to_sym != scope.to_sym
67
70
 
68
71
  session[:credential_for_username] = new_credential.id
69
72
  @credential_for_username = nil
@@ -22,7 +22,7 @@ module Booth
22
22
 
23
23
  log { 'You need Webauth sudo' }
24
24
  public_message = I18n.t('booth.webauth_sudo_timeout', lifespan_minutes: (lifespan / 60))
25
- yield Tron.success(:webauth_sudo_needed, step: :sudo, public_message:)
25
+ yield Tron.success(:webauth_sudo_needed, step: :sudo, authenticators?: authenticators?, public_message:)
26
26
  end
27
27
 
28
28
  # Getters
@@ -47,7 +47,9 @@ module Booth
47
47
 
48
48
  def webauthn_challenge=(new_challenge)
49
49
  if new_challenge
50
- log { "Persisting webauth challenge #{new_challenge.inspect} in sudo session for scope #{scope.inspect}" }
50
+ log do
51
+ "Persisting webauth challenge #{new_challenge.inspect} in sudo session for scope #{scope.inspect}"
52
+ end
51
53
  else
52
54
  log { "Removing webauth challenge from sudo session for scope #{scope.inspect}" }
53
55
  end
@@ -61,6 +63,14 @@ module Booth
61
63
  def session
62
64
  request.session(namespace: :sudo)
63
65
  end
66
+
67
+ def authenticators?
68
+ credential.registered_authenticators?
69
+ end
70
+
71
+ def credential
72
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
73
+ end
64
74
  end
65
75
  end
66
76
  end
data/lib/booth/test.rb CHANGED
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Just in case a developer using Booth makes a mistake.
4
- raise 'Requiring `booth/test` is only intended for testing your code' unless Rails.env.test?
5
-
6
- # raise "Only require 'booth/test' when running incorporation test" if ENV['BOOTH_SKIP_INCORPORATION']
4
+ # Note: We check ENV directly since Rails hasn't been loaded yet at this point.
5
+ raise 'Requiring `booth/test` is only intended for testing your code' unless ENV['RAILS_ENV'] == 'test'
7
6
 
8
7
  def try_to_load_gem(gem_name, require_name = nil)
9
8
  require_name = gem_name if require_name.nil?
@@ -13,26 +12,22 @@ rescue LoadError => e
13
12
  end
14
13
 
15
14
  # Gems
15
+ try_to_load_gem 'calls'
16
16
  try_to_load_gem 'capybara', 'capybara/dsl'
17
- try_to_load_gem 'capybara-lockstep'
18
- try_to_load_gem 'rack_session_access', 'rack_session_access/capybara'
19
- try_to_load_gem 'selenium-devtools', 'selenium/devtools'
20
- try_to_load_gem 'selenium-webdriver'
17
+ try_to_load_gem 'capybara-playwright-driver'
21
18
  try_to_load_gem 'minitest'
22
19
 
23
20
  require 'active_support/testing/time_helpers'
24
21
 
25
- # Support
26
- # require_relative 'test/support/virtual_authenticators/manager'
27
- # require_relative 'test/support/virtual_authenticators/create'
28
-
29
- # # Internal Helpers
22
+ # Internal Booth dependencies
23
+ require_relative '../booth'
24
+ require_relative 'logging'
25
+ require_relative 'testing/support/virtual_authenticator'
26
+ require_relative 'testing/support/virtual_authenticators'
27
+ require_relative 'testing/support/capybara_step_logger'
30
28
  require_relative 'testing/shortcuts'
31
- # # require_relative 'test/support/force_login'
32
29
  require_relative 'testing/incorporation_test_case'
33
30
 
34
- # Tests
35
-
36
31
  # Public API
37
32
  require_relative 'testing/userland'
38
33
 
@@ -54,5 +49,3 @@ Capybara.configure do |config|
54
49
  config.test_id = 'data-booth' # How Booth interacts with your HTML elements.
55
50
  config.default_max_wait_time = 20 if ENV['CHROME'] # For manual debugging
56
51
  end
57
-
58
- Rails.application.config.middleware.use RackSessionAccess::Middleware
@@ -7,7 +7,9 @@ module Booth
7
7
  include ::Capybara::DSL
8
8
  include ::Minitest::Assertions
9
9
  include ::ActiveSupport::Testing::TimeHelpers
10
+ include ::Booth::Testing::Support::CapybaraStepLogger
10
11
  include ::Booth::Testing::Shortcuts
12
+ include ::Booth::Logging
11
13
 
12
14
  option :host, default: -> { 'localhost' }
13
15
  option :scope
@@ -18,7 +20,7 @@ module Booth
18
20
  private
19
21
 
20
22
  def virtual_authenticators
21
- @virtual_authenticators ||= ::Booth::Testing::Support::VirtualAuthenticators::Manager.new
23
+ ::Booth::Testing::Support::VirtualAuthenticators
22
24
  end
23
25
 
24
26
  # So that `Minitest::Assertions` can be included
@@ -45,33 +45,24 @@ module Booth
45
45
  ::Booth::Testing::Support::AssertPartial.call(namespace: :userland, controller:, step:)
46
46
  end
47
47
 
48
- def soft_reset_session
49
- ::Booth::Testing::Support::SoftResetSession.call
48
+ def clear_cookies
49
+ ::Booth::Testing::Support::ClearCookies.call
50
+ end
51
+
52
+ def decrypt_session_cookie(cookie_value)
53
+ ::Booth::Testing::Support::DecryptSessionCookie.call(cookie_value:)
50
54
  end
51
55
 
52
56
  def virtual_authenticators
53
- @virtual_authenticators ||= ::Booth::Testing::Support::VirtualAuthenticators::Manager.new
57
+ ::Booth::Testing::Support::VirtualAuthenticators
54
58
  end
59
+
55
60
  # def debug
56
61
  # puts
57
62
  # puts
58
63
  # puts Nokogiri::XML(page.html, &:noblanks)
59
64
  # puts
60
65
  # end
61
-
62
- # def respond_to_remote
63
- # visit userland_remote_login_path
64
-
65
- # credential = ::Booth::Models::Session.sorted_scope.first.credential
66
- # code = ::Booth::Core::Remotes::Get.call(credential_id: credential.id).formatted_code
67
-
68
- # fill_in 'Code', with: code
69
- # click_on 'Continue'
70
-
71
- # assert_content 'logged in on the other device'
72
- # assert_content 'solved your challenge'
73
- # end
74
-
75
66
  end
76
67
  end
77
68
  end
@@ -14,40 +14,38 @@ module Booth
14
14
  option :username, optional: true
15
15
 
16
16
  def call
17
- # tries ||= 0
18
- ::Capybara::Lockstep.synchronize
19
-
20
17
  browser_session_id = nil
21
- active_sessions.each do |session|
22
- browser_session_id = ::Booth::Testing::Support::GetSessionValue.call(key:)
23
- return true if browser_session_id == session.id.to_s
24
- end
25
18
 
26
- attributes = {
27
- expected_username: credential.username,
28
- expected_credential_id: credential.id,
29
- expected_session_ids: active_sessions.map(&:id),
30
- actual_session_id: browser_session_id,
31
- }
32
- raise AssertionFailedError, "Expected to be logged in. #{attributes}"
33
- # "Expected Credential `#{credential.id}` to be logged in with a session of: #{active_sessions.map(&:id)}"
34
-
35
- # With the Webauth pingpong it sometimes takes a little longer.
36
- # And Capybara Lockstep doesn't seem to be able to detect that.
37
- # rescue AssertionFailedError
38
- # if (tries += 1) < 3
39
- # log { 'Trying again...' }
40
- # sleep 1
41
- # retry
42
- # end
19
+ begin
20
+ ::Timeout.timeout(Capybara.default_max_wait_time) do
21
+ loop do
22
+ log { "Polling to see if #{credential.username} logged in in scope #{scope}..." }
23
+ browser_session_id = ::Booth::Testing::Support::CookieDataFromBrowser.call[key]
24
+ active_sessions.each do |session|
25
+ return true if session.id.to_s == browser_session_id
26
+ end
27
+ sleep 0.1
28
+ end
29
+ end
30
+ rescue ::Timeout::Error
31
+ # Timed out
32
+ end
43
33
 
44
- # raise
34
+ raise AssertionFailedError, "Expected to be logged in as #{credential.username} " \
35
+ "(Credential #{credential.id}) " \
36
+ "with some Session ID #{active_sessions.map(&:id)} " \
37
+ "but the Browser Cookie has #{browser_session_id} "
45
38
  end
46
39
 
47
40
  private
48
41
 
42
+ # Playwright session handling causes ActiveRecord to aggressively cache queries.
43
+ # Randomizing the query seems to be the only reliable way to avoid this.
49
44
  def active_sessions
50
- credential.sessions.active_scope.sorted_scope
45
+ ::Booth::Models::Session.active_scope
46
+ .sorted_scope
47
+ .where(credential_id: credential.id)
48
+ .where.not(id: SecureRandom.uuid)
51
49
  end
52
50
 
53
51
  def credential
@@ -56,7 +54,8 @@ module Booth
56
54
  raise 'Must provide either `credential:` or `username:`'
57
55
  end
58
56
 
59
- manual_credential || ::Booth::Models::Credential.find_by(username:)
57
+ manual_credential ||
58
+ ::Booth::Models::Credential.where.not(id: SecureRandom.uuid).find_by(username:)
60
59
  end
61
60
 
62
61
  def key
@@ -11,21 +11,25 @@ module Booth
11
11
  option :scope
12
12
 
13
13
  def call
14
- ::Capybara::Lockstep.synchronize
14
+ browser_session_id = nil
15
15
 
16
- return unless logged_in?
16
+ begin
17
+ ::Timeout.timeout(Capybara.default_max_wait_time) do
18
+ loop do
19
+ log { 'Polling to see if logged out...' }
20
+ browser_session_id = ::Booth::Testing::Support::CookieDataFromBrowser.call["warden.user.#{scope}.key"]
17
21
 
18
- raise 'Expected nobody to logged in, but somebody is logged in.'
19
- end
20
-
21
- private
22
+ # If no session cookie or it's empty, user is logged out
23
+ return true unless browser_session_id
22
24
 
23
- def logged_in?
24
- ::Booth::Testing::Support::GetSessionValue.call(key:)
25
- end
25
+ sleep 0.1
26
+ end
27
+ end
28
+ rescue ::Timeout::Error
29
+ # Timed out
30
+ end
26
31
 
27
- def key
28
- "warden.user.#{scope}.key"
32
+ raise 'Expected nobody to be logged out, but a session cookie exists.'
29
33
  end
30
34
  end
31
35
  end
@@ -15,41 +15,32 @@ module Booth
15
15
  option :step
16
16
 
17
17
  def call
18
- ::Capybara::Lockstep.synchronize
19
18
  log { "Looking for view `#{partial}`" }
20
-
21
- assert_selector 'template', visible: false
22
-
23
- content = page.html.match(%r{<template>([^<]+)</template>})[1].strip
24
-
25
- raise "Expected view '#{partial}' but got '#{content}'" unless content == partial
26
-
27
- log { "The expected view `#{partial}` was rendered." }
28
- self.class.asserted_partials << partial
29
- nil
30
- end
31
-
32
- def expected_tag
33
- "<template>#{partial}</template>"
34
- end
35
-
36
- def actual_tag
37
- page.html.scan(%r{<template>[^/]+/[^/]+/[^/]+</template>}).uniq.first.presence ||
38
- page.text
19
+ content = nil
20
+ begin
21
+ ::Timeout.timeout(Capybara.default_max_wait_time) do
22
+ loop do
23
+ content = page.html.match(%r{<template>([^<]+)</template>})[1].strip
24
+ unless content == partial
25
+ sleep 0.1
26
+ next
27
+ end
28
+
29
+ log { "The expected view `#{partial}` was rendered." }
30
+ self.class.asserted_partials << partial
31
+ return
32
+ end
33
+ end
34
+ rescue ::Timeout::Error
35
+ raise "Expected view '#{partial}' but timed out with '#{page.html}'"
36
+ end
37
+
38
+ raise "Expected view '#{partial}' but got '#{content}'"
39
39
  end
40
40
 
41
41
  def partial
42
42
  "#{namespace}/#{controller}/#{step}"
43
43
  end
44
-
45
- def test_file_and_line_number
46
- # Called from e.g. `::Booth::Testing::Userland::LoginRemotely`.
47
- candidate = caller[3].split(':in ').first
48
- return candidate unless candidate.include?('calls')
49
-
50
- # Called from e.g. `::Booth::Testing::Support::RegisterNewPasskey`.
51
- caller[2].split(':in ').first
52
- end
53
44
  end
54
45
  end
55
46
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booth
4
+ module Testing
5
+ module Support
6
+ module CapybaraStepLogger
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ %i[visit click_on fill_in check uncheck choose select attach_file submit].each do |meth|
11
+ define_method(meth) do |*args, **kwargs, &block|
12
+ log { Paint['-' * 20, :green] }
13
+ log { Paint["#{meth} #{args} #{kwargs}", :green] }
14
+ log { Paint['-' * 20, :green] }
15
+ super(*args, **kwargs, &block)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -5,18 +5,17 @@ module Booth
5
5
  module Support
6
6
  # Essentially resets the session cookie, but without removing
7
7
  # Chrome's Virtual Authenticator Enviroment. Like a force logout.
8
- class SoftResetSession
8
+ class ClearCookies
9
9
  include ::Calls
10
10
  include ::Capybara::DSL
11
11
  include ::Booth::Logging
12
12
 
13
13
  def call
14
- ::Capybara::Lockstep.synchronize
14
+ log { 'Soft-resetting session via Playwright cookie API...' }
15
15
 
16
- keys = page.get_rack_session.keys
17
- keys_with_nil_values = keys.index_with { nil }
18
-
19
- page.set_rack_session(**keys_with_nil_values)
16
+ page.driver.with_playwright_page do |playwright_page|
17
+ playwright_page.context.clear_cookies
18
+ end
20
19
  end
21
20
  end
22
21
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booth
4
+ module Testing
5
+ module Support
6
+ class CookieDataFromBrowser
7
+ include ::Calls
8
+ include ::Booth::Logging
9
+
10
+ def call
11
+ cipher = ActiveSupport::MessageEncryptor.default_cipher
12
+ key_length = ActiveSupport::MessageEncryptor.key_len(cipher)
13
+ key_generator = Rails.application.key_generator
14
+ salt = Rails.configuration.action_dispatch.authenticated_encrypted_cookie_salt
15
+ secret = key_generator.generate_key(salt, key_length)
16
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher:, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
17
+ cookie = CGI.unescape(encrypted_cookie_data.strip)
18
+ session_name = Rails.application.config.session_options[:key]
19
+
20
+ JSON.parse encryptor.decrypt_and_verify(cookie, purpose: "cookie.#{session_name}")
21
+ end
22
+
23
+ private
24
+
25
+ def encrypted_cookie_data
26
+ session_name = Rails.application.config.session_options[:key]
27
+
28
+ Capybara.current_session.driver.with_playwright_page do |playwright_page|
29
+ cookies = playwright_page.context.cookies
30
+ cookie = cookies.find { it['name'] == session_name }
31
+
32
+ return cookie['value']
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -8,6 +8,8 @@ module Booth
8
8
  class CreateAndOnboard
9
9
  include ::Calls
10
10
  include ::Capybara::DSL
11
+ include ::Booth::Testing::Support::CapybaraStepLogger
12
+ include ::Booth::Logging
11
13
 
12
14
  option :routing_namespace
13
15
  option :scope
@@ -15,6 +17,8 @@ module Booth
15
17
  option :after_credential
16
18
 
17
19
  def call
20
+ log { 'Starting Subroutine...' }
21
+
18
22
  Visit.call(routing_namespace:,
19
23
  controller: :onboardings,
20
24
  action: :show,