workos 0.11.0 → 1.2.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.semaphore/semaphore.yml +2 -2
  4. data/Gemfile.lock +2 -2
  5. data/LICENSE +1 -1
  6. data/README.md +13 -230
  7. data/lib/workos.rb +2 -0
  8. data/lib/workos/client.rb +21 -3
  9. data/lib/workos/connection.rb +5 -1
  10. data/lib/workos/directory_user.rb +4 -1
  11. data/lib/workos/organizations.rb +171 -0
  12. data/lib/workos/passwordless.rb +4 -0
  13. data/lib/workos/portal.rb +0 -80
  14. data/lib/workos/profile.rb +8 -10
  15. data/lib/workos/profile_and_token.rb +28 -0
  16. data/lib/workos/sso.rb +25 -100
  17. data/lib/workos/types/connection_struct.rb +1 -0
  18. data/lib/workos/types/directory_user_struct.rb +1 -0
  19. data/lib/workos/version.rb +1 -1
  20. data/spec/lib/workos/organizations_spec.rb +191 -0
  21. data/spec/lib/workos/portal_spec.rb +0 -113
  22. data/spec/lib/workos/sso_spec.rb +35 -115
  23. data/spec/support/fixtures/vcr_cassettes/organization/delete.yml +72 -0
  24. data/spec/support/fixtures/vcr_cassettes/organization/delete_invalid.yml +72 -0
  25. data/spec/support/fixtures/vcr_cassettes/organization/get.yml +73 -0
  26. data/spec/support/fixtures/vcr_cassettes/{sso/create_connection_with_invalid_source.yml → organization/get_invalid.yml} +26 -12
  27. data/spec/support/fixtures/vcr_cassettes/organization/update.yml +73 -0
  28. data/spec/support/fixtures/vcr_cassettes/organization/update_invalid.yml +73 -0
  29. data/spec/support/fixtures/vcr_cassettes/sso/profile.yml +74 -0
  30. data/workos.gemspec +1 -1
  31. metadata +23 -9
  32. data/spec/support/fixtures/vcr_cassettes/sso/create_connection_with_valid_source.yml +0 -63
@@ -11,6 +11,7 @@ module WorkOS
11
11
  const :connection_type, String
12
12
  const :domains, T::Array[T.untyped]
13
13
  const :organization_id, String
14
+ const :state, String
14
15
  const :status, String
15
16
  end
16
17
  end
@@ -7,6 +7,7 @@ module WorkOS
7
7
  # for the DirectoryUser class
8
8
  class DirectoryUserStruct < T::Struct
9
9
  const :id, String
10
+ const :idp_id, String
10
11
  const :emails, T::Array[T.untyped]
11
12
  const :first_name, T.nilable(String)
12
13
  const :last_name, T.nilable(String)
@@ -2,5 +2,5 @@
2
2
  # typed: strong
3
3
 
4
4
  module WorkOS
5
- VERSION = '0.11.0'
5
+ VERSION = '1.2.0'
6
6
  end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ describe WorkOS::Organizations do
5
+ describe '.create_organization' do
6
+ context 'with valid payload' do
7
+ it 'creates an organization' do
8
+ VCR.use_cassette 'organization/create' do
9
+ organization = described_class.create_organization(
10
+ domains: ['example.com'],
11
+ name: 'Test Organization',
12
+ )
13
+
14
+ expect(organization.id).to eq('org_01EHT88Z8J8795GZNQ4ZP1J81T')
15
+ expect(organization.name).to eq('Test Organization')
16
+ expect(organization.domains.first[:domain]).to eq('example.com')
17
+ end
18
+ end
19
+ end
20
+
21
+ context 'with an invalid payload' do
22
+ it 'returns an error' do
23
+ VCR.use_cassette 'organization/create_invalid' do
24
+ expect do
25
+ described_class.create_organization(
26
+ domains: ['example.com'],
27
+ name: 'Test Organization 2',
28
+ )
29
+ end.to raise_error(
30
+ WorkOS::APIError,
31
+ /An Organization with the domain example.com already exists/,
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '.list_organizations' do
39
+ context 'with no options' do
40
+ it 'returns organizations and metadata' do
41
+ expected_metadata = {
42
+ 'after' => nil,
43
+ 'before' => 'before-id',
44
+ }
45
+
46
+ VCR.use_cassette 'organization/list' do
47
+ organizations = described_class.list_organizations
48
+
49
+ expect(organizations.data.size).to eq(7)
50
+ expect(organizations.list_metadata).to eq(expected_metadata)
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'with the before option' do
56
+ it 'forms the proper request to the API' do
57
+ request_args = [
58
+ '/organizations?before=before-id',
59
+ 'Content-Type' => 'application/json'
60
+ ]
61
+
62
+ expected_request = Net::HTTP::Get.new(*request_args)
63
+
64
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
65
+ and_return(expected_request)
66
+
67
+ VCR.use_cassette 'organization/list', match_requests_on: [:path] do
68
+ organizations = described_class.list_organizations(
69
+ before: 'before-id',
70
+ )
71
+
72
+ expect(organizations.data.size).to eq(7)
73
+ end
74
+ end
75
+ end
76
+
77
+ context 'with the after option' do
78
+ it 'forms the proper request to the API' do
79
+ request_args = [
80
+ '/organizations?after=after-id',
81
+ 'Content-Type' => 'application/json'
82
+ ]
83
+
84
+ expected_request = Net::HTTP::Get.new(*request_args)
85
+
86
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
87
+ and_return(expected_request)
88
+
89
+ VCR.use_cassette 'organization/list', match_requests_on: [:path] do
90
+ organizations = described_class.list_organizations(after: 'after-id')
91
+
92
+ expect(organizations.data.size).to eq(7)
93
+ end
94
+ end
95
+ end
96
+
97
+ context 'with the limit option' do
98
+ it 'forms the proper request to the API' do
99
+ request_args = [
100
+ '/organizations?limit=10',
101
+ 'Content-Type' => 'application/json'
102
+ ]
103
+
104
+ expected_request = Net::HTTP::Get.new(*request_args)
105
+
106
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
107
+ and_return(expected_request)
108
+
109
+ VCR.use_cassette 'organization/list', match_requests_on: [:path] do
110
+ organizations = described_class.list_organizations(limit: 10)
111
+
112
+ expect(organizations.data.size).to eq(7)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ describe '.get_organization' do
119
+ context 'with a valid id' do
120
+ it 'gets the organization details' do
121
+ VCR.use_cassette('organization/get') do
122
+ organization = described_class.get_organization(
123
+ id: 'org_01EZDF20TZEJXKPSX2BJRN6TV6',
124
+ )
125
+
126
+ expect(organization.id).to eq('org_01EZDF20TZEJXKPSX2BJRN6TV6')
127
+ expect(organization.name).to eq('Foo Corp')
128
+ expect(organization.domains.first[:domain]).to eq('foo-corp.com')
129
+ end
130
+ end
131
+ end
132
+
133
+ context 'with an invalid id' do
134
+ it 'raises an error' do
135
+ VCR.use_cassette('organization/get_invalid') do
136
+ expect do
137
+ described_class.get_organization(id: 'invalid')
138
+ end.to raise_error(
139
+ WorkOS::APIError,
140
+ 'Status 404, Not Found - request ID: ',
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '.update_organization' do
148
+ context 'with valid payload' do
149
+ it 'creates an organization' do
150
+ VCR.use_cassette 'organization/update' do
151
+ organization = described_class.update_organization(
152
+ organization: 'org_01F29YJ068E52HGEB8ZQGC9MJG',
153
+ domains: ['example.me'],
154
+ name: 'Test Organization',
155
+ )
156
+
157
+ expect(organization.id).to eq('org_01F29YJ068E52HGEB8ZQGC9MJG')
158
+ expect(organization.name).to eq('Test Organization')
159
+ expect(organization.domains.first[:domain]).to eq('example.me')
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ describe '.delete_organization' do
166
+ context 'with a valid id' do
167
+ it 'returns true' do
168
+ VCR.use_cassette('organization/delete') do
169
+ response = described_class.delete_organization(
170
+ id: 'org_01F4A8TD0B4N1Y9SJ8SH635HDB',
171
+ )
172
+
173
+ expect(response).to be(true)
174
+ end
175
+ end
176
+ end
177
+
178
+ context 'with an invalid id' do
179
+ it 'returns false' do
180
+ VCR.use_cassette('organization/delete_invalid') do
181
+ expect do
182
+ described_class.delete_organization(id: 'invalid')
183
+ end.to raise_error(
184
+ WorkOS::APIError,
185
+ 'Status 404, Not Found - request ID: ',
186
+ )
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -2,39 +2,6 @@
2
2
  # typed: false
3
3
 
4
4
  describe WorkOS::Portal do
5
- describe '.create_organization' do
6
- context 'with valid payload' do
7
- it 'creates an organization' do
8
- VCR.use_cassette 'organization/create' do
9
- organization = described_class.create_organization(
10
- domains: ['example.com'],
11
- name: 'Test Organization',
12
- )
13
-
14
- expect(organization.id).to eq('org_01EHT88Z8J8795GZNQ4ZP1J81T')
15
- expect(organization.name).to eq('Test Organization')
16
- expect(organization.domains.first[:domain]).to eq('example.com')
17
- end
18
- end
19
- end
20
-
21
- context 'with an invalid payload' do
22
- it 'returns an error' do
23
- VCR.use_cassette 'organization/create_invalid' do
24
- expect do
25
- described_class.create_organization(
26
- domains: ['example.com'],
27
- name: 'Test Organization 2',
28
- )
29
- end.to raise_error(
30
- WorkOS::APIError,
31
- /An Organization with the domain example.com already exists/,
32
- )
33
- end
34
- end
35
- end
36
- end
37
-
38
5
  describe '.generate_link' do
39
6
  let(:organization) { 'org_01EHQMYV6MBK39QC5PZXHY59C3' }
40
7
 
@@ -100,84 +67,4 @@ describe WorkOS::Portal do
100
67
  end
101
68
  end
102
69
  end
103
-
104
- describe '.list_organizations' do
105
- context 'with no options' do
106
- it 'returns organizations and metadata' do
107
- expected_metadata = {
108
- 'after' => nil,
109
- 'before' => 'before-id',
110
- }
111
-
112
- VCR.use_cassette 'organization/list' do
113
- organizations = described_class.list_organizations
114
-
115
- expect(organizations.data.size).to eq(7)
116
- expect(organizations.list_metadata).to eq(expected_metadata)
117
- end
118
- end
119
- end
120
-
121
- context 'with the before option' do
122
- it 'forms the proper request to the API' do
123
- request_args = [
124
- '/organizations?before=before-id',
125
- 'Content-Type' => 'application/json'
126
- ]
127
-
128
- expected_request = Net::HTTP::Get.new(*request_args)
129
-
130
- expect(Net::HTTP::Get).to receive(:new).with(*request_args).
131
- and_return(expected_request)
132
-
133
- VCR.use_cassette 'organization/list', match_requests_on: [:path] do
134
- organizations = described_class.list_organizations(
135
- before: 'before-id',
136
- )
137
-
138
- expect(organizations.data.size).to eq(7)
139
- end
140
- end
141
- end
142
-
143
- context 'with the after option' do
144
- it 'forms the proper request to the API' do
145
- request_args = [
146
- '/organizations?after=after-id',
147
- 'Content-Type' => 'application/json'
148
- ]
149
-
150
- expected_request = Net::HTTP::Get.new(*request_args)
151
-
152
- expect(Net::HTTP::Get).to receive(:new).with(*request_args).
153
- and_return(expected_request)
154
-
155
- VCR.use_cassette 'organization/list', match_requests_on: [:path] do
156
- organizations = described_class.list_organizations(after: 'after-id')
157
-
158
- expect(organizations.data.size).to eq(7)
159
- end
160
- end
161
- end
162
-
163
- context 'with the limit option' do
164
- it 'forms the proper request to the API' do
165
- request_args = [
166
- '/organizations?limit=10',
167
- 'Content-Type' => 'application/json'
168
- ]
169
-
170
- expected_request = Net::HTTP::Get.new(*request_args)
171
-
172
- expect(Net::HTTP::Get).to receive(:new).with(*request_args).
173
- and_return(expected_request)
174
-
175
- VCR.use_cassette 'organization/list', match_requests_on: [:path] do
176
- organizations = described_class.list_organizations(limit: 10)
177
-
178
- expect(organizations.data.size).to eq(7)
179
- end
180
- end
181
- end
182
- end
183
70
  end
@@ -147,52 +147,40 @@ describe WorkOS::SSO do
147
147
  )
148
148
  end
149
149
  end
150
+ end
150
151
 
151
- context 'passing the project_id' do
152
- let(:args) do
153
- {
154
- domain: 'foo.com',
155
- project_id: 'workos-proj-123',
156
- redirect_uri: 'foo.com/auth/callback',
157
- state: {
158
- next_page: '/dashboard/edit',
159
- }.to_s,
160
- }
161
- end
162
- it 'raises a deprecation warning' do
163
- expect do
164
- described_class.authorization_url(**args)
165
- end.to output(
166
- "[DEPRECATION] `project_id` is deprecated.
167
- Please use `client_id` instead.\n",
168
- ).to_stderr
169
- end
170
-
171
- it 'returns a valid URL' do
172
- authorization_url = described_class.authorization_url(**args)
173
-
174
- expect(URI.parse(authorization_url)).to be_a URI
175
- end
176
-
177
- it 'returns the expected hostname' do
178
- authorization_url = described_class.authorization_url(**args)
152
+ describe '.get_profile' do
153
+ it 'returns a profile' do
154
+ VCR.use_cassette 'sso/profile' do
155
+ profile = described_class.get_profile(access_token: 'access_token')
179
156
 
180
- expect(URI.parse(authorization_url).host).to eq(WorkOS::API_HOSTNAME)
181
- end
182
-
183
- it 'returns the expected query string' do
184
- authorization_url = described_class.authorization_url(**args)
157
+ expectation = {
158
+ connection_id: 'conn_01E83FVYZHY7DM4S9503JHV0R5',
159
+ connection_type: 'GoogleOAuth',
160
+ email: 'bob.loblaw@workos.com',
161
+ first_name: 'Bob',
162
+ id: 'prof_01EEJTY9SZ1R350RB7B73SNBKF',
163
+ idp_id: '116485463307139932699',
164
+ last_name: 'Loblaw',
165
+ raw_attributes: {
166
+ email: 'bob.loblaw@workos.com',
167
+ family_name: 'Loblaw',
168
+ given_name: 'Bob',
169
+ hd: 'workos.com',
170
+ id: '116485463307139932699',
171
+ locale: 'en',
172
+ name: 'Bob Loblaw',
173
+ picture: 'https://lh3.googleusercontent.com/a-/AOh14GyO2hLlgZvteDQ3Ldi3_-RteZLya0hWH7247Cam=s96-c',
174
+ verified_email: true,
175
+ },
176
+ }
185
177
 
186
- expect(URI.parse(authorization_url).query).to eq(
187
- 'client_id=workos-proj-123&redirect_uri=foo.com%2Fauth%2Fcallback' \
188
- '&response_type=code&state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2F' \
189
- 'edit%22%7D&domain=foo.com',
190
- )
178
+ expect(profile.to_json).to eq(expectation)
191
179
  end
192
180
  end
193
181
  end
194
182
 
195
- describe '.profile' do
183
+ describe '.profile_and_token' do
196
184
  let(:args) do
197
185
  {
198
186
  code: SecureRandom.hex(10),
@@ -225,15 +213,15 @@ describe WorkOS::SSO do
225
213
  end
226
214
 
227
215
  it 'includes the SDK Version header' do
228
- described_class.profile(**args)
216
+ described_class.profile_and_token(**args)
229
217
 
230
218
  expect(a_request(:post, 'https://api.workos.com/sso/token').
231
219
  with(headers: headers, body: request_body)).to have_been_made
232
220
  end
233
221
 
234
- it 'returns a WorkOS::Profile' do
235
- profile = described_class.profile(**args)
236
- expect(profile).to be_a(WorkOS::Profile)
222
+ it 'returns a WorkOS::ProfileAndToken' do
223
+ profile_and_token = described_class.profile_and_token(**args)
224
+ expect(profile_and_token).to be_a(WorkOS::ProfileAndToken)
237
225
 
238
226
  expectation = {
239
227
  connection_id: 'conn_01EMH8WAK20T42N2NBMNBCYHAG',
@@ -252,7 +240,8 @@ describe WorkOS::SSO do
252
240
  },
253
241
  }
254
242
 
255
- expect(profile.to_json).to eq(expectation)
243
+ expect(profile_and_token.access_token).to eq('01DVX6QBS3EG6FHY2ESAA5Q65X')
244
+ expect(profile_and_token.profile.to_json).to eq(expectation)
256
245
  end
257
246
  end
258
247
 
@@ -269,7 +258,7 @@ describe WorkOS::SSO do
269
258
 
270
259
  it 'raises an exception with request ID' do
271
260
  expect do
272
- described_class.profile(**args)
261
+ described_class.profile_and_token(**args)
273
262
  end.to raise_error(
274
263
  WorkOS::APIError,
275
264
  'some error message - request ID: request-id',
@@ -293,7 +282,7 @@ describe WorkOS::SSO do
293
282
 
294
283
  it 'raises an exception' do
295
284
  expect do
296
- described_class.profile(**args)
285
+ described_class.profile_and_token(**args)
297
286
  end.to raise_error(
298
287
  WorkOS::APIError,
299
288
  "The code '01DVX3C5Z367SFHR8QNDMK7V24'" \
@@ -303,75 +292,6 @@ describe WorkOS::SSO do
303
292
  end
304
293
  end
305
294
 
306
- describe '.create_connection' do
307
- context 'with a valid source' do
308
- it 'creates a connection' do
309
- VCR.use_cassette('sso/create_connection_with_valid_source') do
310
- connection = WorkOS::SSO.create_connection(
311
- source: 'draft_conn_01E6PK87QP6NQ29RRX0G100YGV',
312
- )
313
-
314
- expect(connection.id).to eq('conn_01E4F9T2YWZFD218DN04KVFDSY')
315
- expect(connection.connection_type).to eq('GoogleOAuth')
316
- expect(connection.name).to eq('Foo Corp')
317
- expect(connection.domains.first[:domain]).to eq('example.com')
318
- expect(connection.organization_id).to eq('12345')
319
- expect(connection.status).to eq('linked')
320
- end
321
- end
322
- end
323
-
324
- context 'with an invalid source' do
325
- it 'raises an error' do
326
- VCR.use_cassette('sso/create_connection_with_invalid_source') do
327
- expect do
328
- WorkOS::SSO.create_connection(source: 'invalid')
329
- end.to raise_error(
330
- WorkOS::APIError,
331
- 'Status 404, Not Found - request ID: ',
332
- )
333
- end
334
- end
335
- end
336
- end
337
-
338
- describe '.promote_draft_connection' do
339
- let(:token) { 'draft_conn_id' }
340
- let(:client_id) { 'proj_0239u590h' }
341
-
342
- context 'with a valid request' do
343
- before do
344
- stub_request(
345
- :post,
346
- "https://api.workos.com/draft_connections/#{token}/activate",
347
- ).to_return(status: 200)
348
- end
349
- it 'returns true' do
350
- response = described_class.promote_draft_connection(
351
- token: token,
352
- )
353
-
354
- expect(response).to be(true)
355
- end
356
- end
357
-
358
- context 'with an invalid request' do
359
- before do
360
- stub_request(
361
- :post,
362
- "https://api.workos.com/draft_connections/#{token}/activate",
363
- ).to_return(status: 403)
364
- end
365
- it 'returns true' do
366
- response = described_class.promote_draft_connection(
367
- token: token,
368
- )
369
-
370
- expect(response).to be(false)
371
- end
372
- end
373
- end
374
-
375
295
  describe '.list_connections' do
376
296
  context 'with no options' do
377
297
  it 'returns connections and metadata' do