workos 0.11.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) 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/lib/workos.rb +2 -0
  7. data/lib/workos/client.rb +3 -2
  8. data/lib/workos/directory.rb +4 -1
  9. data/lib/workos/directory_user.rb +6 -1
  10. data/lib/workos/errors.rb +13 -2
  11. data/lib/workos/organizations.rb +171 -0
  12. data/lib/workos/portal.rb +0 -133
  13. data/lib/workos/profile.rb +8 -10
  14. data/lib/workos/profile_and_token.rb +28 -0
  15. data/lib/workos/sso.rb +31 -100
  16. data/lib/workos/types/directory_struct.rb +1 -0
  17. data/lib/workos/types/directory_user_struct.rb +1 -0
  18. data/lib/workos/version.rb +1 -1
  19. data/spec/lib/workos/organizations_spec.rb +191 -0
  20. data/spec/lib/workos/portal_spec.rb +0 -160
  21. data/spec/lib/workos/sso_spec.rb +41 -122
  22. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_after.yml +12 -9
  23. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_before.yml +8 -5
  24. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_domain.yml +8 -8
  25. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_limit.yml +9 -9
  26. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_no_options.yml +23 -10
  27. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_search.yml +8 -8
  28. data/spec/support/fixtures/vcr_cassettes/organization/delete.yml +72 -0
  29. data/spec/support/fixtures/vcr_cassettes/{sso/create_connection_with_invalid_source.yml → organization/delete_invalid.yml} +26 -12
  30. data/spec/support/fixtures/vcr_cassettes/sso/profile.yml +74 -0
  31. data/workos.gemspec +1 -1
  32. metadata +15 -9
  33. data/spec/support/fixtures/vcr_cassettes/sso/create_connection_with_valid_source.yml +0 -63
@@ -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,131 +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
-
184
- describe '.get_organization' do
185
- context 'with a valid id' do
186
- it 'gets the organization details' do
187
- VCR.use_cassette('organization/get') do
188
- organization = described_class.get_organization(
189
- id: 'org_01EZDF20TZEJXKPSX2BJRN6TV6',
190
- )
191
-
192
- expect(organization.id).to eq('org_01EZDF20TZEJXKPSX2BJRN6TV6')
193
- expect(organization.name).to eq('Foo Corp')
194
- expect(organization.domains.first[:domain]).to eq('foo-corp.com')
195
- end
196
- end
197
- end
198
-
199
- context 'with an invalid id' do
200
- it 'raises an error' do
201
- VCR.use_cassette('organization/get_invalid') do
202
- expect do
203
- described_class.get_organization(id: 'invalid')
204
- end.to raise_error(
205
- WorkOS::APIError,
206
- 'Status 404, Not Found - request ID: ',
207
- )
208
- end
209
- end
210
- end
211
- end
212
-
213
- describe '.update_organization' do
214
- context 'with valid payload' do
215
- it 'creates an organization' do
216
- VCR.use_cassette 'organization/update' do
217
- organization = described_class.update_organization(
218
- organization: 'org_01F29YJ068E52HGEB8ZQGC9MJG',
219
- domains: ['example.me'],
220
- name: 'Test Organization',
221
- )
222
-
223
- expect(organization.id).to eq('org_01F29YJ068E52HGEB8ZQGC9MJG')
224
- expect(organization.name).to eq('Test Organization')
225
- expect(organization.domains.first[:domain]).to eq('example.me')
226
- end
227
- end
228
- end
229
- end
230
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
 
@@ -263,16 +252,16 @@ describe WorkOS::SSO do
263
252
  to_return(
264
253
  headers: { 'X-Request-ID' => 'request-id' },
265
254
  status: 422,
266
- body: { "message": 'some error message' }.to_json,
255
+ body: { "error": 'some error', "error_description": 'some error description' }.to_json,
267
256
  )
268
257
  end
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
- 'some error message - request ID: request-id',
264
+ 'error: some error, error_description: some error description - request ID: request-id',
276
265
  )
277
266
  end
278
267
  end
@@ -282,97 +271,27 @@ describe WorkOS::SSO do
282
271
  stub_request(:post, 'https://api.workos.com/sso/token').
283
272
  with(body: request_body).
284
273
  to_return(
285
- status: 201,
274
+ status: 400,
286
275
  headers: { 'X-Request-ID' => 'request-id' },
287
276
  body: {
288
- message: "The code '01DVX3C5Z367SFHR8QNDMK7V24'" \
289
- ' has expired or is invalid.',
277
+ "error": 'invalid_grant',
278
+ "error_description": "The code '01DVX3C5Z367SFHR8QNDMK7V24' has expired or is invalid.",
290
279
  }.to_json,
291
280
  )
292
281
  end
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
- "The code '01DVX3C5Z367SFHR8QNDMK7V24'" \
288
+ "error: invalid_grant, error_description: The code '01DVX3C5Z367SFHR8QNDMK7V24'" \
300
289
  ' has expired or is invalid. - request ID: request-id',
301
290
  )
302
291
  end
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.state).to eq('active')
320
- expect(connection.status).to eq('linked')
321
- end
322
- end
323
- end
324
-
325
- context 'with an invalid source' do
326
- it 'raises an error' do
327
- VCR.use_cassette('sso/create_connection_with_invalid_source') do
328
- expect do
329
- WorkOS::SSO.create_connection(source: 'invalid')
330
- end.to raise_error(
331
- WorkOS::APIError,
332
- 'Status 404, Not Found - request ID: ',
333
- )
334
- end
335
- end
336
- end
337
- end
338
-
339
- describe '.promote_draft_connection' do
340
- let(:token) { 'draft_conn_id' }
341
- let(:client_id) { 'proj_0239u590h' }
342
-
343
- context 'with a valid request' do
344
- before do
345
- stub_request(
346
- :post,
347
- "https://api.workos.com/draft_connections/#{token}/activate",
348
- ).to_return(status: 200)
349
- end
350
- it 'returns true' do
351
- response = described_class.promote_draft_connection(
352
- token: token,
353
- )
354
-
355
- expect(response).to be(true)
356
- end
357
- end
358
-
359
- context 'with an invalid request' do
360
- before do
361
- stub_request(
362
- :post,
363
- "https://api.workos.com/draft_connections/#{token}/activate",
364
- ).to_return(status: 403)
365
- end
366
- it 'returns true' do
367
- response = described_class.promote_draft_connection(
368
- token: token,
369
- )
370
-
371
- expect(response).to be(false)
372
- end
373
- end
374
- end
375
-
376
295
  describe '.list_connections' do
377
296
  context 'with no options' do
378
297
  it 'returns connections and metadata' do
@@ -14,7 +14,7 @@ http_interactions:
14
14
  Accept:
15
15
  - "*/*"
16
16
  User-Agent:
17
- - WorkOS; ruby/2.7.1; x86_64-darwin19; v0.10.3
17
+ - WorkOS; ruby/3.0.1; x86_64-darwin19; v1.2.1
18
18
  Authorization:
19
19
  - Bearer <API_KEY>
20
20
  response:
@@ -53,20 +53,23 @@ http_interactions:
53
53
  X-Xss-Protection:
54
54
  - '0'
55
55
  X-Request-Id:
56
- - 1e6d2f37-ca39-4e6c-8d04-4a56fed444ce
56
+ - 167e0fa2-cee2-4834-a0aa-4f68fd0a3796
57
57
  Content-Type:
58
58
  - application/json; charset=utf-8
59
+ Content-Length:
60
+ - '784'
59
61
  Etag:
60
- - W/"7dc-9PMr4siidbvsdQqw0uiJT6E94Dc"
62
+ - W/"310-fBrsCTIA95j4JLo4UR8X2zBThYQ"
61
63
  Date:
62
- - Thu, 22 Apr 2021 21:33:48 GMT
63
- Transfer-Encoding:
64
- - chunked
64
+ - Mon, 07 Jun 2021 17:55:30 GMT
65
65
  Via:
66
66
  - 1.1 vegur
67
67
  body:
68
- encoding: ASCII-8BIT
69
- string: '{"object":"list","listMetadata":{"before":"before-id","after":null},"data":[{"object":"directory","id":"directory_edp_1","external_key":"lA3gS1kCZMCkk82E","state":"linked","type":"gsuite directory","name":"Foo Corp","bearer_token":null,"client_id":"project_XXX","domain":"foo-corp.com"}, {"object":"directory","id":"directory_edp_2","external_key":"lA3g","state":"linked","type":"okta scim v2.0","name":"Example", "bearer_token":null,"client_id":"project_XXX","domain":"example.com"}, {"object":"directory","id":"directory_edp_3","external_key":"lA3gS1kC","state":"linked","type":"bamboohr","name":"Acme","bearer_token":null,"client_id":"project_XXX","domain":"acme.com"}]}'
68
+ encoding: UTF-8
69
+ string: '{"object":"list","listMetadata":{"before":null,"after":null},"data":[{"object":"directory","id":"directory_01F7796W20KW0CXEQQEYENT0ZC","organization_id":"org_01EZDF20TZEJXKPSX2BJRN6TV6","name":"Bamboo
70
+ Test","external_key":"rPzV4pdpbaUiKsc6","type":"bamboohr","state":"unlinked","domain":"foo-corp.com"},{"object":"directory","id":"directory_01F5ZY7XVQZ3DRYEZTH1EPA8BS","organization_id":"org_01EZDF20TZEJXKPSX2BJRN6TV6","name":"Foo
71
+ Corp","external_key":"qV4eyK99QGUaYYa0","type":"okta scim v2.0","state":"linked","domain":"foo-corp.com"},{"object":"directory","id":"directory_01F5XHH1QHX6C2F0Z6WG9YPGCJ","organization_id":"org_01F29YJ068E52HGEB8ZQGC9MJG","name":"Example
72
+ Azure SCIM","external_key":"YDKJvbWHKKg66cSk","type":"azure scim v2.0","state":"linked","domain":"example.com"}]}'
70
73
  http_version:
71
- recorded_at: Thu, 22 Apr 2021 21:33:49 GMT
74
+ recorded_at: Mon, 07 Jun 2021 17:55:30 GMT
72
75
  recorded_with: VCR 5.0.0
@@ -14,7 +14,7 @@ http_interactions:
14
14
  Accept:
15
15
  - "*/*"
16
16
  User-Agent:
17
- - WorkOS; ruby/2.7.1; x86_64-darwin19; v0.10.3
17
+ - WorkOS; ruby/3.0.1; x86_64-darwin19; v1.2.1
18
18
  Authorization:
19
19
  - Bearer <API_KEY>
20
20
  response:
@@ -53,7 +53,7 @@ http_interactions:
53
53
  X-Xss-Protection:
54
54
  - '0'
55
55
  X-Request-Id:
56
- - ee432766-24b5-4d6d-baa9-05b7d56ac582
56
+ - d5e27591-7a56-468c-bffe-3a035f3bf26c
57
57
  Content-Type:
58
58
  - application/json; charset=utf-8
59
59
  Content-Length:
@@ -61,12 +61,15 @@ http_interactions:
61
61
  Etag:
62
62
  - W/"47-5KOnfOsRy36pnaPjBxvaf6LRiGc"
63
63
  Date:
64
- - Thu, 22 Apr 2021 21:33:48 GMT
64
+ - Mon, 07 Jun 2021 17:55:30 GMT
65
65
  Via:
66
66
  - 1.1 vegur
67
67
  body:
68
68
  encoding: UTF-8
69
- string: '{"object":"list","listMetadata":{"before":"before-id","after":null},"data":[{"object":"directory","id":"directory_edp_1","external_key":"lA3gS1kCZMCkk82E","state":"linked","type":"gsuite directory","name":"Foo Corp","bearer_token":null,"client_id":"project_XXX","domain":"foo-corp.com"}, {"object":"directory","id":"directory_edp_2","external_key":"lA3g","state":"linked","type":"okta scim v2.0","name":"Example", "bearer_token":null,"client_id":"project_XXX","domain":"example.com"}, {"object":"directory","id":"directory_edp_3","external_key":"lA3gS1kC","state":"linked","type":"bamboohr","name":"Acme","bearer_token":null,"client_id":"project_XXX","domain":"acme.com"}]}'
69
+ string: '{"object":"list","listMetadata":{"before":null,"after":null},"data":[{"object":"directory","id":"directory_01F7796W20KW0CXEQQEYENT0ZC","organization_id":"org_01EZDF20TZEJXKPSX2BJRN6TV6","name":"Bamboo
70
+ Test","external_key":"rPzV4pdpbaUiKsc6","type":"bamboohr","state":"unlinked","domain":"foo-corp.com"},{"object":"directory","id":"directory_01F5ZY7XVQZ3DRYEZTH1EPA8BS","organization_id":"org_01EZDF20TZEJXKPSX2BJRN6TV6","name":"Foo
71
+ Corp","external_key":"qV4eyK99QGUaYYa0","type":"okta scim v2.0","state":"linked","domain":"foo-corp.com"},{"object":"directory","id":"directory_01F5XHH1QHX6C2F0Z6WG9YPGCJ","organization_id":"org_01F29YJ068E52HGEB8ZQGC9MJG","name":"Example
72
+ Azure SCIM","external_key":"YDKJvbWHKKg66cSk","type":"azure scim v2.0","state":"linked","domain":"example.com"}]}'
70
73
  http_version:
71
- recorded_at: Thu, 22 Apr 2021 21:33:48 GMT
74
+ recorded_at: Mon, 07 Jun 2021 17:55:30 GMT
72
75
  recorded_with: VCR 5.0.0