workos 5.3.0 → 6.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/CODEOWNERS +1 -1
- data/.github/workflows/ci.yml +2 -4
- data/.github/workflows/lint-pr-title.yml +20 -0
- data/.github/workflows/release-please.yml +25 -0
- data/.github/workflows/release.yml +22 -25
- data/.gitignore +1 -0
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +11 -8
- data/.rubocop_todo.yml +94 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +15 -0
- data/Gemfile.lock +32 -18
- data/Rakefile +8 -0
- data/context7.json +4 -0
- data/lib/workos/authentication_response.rb +32 -4
- data/lib/workos/cache.rb +94 -0
- data/lib/workos/client.rb +9 -1
- data/lib/workos/directory_sync.rb +1 -1
- data/lib/workos/directory_user.rb +31 -3
- data/lib/workos/encryptors/aes_gcm.rb +49 -0
- data/lib/workos/encryptors.rb +9 -0
- data/lib/workos/errors.rb +4 -0
- data/lib/workos/feature_flag.rb +34 -0
- data/lib/workos/mfa.rb +0 -1
- data/lib/workos/oauth_tokens.rb +29 -0
- data/lib/workos/organization.rb +14 -1
- data/lib/workos/organization_membership.rb +5 -1
- data/lib/workos/organizations.rb +87 -3
- data/lib/workos/profile.rb +10 -2
- data/lib/workos/refresh_authentication_response.rb +29 -2
- data/lib/workos/role.rb +38 -0
- data/lib/workos/session.rb +187 -0
- data/lib/workos/sso.rb +3 -24
- data/lib/workos/types/intent.rb +3 -1
- data/lib/workos/types/provider.rb +1 -1
- data/lib/workos/types/widget_scope.rb +15 -0
- data/lib/workos/types.rb +1 -0
- data/lib/workos/user.rb +7 -1
- data/lib/workos/user_management/session.rb +57 -0
- data/lib/workos/user_management.rb +213 -45
- data/lib/workos/version.rb +1 -1
- data/lib/workos/widgets.rb +46 -0
- data/lib/workos.rb +8 -0
- data/release-please-config.json +12 -0
- data/spec/lib/workos/cache_spec.rb +94 -0
- data/spec/lib/workos/directory_user_spec.rb +13 -3
- data/spec/lib/workos/encryptors/aes_gcm_spec.rb +41 -0
- data/spec/lib/workos/organizations_spec.rb +258 -1
- data/spec/lib/workos/portal_spec.rb +30 -0
- data/spec/lib/workos/role_spec.rb +142 -0
- data/spec/lib/workos/session_spec.rb +475 -0
- data/spec/lib/workos/sso_spec.rb +106 -5
- data/spec/lib/workos/user_management_spec.rb +496 -1
- data/spec/lib/workos/widgets_spec.rb +73 -0
- data/spec/support/fixtures/vcr_cassettes/directory_sync/get_user.yml +1 -1
- data/spec/support/fixtures/vcr_cassettes/organization/create_with_external_id.yml +83 -0
- data/spec/support/fixtures/vcr_cassettes/organization/list_organization_feature_flags.yml +78 -0
- data/spec/support/fixtures/vcr_cassettes/organization/list_organization_roles.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/organization/update_with_external_id.yml +78 -0
- data/spec/support/fixtures/vcr_cassettes/organization/update_with_external_id_null.yml +78 -0
- data/spec/support/fixtures/vcr_cassettes/organization/update_with_stripe_customer_id.yml +78 -0
- data/spec/support/fixtures/vcr_cassettes/organization/update_without_name.yml +85 -0
- data/spec/support/fixtures/vcr_cassettes/portal/generate_link_certificate_renewal.yml +72 -0
- data/spec/support/fixtures/vcr_cassettes/portal/generate_link_domain_verification.yml +72 -0
- data/spec/support/fixtures/vcr_cassettes/sso/profile.yml +1 -1
- data/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_code/valid_with_oauth_tokens.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_password/unverified.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_refresh_token/valid.yml +79 -78
- data/spec/support/fixtures/vcr_cassettes/user_management/create_organization_membership/valid_multiple_roles.yml +76 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/create_user_with_external_id.yml +77 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/get_user.yml +1 -1
- data/spec/support/fixtures/vcr_cassettes/user_management/list_sessions/valid.yml +38 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/resend_invitation/accepted.yml +83 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/resend_invitation/expired.yml +83 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/resend_invitation/invalid.yml +83 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/resend_invitation/revoked.yml +83 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/resend_invitation/valid.yml +83 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/reset_password/valid.yml +1 -1
- data/spec/support/fixtures/vcr_cassettes/user_management/update_organization_membership/valid_multiple_roles.yml +76 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/update_user/email.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/update_user/locale.yml +76 -0
- data/spec/support/fixtures/vcr_cassettes/user_management/update_user/valid.yml +2 -2
- data/spec/support/fixtures/vcr_cassettes/user_management/update_user_external_id_null.yml +77 -0
- data/spec/support/fixtures/vcr_cassettes/widgets/get_token.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/widgets/get_token_invalid_organization_id.yml +74 -0
- data/spec/support/fixtures/vcr_cassettes/widgets/get_token_invalid_user_id.yml +74 -0
- data/spec/support/profile.txt +1 -1
- data/workos.gemspec +7 -3
- metadata +132 -10
|
@@ -36,6 +36,32 @@ describe WorkOS::UserManagement do
|
|
|
36
36
|
'edit%22%7D&provider=authkit',
|
|
37
37
|
)
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
context 'with provider_scopes' do
|
|
41
|
+
it 'returns a valid authorization URL that includes provider_scopes' do
|
|
42
|
+
url = WorkOS::UserManagement.authorization_url(
|
|
43
|
+
provider: 'GoogleOAuth',
|
|
44
|
+
provider_scopes: %w[custom-scope-1 custom-scope-2],
|
|
45
|
+
client_id: 'workos-proj-123',
|
|
46
|
+
redirect_uri: 'foo.com/auth/callback',
|
|
47
|
+
state: {
|
|
48
|
+
next_page: '/dashboard/edit',
|
|
49
|
+
}.to_s,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
expect(url).to eq(
|
|
53
|
+
'https://api.workos.com/user_management/authorize?' \
|
|
54
|
+
'client_id=workos-proj-123' \
|
|
55
|
+
'&redirect_uri=foo.com%2Fauth%2Fcallback' \
|
|
56
|
+
'&response_type=code' \
|
|
57
|
+
'&state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2F' \
|
|
58
|
+
'edit%22%7D' \
|
|
59
|
+
'&provider=GoogleOAuth' \
|
|
60
|
+
'&provider_scopes=custom-scope-1' \
|
|
61
|
+
'&provider_scopes=custom-scope-2',
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
39
65
|
end
|
|
40
66
|
|
|
41
67
|
context 'with a connection selector' do
|
|
@@ -176,6 +202,41 @@ describe WorkOS::UserManagement do
|
|
|
176
202
|
end
|
|
177
203
|
end
|
|
178
204
|
|
|
205
|
+
context 'with a screen hint' do
|
|
206
|
+
let(:args) do
|
|
207
|
+
{
|
|
208
|
+
provider: 'authkit',
|
|
209
|
+
screen_hint: 'sign_up',
|
|
210
|
+
client_id: 'workos-proj-123',
|
|
211
|
+
redirect_uri: 'foo.com/auth/callback',
|
|
212
|
+
state: {
|
|
213
|
+
next_page: '/dashboard/edit',
|
|
214
|
+
}.to_s,
|
|
215
|
+
}
|
|
216
|
+
end
|
|
217
|
+
it 'returns a valid URL' do
|
|
218
|
+
authorization_url = described_class.authorization_url(**args)
|
|
219
|
+
|
|
220
|
+
expect(URI.parse(authorization_url)).to be_a URI
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'returns the expected hostname' do
|
|
224
|
+
authorization_url = described_class.authorization_url(**args)
|
|
225
|
+
|
|
226
|
+
expect(URI.parse(authorization_url).host).to eq(WorkOS.config.api_hostname)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'returns the expected query string' do
|
|
230
|
+
authorization_url = described_class.authorization_url(**args)
|
|
231
|
+
|
|
232
|
+
expect(URI.parse(authorization_url).query).to eq(
|
|
233
|
+
'client_id=workos-proj-123&redirect_uri=foo.com%2Fauth%2Fcallback' \
|
|
234
|
+
'&response_type=code&state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2F' \
|
|
235
|
+
'edit%22%7D&screen_hint=sign_up&provider=authkit',
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
179
240
|
context 'with neither connection_id, organization_id or provider' do
|
|
180
241
|
let(:args) do
|
|
181
242
|
{
|
|
@@ -213,7 +274,7 @@ describe WorkOS::UserManagement do
|
|
|
213
274
|
end.to raise_error(
|
|
214
275
|
ArgumentError,
|
|
215
276
|
'Okta is not a valid value. `provider` must be in ' \
|
|
216
|
-
'["GitHubOAuth", "GoogleOAuth", "MicrosoftOAuth", "authkit"]',
|
|
277
|
+
'["AppleOAuth", "GitHubOAuth", "GoogleOAuth", "MicrosoftOAuth", "authkit"]',
|
|
217
278
|
)
|
|
218
279
|
end
|
|
219
280
|
end
|
|
@@ -229,6 +290,12 @@ describe WorkOS::UserManagement do
|
|
|
229
290
|
|
|
230
291
|
expect(user.id.instance_of?(String))
|
|
231
292
|
expect(user.instance_of?(WorkOS::User))
|
|
293
|
+
expect(user.first_name).to eq('Bob')
|
|
294
|
+
expect(user.last_name).to eq('Loblaw')
|
|
295
|
+
expect(user.email).to eq('bob@example.com')
|
|
296
|
+
expect(user.email_verified).to eq(false)
|
|
297
|
+
expect(user.profile_picture_url).to eq(nil)
|
|
298
|
+
expect(user.last_sign_in_at).to eq('2024-02-06T23:13:18.137Z')
|
|
232
299
|
end
|
|
233
300
|
end
|
|
234
301
|
end
|
|
@@ -306,6 +373,42 @@ describe WorkOS::UserManagement do
|
|
|
306
373
|
end
|
|
307
374
|
end
|
|
308
375
|
|
|
376
|
+
it 'only sends non-nil values in request body' do
|
|
377
|
+
expect(described_class).to receive(:post_request) do |options|
|
|
378
|
+
body = options[:body]
|
|
379
|
+
expect(body).to eq({ email: 'test@example.com', first_name: 'John' })
|
|
380
|
+
expect(body).not_to have_key(:last_name)
|
|
381
|
+
expect(body).not_to have_key(:email_verified)
|
|
382
|
+
|
|
383
|
+
double('request')
|
|
384
|
+
end.and_return(double('request'))
|
|
385
|
+
|
|
386
|
+
expect(described_class).to receive(:execute_request).and_return(
|
|
387
|
+
double('response', body: '{"id": "test_user", "email": "test@example.com"}'),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
described_class.create_user(
|
|
391
|
+
email: 'test@example.com',
|
|
392
|
+
first_name: 'John',
|
|
393
|
+
)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it 'creates a user with external_id' do
|
|
397
|
+
VCR.use_cassette 'user_management/create_user_with_external_id' do
|
|
398
|
+
user = described_class.create_user(
|
|
399
|
+
email: 'external@example.com',
|
|
400
|
+
first_name: 'External',
|
|
401
|
+
last_name: 'User',
|
|
402
|
+
external_id: 'ext_user_123',
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
expect(user.first_name).to eq('External')
|
|
406
|
+
expect(user.last_name).to eq('User')
|
|
407
|
+
expect(user.email).to eq('external@example.com')
|
|
408
|
+
expect(user.external_id).to eq('ext_user_123')
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
309
412
|
context 'with an invalid payload' do
|
|
310
413
|
it 'returns an error' do
|
|
311
414
|
VCR.use_cassette 'user_management/create_user_invalid' do
|
|
@@ -330,13 +433,80 @@ describe WorkOS::UserManagement do
|
|
|
330
433
|
first_name: 'Jane',
|
|
331
434
|
last_name: 'Doe',
|
|
332
435
|
email_verified: false,
|
|
436
|
+
external_id: '123',
|
|
333
437
|
)
|
|
334
438
|
expect(user.first_name).to eq('Jane')
|
|
335
439
|
expect(user.last_name).to eq('Doe')
|
|
336
440
|
expect(user.email_verified).to eq(false)
|
|
441
|
+
expect(user.external_id).to eq('123')
|
|
337
442
|
end
|
|
338
443
|
end
|
|
339
444
|
|
|
445
|
+
it 'can update user locale' do
|
|
446
|
+
VCR.use_cassette 'user_management/update_user/locale' do
|
|
447
|
+
user = described_class.update_user(
|
|
448
|
+
id: 'user_01K78B3ZB5B7119MYEXTQE5KNE',
|
|
449
|
+
locale: 'en-US',
|
|
450
|
+
)
|
|
451
|
+
expect(user.locale).to eq('en-US')
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
it 'can update email addresses' do
|
|
456
|
+
VCR.use_cassette 'user_management/update_user/email' do
|
|
457
|
+
user = described_class.update_user(
|
|
458
|
+
id: 'user_01H7TVSKS45SDHN5V9XPSM6H44',
|
|
459
|
+
email: 'jane@example.com',
|
|
460
|
+
)
|
|
461
|
+
expect(user.email).to eq('jane@example.com')
|
|
462
|
+
expect(user.email_verified).to eq(false)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
it 'only sends non-nil values in request body' do
|
|
467
|
+
# Mock the request to inspect what's being sent
|
|
468
|
+
expect(described_class).to receive(:put_request) do |options|
|
|
469
|
+
# Verify that the body only contains non-nil values
|
|
470
|
+
body = options[:body]
|
|
471
|
+
expect(body).to eq({ email_verified: true })
|
|
472
|
+
expect(body).not_to have_key(:first_name)
|
|
473
|
+
expect(body).not_to have_key(:last_name)
|
|
474
|
+
expect(body).not_to have_key(:email)
|
|
475
|
+
expect(body).not_to have_key(:locale)
|
|
476
|
+
|
|
477
|
+
# Return a mock request object
|
|
478
|
+
double('request')
|
|
479
|
+
end.and_return(double('request'))
|
|
480
|
+
|
|
481
|
+
expect(described_class).to receive(:execute_request).and_return(
|
|
482
|
+
double('response', body: '{"id": "test_user", "email_verified": true}'),
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
described_class.update_user(
|
|
486
|
+
id: 'user_01H7TVSKS45SDHN5V9XPSM6H44',
|
|
487
|
+
email_verified: true,
|
|
488
|
+
)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
it 'can set external_id to null explicitly' do
|
|
492
|
+
original_method = described_class.method(:put_request)
|
|
493
|
+
allow(described_class).to receive(:put_request) do |kwargs|
|
|
494
|
+
original_method.call(**kwargs)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
VCR.use_cassette 'user_management/update_user_external_id_null' do
|
|
498
|
+
described_class.update_user(
|
|
499
|
+
id: 'user_01K0SR53HJ58M957MYAB6TDZ9X',
|
|
500
|
+
first_name: 'John',
|
|
501
|
+
external_id: nil,
|
|
502
|
+
)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
expect(described_class).to have_received(:put_request).with(
|
|
506
|
+
hash_including(body: hash_including(external_id: nil)),
|
|
507
|
+
)
|
|
508
|
+
end
|
|
509
|
+
|
|
340
510
|
context 'with an invalid payload' do
|
|
341
511
|
it 'returns an error' do
|
|
342
512
|
VCR.use_cassette 'user_management/update_user/invalid' do
|
|
@@ -404,6 +574,42 @@ describe WorkOS::UserManagement do
|
|
|
404
574
|
end
|
|
405
575
|
end
|
|
406
576
|
end
|
|
577
|
+
|
|
578
|
+
context 'with an unverified user' do
|
|
579
|
+
it 'raises a ForbiddenRequestError' do
|
|
580
|
+
VCR.use_cassette('user_management/authenticate_with_password/unverified') do
|
|
581
|
+
expect do
|
|
582
|
+
WorkOS::UserManagement.authenticate_with_password(
|
|
583
|
+
email: 'unverified@workos.app',
|
|
584
|
+
password: '7YtYic00VWcXatPb',
|
|
585
|
+
client_id: 'client_123',
|
|
586
|
+
)
|
|
587
|
+
end.to raise_error(WorkOS::ForbiddenRequestError, /Email ownership must be verified before authentication/)
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
context 'with an invitation_token' do
|
|
593
|
+
it 'includes invitation_token in the request body' do
|
|
594
|
+
expect(described_class).to receive(:post_request) do |options|
|
|
595
|
+
body = options[:body]
|
|
596
|
+
expect(body[:invitation_token]).to eq('invitation_token_123')
|
|
597
|
+
|
|
598
|
+
double('request')
|
|
599
|
+
end.and_return(double('request'))
|
|
600
|
+
|
|
601
|
+
expect(described_class).to receive(:execute_request).and_return(
|
|
602
|
+
double('response', body: '{"user": {"id": "user_123"}, "access_token": "token", "refresh_token": "refresh"}'),
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
described_class.authenticate_with_password(
|
|
606
|
+
email: 'test@workos.app',
|
|
607
|
+
password: 'password123',
|
|
608
|
+
client_id: 'client_123',
|
|
609
|
+
invitation_token: 'invitation_token_123',
|
|
610
|
+
)
|
|
611
|
+
end
|
|
612
|
+
end
|
|
407
613
|
end
|
|
408
614
|
|
|
409
615
|
describe '.authenticate_with_code' do
|
|
@@ -422,6 +628,40 @@ describe WorkOS::UserManagement do
|
|
|
422
628
|
end
|
|
423
629
|
end
|
|
424
630
|
|
|
631
|
+
context 'when oauth_tokens is present in the api response' do
|
|
632
|
+
it 'returns an oauth_tokens object' do
|
|
633
|
+
VCR.use_cassette('user_management/authenticate_with_code/valid_with_oauth_tokens') do
|
|
634
|
+
authentication_response = WorkOS::UserManagement.authenticate_with_code(
|
|
635
|
+
code: '01H93ZZHA0JBHFJH9RR11S83YN',
|
|
636
|
+
client_id: 'client_123',
|
|
637
|
+
ip_address: '200.240.210.16',
|
|
638
|
+
user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/108.0.0.0 Safari/537.36',
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
expect(authentication_response.oauth_tokens).to be_a(WorkOS::OAuthTokens)
|
|
642
|
+
expect(authentication_response.oauth_tokens.access_token).to eq('oauth_access_token')
|
|
643
|
+
expect(authentication_response.oauth_tokens.refresh_token).to eq('oauth_refresh_token')
|
|
644
|
+
expect(authentication_response.oauth_tokens.scopes).to eq(%w[read write])
|
|
645
|
+
expect(authentication_response.oauth_tokens.expires_at).to eq(1_234_567_890)
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
context 'when oauth_tokens is not present in the api response' do
|
|
651
|
+
it 'returns nil oauth_tokens' do
|
|
652
|
+
VCR.use_cassette('user_management/authenticate_with_code/valid') do
|
|
653
|
+
authentication_response = WorkOS::UserManagement.authenticate_with_code(
|
|
654
|
+
code: '01H93ZZHA0JBHFJH9RR11S83YN',
|
|
655
|
+
client_id: 'client_123',
|
|
656
|
+
ip_address: '200.240.210.16',
|
|
657
|
+
user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/108.0.0.0 Safari/537.36',
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
expect(authentication_response.oauth_tokens).to be_nil
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
425
665
|
context 'when the user is being impersonated' do
|
|
426
666
|
it 'contains the impersonator metadata' do
|
|
427
667
|
VCR.use_cassette('user_management/authenticate_with_code/valid_with_impersonator') do
|
|
@@ -453,6 +693,27 @@ describe WorkOS::UserManagement do
|
|
|
453
693
|
end
|
|
454
694
|
end
|
|
455
695
|
end
|
|
696
|
+
|
|
697
|
+
context 'with an invitation_token' do
|
|
698
|
+
it 'includes invitation_token in the request body' do
|
|
699
|
+
expect(described_class).to receive(:post_request) do |options|
|
|
700
|
+
body = options[:body]
|
|
701
|
+
expect(body[:invitation_token]).to eq('invitation_token_123')
|
|
702
|
+
|
|
703
|
+
double('request')
|
|
704
|
+
end.and_return(double('request'))
|
|
705
|
+
|
|
706
|
+
expect(described_class).to receive(:execute_request).and_return(
|
|
707
|
+
double('response', body: '{"user": {"id": "user_123"}, "access_token": "token", "refresh_token": "refresh"}'),
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
described_class.authenticate_with_code(
|
|
711
|
+
code: '01H93ZZHA0JBHFJH9RR11S83YN',
|
|
712
|
+
client_id: 'client_123',
|
|
713
|
+
invitation_token: 'invitation_token_123',
|
|
714
|
+
)
|
|
715
|
+
end
|
|
716
|
+
end
|
|
456
717
|
end
|
|
457
718
|
|
|
458
719
|
describe '.authenticate_with_refresh_token' do
|
|
@@ -467,6 +728,7 @@ describe WorkOS::UserManagement do
|
|
|
467
728
|
)
|
|
468
729
|
expect(authentication_response.access_token).to eq('<ACCESS_TOKEN>')
|
|
469
730
|
expect(authentication_response.refresh_token).to eq('<REFRESH_TOKEN>')
|
|
731
|
+
expect(authentication_response.user.id).to eq('user_01H93WD0R0KWF8Q7BK02C0RPYJ')
|
|
470
732
|
end
|
|
471
733
|
end
|
|
472
734
|
end
|
|
@@ -516,6 +778,28 @@ describe WorkOS::UserManagement do
|
|
|
516
778
|
end
|
|
517
779
|
end
|
|
518
780
|
end
|
|
781
|
+
|
|
782
|
+
context 'with an invitation_token' do
|
|
783
|
+
it 'includes invitation_token in the request body' do
|
|
784
|
+
expect(described_class).to receive(:post_request) do |options|
|
|
785
|
+
body = options[:body]
|
|
786
|
+
expect(body[:invitation_token]).to eq('invitation_token_123')
|
|
787
|
+
|
|
788
|
+
double('request')
|
|
789
|
+
end.and_return(double('request'))
|
|
790
|
+
|
|
791
|
+
expect(described_class).to receive(:execute_request).and_return(
|
|
792
|
+
double('response', body: '{"user": {"id": "user_123"}, "access_token": "token", "refresh_token": "refresh"}'),
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
described_class.authenticate_with_magic_auth(
|
|
796
|
+
code: '452079',
|
|
797
|
+
client_id: 'client_123',
|
|
798
|
+
email: 'test@workos.com',
|
|
799
|
+
invitation_token: 'invitation_token_123',
|
|
800
|
+
)
|
|
801
|
+
end
|
|
802
|
+
end
|
|
519
803
|
end
|
|
520
804
|
|
|
521
805
|
describe '.authenticate_with_organization_selection' do
|
|
@@ -684,6 +968,28 @@ describe WorkOS::UserManagement do
|
|
|
684
968
|
expect(authentication_response.authentication_challenge.id).to eq('auth_challenge_01H96FETXGTW1QMBSBT2T36PW0')
|
|
685
969
|
end
|
|
686
970
|
end
|
|
971
|
+
|
|
972
|
+
it 'only sends non-nil values in request body' do
|
|
973
|
+
expect(described_class).to receive(:post_request) do |options|
|
|
974
|
+
body = options[:body]
|
|
975
|
+
expect(body).to eq({ type: 'totp', totp_issuer: 'Test App' })
|
|
976
|
+
expect(body).not_to have_key(:totp_user)
|
|
977
|
+
expect(body).not_to have_key(:totp_secret)
|
|
978
|
+
|
|
979
|
+
double('request')
|
|
980
|
+
end.and_return(double('request'))
|
|
981
|
+
|
|
982
|
+
expect(described_class).to receive(:execute_request).and_return(
|
|
983
|
+
double('response',
|
|
984
|
+
body: '{"authentication_factor": {"id": "test"}, "authentication_challenge": {"id": "test"}}',),
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
described_class.enroll_auth_factor(
|
|
988
|
+
user_id: 'user_123',
|
|
989
|
+
type: 'totp',
|
|
990
|
+
totp_issuer: 'Test App',
|
|
991
|
+
)
|
|
992
|
+
end
|
|
687
993
|
end
|
|
688
994
|
|
|
689
995
|
context 'with an incorrect user id' do
|
|
@@ -1072,6 +1378,23 @@ describe WorkOS::UserManagement do
|
|
|
1072
1378
|
end
|
|
1073
1379
|
end
|
|
1074
1380
|
end
|
|
1381
|
+
|
|
1382
|
+
context 'with role slugs' do
|
|
1383
|
+
it 'creates an organization membership with multiple roles' do
|
|
1384
|
+
VCR.use_cassette 'user_management/create_organization_membership/valid_multiple_roles' do
|
|
1385
|
+
organization_membership = described_class.create_organization_membership(
|
|
1386
|
+
user_id: 'user_01H5JQDV7R7ATEYZDEG0W5PRYS',
|
|
1387
|
+
organization_id: 'org_01H5JQDV7R7ATEYZDEG0W5PRYS',
|
|
1388
|
+
role_slugs: %w[admin member],
|
|
1389
|
+
)
|
|
1390
|
+
|
|
1391
|
+
expect(organization_membership.organization_id).to eq('organization_01H5JQDV7R7ATEYZDEG0W5PRYS')
|
|
1392
|
+
expect(organization_membership.user_id).to eq('user_01H5JQDV7R7ATEYZDEG0W5PRYS')
|
|
1393
|
+
expect(organization_membership.roles).to be_an(Array)
|
|
1394
|
+
expect(organization_membership.roles.length).to eq(2)
|
|
1395
|
+
end
|
|
1396
|
+
end
|
|
1397
|
+
end
|
|
1075
1398
|
end
|
|
1076
1399
|
|
|
1077
1400
|
describe '.update_organization_membership' do
|
|
@@ -1099,6 +1422,22 @@ describe WorkOS::UserManagement do
|
|
|
1099
1422
|
end
|
|
1100
1423
|
end
|
|
1101
1424
|
end
|
|
1425
|
+
|
|
1426
|
+
context 'with role slugs' do
|
|
1427
|
+
it 'updates an organization membership with multiple roles' do
|
|
1428
|
+
VCR.use_cassette('user_management/update_organization_membership/valid_multiple_roles') do
|
|
1429
|
+
organization_membership = WorkOS::UserManagement.update_organization_membership(
|
|
1430
|
+
id: 'om_01H5JQDV7R7ATEYZDEG0W5PRYS',
|
|
1431
|
+
role_slugs: %w[admin editor],
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
expect(organization_membership.organization_id).to eq('organization_01H5JQDV7R7ATEYZDEG0W5PRYS')
|
|
1435
|
+
expect(organization_membership.user_id).to eq('user_01H5JQDV7R7ATEYZDEG0W5PRYS')
|
|
1436
|
+
expect(organization_membership.roles).to be_an(Array)
|
|
1437
|
+
expect(organization_membership.roles.length).to eq(2)
|
|
1438
|
+
end
|
|
1439
|
+
end
|
|
1440
|
+
end
|
|
1102
1441
|
end
|
|
1103
1442
|
|
|
1104
1443
|
describe '.delete_organization_membership' do
|
|
@@ -1350,6 +1689,27 @@ describe WorkOS::UserManagement do
|
|
|
1350
1689
|
expect(invitation.email).to eq('test@workos.com')
|
|
1351
1690
|
end
|
|
1352
1691
|
end
|
|
1692
|
+
|
|
1693
|
+
it 'only sends non-nil values in request body' do
|
|
1694
|
+
expect(described_class).to receive(:post_request) do |options|
|
|
1695
|
+
body = options[:body]
|
|
1696
|
+
expect(body).to eq({ email: 'test@workos.com', organization_id: 'org_123' })
|
|
1697
|
+
expect(body).not_to have_key(:expires_in_days)
|
|
1698
|
+
expect(body).not_to have_key(:inviter_user_id)
|
|
1699
|
+
expect(body).not_to have_key(:role_slug)
|
|
1700
|
+
|
|
1701
|
+
double('request')
|
|
1702
|
+
end.and_return(double('request'))
|
|
1703
|
+
|
|
1704
|
+
expect(described_class).to receive(:execute_request).and_return(
|
|
1705
|
+
double('response', body: '{"id": "test_invitation"}'),
|
|
1706
|
+
)
|
|
1707
|
+
|
|
1708
|
+
described_class.send_invitation(
|
|
1709
|
+
email: 'test@workos.com',
|
|
1710
|
+
organization_id: 'org_123',
|
|
1711
|
+
)
|
|
1712
|
+
end
|
|
1353
1713
|
end
|
|
1354
1714
|
|
|
1355
1715
|
context 'with an invalid payload' do
|
|
@@ -1398,6 +1758,81 @@ describe WorkOS::UserManagement do
|
|
|
1398
1758
|
end
|
|
1399
1759
|
end
|
|
1400
1760
|
|
|
1761
|
+
describe '.resend_invitation' do
|
|
1762
|
+
context 'with valid payload' do
|
|
1763
|
+
it 'resends invitation' do
|
|
1764
|
+
VCR.use_cassette 'user_management/resend_invitation/valid' do
|
|
1765
|
+
invitation = described_class.resend_invitation(
|
|
1766
|
+
id: 'invitation_01H5JQDV7R7ATEYZDEG0W5PRYS',
|
|
1767
|
+
)
|
|
1768
|
+
|
|
1769
|
+
expect(invitation.id).to eq('invitation_01H5JQDV7R7ATEYZDEG0W5PRYS')
|
|
1770
|
+
expect(invitation.email).to eq('test@workos.com')
|
|
1771
|
+
end
|
|
1772
|
+
end
|
|
1773
|
+
end
|
|
1774
|
+
|
|
1775
|
+
context 'with an invalid id' do
|
|
1776
|
+
it 'returns an error' do
|
|
1777
|
+
VCR.use_cassette 'user_management/resend_invitation/invalid' do
|
|
1778
|
+
expect do
|
|
1779
|
+
described_class.resend_invitation(
|
|
1780
|
+
id: 'invalid_id',
|
|
1781
|
+
)
|
|
1782
|
+
end.to raise_error(
|
|
1783
|
+
WorkOS::NotFoundError,
|
|
1784
|
+
/Invitation not found/,
|
|
1785
|
+
)
|
|
1786
|
+
end
|
|
1787
|
+
end
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
context 'when invitation has expired' do
|
|
1791
|
+
it 'returns an error' do
|
|
1792
|
+
VCR.use_cassette 'user_management/resend_invitation/expired' do
|
|
1793
|
+
expect do
|
|
1794
|
+
described_class.resend_invitation(
|
|
1795
|
+
id: 'invitation_01H5JQDV7R7ATEYZDEG0W5PRYS',
|
|
1796
|
+
)
|
|
1797
|
+
end.to raise_error(
|
|
1798
|
+
WorkOS::InvalidRequestError,
|
|
1799
|
+
/Invite has expired/,
|
|
1800
|
+
)
|
|
1801
|
+
end
|
|
1802
|
+
end
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
context 'when invitation has been revoked' do
|
|
1806
|
+
it 'returns an error' do
|
|
1807
|
+
VCR.use_cassette 'user_management/resend_invitation/revoked' do
|
|
1808
|
+
expect do
|
|
1809
|
+
described_class.resend_invitation(
|
|
1810
|
+
id: 'invitation_01H5JQDV7R7ATEYZDEG0W5PRYS',
|
|
1811
|
+
)
|
|
1812
|
+
end.to raise_error(
|
|
1813
|
+
WorkOS::InvalidRequestError,
|
|
1814
|
+
/Invite has been revoked/,
|
|
1815
|
+
)
|
|
1816
|
+
end
|
|
1817
|
+
end
|
|
1818
|
+
end
|
|
1819
|
+
|
|
1820
|
+
context 'when invitation has already been accepted' do
|
|
1821
|
+
it 'returns an error' do
|
|
1822
|
+
VCR.use_cassette 'user_management/resend_invitation/accepted' do
|
|
1823
|
+
expect do
|
|
1824
|
+
described_class.resend_invitation(
|
|
1825
|
+
id: 'invitation_01H5JQDV7R7ATEYZDEG0W5PRYS',
|
|
1826
|
+
)
|
|
1827
|
+
end.to raise_error(
|
|
1828
|
+
WorkOS::InvalidRequestError,
|
|
1829
|
+
/Invite has already been accepted/,
|
|
1830
|
+
)
|
|
1831
|
+
end
|
|
1832
|
+
end
|
|
1833
|
+
end
|
|
1834
|
+
end
|
|
1835
|
+
|
|
1401
1836
|
describe '.revoke_session' do
|
|
1402
1837
|
context 'with valid payload' do
|
|
1403
1838
|
it 'revokes session' do
|
|
@@ -1426,4 +1861,64 @@ describe WorkOS::UserManagement do
|
|
|
1426
1861
|
end
|
|
1427
1862
|
end
|
|
1428
1863
|
end
|
|
1864
|
+
|
|
1865
|
+
describe '.list_sessions' do
|
|
1866
|
+
context 'with a valid user_id' do
|
|
1867
|
+
it 'returns a list of sessions' do
|
|
1868
|
+
VCR.use_cassette('user_management/list_sessions/valid') do
|
|
1869
|
+
result = described_class.list_sessions(
|
|
1870
|
+
user_id: 'user_01H7TVSKS45SDHN5V9XPSM6H44',
|
|
1871
|
+
)
|
|
1872
|
+
|
|
1873
|
+
expect(result.data).to be_an(Array)
|
|
1874
|
+
expect(result.data.first).to be_a(WorkOS::UserManagement::Session)
|
|
1875
|
+
expect(result.data.first.id).to eq('session_01H96FETXGTW2S0V5V9XPSM6H44')
|
|
1876
|
+
expect(result.data.first.status).to eq('active')
|
|
1877
|
+
expect(result.data.first.auth_method).to eq('password')
|
|
1878
|
+
end
|
|
1879
|
+
end
|
|
1880
|
+
|
|
1881
|
+
it 'returns sessions that can be revoked' do
|
|
1882
|
+
VCR.use_cassette('user_management/list_sessions/valid') do
|
|
1883
|
+
result = described_class.list_sessions(
|
|
1884
|
+
user_id: 'user_01H7TVSKS45SDHN5V9XPSM6H44',
|
|
1885
|
+
)
|
|
1886
|
+
session = result.data.first
|
|
1887
|
+
|
|
1888
|
+
expect(described_class).to receive(:post_request) do |options|
|
|
1889
|
+
expect(options[:path]).to eq('/user_management/sessions/revoke')
|
|
1890
|
+
expect(options[:body]).to eq({ session_id: 'session_01H96FETXGTW2S0V5V9XPSM6H44' })
|
|
1891
|
+
expect(options[:auth]).to be true
|
|
1892
|
+
end.and_return(double('request'))
|
|
1893
|
+
|
|
1894
|
+
expect(described_class).to receive(:execute_request).and_return(
|
|
1895
|
+
double('response', is_a?: true),
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
expect(session.revoke).to be true
|
|
1899
|
+
end
|
|
1900
|
+
end
|
|
1901
|
+
end
|
|
1902
|
+
end
|
|
1903
|
+
|
|
1904
|
+
describe '.get_logout_url' do
|
|
1905
|
+
it 'returns a logout url for the given session ID' do
|
|
1906
|
+
result = described_class.get_logout_url(
|
|
1907
|
+
session_id: 'session_01HRX85ATNADY1GQ053AHRFFN6',
|
|
1908
|
+
)
|
|
1909
|
+
|
|
1910
|
+
expect(result).to eq 'https://api.workos.com/user_management/sessions/logout?session_id=session_01HRX85ATNADY1GQ053AHRFFN6'
|
|
1911
|
+
end
|
|
1912
|
+
|
|
1913
|
+
context 'when a `return_to` is given' do
|
|
1914
|
+
it 'returns a logout url with the `return_to` query parameter' do
|
|
1915
|
+
result = described_class.get_logout_url(
|
|
1916
|
+
session_id: 'session_01HRX85ATNADY1GQ053AHRFFN6',
|
|
1917
|
+
return_to: 'https://example.com/signed-out',
|
|
1918
|
+
)
|
|
1919
|
+
|
|
1920
|
+
expect(result).to eq 'https://api.workos.com/user_management/sessions/logout?session_id=session_01HRX85ATNADY1GQ053AHRFFN6&return_to=https%3A%2F%2Fexample.com%2Fsigned-out'
|
|
1921
|
+
end
|
|
1922
|
+
end
|
|
1923
|
+
end
|
|
1429
1924
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
describe WorkOS::Widgets do
|
|
4
|
+
it_behaves_like 'client'
|
|
5
|
+
|
|
6
|
+
describe '.get_token' do
|
|
7
|
+
let(:organization_id) { 'org_01JCP9G67MNAH0KC4B72XZ67M7' }
|
|
8
|
+
let(:user_id) { 'user_01JCP9H4SHS4N3J6XTKDT7JNPE' }
|
|
9
|
+
|
|
10
|
+
describe 'with a valid organization_id and user_id and scopes' do
|
|
11
|
+
it 'returns a widget token' do
|
|
12
|
+
VCR.use_cassette 'widgets/get_token' do
|
|
13
|
+
token = described_class.get_token(
|
|
14
|
+
organization_id: organization_id,
|
|
15
|
+
user_id: user_id,
|
|
16
|
+
scopes: ['widgets:users-table:manage'],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
expect(token).to start_with('eyJhbGciOiJSUzI1NiIsImtpZ')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe 'with an invalid organization_id' do
|
|
25
|
+
it 'raises an error' do
|
|
26
|
+
VCR.use_cassette 'widgets/get_token_invalid_organization_id' do
|
|
27
|
+
expect do
|
|
28
|
+
described_class.get_token(
|
|
29
|
+
organization_id: 'bogus-id',
|
|
30
|
+
user_id: user_id,
|
|
31
|
+
scopes: ['widgets:users-table:manage'],
|
|
32
|
+
)
|
|
33
|
+
end.to raise_error(
|
|
34
|
+
WorkOS::NotFoundError,
|
|
35
|
+
/Organization not found: 'bogus-id'/,
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe 'with an invalid user_id' do
|
|
42
|
+
it 'raises an error' do
|
|
43
|
+
VCR.use_cassette 'widgets/get_token_invalid_user_id' do
|
|
44
|
+
expect do
|
|
45
|
+
described_class.get_token(
|
|
46
|
+
organization_id: organization_id,
|
|
47
|
+
user_id: 'bogus-id',
|
|
48
|
+
scopes: ['widgets:users-table:manage'],
|
|
49
|
+
)
|
|
50
|
+
end.to raise_error(
|
|
51
|
+
WorkOS::NotFoundError,
|
|
52
|
+
/User not found: 'bogus-id'/,
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe 'with invalid scopes' do
|
|
59
|
+
it 'raises an error' do
|
|
60
|
+
expect do
|
|
61
|
+
described_class.get_token(
|
|
62
|
+
organization_id: organization_id,
|
|
63
|
+
user_id: user_id,
|
|
64
|
+
scopes: ['bogus-scope'],
|
|
65
|
+
)
|
|
66
|
+
end.to raise_error(
|
|
67
|
+
ArgumentError,
|
|
68
|
+
/scopes contains an invalid value/,
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|