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
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "ruby",
6
+ "package-name": "workos",
7
+ "version-file": "lib/workos/version.rb",
8
+ "changelog-path": "CHANGELOG.md",
9
+ "include-component-in-tag": false
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe WorkOS::Cache do
4
+ before { described_class.clear }
5
+
6
+ describe '.write and .read' do
7
+ it 'stores and retrieves data' do
8
+ described_class.write('key', 'value')
9
+ expect(described_class.read('key')).to eq('value')
10
+ end
11
+
12
+ it 'returns nil if key does not exist' do
13
+ expect(described_class.read('missing')).to be_nil
14
+ end
15
+ end
16
+
17
+ describe '.fetch' do
18
+ it 'returns cached value when present and not expired' do
19
+ described_class.write('key', 'value')
20
+ fetch_value = described_class.fetch('key') { 'new_value' }
21
+ expect(fetch_value).to eq('value')
22
+ end
23
+
24
+ it 'executes block and caches value when not present' do
25
+ fetch_value = described_class.fetch('key') { 'new_value' }
26
+ expect(fetch_value).to eq('new_value')
27
+ end
28
+
29
+ it 'executes block and caches value when force is true' do
30
+ described_class.write('key', 'value')
31
+ fetch_value = described_class.fetch('key', force: true) { 'new_value' }
32
+ expect(fetch_value).to eq('new_value')
33
+ end
34
+ end
35
+
36
+ describe 'expiration' do
37
+ it 'expires values after specified time' do
38
+ described_class.write('key', 'value', expires_in: 0.1)
39
+ expect(described_class.read('key')).to eq('value')
40
+ sleep 0.2
41
+ expect(described_class.read('key')).to be_nil
42
+ end
43
+
44
+ it 'executes block and caches new value when expired' do
45
+ described_class.write('key', 'old_value', expires_in: 0.1)
46
+ sleep 0.2
47
+ fetch_value = described_class.fetch('key') { 'new_value' }
48
+ expect(fetch_value).to eq('new_value')
49
+ end
50
+
51
+ it 'does not expire values when expires_in is nil' do
52
+ described_class.write('key', 'value', expires_in: nil)
53
+ sleep 0.2
54
+ expect(described_class.read('key')).to eq('value')
55
+ end
56
+ end
57
+
58
+ describe '.exist?' do
59
+ it 'returns true if key exists' do
60
+ described_class.write('key', 'value')
61
+ expect(described_class.exist?('key')).to be true
62
+ end
63
+
64
+ it 'returns false if expired' do
65
+ described_class.write('key', 'value', expires_in: 0.1)
66
+ sleep 0.2
67
+ expect(described_class.exist?('key')).to be false
68
+ end
69
+
70
+ it 'returns false if key does not exist' do
71
+ expect(described_class.exist?('missing')).to be false
72
+ end
73
+ end
74
+
75
+ describe '.delete' do
76
+ it 'deletes key' do
77
+ described_class.write('key', 'value')
78
+ described_class.delete('key')
79
+ expect(described_class.read('key')).to be_nil
80
+ end
81
+ end
82
+
83
+ describe '.clear' do
84
+ it 'removes all keys from the cache' do
85
+ described_class.write('key1', 'value1')
86
+ described_class.write('key2', 'value2')
87
+
88
+ described_class.clear
89
+
90
+ expect(described_class.read('key1')).to be_nil
91
+ expect(described_class.read('key2')).to be_nil
92
+ end
93
+ end
94
+ end
@@ -37,13 +37,23 @@ describe WorkOS::DirectoryUser do
37
37
  it 'returns no role' do
38
38
  user = WorkOS::DirectoryUser.new('{"object":"directory_user","id":"directory_user_01FAZYNPC8M0HRYTKFP2GNX852","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","idp_id":"6092c280a3f1e19ef6d8cef8","username":"bob.fakename@workos.com","emails":[{"primary":true,"value":"bob.fakename@workos.com"}, {"primary":false,"value":"bob.fakename@gmail.com"}],"first_name":"Bob","last_name":"Gingerich","job_title":"Developer Success Engineer","state":"active","raw_attributes":{},"custom_attributes":{},"groups":[],"created_at":"2022-05-13T17:45:31.732Z", "updated_at":"2022-07-13T17:45:42.618Z"}')
39
39
  expect(user.role).to eq(nil)
40
+ expect(user.roles).to eq(nil)
40
41
  end
41
42
  end
42
43
 
43
- context 'with a role' do
44
- it 'returns the role slug' do
45
- user = WorkOS::DirectoryUser.new('{"object":"directory_user","id":"directory_user_01FAZYNPC8M0HRYTKFP2GNX852","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","idp_id":"6092c280a3f1e19ef6d8cef8","username":"bob.fakename@workos.com","emails":[{"primary":true,"value":"bob.fakename@workos.com"}, {"primary":false,"value":"bob.fakename@gmail.com"}],"first_name":"Bob","last_name":"Gingerich","job_title":"Developer Success Engineer","state":"active","raw_attributes":{},"custom_attributes":{},"groups":[],"role":{"slug":"member"},"created_at":"2022-05-13T17:45:31.732Z", "updated_at":"2022-07-13T17:45:42.618Z"}')
44
+ context 'with a single role' do
45
+ it 'returns the highest priority role slug and roles array' do
46
+ user = WorkOS::DirectoryUser.new('{"object":"directory_user","id":"directory_user_01FAZYNPC8M0HRYTKFP2GNX852","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","idp_id":"6092c280a3f1e19ef6d8cef8","username":"bob.fakename@workos.com","emails":[{"primary":true,"value":"bob.fakename@workos.com"}, {"primary":false,"value":"bob.fakename@gmail.com"}],"first_name":"Bob","last_name":"Gingerich","job_title":"Developer Success Engineer","state":"active","raw_attributes":{},"custom_attributes":{},"groups":[],"role":{"slug":"member"},"roles":[{"slug":"member"}],"created_at":"2022-05-13T17:45:31.732Z", "updated_at":"2022-07-13T17:45:42.618Z"}')
46
47
  expect(user.role).to eq({ slug: 'member' })
48
+ expect(user.roles).to eq([{ slug: 'member' }])
49
+ end
50
+ end
51
+
52
+ context 'with multiple roles' do
53
+ it 'returns the highest priority role slug and roles array' do
54
+ user = WorkOS::DirectoryUser.new('{"object":"directory_user","id":"directory_user_01FAZYNPC8M0HRYTKFP2GNX852","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","idp_id":"6092c280a3f1e19ef6d8cef8","username":"bob.fakename@workos.com","emails":[{"primary":true,"value":"bob.fakename@workos.com"}, {"primary":false,"value":"bob.fakename@gmail.com"}],"first_name":"Bob","last_name":"Gingerich","job_title":"Developer Success Engineer","state":"active","raw_attributes":{},"custom_attributes":{},"groups":[],"role":{"slug":"admin"},"roles":[{"slug":"member"}, {"slug":"admin"}],"created_at":"2022-05-13T17:45:31.732Z", "updated_at":"2022-07-13T17:45:42.618Z"}')
55
+ expect(user.role).to eq({ slug: 'admin' })
56
+ expect(user.roles).to eq([{ slug: 'member' }, { slug: 'admin' }])
47
57
  end
48
58
  end
49
59
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe WorkOS::Encryptors::AesGcm do
4
+ subject(:encryptor) { described_class.new }
5
+
6
+ let(:key) { 'a' * 32 }
7
+ let(:data) { { access_token: 'tok_123', user: { id: 'user_01' } } }
8
+
9
+ describe '#seal' do
10
+ it 'returns a base64-encoded string' do
11
+ sealed = encryptor.seal(data, key)
12
+ expect(sealed).to be_a(String)
13
+ expect { Base64.decode64(sealed) }.not_to raise_error
14
+ end
15
+
16
+ it 'produces different output each time (random IV)' do
17
+ sealed1 = encryptor.seal(data, key)
18
+ sealed2 = encryptor.seal(data, key)
19
+ expect(sealed1).not_to eq(sealed2)
20
+ end
21
+ end
22
+
23
+ describe '#unseal' do
24
+ it 'round-trips data correctly' do
25
+ sealed = encryptor.seal(data, key)
26
+ unsealed = encryptor.unseal(sealed, key)
27
+ expect(unsealed).to eq(data)
28
+ end
29
+
30
+ it 'returns hash with symbolized keys' do
31
+ sealed = encryptor.seal({ 'string_key' => 'value' }, key)
32
+ unsealed = encryptor.unseal(sealed, key)
33
+ expect(unsealed.keys.first).to be_a(Symbol)
34
+ end
35
+
36
+ it 'raises error with wrong key' do
37
+ sealed = encryptor.seal(data, key)
38
+ expect { encryptor.unseal(sealed, 'b' * 32) }.to raise_error(OpenSSL::Cipher::CipherError)
39
+ end
40
+ end
41
+ end
@@ -33,6 +33,21 @@ describe WorkOS::Organizations do
33
33
  end
34
34
  end
35
35
 
36
+ context 'with external_id' do
37
+ it 'creates an organization with external_id' do
38
+ VCR.use_cassette 'organization/create_with_external_id' do
39
+ organization = described_class.create_organization(
40
+ name: 'Test Organization with External ID',
41
+ external_id: 'ext_org_123',
42
+ )
43
+
44
+ expect(organization.id).to start_with('org_')
45
+ expect(organization.name).to eq('Test Organization with External ID')
46
+ expect(organization.external_id).to eq('ext_org_123')
47
+ end
48
+ end
49
+ end
50
+
36
51
  context 'with domains' do
37
52
  it 'creates an organization and warns' do
38
53
  VCR.use_cassette 'organization/create_with_domains' do
@@ -267,7 +282,7 @@ describe WorkOS::Organizations do
267
282
 
268
283
  describe '.update_organization' do
269
284
  context 'with valid payload' do
270
- it 'creates an organization' do
285
+ it 'updates the organization' do
271
286
  VCR.use_cassette 'organization/update' do
272
287
  organization = described_class.update_organization(
273
288
  organization: 'org_01F6Q6TFP7RD2PF6J03ANNWDKV',
@@ -281,6 +296,72 @@ describe WorkOS::Organizations do
281
296
  end
282
297
  end
283
298
  end
299
+ context 'without a name' do
300
+ it 'updates the organization' do
301
+ VCR.use_cassette 'organization/update_without_name' do
302
+ organization = described_class.update_organization(
303
+ organization: 'org_01F6Q6TFP7RD2PF6J03ANNWDKV',
304
+ domains: ['example.me'],
305
+ )
306
+
307
+ expect(organization.id).to eq('org_01F6Q6TFP7RD2PF6J03ANNWDKV')
308
+ expect(organization.name).to eq('Test Organization')
309
+ expect(organization.domains.first[:domain]).to eq('example.me')
310
+ end
311
+ end
312
+ end
313
+ context 'with a stripe_customer_id' do
314
+ it 'updates the organization' do
315
+ VCR.use_cassette 'organization/update_with_stripe_customer_id' do
316
+ organization = described_class.update_organization(
317
+ organization: 'org_01JJ5H14CAA2SQ5G9HNN6TBZ05',
318
+ name: 'Test Organization',
319
+ stripe_customer_id: 'cus_123',
320
+ )
321
+
322
+ expect(organization.id).to eq('org_01JJ5H14CAA2SQ5G9HNN6TBZ05')
323
+ expect(organization.name).to eq('Test Organization')
324
+ expect(organization.stripe_customer_id).to eq('cus_123')
325
+ end
326
+ end
327
+ end
328
+ context 'with an external_id' do
329
+ it 'updates the organization' do
330
+ VCR.use_cassette 'organization/update_with_external_id' do
331
+ organization = described_class.update_organization(
332
+ organization: 'org_01K0SQV0S6EPWK2ZDEFD1CP1JC',
333
+ name: 'Test Organization',
334
+ external_id: 'ext_org_456',
335
+ )
336
+
337
+ expect(organization.id).to eq('org_01K0SQV0S6EPWK2ZDEFD1CP1JC')
338
+ expect(organization.name).to eq('Test Organization')
339
+ expect(organization.external_id).to eq('ext_org_456')
340
+ end
341
+ end
342
+ end
343
+
344
+ context 'can set external_id to null explicitly' do
345
+ it 'includes external_id null in request body' do
346
+ original_method = described_class.method(:put_request)
347
+ allow(described_class).to receive(:put_request) do |kwargs|
348
+ original_method.call(**kwargs)
349
+ end
350
+
351
+ VCR.use_cassette 'organization/update_with_external_id_null' do
352
+ described_class.update_organization(
353
+ organization: 'org_01K0SQV0S6EPWK2ZDEFD1CP1JC',
354
+ name: 'Test Organization',
355
+ external_id: nil,
356
+ )
357
+ end
358
+
359
+ # Verify the spy captured the right call
360
+ expect(described_class).to have_received(:put_request).with(
361
+ hash_including(body: hash_including(external_id: nil)),
362
+ )
363
+ end
364
+ end
284
365
  end
285
366
 
286
367
  describe '.delete_organization' do
@@ -309,4 +390,180 @@ describe WorkOS::Organizations do
309
390
  end
310
391
  end
311
392
  end
393
+
394
+ describe '.list_organization_roles' do
395
+ context 'with no options' do
396
+ it 'returns roles for organization' do
397
+ expected_metadata = {
398
+ after: nil,
399
+ before: nil,
400
+ }
401
+
402
+ VCR.use_cassette 'organization/list_organization_roles' do
403
+ roles = described_class.list_organization_roles(organization_id: 'org_01JEXP6Z3X7HE4CB6WQSH9ZAFE')
404
+
405
+ expect(roles.data.size).to eq(7)
406
+ expect(roles.list_metadata).to eq(expected_metadata)
407
+ end
408
+ end
409
+
410
+ it 'returns properly initialized Role objects with all attributes' do
411
+ VCR.use_cassette 'organization/list_organization_roles' do
412
+ roles = described_class.list_organization_roles(organization_id: 'org_01JEXP6Z3X7HE4CB6WQSH9ZAFE')
413
+
414
+ first_role = roles.data.first
415
+ expect(first_role).to be_a(WorkOS::Role)
416
+ expect(first_role.id).to eq('role_01HS1C7GRJE08PBR3M6Y0ZYGDZ')
417
+ expect(first_role.name).to eq('Admin')
418
+ expect(first_role.slug).to eq('admin')
419
+ expect(first_role.description).to eq('Write access to every resource available')
420
+ expect(first_role.permissions).to eq(['admin:all', 'read:users', 'write:users', 'manage:roles'])
421
+ expect(first_role.type).to eq('EnvironmentRole')
422
+ expect(first_role.created_at).to eq('2024-03-15T15:38:29.521Z')
423
+ expect(first_role.updated_at).to eq('2024-11-14T17:08:00.556Z')
424
+ end
425
+ end
426
+
427
+ it 'handles roles with empty permissions arrays' do
428
+ VCR.use_cassette 'organization/list_organization_roles' do
429
+ roles = described_class.list_organization_roles(organization_id: 'org_01JEXP6Z3X7HE4CB6WQSH9ZAFE')
430
+
431
+ platform_manager_role = roles.data.find { |role| role.slug == 'org-platform-manager' }
432
+ expect(platform_manager_role).to be_a(WorkOS::Role)
433
+ expect(platform_manager_role.permissions).to eq([])
434
+ end
435
+ end
436
+
437
+ it 'properly serializes Role objects including permissions' do
438
+ VCR.use_cassette 'organization/list_organization_roles' do
439
+ roles = described_class.list_organization_roles(organization_id: 'org_01JEXP6Z3X7HE4CB6WQSH9ZAFE')
440
+
441
+ billing_role = roles.data.find { |role| role.slug == 'billing' }
442
+ serialized = billing_role.to_json
443
+
444
+ expect(serialized[:id]).to eq('role_01JA8GJZRDSZEB9289DQXJ3N9Z')
445
+ expect(serialized[:name]).to eq('Billing Manager')
446
+ expect(serialized[:slug]).to eq('billing')
447
+ expect(serialized[:permissions]).to eq(['read:billing', 'write:billing'])
448
+ expect(serialized[:type]).to eq('EnvironmentRole')
449
+ end
450
+ end
451
+ end
452
+ end
453
+
454
+ describe '.list_organization_feature_flags' do
455
+ context 'with no options' do
456
+ it 'returns feature flags for organization' do
457
+ expected_metadata = {
458
+ after: nil,
459
+ before: nil,
460
+ }
461
+
462
+ VCR.use_cassette 'organization/list_organization_feature_flags' do
463
+ feature_flags = described_class.list_organization_feature_flags(
464
+ organization_id: 'org_01HX7Q7R12H1JMAKN75SH2G529',
465
+ )
466
+
467
+ expect(feature_flags.data.size).to eq(2)
468
+ expect(feature_flags.list_metadata).to eq(expected_metadata)
469
+ end
470
+ end
471
+ end
472
+
473
+ context 'with the before option' do
474
+ it 'forms the proper request to the API' do
475
+ request_args = [
476
+ '/organizations/org_01HX7Q7R12H1JMAKN75SH2G529/feature-flags?before=before-id&'\
477
+ 'order=desc',
478
+ 'Content-Type' => 'application/json'
479
+ ]
480
+
481
+ expected_request = Net::HTTP::Get.new(*request_args)
482
+
483
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
484
+ and_return(expected_request)
485
+
486
+ VCR.use_cassette 'organization/list_organization_feature_flags', match_requests_on: [:path] do
487
+ feature_flags = described_class.list_organization_feature_flags(
488
+ organization_id: 'org_01HX7Q7R12H1JMAKN75SH2G529',
489
+ options: { before: 'before-id' },
490
+ )
491
+
492
+ expect(feature_flags.data.size).to eq(2)
493
+ end
494
+ end
495
+ end
496
+
497
+ context 'with the after option' do
498
+ it 'forms the proper request to the API' do
499
+ request_args = [
500
+ '/organizations/org_01HX7Q7R12H1JMAKN75SH2G529/feature-flags?after=after-id&'\
501
+ 'order=desc',
502
+ 'Content-Type' => 'application/json'
503
+ ]
504
+
505
+ expected_request = Net::HTTP::Get.new(*request_args)
506
+
507
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
508
+ and_return(expected_request)
509
+
510
+ VCR.use_cassette 'organization/list_organization_feature_flags', match_requests_on: [:path] do
511
+ feature_flags = described_class.list_organization_feature_flags(
512
+ organization_id: 'org_01HX7Q7R12H1JMAKN75SH2G529',
513
+ options: { after: 'after-id' },
514
+ )
515
+
516
+ expect(feature_flags.data.size).to eq(2)
517
+ end
518
+ end
519
+ end
520
+
521
+ context 'with the limit option' do
522
+ it 'forms the proper request to the API' do
523
+ request_args = [
524
+ '/organizations/org_01HX7Q7R12H1JMAKN75SH2G529/feature-flags?limit=10&'\
525
+ 'order=desc',
526
+ 'Content-Type' => 'application/json'
527
+ ]
528
+
529
+ expected_request = Net::HTTP::Get.new(*request_args)
530
+
531
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
532
+ and_return(expected_request)
533
+
534
+ VCR.use_cassette 'organization/list_organization_feature_flags', match_requests_on: [:path] do
535
+ feature_flags = described_class.list_organization_feature_flags(
536
+ organization_id: 'org_01HX7Q7R12H1JMAKN75SH2G529',
537
+ options: { limit: 10 },
538
+ )
539
+
540
+ expect(feature_flags.data.size).to eq(2)
541
+ end
542
+ end
543
+ end
544
+
545
+ context 'with multiple pagination options' do
546
+ it 'forms the proper request to the API' do
547
+ request_args = [
548
+ '/organizations/org_01HX7Q7R12H1JMAKN75SH2G529/feature-flags?after=after-id&'\
549
+ 'limit=5&order=asc',
550
+ 'Content-Type' => 'application/json'
551
+ ]
552
+
553
+ expected_request = Net::HTTP::Get.new(*request_args)
554
+
555
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
556
+ and_return(expected_request)
557
+
558
+ VCR.use_cassette 'organization/list_organization_feature_flags', match_requests_on: [:path] do
559
+ feature_flags = described_class.list_organization_feature_flags(
560
+ organization_id: 'org_01HX7Q7R12H1JMAKN75SH2G529',
561
+ options: { after: 'after-id', limit: 5, order: 'asc' },
562
+ )
563
+
564
+ expect(feature_flags.data.size).to eq(2)
565
+ end
566
+ end
567
+ end
568
+ end
312
569
  end
@@ -51,6 +51,36 @@ describe WorkOS::Portal do
51
51
  end
52
52
  end
53
53
  end
54
+
55
+ describe 'with the certificate_renewal intent' do
56
+ it 'returns an Admin Portal link' do
57
+ VCR.use_cassette 'portal/generate_link_certificate_renewal', match_requests_on: %i[path body] do
58
+ portal_link = described_class.generate_link(
59
+ intent: 'certificate_renewal',
60
+ organization: organization,
61
+ )
62
+
63
+ expect(portal_link).to eq(
64
+ 'https://id.workos.com/portal/launch?secret=secret',
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ describe 'with the domain_verification intent' do
72
+ it 'returns an Admin Portal link' do
73
+ VCR.use_cassette 'portal/generate_link_domain_verification', match_requests_on: %i[path body] do
74
+ portal_link = described_class.generate_link(
75
+ intent: 'domain_verification',
76
+ organization: organization,
77
+ )
78
+
79
+ expect(portal_link).to eq(
80
+ 'https://id.workos.com/portal/launch?secret=secret',
81
+ )
82
+ end
83
+ end
54
84
  end
55
85
 
56
86
  describe 'with an invalid organization' do
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe WorkOS::Role do
4
+ describe '.initialize' do
5
+ context 'with full role data including permissions' do
6
+ it 'initializes all attributes correctly' do
7
+ role_json = {
8
+ id: 'role_01FAEAJCJ3P1Z6WP5Y9VQPN2XY',
9
+ name: 'Admin',
10
+ slug: 'admin',
11
+ description: 'Administrator role with full access',
12
+ permissions: ['read:users', 'write:users', 'admin:all'],
13
+ type: 'system',
14
+ created_at: '2022-05-13T17:45:31.732Z',
15
+ updated_at: '2022-07-13T17:45:42.618Z',
16
+ }.to_json
17
+
18
+ role = described_class.new(role_json)
19
+
20
+ expect(role.id).to eq('role_01FAEAJCJ3P1Z6WP5Y9VQPN2XY')
21
+ expect(role.name).to eq('Admin')
22
+ expect(role.slug).to eq('admin')
23
+ expect(role.description).to eq('Administrator role with full access')
24
+ expect(role.permissions).to eq(['read:users', 'write:users', 'admin:all'])
25
+ expect(role.type).to eq('system')
26
+ expect(role.created_at).to eq('2022-05-13T17:45:31.732Z')
27
+ expect(role.updated_at).to eq('2022-07-13T17:45:42.618Z')
28
+ end
29
+ end
30
+
31
+ context 'with role data without permissions' do
32
+ it 'initializes permissions as empty array' do
33
+ role_json = {
34
+ id: 'role_01FAEAJCJ3P1Z6WP5Y9VQPN2XY',
35
+ name: 'User',
36
+ slug: 'user',
37
+ description: 'Basic user role',
38
+ type: 'custom',
39
+ created_at: '2022-05-13T17:45:31.732Z',
40
+ updated_at: '2022-07-13T17:45:42.618Z',
41
+ }.to_json
42
+
43
+ role = described_class.new(role_json)
44
+
45
+ expect(role.id).to eq('role_01FAEAJCJ3P1Z6WP5Y9VQPN2XY')
46
+ expect(role.name).to eq('User')
47
+ expect(role.slug).to eq('user')
48
+ expect(role.description).to eq('Basic user role')
49
+ expect(role.permissions).to eq([])
50
+ expect(role.type).to eq('custom')
51
+ expect(role.created_at).to eq('2022-05-13T17:45:31.732Z')
52
+ expect(role.updated_at).to eq('2022-07-13T17:45:42.618Z')
53
+ end
54
+ end
55
+
56
+ context 'with role data with null permissions' do
57
+ it 'initializes permissions as empty array' do
58
+ role_json = {
59
+ id: 'role_01FAEAJCJ3P1Z6WP5Y9VQPN2XY',
60
+ name: 'User',
61
+ slug: 'user',
62
+ description: 'Basic user role',
63
+ permissions: nil,
64
+ type: 'custom',
65
+ created_at: '2022-05-13T17:45:31.732Z',
66
+ updated_at: '2022-07-13T17:45:42.618Z',
67
+ }.to_json
68
+
69
+ role = described_class.new(role_json)
70
+
71
+ expect(role.permissions).to eq([])
72
+ end
73
+ end
74
+
75
+ context 'with role data with empty permissions array' do
76
+ it 'preserves empty permissions array' do
77
+ role_json = {
78
+ id: 'role_01FAEAJCJ3P1Z6WP5Y9VQPN2XY',
79
+ name: 'User',
80
+ slug: 'user',
81
+ description: 'Basic user role',
82
+ permissions: [],
83
+ type: 'custom',
84
+ created_at: '2022-05-13T17:45:31.732Z',
85
+ updated_at: '2022-07-13T17:45:42.618Z',
86
+ }.to_json
87
+
88
+ role = described_class.new(role_json)
89
+
90
+ expect(role.permissions).to eq([])
91
+ end
92
+ end
93
+ end
94
+
95
+ describe '.to_json' do
96
+ context 'with role that has permissions' do
97
+ it 'includes permissions in serialized output' do
98
+ role_json = {
99
+ id: 'role_01FAEAJCJ3P1Z6WP5Y9VQPN2XY',
100
+ name: 'Admin',
101
+ slug: 'admin',
102
+ description: 'Administrator role',
103
+ permissions: ['read:all', 'write:all'],
104
+ type: 'system',
105
+ created_at: '2022-05-13T17:45:31.732Z',
106
+ updated_at: '2022-07-13T17:45:42.618Z',
107
+ }.to_json
108
+
109
+ role = described_class.new(role_json)
110
+ serialized = role.to_json
111
+
112
+ expect(serialized[:id]).to eq('role_01FAEAJCJ3P1Z6WP5Y9VQPN2XY')
113
+ expect(serialized[:name]).to eq('Admin')
114
+ expect(serialized[:slug]).to eq('admin')
115
+ expect(serialized[:description]).to eq('Administrator role')
116
+ expect(serialized[:permissions]).to eq(['read:all', 'write:all'])
117
+ expect(serialized[:type]).to eq('system')
118
+ expect(serialized[:created_at]).to eq('2022-05-13T17:45:31.732Z')
119
+ expect(serialized[:updated_at]).to eq('2022-07-13T17:45:42.618Z')
120
+ end
121
+ end
122
+
123
+ context 'with role that has no permissions' do
124
+ it 'includes empty permissions array in serialized output' do
125
+ role_json = {
126
+ id: 'role_01FAEAJCJ3P1Z6WP5Y9VQPN2XY',
127
+ name: 'User',
128
+ slug: 'user',
129
+ description: 'Basic user role',
130
+ type: 'custom',
131
+ created_at: '2022-05-13T17:45:31.732Z',
132
+ updated_at: '2022-07-13T17:45:42.618Z',
133
+ }.to_json
134
+
135
+ role = described_class.new(role_json)
136
+ serialized = role.to_json
137
+
138
+ expect(serialized[:permissions]).to eq([])
139
+ end
140
+ end
141
+ end
142
+ end