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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/app/assets/images/booth/fido/passkey_mark_b_reverse.svg +55 -0
- data/config/locales/de.yml +5 -5
- data/lib/booth/core/sessions/to_passport.rb +11 -1
- data/lib/booth/core/webauth/authentication_verification.rb +5 -5
- data/lib/booth/models/authenticator.rb +11 -0
- data/lib/booth/passport.rb +15 -0
- data/lib/booth/requests/authentication.rb +1 -1
- data/lib/booth/requests/storages/login.rb +4 -1
- data/lib/booth/requests/sudo.rb +12 -2
- data/lib/booth/test.rb +10 -17
- data/lib/booth/testing/incorporation_test_case.rb +3 -1
- data/lib/booth/testing/shortcuts.rb +8 -17
- data/lib/booth/testing/support/assert_logged_in.rb +26 -27
- data/lib/booth/testing/support/assert_logged_out.rb +15 -11
- data/lib/booth/testing/support/assert_partial.rb +20 -29
- data/lib/booth/testing/support/capybara_step_logger.rb +22 -0
- data/lib/booth/testing/support/{soft_reset_session.rb → clear_cookies.rb} +5 -6
- data/lib/booth/testing/support/cookie_data_from_browser.rb +38 -0
- data/lib/booth/testing/support/shortcuts/create_and_onboard.rb +4 -0
- data/lib/booth/testing/support/shortcuts/login_with_passkey.rb +6 -4
- data/lib/booth/testing/support/shortcuts/register_new_passkey.rb +6 -2
- data/lib/booth/testing/support/virtual_authenticator.rb +196 -0
- data/lib/booth/testing/support/virtual_authenticators/create.rb +22 -7
- data/lib/booth/testing/support/virtual_authenticators/destroy.rb +14 -9
- data/lib/booth/testing/support/virtual_authenticators/enable.rb +14 -2
- data/lib/booth/testing/support/virtual_authenticators/load.rb +7 -19
- data/lib/booth/testing/support/virtual_authenticators.rb +106 -0
- data/lib/booth/testing/support/visit.rb +1 -0
- data/lib/booth/testing/userland/login_remotely.rb +2 -2
- data/lib/booth/testing/userland/onboarding_first_time.rb +5 -4
- data/lib/booth/testing/userland/onboarding_to_reset_passkeys.rb +3 -3
- data/lib/booth/testing/userland/registration_with_passkey.rb +9 -6
- data/lib/booth/testing/userland/registration_without_passkey.rb +11 -7
- data/lib/booth/testing/userland/sessions_manage_behavior.rb +14 -3
- data/lib/booth/userland/webauths/index.rb +9 -3
- data/lib/booth/userland/webauths/new.rb +10 -2
- data/lib/booth/userland/webauths/transitions/create/choose_nickname.rb +1 -1
- data/lib/booth/userland/webauths/transitions/sudo/authentication_initiation.rb +5 -0
- data/lib/booth/userland/webauths/transitions/sudo/authentication_verification.rb +1 -1
- data/lib/booth/version.rb +1 -1
- metadata +8 -4
- data/lib/booth/testing/support/get_session_value.rb +0 -37
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edffbca58e6e40fa8b572564a2fdefd1243becc6bc64247765adae3c2bc4cfe0
|
|
4
|
+
data.tar.gz: 9ca0edd3a0fd70873623f5cb7f109bab2082f4c3fd5546056dfdd4d0d29b54e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb38a8a59631b6590de981e724c3dce086f15f70e6d579b4948a5b8051af01b85321c58d8a92a9e7742da39f75f32c3c3e424d6e55c9ffd2b470411864fa7b8b
|
|
7
|
+
data.tar.gz: 8d2e7119be3efe18a9ff5ef840d75e332220f33ec876f1ef46b87a7ccebca508b39e35443d873f4283ee62d0725840d391ef1ee3011dba22ec4152fdb9b113f5
|
data/CHANGELOG.md
CHANGED
|
@@ -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>
|
data/config/locales/de.yml
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
44
|
-
try_again_cooldown:
|
|
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:
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
data/lib/booth/requests/sudo.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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-
|
|
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
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
49
|
-
::Booth::Testing::Support::
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
14
|
+
browser_session_id = nil
|
|
15
15
|
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
sleep 0.1
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
rescue ::Timeout::Error
|
|
29
|
+
# Timed out
|
|
30
|
+
end
|
|
26
31
|
|
|
27
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
8
|
+
class ClearCookies
|
|
9
9
|
include ::Calls
|
|
10
10
|
include ::Capybara::DSL
|
|
11
11
|
include ::Booth::Logging
|
|
12
12
|
|
|
13
13
|
def call
|
|
14
|
-
|
|
14
|
+
log { 'Soft-resetting session via Playwright cookie API...' }
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
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,
|