descope 1.0.6 → 1.1.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/ci.yaml +51 -12
- data/.github/workflows/publish-gem.yaml +6 -26
- data/.github/workflows/release-please.yaml +36 -0
- data/.gitignore +5 -2
- data/.release-please-manifest.json +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +21 -0
- data/Gemfile +8 -7
- data/Gemfile.lock +70 -56
- data/README.md +170 -51
- data/examples/ruby-on-rails-api/descope/Gemfile +8 -8
- data/examples/ruby-on-rails-api/descope/Gemfile.lock +1 -1
- data/examples/ruby-on-rails-api/descope/package-lock.json +203 -141
- data/examples/ruby-on-rails-api/descope/package.json +1 -1
- data/examples/ruby-on-rails-api/descope/yarn.lock +185 -87
- data/lib/descope/api/v1/auth/enchantedlink.rb +3 -1
- data/lib/descope/api/v1/auth/magiclink.rb +3 -1
- data/lib/descope/api/v1/auth/otp.rb +3 -1
- data/lib/descope/api/v1/auth/password.rb +6 -2
- data/lib/descope/api/v1/auth/totp.rb +3 -1
- data/lib/descope/api/v1/auth.rb +47 -12
- data/lib/descope/api/v1/management/common.rb +20 -5
- data/lib/descope/api/v1/management/sso_application.rb +236 -0
- data/lib/descope/api/v1/management/sso_settings.rb +2 -24
- data/lib/descope/api/v1/management/user.rb +151 -13
- data/lib/descope/api/v1/management.rb +2 -0
- data/lib/descope/api/v1/session.rb +37 -4
- data/lib/descope/mixins/common.rb +1 -0
- data/lib/descope/mixins/http.rb +60 -9
- data/lib/descope/mixins/initializer.rb +5 -2
- data/lib/descope/mixins/logging.rb +12 -4
- data/lib/descope/version.rb +1 -1
- data/spec/descope/api/v1/auth_spec.rb +29 -0
- data/spec/descope/api/v1/auth_token_extraction_spec.rb +126 -0
- data/spec/descope/api/v1/session_refresh_spec.rb +98 -0
- data/spec/factories/user.rb +1 -1
- data/spec/integration/lib.descope/api/v1/auth/enchantedlink_spec.rb +20 -22
- data/spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb +6 -2
- data/spec/integration/lib.descope/api/v1/auth/otp_spec.rb +6 -2
- data/spec/integration/lib.descope/api/v1/auth/session_spec.rb +68 -0
- data/spec/integration/lib.descope/api/v1/auth/totp_spec.rb +6 -2
- data/spec/integration/lib.descope/api/v1/management/access_key_spec.rb +12 -1
- data/spec/integration/lib.descope/api/v1/management/audit_spec.rb +5 -3
- data/spec/integration/lib.descope/api/v1/management/authz_spec.rb +28 -5
- data/spec/integration/lib.descope/api/v1/management/flow_spec.rb +3 -1
- data/spec/integration/lib.descope/api/v1/management/permissions_spec.rb +22 -2
- data/spec/integration/lib.descope/api/v1/management/project_spec.rb +18 -2
- data/spec/integration/lib.descope/api/v1/management/roles_spec.rb +116 -36
- data/spec/integration/lib.descope/api/v1/management/user_spec.rb +74 -8
- data/spec/lib.descope/api/v1/auth/enchantedlink_spec.rb +11 -2
- data/spec/lib.descope/api/v1/auth/password_spec.rb +10 -1
- data/spec/lib.descope/api/v1/auth_spec.rb +167 -5
- data/spec/lib.descope/api/v1/cookie_domain_fix_integration_spec.rb +245 -0
- data/spec/lib.descope/api/v1/management/sso_application_spec.rb +217 -0
- data/spec/lib.descope/api/v1/management/sso_settings_spec.rb +2 -2
- data/spec/lib.descope/api/v1/management/user_spec.rb +134 -46
- data/spec/lib.descope/api/v1/session_spec.rb +119 -6
- data/spec/lib.descope/mixins/http_spec.rb +229 -0
- data/spec/support/client_config.rb +0 -1
- data/spec/support/utils.rb +21 -0
- metadata +14 -8
|
@@ -9,55 +9,69 @@ describe Descope::Api::V1::Management::User do
|
|
|
9
9
|
@instance = dummy_instance
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
context '.
|
|
12
|
+
context '.create_user_and_test_user' do
|
|
13
13
|
it 'is expected to respond to a user create method' do
|
|
14
14
|
expect(@instance).to respond_to(:create_user)
|
|
15
|
+
expect(@instance).to respond_to(:create_test_user)
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
user_tenants_args = [
|
|
19
|
+
{
|
|
20
|
+
tenant_id: 'tenant1'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
tenant_id: 'tenant2',
|
|
24
|
+
role_names: %w[role1 role2]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
params = {
|
|
29
|
+
loginId: 'name@mail.com',
|
|
30
|
+
email: 'name@mail.com',
|
|
31
|
+
phone: '+1-212-669-2542',
|
|
32
|
+
name: 'name',
|
|
33
|
+
givenName: 'name',
|
|
34
|
+
familyName: 'Ruby SDK',
|
|
35
|
+
userTenants: associated_tenants_to_hash_array(user_tenants_args),
|
|
36
|
+
test: false,
|
|
37
|
+
picture: 'https://www.example.com/picture.png',
|
|
38
|
+
customAttributes: { 'attr1' => 'value1', 'attr2' => 'value2' },
|
|
39
|
+
additionalIdentifiers: %w[id-1 id-2],
|
|
40
|
+
password: 's3cr3t',
|
|
41
|
+
ssoAppIds: %w[app1 app2],
|
|
42
|
+
invite: false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
args = {
|
|
46
|
+
login_id: 'name@mail.com',
|
|
47
|
+
email: 'name@mail.com',
|
|
48
|
+
phone: '+1-212-669-2542',
|
|
49
|
+
name: 'name',
|
|
50
|
+
given_name: 'name',
|
|
51
|
+
family_name: 'Ruby SDK',
|
|
52
|
+
user_tenants: user_tenants_args,
|
|
53
|
+
picture: 'https://www.example.com/picture.png',
|
|
54
|
+
custom_attributes: { 'attr1' => 'value1', 'attr2' => 'value2' },
|
|
55
|
+
additional_identifiers: %w[id-1 id-2],
|
|
56
|
+
password: 's3cr3t',
|
|
57
|
+
sso_app_ids: %w[app1 app2]
|
|
58
|
+
}
|
|
59
|
+
|
|
17
60
|
it 'is expected to create a user with user data' do
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
tenant_id: 'tenant1'
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
tenant_id: 'tenant2',
|
|
24
|
-
role_names: %w[role1 role2]
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
expect(@instance).to receive(:post).with(
|
|
28
|
-
USER_CREATE_PATH, {
|
|
29
|
-
loginId: 'name@mail.com',
|
|
30
|
-
email: 'name@mail.com',
|
|
31
|
-
phone: '+1-212-669-2542',
|
|
32
|
-
name: 'name',
|
|
33
|
-
givenName: 'name',
|
|
34
|
-
familyName: 'Ruby SDK',
|
|
35
|
-
userTenants: associated_tenants_to_hash_array(user_tenants_args),
|
|
36
|
-
test: false,
|
|
37
|
-
picture: 'https://www.example.com/picture.png',
|
|
38
|
-
customAttributes: { 'attr1' => 'value1', 'attr2' => 'value2' },
|
|
39
|
-
additionalIdentifiers: %w[id-1 id-2],
|
|
40
|
-
password: 's3cr3t',
|
|
41
|
-
ssoAppIds: %w[app1 app2],
|
|
42
|
-
invite: false
|
|
43
|
-
}
|
|
44
|
-
)
|
|
61
|
+
expect(@instance).to receive(:post).with(USER_CREATE_PATH, params)
|
|
45
62
|
|
|
46
63
|
expect do
|
|
47
|
-
@instance.create_user(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
password: 's3cr3t',
|
|
59
|
-
sso_app_ids: %w[app1 app2]
|
|
60
|
-
)
|
|
64
|
+
@instance.create_user(**args)
|
|
65
|
+
end.not_to raise_error
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'is expected to create a test user with user data' do
|
|
69
|
+
params[:test] = true
|
|
70
|
+
expect(@instance).to receive(:post).with(TEST_USER_CREATE_PATH, params)
|
|
71
|
+
|
|
72
|
+
expect do
|
|
73
|
+
args[:test] = true
|
|
74
|
+
@instance.create_test_user(**args)
|
|
61
75
|
end.not_to raise_error
|
|
62
76
|
end
|
|
63
77
|
end
|
|
@@ -108,14 +122,16 @@ describe Descope::Api::V1::Management::User do
|
|
|
108
122
|
loginId: 'name@mail.com',
|
|
109
123
|
email: 'name@mail.com',
|
|
110
124
|
test: false,
|
|
111
|
-
invite: true
|
|
125
|
+
invite: true,
|
|
126
|
+
templateId: "tid",
|
|
112
127
|
}
|
|
113
128
|
)
|
|
114
129
|
|
|
115
130
|
expect do
|
|
116
131
|
@instance.invite_user(
|
|
117
132
|
login_id: 'name@mail.com',
|
|
118
|
-
email: 'name@mail.com'
|
|
133
|
+
email: 'name@mail.com',
|
|
134
|
+
template_id: "tid",
|
|
119
135
|
)
|
|
120
136
|
end.not_to raise_error
|
|
121
137
|
end
|
|
@@ -223,6 +239,8 @@ describe Descope::Api::V1::Management::User do
|
|
|
223
239
|
it 'is expected to respond to a search_all method' do
|
|
224
240
|
expect(@instance).to respond_to(:search_all_users)
|
|
225
241
|
|
|
242
|
+
tenant_role_ids = { 'tenant1' => ['roleA', 'roleB'] }
|
|
243
|
+
tenant_role_names = { 'tenant1' => ['roleName1', 'roleName2'] }
|
|
226
244
|
expect(@instance).to receive(:post).with(
|
|
227
245
|
USERS_SEARCH_PATH, {
|
|
228
246
|
loginId: 'someone@example.com',
|
|
@@ -234,7 +252,13 @@ describe Descope::Api::V1::Management::User do
|
|
|
234
252
|
ssoOnly: false,
|
|
235
253
|
text: 'some text',
|
|
236
254
|
testUsersOnly: false,
|
|
237
|
-
withTestUser: false
|
|
255
|
+
withTestUser: false,
|
|
256
|
+
tenantRoleIds: {
|
|
257
|
+
'tenant1' => { values: ['roleA', 'roleB'] }
|
|
258
|
+
},
|
|
259
|
+
tenantRoleNames: {
|
|
260
|
+
'tenant1' => { values: ['roleName1', 'roleName2'] }
|
|
261
|
+
}
|
|
238
262
|
}
|
|
239
263
|
)
|
|
240
264
|
|
|
@@ -248,7 +272,9 @@ describe Descope::Api::V1::Management::User do
|
|
|
248
272
|
page: 1,
|
|
249
273
|
sso_app_ids: [],
|
|
250
274
|
test_users_only: false,
|
|
251
|
-
with_test_user: false
|
|
275
|
+
with_test_user: false,
|
|
276
|
+
tenant_role_ids: tenant_role_ids,
|
|
277
|
+
tenant_role_names: tenant_role_names
|
|
252
278
|
)
|
|
253
279
|
end.not_to raise_error
|
|
254
280
|
end
|
|
@@ -704,4 +730,66 @@ describe Descope::Api::V1::Management::User do
|
|
|
704
730
|
end.not_to raise_error
|
|
705
731
|
end
|
|
706
732
|
end
|
|
733
|
+
|
|
734
|
+
context '.patch_user' do
|
|
735
|
+
it 'is expected to respond to a patch user method' do
|
|
736
|
+
expect(@instance).to respond_to(:patch_user)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
it 'is expected to respond to a user patch method' do
|
|
740
|
+
expect(@instance).to receive(:patch).with(
|
|
741
|
+
USER_PATCH_PATH, {
|
|
742
|
+
loginId: 'name@mail.com',
|
|
743
|
+
email: 'name@mail.com',
|
|
744
|
+
givenName: 'mister',
|
|
745
|
+
name: 'something else',
|
|
746
|
+
test: false,
|
|
747
|
+
invite: false
|
|
748
|
+
}
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
expect do
|
|
752
|
+
@instance.patch_user(
|
|
753
|
+
login_id: 'name@mail.com',
|
|
754
|
+
email: 'name@mail.com',
|
|
755
|
+
given_name: 'mister',
|
|
756
|
+
name: 'something else'
|
|
757
|
+
)
|
|
758
|
+
end.not_to raise_error
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
context '.search_all_test_users' do
|
|
763
|
+
it 'is expected to respond to a search_all_test_users method' do
|
|
764
|
+
expect(@instance).to respond_to(:search_all_test_users)
|
|
765
|
+
|
|
766
|
+
tenant_role_ids = { 'tenant1' => ['roleA', 'roleB'] }
|
|
767
|
+
tenant_role_names = { 'tenant1' => ['roleName1', 'roleName2'] }
|
|
768
|
+
expect(@instance).to receive(:post).with(
|
|
769
|
+
TEST_USERS_SEARCH_PATH, {
|
|
770
|
+
tenantIds: %w[t1 t2],
|
|
771
|
+
roleNames: %w[r1 r2],
|
|
772
|
+
limit: 0,
|
|
773
|
+
page: 0,
|
|
774
|
+
testUsersOnly: true,
|
|
775
|
+
withTestUser: true,
|
|
776
|
+
tenantRoleIds: {
|
|
777
|
+
'tenant1' => { values: ['roleA', 'roleB'] }
|
|
778
|
+
},
|
|
779
|
+
tenantRoleNames: {
|
|
780
|
+
'tenant1' => { values: ['roleName1', 'roleName2'] }
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
expect do
|
|
786
|
+
@instance.search_all_test_users(
|
|
787
|
+
tenant_ids: %w[t1 t2],
|
|
788
|
+
role_names: %w[r1 r2],
|
|
789
|
+
tenant_role_ids: tenant_role_ids,
|
|
790
|
+
tenant_role_names: tenant_role_names
|
|
791
|
+
)
|
|
792
|
+
end.not_to raise_error
|
|
793
|
+
end
|
|
794
|
+
end
|
|
707
795
|
end
|
|
@@ -31,12 +31,32 @@ describe Descope::Api::V1::Session do
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
it 'is expected to post refresh session' do
|
|
34
|
-
jwt_response = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
jwt_response = {
|
|
35
|
+
'sessionJwt' => 'fake_session_jwt',
|
|
36
|
+
'refreshJwt' => 'fake_refresh_jwt',
|
|
37
|
+
'cookies' => {
|
|
38
|
+
'refresh_token' => 'fake_refresh_cookie'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
refresh_token = 'refresh_token'
|
|
42
|
+
audience = nil
|
|
43
|
+
|
|
44
|
+
allow(@instance).to receive(:validate_refresh_token_not_nil).with(refresh_token).and_return(true)
|
|
45
|
+
allow(@instance).to receive(:validate_token).with(refresh_token, audience).and_return(true)
|
|
46
|
+
allow(@instance).to receive(:post).with(REFRESH_TOKEN_PATH, {}, {}, refresh_token).and_return(jwt_response)
|
|
47
|
+
refresh_cookie = jwt_response['cookies'][REFRESH_SESSION_COOKIE_NAME] || jwt_response['refreshJwt']
|
|
48
|
+
|
|
49
|
+
allow(@instance).to receive(:generate_jwt_response).with(
|
|
50
|
+
response_body: jwt_response,
|
|
51
|
+
refresh_cookie:,
|
|
52
|
+
audience:
|
|
53
|
+
).and_return(jwt_response)
|
|
54
|
+
|
|
55
|
+
expect { @instance.refresh_session(refresh_token:, audience:) }.not_to raise_error
|
|
56
|
+
|
|
57
|
+
# Optionally verify the response if needed
|
|
58
|
+
result = @instance.refresh_session(refresh_token:, audience:)
|
|
59
|
+
expect(result).to eq(jwt_response)
|
|
40
60
|
end
|
|
41
61
|
end
|
|
42
62
|
|
|
@@ -114,4 +134,97 @@ describe Descope::Api::V1::Session do
|
|
|
114
134
|
expect { @instance.validate_and_refresh_session(session_token: 'session_token', refresh_token: 'refresh_token') }.to_not raise_error
|
|
115
135
|
end
|
|
116
136
|
end
|
|
137
|
+
|
|
138
|
+
context 'cookie domain fix for refresh_session' do
|
|
139
|
+
let(:refresh_token) { 'test_refresh_token' }
|
|
140
|
+
let(:session_jwt) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.signature' }
|
|
141
|
+
let(:refresh_jwt) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.refresh_sig' }
|
|
142
|
+
|
|
143
|
+
context 'when using cookie-only tokens with custom domain' do
|
|
144
|
+
let(:cookie_only_response) do
|
|
145
|
+
{
|
|
146
|
+
'userId' => 'test123',
|
|
147
|
+
'cookieExpiration' => 1640704758,
|
|
148
|
+
'cookieDomain' => 'dev.lulukuku.com',
|
|
149
|
+
'cookies' => {
|
|
150
|
+
'DS' => session_jwt,
|
|
151
|
+
'DSR' => refresh_jwt
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'extracts tokens from cookies when not in response body' do
|
|
157
|
+
allow(@instance).to receive(:validate_refresh_token_not_nil).and_return(true)
|
|
158
|
+
allow(@instance).to receive(:validate_token).and_return({})
|
|
159
|
+
allow(@instance).to receive(:post).and_return(cookie_only_response)
|
|
160
|
+
allow(@instance).to receive(:generate_jwt_response).and_return(cookie_only_response)
|
|
161
|
+
|
|
162
|
+
expect { @instance.refresh_session(refresh_token: refresh_token) }.not_to raise_error
|
|
163
|
+
|
|
164
|
+
result = @instance.refresh_session(refresh_token: refresh_token)
|
|
165
|
+
expect(result).to eq(cookie_only_response)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'passes correct refresh_cookie to generate_jwt_response' do
|
|
169
|
+
allow(@instance).to receive(:validate_refresh_token_not_nil).and_return(true)
|
|
170
|
+
allow(@instance).to receive(:validate_token).and_return({})
|
|
171
|
+
allow(@instance).to receive(:post).and_return(cookie_only_response)
|
|
172
|
+
|
|
173
|
+
# Verify that refresh_cookie is extracted correctly from cookies
|
|
174
|
+
expected_refresh_cookie = refresh_jwt
|
|
175
|
+
expect(@instance).to receive(:generate_jwt_response).with(
|
|
176
|
+
response_body: cookie_only_response,
|
|
177
|
+
refresh_cookie: expected_refresh_cookie,
|
|
178
|
+
audience: nil
|
|
179
|
+
).and_return(cookie_only_response)
|
|
180
|
+
|
|
181
|
+
@instance.refresh_session(refresh_token: refresh_token)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
context 'when using mixed configuration (some tokens in body, some in cookies)' do
|
|
186
|
+
let(:mixed_response) do
|
|
187
|
+
{
|
|
188
|
+
'sessionJwt' => session_jwt, # Session token in response body
|
|
189
|
+
'userId' => 'test123',
|
|
190
|
+
'cookies' => {
|
|
191
|
+
'DSR' => refresh_jwt # Refresh token in cookies only
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'handles mixed token locations correctly' do
|
|
197
|
+
allow(@instance).to receive(:validate_refresh_token_not_nil).and_return(true)
|
|
198
|
+
allow(@instance).to receive(:validate_token).and_return({})
|
|
199
|
+
allow(@instance).to receive(:post).and_return(mixed_response)
|
|
200
|
+
allow(@instance).to receive(:generate_jwt_response).and_return(mixed_response)
|
|
201
|
+
|
|
202
|
+
expect { @instance.refresh_session(refresh_token: refresh_token) }.not_to raise_error
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
context 'backward compatibility with traditional response body tokens' do
|
|
207
|
+
let(:traditional_response) do
|
|
208
|
+
{
|
|
209
|
+
'sessionJwt' => session_jwt,
|
|
210
|
+
'refreshJwt' => refresh_jwt,
|
|
211
|
+
'userId' => 'test123'
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it 'continues to work with response body tokens' do
|
|
216
|
+
allow(@instance).to receive(:validate_refresh_token_not_nil).and_return(true)
|
|
217
|
+
allow(@instance).to receive(:validate_token).and_return({})
|
|
218
|
+
allow(@instance).to receive(:post).and_return(traditional_response)
|
|
219
|
+
|
|
220
|
+
expect(@instance).to receive(:generate_jwt_response).with(
|
|
221
|
+
response_body: traditional_response,
|
|
222
|
+
refresh_cookie: refresh_jwt, # Should use refreshJwt from response body
|
|
223
|
+
audience: nil
|
|
224
|
+
).and_return(traditional_response)
|
|
225
|
+
|
|
226
|
+
@instance.refresh_session(refresh_token: refresh_token)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
117
230
|
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Descope::Mixins::HTTP do
|
|
6
|
+
before(:all) do
|
|
7
|
+
dummy_instance = DummyClass.new
|
|
8
|
+
dummy_instance.extend(Descope::Mixins::HTTP)
|
|
9
|
+
@instance = dummy_instance
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe '#parse_cookie_value' do
|
|
13
|
+
it 'extracts cookie value from Set-Cookie header' do
|
|
14
|
+
cookie_header = 'DS=jwt_token_value; Path=/; Domain=example.com; HttpOnly; Secure'
|
|
15
|
+
result = @instance.parse_cookie_value(cookie_header, 'DS')
|
|
16
|
+
expect(result).to eq('jwt_token_value')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'extracts cookie value with complex JWT token' do
|
|
20
|
+
jwt_token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwaS5kZXNjb3BlLmNvbS9QMmFiY2RlMTIzNDUifQ.signature'
|
|
21
|
+
cookie_header = "DSR=#{jwt_token}; Path=/; Domain=dev.example.com; HttpOnly; Secure; Max-Age=2592000"
|
|
22
|
+
result = @instance.parse_cookie_value(cookie_header, 'DSR')
|
|
23
|
+
expect(result).to eq(jwt_token)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'returns nil when cookie name is not found' do
|
|
27
|
+
cookie_header = 'OTHER=value; Path=/; Domain=example.com'
|
|
28
|
+
result = @instance.parse_cookie_value(cookie_header, 'DS')
|
|
29
|
+
expect(result).to be_nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'handles cookie value with special characters' do
|
|
33
|
+
cookie_header = 'DS=token.with-special_chars123; Path=/; Domain=example.com'
|
|
34
|
+
result = @instance.parse_cookie_value(cookie_header, 'DS')
|
|
35
|
+
expect(result).to eq('token.with-special_chars123')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'handles cookie header with spaces around value' do
|
|
39
|
+
cookie_header = 'DS= spaced_token ; Path=/; Domain=example.com'
|
|
40
|
+
result = @instance.parse_cookie_value(cookie_header, 'DS')
|
|
41
|
+
expect(result).to eq('spaced_token')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#safe_parse_json with cookie handling' do
|
|
46
|
+
let(:mock_body) do
|
|
47
|
+
{
|
|
48
|
+
'userId' => 'test123',
|
|
49
|
+
'cookieExpiration' => 1640704758,
|
|
50
|
+
'cookieDomain' => 'dev.example.com'
|
|
51
|
+
}.to_json
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
context 'when RestClient cookies are available (same domain)' do
|
|
55
|
+
it 'uses RestClient cookies when available' do
|
|
56
|
+
mock_cookies = {
|
|
57
|
+
'DS' => 'session_token_from_restclient',
|
|
58
|
+
'DSR' => 'refresh_token_from_restclient'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
result = @instance.safe_parse_json(mock_body, cookies: mock_cookies, headers: {})
|
|
62
|
+
|
|
63
|
+
expect(result['cookies']).to eq(mock_cookies)
|
|
64
|
+
expect(result['cookies']['DS']).to eq('session_token_from_restclient')
|
|
65
|
+
expect(result['cookies']['DSR']).to eq('refresh_token_from_restclient')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'handles only refresh token in RestClient cookies' do
|
|
69
|
+
mock_cookies = { 'DSR' => 'refresh_token_only' }
|
|
70
|
+
|
|
71
|
+
result = @instance.safe_parse_json(mock_body, cookies: mock_cookies, headers: {})
|
|
72
|
+
|
|
73
|
+
expect(result['cookies']).to eq(mock_cookies)
|
|
74
|
+
expect(result['cookies']['DSR']).to eq('refresh_token_only')
|
|
75
|
+
expect(result['cookies']['DS']).to be_nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
context 'when RestClient cookies are empty (custom domain)' do
|
|
80
|
+
let(:set_cookie_headers) do
|
|
81
|
+
[
|
|
82
|
+
'DS=session_jwt_token; Path=/; Domain=dev.example.com; HttpOnly; Secure; SameSite=None',
|
|
83
|
+
'DSR=refresh_jwt_token; Path=/; Domain=dev.example.com; HttpOnly; Secure; SameSite=None; Max-Age=2592000'
|
|
84
|
+
]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'parses cookies from Set-Cookie headers when RestClient cookies are empty' do
|
|
88
|
+
mock_headers = { 'set-cookie' => set_cookie_headers }
|
|
89
|
+
|
|
90
|
+
result = @instance.safe_parse_json(mock_body, cookies: {}, headers: mock_headers)
|
|
91
|
+
|
|
92
|
+
expect(result['cookies']).to_not be_nil
|
|
93
|
+
expect(result['cookies']['DS']).to eq('session_jwt_token')
|
|
94
|
+
expect(result['cookies']['DSR']).to eq('refresh_jwt_token')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'parses cookies from Set-Cookie headers with symbol key (real RestClient behavior)' do
|
|
98
|
+
# RestClient returns headers with symbol keys like :set_cookie, not string keys
|
|
99
|
+
mock_headers = { set_cookie: set_cookie_headers }
|
|
100
|
+
|
|
101
|
+
result = @instance.safe_parse_json(mock_body, cookies: {}, headers: mock_headers)
|
|
102
|
+
|
|
103
|
+
expect(result['cookies']).to_not be_nil
|
|
104
|
+
expect(result['cookies']['DS']).to eq('session_jwt_token')
|
|
105
|
+
expect(result['cookies']['DSR']).to eq('refresh_jwt_token')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'parses cookies from Set-Cookie header when headers is a string' do
|
|
109
|
+
mock_headers = { 'Set-Cookie' => set_cookie_headers.first }
|
|
110
|
+
|
|
111
|
+
result = @instance.safe_parse_json(mock_body, cookies: {}, headers: mock_headers)
|
|
112
|
+
|
|
113
|
+
expect(result['cookies']).to_not be_nil
|
|
114
|
+
expect(result['cookies']['DS']).to eq('session_jwt_token')
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'handles case-insensitive Set-Cookie header names' do
|
|
118
|
+
mock_headers = { 'Set-Cookie' => set_cookie_headers }
|
|
119
|
+
|
|
120
|
+
result = @instance.safe_parse_json(mock_body, cookies: {}, headers: mock_headers)
|
|
121
|
+
|
|
122
|
+
expect(result['cookies']['DS']).to eq('session_jwt_token')
|
|
123
|
+
expect(result['cookies']['DSR']).to eq('refresh_jwt_token')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'handles complex JWT tokens in Set-Cookie headers' do
|
|
127
|
+
jwt_session = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwaS5kZXNjb3BlLmNvbSJ9.session_sig'
|
|
128
|
+
jwt_refresh = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwaS5kZXNjb3BlLmNvbSJ9.refresh_sig'
|
|
129
|
+
|
|
130
|
+
complex_headers = [
|
|
131
|
+
"DS=#{jwt_session}; Path=/; Domain=custom.example.com; HttpOnly; Secure; SameSite=None",
|
|
132
|
+
"DSR=#{jwt_refresh}; Path=/; Domain=custom.example.com; HttpOnly; Secure; SameSite=None; Max-Age=2592000"
|
|
133
|
+
]
|
|
134
|
+
mock_headers = { set_cookie: complex_headers }
|
|
135
|
+
|
|
136
|
+
result = @instance.safe_parse_json(mock_body, cookies: {}, headers: mock_headers)
|
|
137
|
+
|
|
138
|
+
expect(result['cookies']['DS']).to eq(jwt_session)
|
|
139
|
+
expect(result['cookies']['DSR']).to eq(jwt_refresh)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'ignores non-Descope cookies in Set-Cookie headers' do
|
|
143
|
+
mixed_headers = [
|
|
144
|
+
'DS=session_token; Path=/; Domain=dev.example.com; HttpOnly',
|
|
145
|
+
'CLOUDFLARE_SESSION=cf_token; Path=/; Domain=.example.com; HttpOnly',
|
|
146
|
+
'DSR=refresh_token; Path=/; Domain=dev.example.com; HttpOnly'
|
|
147
|
+
]
|
|
148
|
+
mock_headers = { set_cookie: mixed_headers }
|
|
149
|
+
|
|
150
|
+
result = @instance.safe_parse_json(mock_body, cookies: {}, headers: mock_headers)
|
|
151
|
+
|
|
152
|
+
expect(result['cookies']['DS']).to eq('session_token')
|
|
153
|
+
expect(result['cookies']['DSR']).to eq('refresh_token')
|
|
154
|
+
expect(result['cookies']['CLOUDFLARE_SESSION']).to be_nil
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
context 'when no cookies are available anywhere' do
|
|
159
|
+
it 'does not add cookies key to response' do
|
|
160
|
+
result = @instance.safe_parse_json(mock_body, cookies: {}, headers: {})
|
|
161
|
+
|
|
162
|
+
expect(result).not_to have_key('cookies')
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'handles missing Set-Cookie headers gracefully' do
|
|
166
|
+
mock_headers = { 'content-type' => 'application/json' }
|
|
167
|
+
|
|
168
|
+
result = @instance.safe_parse_json(mock_body, cookies: {}, headers: mock_headers)
|
|
169
|
+
|
|
170
|
+
expect(result).not_to have_key('cookies')
|
|
171
|
+
expect(result['userId']).to eq('test123')
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'handles nil headers gracefully' do
|
|
175
|
+
result = @instance.safe_parse_json(mock_body, cookies: {}, headers: nil)
|
|
176
|
+
|
|
177
|
+
expect(result).not_to have_key('cookies')
|
|
178
|
+
expect(result['userId']).to eq('test123')
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
context 'edge cases' do
|
|
183
|
+
it 'handles malformed Set-Cookie headers' do
|
|
184
|
+
malformed_headers = [
|
|
185
|
+
'MALFORMED_COOKIE_NO_EQUALS',
|
|
186
|
+
'DS=; Path=/; Domain=example.com', # Empty value
|
|
187
|
+
'=value_without_name; Path=/', # No name
|
|
188
|
+
]
|
|
189
|
+
mock_headers = { set_cookie: malformed_headers }
|
|
190
|
+
|
|
191
|
+
result = @instance.safe_parse_json(mock_body, cookies: {}, headers: mock_headers)
|
|
192
|
+
|
|
193
|
+
# Should handle gracefully and not crash
|
|
194
|
+
expect(result['userId']).to eq('test123')
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'prefers RestClient cookies over Set-Cookie headers when both available' do
|
|
198
|
+
mock_cookies = { 'DS' => 'restclient_token' }
|
|
199
|
+
set_cookie_headers = ['DS=header_token; Path=/; Domain=example.com']
|
|
200
|
+
mock_headers = { set_cookie: set_cookie_headers }
|
|
201
|
+
|
|
202
|
+
result = @instance.safe_parse_json(mock_body, cookies: mock_cookies, headers: mock_headers)
|
|
203
|
+
|
|
204
|
+
# Should prefer RestClient cookies
|
|
205
|
+
expect(result['cookies']['DS']).to eq('restclient_token')
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
describe 'integration with request method' do
|
|
211
|
+
it 'passes headers parameter to safe_parse_json' do
|
|
212
|
+
# Mock RestClient response with custom domain cookies
|
|
213
|
+
mock_response = double('response')
|
|
214
|
+
allow(mock_response).to receive(:code).and_return(200)
|
|
215
|
+
allow(mock_response).to receive(:body).and_return('{"success": true}')
|
|
216
|
+
allow(mock_response).to receive(:cookies).and_return({})
|
|
217
|
+
allow(mock_response).to receive(:headers).and_return({
|
|
218
|
+
set_cookie: ['DS=test_token; Domain=custom.example.com']
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
allow(@instance).to receive(:call).and_return(mock_response)
|
|
222
|
+
|
|
223
|
+
result = @instance.request(:get, '/test', {}, {})
|
|
224
|
+
|
|
225
|
+
expect(result['cookies']).to_not be_nil
|
|
226
|
+
expect(result['cookies']['DS']).to eq('test_token')
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
data/spec/support/utils.rb
CHANGED
|
@@ -29,4 +29,25 @@ module SpecUtils
|
|
|
29
29
|
value.each { |v| deep_stringify_keys(v) if v.is_a? Hash } if value.is_a? Array
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
|
+
|
|
33
|
+
def build_prefix
|
|
34
|
+
# Use GITHUB_RUN_NUMBER as the primary identifier, fall back to a timestamp if not available
|
|
35
|
+
prefix = ENV['GITHUB_RUN_NUMBER'] || ENV['GITHUB_RUN_ID']
|
|
36
|
+
prefix ? "build#{prefix}-" : "local-#{Time.now.to_i}-"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def wait_for_condition(max_wait: 60, interval: 2, description: 'condition')
|
|
40
|
+
start = Time.now
|
|
41
|
+
loop do
|
|
42
|
+
result = yield
|
|
43
|
+
return result if result
|
|
44
|
+
|
|
45
|
+
elapsed = Time.now - start
|
|
46
|
+
if elapsed > max_wait
|
|
47
|
+
raise "Timeout after #{max_wait}s waiting for #{description}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sleep interval
|
|
51
|
+
end
|
|
52
|
+
end
|
|
32
53
|
end
|