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
|
@@ -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 '
|
|
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
|