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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/workflows/ci.yml +2 -4
  4. data/.github/workflows/lint-pr-title.yml +20 -0
  5. data/.github/workflows/release-please.yml +25 -0
  6. data/.github/workflows/release.yml +22 -25
  7. data/.gitignore +1 -0
  8. data/.release-please-manifest.json +3 -0
  9. data/.rubocop.yml +11 -8
  10. data/.rubocop_todo.yml +94 -0
  11. data/.ruby-version +1 -1
  12. data/CHANGELOG.md +15 -0
  13. data/Gemfile.lock +32 -18
  14. data/Rakefile +8 -0
  15. data/context7.json +4 -0
  16. data/lib/workos/authentication_response.rb +32 -4
  17. data/lib/workos/cache.rb +94 -0
  18. data/lib/workos/client.rb +9 -1
  19. data/lib/workos/directory_sync.rb +1 -1
  20. data/lib/workos/directory_user.rb +31 -3
  21. data/lib/workos/encryptors/aes_gcm.rb +49 -0
  22. data/lib/workos/encryptors.rb +9 -0
  23. data/lib/workos/errors.rb +4 -0
  24. data/lib/workos/feature_flag.rb +34 -0
  25. data/lib/workos/mfa.rb +0 -1
  26. data/lib/workos/oauth_tokens.rb +29 -0
  27. data/lib/workos/organization.rb +14 -1
  28. data/lib/workos/organization_membership.rb +5 -1
  29. data/lib/workos/organizations.rb +87 -3
  30. data/lib/workos/profile.rb +10 -2
  31. data/lib/workos/refresh_authentication_response.rb +29 -2
  32. data/lib/workos/role.rb +38 -0
  33. data/lib/workos/session.rb +187 -0
  34. data/lib/workos/sso.rb +3 -24
  35. data/lib/workos/types/intent.rb +3 -1
  36. data/lib/workos/types/provider.rb +1 -1
  37. data/lib/workos/types/widget_scope.rb +15 -0
  38. data/lib/workos/types.rb +1 -0
  39. data/lib/workos/user.rb +7 -1
  40. data/lib/workos/user_management/session.rb +57 -0
  41. data/lib/workos/user_management.rb +213 -45
  42. data/lib/workos/version.rb +1 -1
  43. data/lib/workos/widgets.rb +46 -0
  44. data/lib/workos.rb +8 -0
  45. data/release-please-config.json +12 -0
  46. data/spec/lib/workos/cache_spec.rb +94 -0
  47. data/spec/lib/workos/directory_user_spec.rb +13 -3
  48. data/spec/lib/workos/encryptors/aes_gcm_spec.rb +41 -0
  49. data/spec/lib/workos/organizations_spec.rb +258 -1
  50. data/spec/lib/workos/portal_spec.rb +30 -0
  51. data/spec/lib/workos/role_spec.rb +142 -0
  52. data/spec/lib/workos/session_spec.rb +475 -0
  53. data/spec/lib/workos/sso_spec.rb +106 -5
  54. data/spec/lib/workos/user_management_spec.rb +496 -1
  55. data/spec/lib/workos/widgets_spec.rb +73 -0
  56. data/spec/support/fixtures/vcr_cassettes/directory_sync/get_user.yml +1 -1
  57. data/spec/support/fixtures/vcr_cassettes/organization/create_with_external_id.yml +83 -0
  58. data/spec/support/fixtures/vcr_cassettes/organization/list_organization_feature_flags.yml +78 -0
  59. data/spec/support/fixtures/vcr_cassettes/organization/list_organization_roles.yml +82 -0
  60. data/spec/support/fixtures/vcr_cassettes/organization/update_with_external_id.yml +78 -0
  61. data/spec/support/fixtures/vcr_cassettes/organization/update_with_external_id_null.yml +78 -0
  62. data/spec/support/fixtures/vcr_cassettes/organization/update_with_stripe_customer_id.yml +78 -0
  63. data/spec/support/fixtures/vcr_cassettes/organization/update_without_name.yml +85 -0
  64. data/spec/support/fixtures/vcr_cassettes/portal/generate_link_certificate_renewal.yml +72 -0
  65. data/spec/support/fixtures/vcr_cassettes/portal/generate_link_domain_verification.yml +72 -0
  66. data/spec/support/fixtures/vcr_cassettes/sso/profile.yml +1 -1
  67. data/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_code/valid_with_oauth_tokens.yml +82 -0
  68. data/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_password/unverified.yml +82 -0
  69. data/spec/support/fixtures/vcr_cassettes/user_management/authenticate_with_refresh_token/valid.yml +79 -78
  70. data/spec/support/fixtures/vcr_cassettes/user_management/create_organization_membership/valid_multiple_roles.yml +76 -0
  71. data/spec/support/fixtures/vcr_cassettes/user_management/create_user_with_external_id.yml +77 -0
  72. data/spec/support/fixtures/vcr_cassettes/user_management/get_user.yml +1 -1
  73. data/spec/support/fixtures/vcr_cassettes/user_management/list_sessions/valid.yml +38 -0
  74. data/spec/support/fixtures/vcr_cassettes/user_management/resend_invitation/accepted.yml +83 -0
  75. data/spec/support/fixtures/vcr_cassettes/user_management/resend_invitation/expired.yml +83 -0
  76. data/spec/support/fixtures/vcr_cassettes/user_management/resend_invitation/invalid.yml +83 -0
  77. data/spec/support/fixtures/vcr_cassettes/user_management/resend_invitation/revoked.yml +83 -0
  78. data/spec/support/fixtures/vcr_cassettes/user_management/resend_invitation/valid.yml +83 -0
  79. data/spec/support/fixtures/vcr_cassettes/user_management/reset_password/valid.yml +1 -1
  80. data/spec/support/fixtures/vcr_cassettes/user_management/update_organization_membership/valid_multiple_roles.yml +76 -0
  81. data/spec/support/fixtures/vcr_cassettes/user_management/update_user/email.yml +82 -0
  82. data/spec/support/fixtures/vcr_cassettes/user_management/update_user/locale.yml +76 -0
  83. data/spec/support/fixtures/vcr_cassettes/user_management/update_user/valid.yml +2 -2
  84. data/spec/support/fixtures/vcr_cassettes/user_management/update_user_external_id_null.yml +77 -0
  85. data/spec/support/fixtures/vcr_cassettes/widgets/get_token.yml +82 -0
  86. data/spec/support/fixtures/vcr_cassettes/widgets/get_token_invalid_organization_id.yml +74 -0
  87. data/spec/support/fixtures/vcr_cassettes/widgets/get_token_invalid_user_id.yml +74 -0
  88. data/spec/support/profile.txt +1 -1
  89. data/workos.gemspec +7 -3
  90. 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