workos 1.4.0 → 1.6.1

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.ruby-version +1 -1
  4. data/.semaphore/semaphore.yml +2 -2
  5. data/Gemfile.lock +3 -3
  6. data/lib/workos/client.rb +3 -6
  7. data/lib/workos/connection.rb +9 -1
  8. data/lib/workos/directory.rb +10 -2
  9. data/lib/workos/directory_user.rb +4 -1
  10. data/lib/workos/errors.rb +4 -0
  11. data/lib/workos/organization.rb +10 -1
  12. data/lib/workos/organizations.rb +18 -4
  13. data/lib/workos/profile.rb +7 -2
  14. data/lib/workos/types/connection_struct.rb +2 -0
  15. data/lib/workos/types/directory_struct.rb +3 -1
  16. data/lib/workos/types/directory_user_struct.rb +1 -0
  17. data/lib/workos/types/organization_struct.rb +3 -0
  18. data/lib/workos/types/profile_struct.rb +1 -0
  19. data/lib/workos/types/webhook_struct.rb +14 -0
  20. data/lib/workos/types.rb +1 -0
  21. data/lib/workos/version.rb +1 -1
  22. data/lib/workos/webhook.rb +47 -0
  23. data/lib/workos/webhooks.rb +168 -0
  24. data/lib/workos.rb +3 -0
  25. data/spec/lib/workos/audit_trail_spec.rb +2 -0
  26. data/spec/lib/workos/directory_sync_spec.rb +32 -29
  27. data/spec/lib/workos/organizations_spec.rb +13 -11
  28. data/spec/lib/workos/passwordless_spec.rb +2 -0
  29. data/spec/lib/workos/portal_spec.rb +2 -0
  30. data/spec/lib/workos/sso_spec.rb +16 -12
  31. data/spec/lib/workos/webhooks_spec.rb +190 -0
  32. data/spec/spec_helper.rb +3 -0
  33. data/spec/support/fixtures/vcr_cassettes/directory_sync/get_user.yml +40 -16
  34. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_after.yml +34 -22
  35. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_before.yml +36 -22
  36. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_domain.yml +30 -19
  37. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_limit.yml +31 -20
  38. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_no_options.yml +39 -21
  39. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_search.yml +32 -20
  40. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_users/with_after.yml +128 -28
  41. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_users/with_before.yml +31 -18
  42. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_users/with_directory.yml +136 -35
  43. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_users/with_group.yml +128 -18
  44. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_users/with_limit.yml +131 -17
  45. data/spec/support/fixtures/vcr_cassettes/organization/create.yml +28 -16
  46. data/spec/support/fixtures/vcr_cassettes/organization/get.yml +27 -16
  47. data/spec/support/fixtures/vcr_cassettes/organization/list.yml +29 -14
  48. data/spec/support/fixtures/vcr_cassettes/organization/update.yml +27 -16
  49. data/spec/support/fixtures/vcr_cassettes/sso/get_connection_with_valid_id.yml +28 -16
  50. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_after.yml +25 -15
  51. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_before.yml +28 -15
  52. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_connection_type.yml +31 -14
  53. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_domain.yml +27 -13
  54. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_limit.yml +24 -15
  55. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_no_options.yml +30 -14
  56. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_organization_id.yml +28 -14
  57. data/spec/support/fixtures/vcr_cassettes/sso/profile.yml +1 -1
  58. data/spec/support/profile.txt +1 -1
  59. data/spec/support/shared_examples/client_spec.rb +16 -0
  60. data/spec/support/webhook_payload.txt +1 -0
  61. metadata +12 -5
  62. data/spec/support/fixtures/vcr_cassettes/organization/update_invalid.yml +0 -73
@@ -2,6 +2,8 @@
2
2
  # typed: false
3
3
 
4
4
  describe WorkOS::DirectorySync do
5
+ it_behaves_like 'client'
6
+
5
7
  describe '.list_directories' do
6
8
  context 'with no options' do
7
9
  it 'returns directories and metadata' do
@@ -13,7 +15,7 @@ describe WorkOS::DirectorySync do
13
15
  VCR.use_cassette 'directory_sync/list_directories/with_no_options' do
14
16
  directories = described_class.list_directories
15
17
 
16
- expect(directories.data.size).to eq(3)
18
+ expect(directories.data.size).to eq(10)
17
19
  expect(directories.list_metadata).to eq(expected_metadata)
18
20
  end
19
21
  end
@@ -44,7 +46,7 @@ describe WorkOS::DirectorySync do
44
46
  context 'with search option' do
45
47
  it 'forms the proper request to the API' do
46
48
  request_args = [
47
- '/directories?search=Foo',
49
+ '/directories?search=Testing',
48
50
  'Content-Type' => 'application/json'
49
51
  ]
50
52
 
@@ -55,10 +57,11 @@ describe WorkOS::DirectorySync do
55
57
 
56
58
  VCR.use_cassette 'directory_sync/list_directories/with_search' do
57
59
  directories = described_class.list_directories(
58
- search: 'Foo',
60
+ search: 'Testing',
59
61
  )
60
62
 
61
- expect(directories.data.size).to eq(1)
63
+ expect(directories.data.size).to eq(2)
64
+ expect(directories.data[0].name).to include('Testing')
62
65
  end
63
66
  end
64
67
  end
@@ -66,7 +69,7 @@ describe WorkOS::DirectorySync do
66
69
  context 'with the before option' do
67
70
  it 'forms the proper request to the API' do
68
71
  request_args = [
69
- '/directories?before=before-id',
72
+ '/directories?before=directory_01FGCPNV312FHFRCX0BYWHVSE1',
70
73
  'Content-Type' => 'application/json'
71
74
  ]
72
75
 
@@ -77,10 +80,10 @@ describe WorkOS::DirectorySync do
77
80
 
78
81
  VCR.use_cassette 'directory_sync/list_directories/with_before' do
79
82
  directories = described_class.list_directories(
80
- before: 'before-id',
83
+ before: 'directory_01FGCPNV312FHFRCX0BYWHVSE1',
81
84
  )
82
85
 
83
- expect(directories.data.size).to eq(3)
86
+ expect(directories.data.size).to eq(6)
84
87
  end
85
88
  end
86
89
  end
@@ -88,7 +91,7 @@ describe WorkOS::DirectorySync do
88
91
  context 'with the after option' do
89
92
  it 'forms the proper request to the API' do
90
93
  request_args = [
91
- '/directories?after=after-id',
94
+ '/directories?after=directory_01FGCPNV312FHFRCX0BYWHVSE1',
92
95
  'Content-Type' => 'application/json'
93
96
  ]
94
97
 
@@ -98,9 +101,9 @@ describe WorkOS::DirectorySync do
98
101
  and_return(expected_request)
99
102
 
100
103
  VCR.use_cassette 'directory_sync/list_directories/with_after' do
101
- directories = described_class.list_directories(after: 'after-id')
104
+ directories = described_class.list_directories(after: 'directory_01FGCPNV312FHFRCX0BYWHVSE1')
102
105
 
103
- expect(directories.data.size).to eq(3)
106
+ expect(directories.data.size).to eq(4)
104
107
  end
105
108
  end
106
109
  end
@@ -282,7 +285,7 @@ describe WorkOS::DirectorySync do
282
285
  context 'with directory option' do
283
286
  it 'forms the proper request to the API' do
284
287
  request_args = [
285
- '/directory_users?directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
288
+ '/directory_users?directory=directory_01FAZYMST676QMTFN1DDJZZX87',
286
289
  'Content-Type' => 'application/json'
287
290
  ]
288
291
 
@@ -293,10 +296,10 @@ describe WorkOS::DirectorySync do
293
296
 
294
297
  VCR.use_cassette 'directory_sync/list_users/with_directory' do
295
298
  users = described_class.list_users(
296
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
299
+ directory: 'directory_01FAZYMST676QMTFN1DDJZZX87',
297
300
  )
298
301
 
299
- expect(users.data.size).to eq(10)
302
+ expect(users.data.size).to eq(4)
300
303
  end
301
304
  end
302
305
  end
@@ -304,7 +307,7 @@ describe WorkOS::DirectorySync do
304
307
  context 'with group option' do
305
308
  it 'forms the proper request to the API' do
306
309
  request_args = [
307
- '/directory_users?group=directory_group_01EQ7V7C6Y4RPMCH3KNB9853FF',
310
+ '/directory_users?group=directory_group_01FBXGP79EJAYKW0WS9JCK1V6E',
308
311
  'Content-Type' => 'application/json'
309
312
  ]
310
313
 
@@ -315,10 +318,10 @@ describe WorkOS::DirectorySync do
315
318
 
316
319
  VCR.use_cassette 'directory_sync/list_users/with_group' do
317
320
  users = described_class.list_users(
318
- group: 'directory_group_01EQ7V7C6Y4RPMCH3KNB9853FF',
321
+ group: 'directory_group_01FBXGP79EJAYKW0WS9JCK1V6E',
319
322
  )
320
323
 
321
- expect(users.data.size).to eq(2)
324
+ expect(users.data.size).to eq(1)
322
325
  end
323
326
  end
324
327
  end
@@ -326,8 +329,8 @@ describe WorkOS::DirectorySync do
326
329
  context 'with the before option' do
327
330
  it 'forms the proper request to the API' do
328
331
  request_args = [
329
- '/directory_users?before=before-id&'\
330
- 'directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
332
+ '/directory_users?before=directory_user_01FAZYNPC8TJBP7Y2ERT51MGDF&'\
333
+ 'directory=directory_01FAZYMST676QMTFN1DDJZZX87',
331
334
  'Content-Type' => 'application/json'
332
335
  ]
333
336
 
@@ -338,8 +341,8 @@ describe WorkOS::DirectorySync do
338
341
 
339
342
  VCR.use_cassette 'directory_sync/list_users/with_before' do
340
343
  users = described_class.list_users(
341
- before: 'before-id',
342
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
344
+ before: 'directory_user_01FAZYNPC8TJBP7Y2ERT51MGDF',
345
+ directory: 'directory_01FAZYMST676QMTFN1DDJZZX87',
343
346
  )
344
347
 
345
348
  expect(users.data.size).to eq(2)
@@ -350,8 +353,8 @@ describe WorkOS::DirectorySync do
350
353
  context 'with the after option' do
351
354
  it 'forms the proper request to the API' do
352
355
  request_args = [
353
- '/directory_users?after=after-id&' \
354
- 'directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
356
+ '/directory_users?after=directory_user_01FAZYNPC8TJBP7Y2ERT51MGDF&' \
357
+ 'directory=directory_01FAZYMST676QMTFN1DDJZZX87',
355
358
  'Content-Type' => 'application/json'
356
359
  ]
357
360
 
@@ -362,11 +365,11 @@ describe WorkOS::DirectorySync do
362
365
 
363
366
  VCR.use_cassette 'directory_sync/list_users/with_after' do
364
367
  users = described_class.list_users(
365
- after: 'after-id',
366
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
368
+ after: 'directory_user_01FAZYNPC8TJBP7Y2ERT51MGDF',
369
+ directory: 'directory_01FAZYMST676QMTFN1DDJZZX87',
367
370
  )
368
371
 
369
- expect(users.data.size).to eq(10)
372
+ expect(users.data.size).to eq(1)
370
373
  end
371
374
  end
372
375
  end
@@ -375,7 +378,7 @@ describe WorkOS::DirectorySync do
375
378
  it 'forms the proper request to the API' do
376
379
  request_args = [
377
380
  '/directory_users?limit=2&' \
378
- 'directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
381
+ 'directory=directory_01FAZYMST676QMTFN1DDJZZX87',
379
382
  'Content-Type' => 'application/json'
380
383
  ]
381
384
 
@@ -387,7 +390,7 @@ describe WorkOS::DirectorySync do
387
390
  VCR.use_cassette 'directory_sync/list_users/with_limit' do
388
391
  users = described_class.list_users(
389
392
  limit: 2,
390
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
393
+ directory: 'directory_01FAZYMST676QMTFN1DDJZZX87',
391
394
  )
392
395
 
393
396
  expect(users.data.size).to eq(2)
@@ -425,10 +428,10 @@ describe WorkOS::DirectorySync do
425
428
  it 'returns a user' do
426
429
  VCR.use_cassette('directory_sync/get_user') do
427
430
  user = WorkOS::DirectorySync.get_user(
428
- 'directory_usr_01E64QS50EAY48S0XJ1AA4WX4D',
431
+ 'directory_user_01FAZYNPC8M0HRYTKFP2GNX852',
429
432
  )
430
433
 
431
- expect(user['first_name']).to eq('Mark')
434
+ expect(user['first_name']).to eq('Logan')
432
435
  end
433
436
  end
434
437
  end
@@ -2,18 +2,20 @@
2
2
  # typed: false
3
3
 
4
4
  describe WorkOS::Organizations do
5
+ it_behaves_like 'client'
6
+
5
7
  describe '.create_organization' do
6
8
  context 'with valid payload' do
7
9
  it 'creates an organization' do
8
10
  VCR.use_cassette 'organization/create' do
9
11
  organization = described_class.create_organization(
10
- domains: ['example.com'],
12
+ domains: ['example.io'],
11
13
  name: 'Test Organization',
12
14
  )
13
15
 
14
- expect(organization.id).to eq('org_01EHT88Z8J8795GZNQ4ZP1J81T')
16
+ expect(organization.id).to eq('org_01FCPEJXEZR4DSBA625YMGQT9N')
15
17
  expect(organization.name).to eq('Test Organization')
16
- expect(organization.domains.first[:domain]).to eq('example.com')
18
+ expect(organization.domains.first[:domain]).to eq('example.io')
17
19
  end
18
20
  end
19
21
  end
@@ -46,7 +48,7 @@ describe WorkOS::Organizations do
46
48
  VCR.use_cassette 'organization/list' do
47
49
  organizations = described_class.list_organizations
48
50
 
49
- expect(organizations.data.size).to eq(7)
51
+ expect(organizations.data.size).to eq(6)
50
52
  expect(organizations.list_metadata).to eq(expected_metadata)
51
53
  end
52
54
  end
@@ -69,7 +71,7 @@ describe WorkOS::Organizations do
69
71
  before: 'before-id',
70
72
  )
71
73
 
72
- expect(organizations.data.size).to eq(7)
74
+ expect(organizations.data.size).to eq(6)
73
75
  end
74
76
  end
75
77
  end
@@ -89,7 +91,7 @@ describe WorkOS::Organizations do
89
91
  VCR.use_cassette 'organization/list', match_requests_on: [:path] do
90
92
  organizations = described_class.list_organizations(after: 'after-id')
91
93
 
92
- expect(organizations.data.size).to eq(7)
94
+ expect(organizations.data.size).to eq(6)
93
95
  end
94
96
  end
95
97
  end
@@ -109,7 +111,7 @@ describe WorkOS::Organizations do
109
111
  VCR.use_cassette 'organization/list', match_requests_on: [:path] do
110
112
  organizations = described_class.list_organizations(limit: 10)
111
113
 
112
- expect(organizations.data.size).to eq(7)
114
+ expect(organizations.data.size).to eq(6)
113
115
  end
114
116
  end
115
117
  end
@@ -120,10 +122,10 @@ describe WorkOS::Organizations do
120
122
  it 'gets the organization details' do
121
123
  VCR.use_cassette('organization/get') do
122
124
  organization = described_class.get_organization(
123
- id: 'org_01EZDF20TZEJXKPSX2BJRN6TV6',
125
+ id: 'org_01F9293WD2PDEEV4Y625XPZVG7',
124
126
  )
125
127
 
126
- expect(organization.id).to eq('org_01EZDF20TZEJXKPSX2BJRN6TV6')
128
+ expect(organization.id).to eq('org_01F9293WD2PDEEV4Y625XPZVG7')
127
129
  expect(organization.name).to eq('Foo Corp')
128
130
  expect(organization.domains.first[:domain]).to eq('foo-corp.com')
129
131
  end
@@ -149,12 +151,12 @@ describe WorkOS::Organizations do
149
151
  it 'creates an organization' do
150
152
  VCR.use_cassette 'organization/update' do
151
153
  organization = described_class.update_organization(
152
- organization: 'org_01F29YJ068E52HGEB8ZQGC9MJG',
154
+ organization: 'org_01F6Q6TFP7RD2PF6J03ANNWDKV',
153
155
  domains: ['example.me'],
154
156
  name: 'Test Organization',
155
157
  )
156
158
 
157
- expect(organization.id).to eq('org_01F29YJ068E52HGEB8ZQGC9MJG')
159
+ expect(organization.id).to eq('org_01F6Q6TFP7RD2PF6J03ANNWDKV')
158
160
  expect(organization.name).to eq('Test Organization')
159
161
  expect(organization.domains.first[:domain]).to eq('example.me')
160
162
  end
@@ -2,6 +2,8 @@
2
2
  # typed: false
3
3
 
4
4
  describe WorkOS::Passwordless do
5
+ it_behaves_like 'client'
6
+
5
7
  describe '.create_session' do
6
8
  context 'with valid options payload' do
7
9
  let(:valid_options) do
@@ -2,6 +2,8 @@
2
2
  # typed: false
3
3
 
4
4
  describe WorkOS::Portal do
5
+ it_behaves_like 'client'
6
+
5
7
  describe '.generate_link' do
6
8
  let(:organization) { 'org_01EHQMYV6MBK39QC5PZXHY59C3' }
7
9
 
@@ -4,6 +4,8 @@
4
4
  require 'securerandom'
5
5
 
6
6
  describe WorkOS::SSO do
7
+ it_behaves_like 'client'
8
+
7
9
  describe '.authorization_url' do
8
10
  context 'with a domain' do
9
11
  let(:args) do
@@ -162,6 +164,7 @@ describe WorkOS::SSO do
162
164
  id: 'prof_01EEJTY9SZ1R350RB7B73SNBKF',
163
165
  idp_id: '116485463307139932699',
164
166
  last_name: 'Loblaw',
167
+ organization_id: 'org_01FG53X8636WSNW2WEKB2C31ZB',
165
168
  raw_attributes: {
166
169
  email: 'bob.loblaw@workos.com',
167
170
  family_name: 'Loblaw',
@@ -231,6 +234,7 @@ describe WorkOS::SSO do
231
234
  id: 'prof_01DRA1XNSJDZ19A31F183ECQW5',
232
235
  idp_id: '00u1klkowm8EGah2H357',
233
236
  last_name: 'Demo',
237
+ organization_id: 'org_01FG53X8636WSNW2WEKB2C31ZB',
234
238
  raw_attributes: {
235
239
  email: 'demo@workos-okta.com',
236
240
  first_name: 'WorkOS',
@@ -303,7 +307,7 @@ describe WorkOS::SSO do
303
307
  VCR.use_cassette 'sso/list_connections/with_no_options' do
304
308
  connections = described_class.list_connections
305
309
 
306
- expect(connections.data.size).to eq(3)
310
+ expect(connections.data.size).to eq(6)
307
311
  expect(connections.list_metadata).to eq(expected_metadata)
308
312
  end
309
313
  end
@@ -326,7 +330,7 @@ describe WorkOS::SSO do
326
330
  connection_type: 'OktaSAML',
327
331
  )
328
332
 
329
- expect(connections.data.size).to eq(3)
333
+ expect(connections.data.size).to eq(10)
330
334
  expect(connections.data.first.connection_type).to eq('OktaSAML')
331
335
  end
332
336
  end
@@ -357,7 +361,7 @@ describe WorkOS::SSO do
357
361
  context 'with organization_id option' do
358
362
  it 'forms the proper request to the API' do
359
363
  request_args = [
360
- '/connections?organization_id=org_01EGS4P7QR31EZ4YWD1Z1XA176',
364
+ '/connections?organization_id=org_01F9293WD2PDEEV4Y625XPZVG7',
361
365
  'Content-Type' => 'application/json'
362
366
  ]
363
367
 
@@ -368,12 +372,12 @@ describe WorkOS::SSO do
368
372
 
369
373
  VCR.use_cassette 'sso/list_connections/with_organization_id' do
370
374
  connections = described_class.list_connections(
371
- organization_id: 'org_01EGS4P7QR31EZ4YWD1Z1XA176',
375
+ organization_id: 'org_01F9293WD2PDEEV4Y625XPZVG7',
372
376
  )
373
377
 
374
378
  expect(connections.data.size).to eq(1)
375
379
  expect(connections.data.first.organization_id).to eq(
376
- 'org_01EGS4P7QR31EZ4YWD1Z1XA176',
380
+ 'org_01F9293WD2PDEEV4Y625XPZVG7',
377
381
  )
378
382
  end
379
383
  end
@@ -404,7 +408,7 @@ describe WorkOS::SSO do
404
408
  context 'with before option' do
405
409
  it 'forms the proper request to the API' do
406
410
  request_args = [
407
- '/connections?before=conn_01EQKPMQAPV02H270HKVNS4CTA',
411
+ '/connections?before=conn_01FA3WGCWPCCY1V2FGES2FDNP7',
408
412
  'Content-Type' => 'application/json'
409
413
  ]
410
414
 
@@ -415,7 +419,7 @@ describe WorkOS::SSO do
415
419
 
416
420
  VCR.use_cassette 'sso/list_connections/with_before' do
417
421
  connections = described_class.list_connections(
418
- before: 'conn_01EQKPMQAPV02H270HKVNS4CTA',
422
+ before: 'conn_01FA3WGCWPCCY1V2FGES2FDNP7',
419
423
  )
420
424
 
421
425
  expect(connections.data.size).to eq(3)
@@ -426,7 +430,7 @@ describe WorkOS::SSO do
426
430
  context 'with after option' do
427
431
  it 'forms the proper request to the API' do
428
432
  request_args = [
429
- '/connections?after=conn_01EQKPMQAPV02H270HKVNS4CTA',
433
+ '/connections?after=conn_01FA3WGCWPCCY1V2FGES2FDNP7',
430
434
  'Content-Type' => 'application/json'
431
435
  ]
432
436
 
@@ -437,10 +441,10 @@ describe WorkOS::SSO do
437
441
 
438
442
  VCR.use_cassette 'sso/list_connections/with_after' do
439
443
  connections = described_class.list_connections(
440
- after: 'conn_01EQKPMQAPV02H270HKVNS4CTA',
444
+ after: 'conn_01FA3WGCWPCCY1V2FGES2FDNP7',
441
445
  )
442
446
 
443
- expect(connections.data.size).to eq(3)
447
+ expect(connections.data.size).to eq(2)
444
448
  end
445
449
  end
446
450
  end
@@ -451,10 +455,10 @@ describe WorkOS::SSO do
451
455
  it 'gets the connection details' do
452
456
  VCR.use_cassette('sso/get_connection_with_valid_id') do
453
457
  connection = WorkOS::SSO.get_connection(
454
- id: 'conn_01EX00NB050H354WKGC7990AR2',
458
+ id: 'conn_01FA3WGCWPCCY1V2FGES2FDNP7',
455
459
  )
456
460
 
457
- expect(connection.id).to eq('conn_01EX00NB050H354WKGC7990AR2')
461
+ expect(connection.id).to eq('conn_01FA3WGCWPCCY1V2FGES2FDNP7')
458
462
  expect(connection.connection_type).to eq('OktaSAML')
459
463
  expect(connection.name).to eq('Foo Corp')
460
464
  expect(connection.domains.first[:domain]).to eq('foo-corp.com')
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ require 'json'
5
+ require 'openssl'
6
+
7
+ describe WorkOS::Webhooks do
8
+ describe '.construct_event' do
9
+ before(:each) do
10
+ @payload = File.read("#{SPEC_ROOT}/support/webhook_payload.txt")
11
+ @secret = 'secret'
12
+ @timestamp = Time.at(Time.now.to_i * 1000)
13
+ unhashed_string = "#{@timestamp.to_i}.#{@payload}"
14
+ digest = OpenSSL::Digest.new('sha256')
15
+ @signature_hash = OpenSSL::HMAC.hexdigest(digest, @secret, unhashed_string)
16
+ @expectation = {
17
+ id: 'directory_user_01FAEAJCR3ZBZ30D8BD1924TVG',
18
+ state: 'active',
19
+ emails: [{
20
+ type: 'work',
21
+ value: 'blair@foo-corp.com',
22
+ primary: true,
23
+ }],
24
+ idp_id: '00u1e8mutl6wlH3lL4x7',
25
+ object: 'directory_user',
26
+ username: 'blair@foo-corp.com',
27
+ last_name: 'Lunceford',
28
+ first_name: 'Blair',
29
+ directory_id: 'directory_01F9M7F68PZP8QXP8G7X5QRHS7',
30
+ raw_attributes: {
31
+ name: {
32
+ givenName: 'Blair',
33
+ familyName: 'Lunceford',
34
+ middleName: 'Elizabeth',
35
+ honorificPrefix: 'Ms.',
36
+ },
37
+ title: 'Developer Success Engineer',
38
+ active: true,
39
+ emails: [{
40
+ type: 'work',
41
+ value: 'blair@foo-corp.com',
42
+ primary: true,
43
+ }],
44
+ groups: [],
45
+ locale: 'en-US',
46
+ schemas: [
47
+ 'urn:ietf:params:scim:schemas:core:2.0:User',
48
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
49
+ ],
50
+ userName: 'blair@foo-corp.com',
51
+ addresses: [{
52
+ region: 'CO',
53
+ primary: true,
54
+ locality: 'Steamboat Springs',
55
+ postalCode: '80487',
56
+ }],
57
+ externalId: '00u1e8mutl6wlH3lL4x7',
58
+ displayName: 'Blair Lunceford',
59
+ "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
60
+ manager: {
61
+ value: '2',
62
+ displayName: 'Kathleen Chung',
63
+ },
64
+ division: 'Engineering',
65
+ department: 'Customer Success',
66
+ },
67
+ },
68
+ }
69
+ end
70
+
71
+ context 'with the correct payload, sig_header, and secret' do
72
+ it 'returns a webhook event' do
73
+ webhook = described_class.construct_event(
74
+ payload: @payload,
75
+ sig_header: "t=#{@timestamp.to_i}, v1=#{@signature_hash}",
76
+ secret: @secret,
77
+ )
78
+
79
+ expect(webhook.data).to eq(@expectation)
80
+ expect(webhook.event).to eq('dsync.user.created')
81
+ expect(webhook.id).to eq('wh_123')
82
+ end
83
+ end
84
+
85
+ context 'with the correct payload, sig_header, secret, and tolerance' do
86
+ it 'returns a webhook event' do
87
+ webhook = described_class.construct_event(
88
+ payload: @payload,
89
+ sig_header: "t=#{@timestamp.to_i}, v1=#{@signature_hash}",
90
+ secret: @secret,
91
+ tolerance: 300,
92
+ )
93
+
94
+ expect(webhook.data).to eq(@expectation)
95
+ expect(webhook.event).to eq('dsync.user.created')
96
+ expect(webhook.id).to eq('wh_123')
97
+ end
98
+ end
99
+
100
+ context 'with an empty header' do
101
+ it 'raises an error' do
102
+ expect do
103
+ described_class.construct_event(
104
+ payload: @payload,
105
+ sig_header: '',
106
+ secret: @secret,
107
+ )
108
+ end.to raise_error(
109
+ WorkOS::SignatureVerificationError,
110
+ 'Unable to extract timestamp and signature hash from header',
111
+ )
112
+ end
113
+ end
114
+
115
+ context 'with an empty signature hash' do
116
+ it 'raises an error' do
117
+ expect do
118
+ described_class.construct_event(
119
+ payload: @payload,
120
+ sig_header: "t=#{@timestamp.to_i}, v1=",
121
+ secret: @secret,
122
+ )
123
+ end.to raise_error(
124
+ WorkOS::SignatureVerificationError,
125
+ 'No signature hash found with expected scheme v1',
126
+ )
127
+ end
128
+ end
129
+
130
+ context 'with an incorrect signature hash' do
131
+ it 'raises an error' do
132
+ expect do
133
+ described_class.construct_event(
134
+ payload: @payload,
135
+ sig_header: "t=#{@timestamp.to_i}, v1=99999",
136
+ secret: @secret,
137
+ )
138
+ end.to raise_error(
139
+ WorkOS::SignatureVerificationError,
140
+ 'Signature hash does not match the expected signature hash for payload',
141
+ )
142
+ end
143
+ end
144
+
145
+ context 'with an incorrect payload' do
146
+ it 'raises an error' do
147
+ expect do
148
+ described_class.construct_event(
149
+ payload: 'invalid',
150
+ sig_header: "t=#{@timestamp.to_i}, v1=#{@signature_hash}",
151
+ secret: @secret,
152
+ )
153
+ end.to raise_error(
154
+ WorkOS::SignatureVerificationError,
155
+ 'Signature hash does not match the expected signature hash for payload',
156
+ )
157
+ end
158
+ end
159
+
160
+ context 'with an incorrect webhook secret' do
161
+ it 'raises an error' do
162
+ expect do
163
+ described_class.construct_event(
164
+ payload: @payload,
165
+ sig_header: "t=#{@timestamp.to_i}, v1=#{@signature_hash}",
166
+ secret: 'invalid',
167
+ )
168
+ end.to raise_error(
169
+ WorkOS::SignatureVerificationError,
170
+ 'Signature hash does not match the expected signature hash for payload',
171
+ )
172
+ end
173
+ end
174
+
175
+ context 'with a timestamp outside tolerance' do
176
+ it 'raises an error' do
177
+ expect do
178
+ described_class.construct_event(
179
+ payload: @payload,
180
+ sig_header: "t=9999, v1=#{@signature_hash}",
181
+ secret: @secret,
182
+ )
183
+ end.to raise_error(
184
+ WorkOS::SignatureVerificationError,
185
+ 'Timestamp outside the tolerance zone',
186
+ )
187
+ end
188
+ end
189
+ end
190
+ end
data/spec/spec_helper.rb CHANGED
@@ -18,6 +18,9 @@ require 'webmock/rspec'
18
18
  require 'workos'
19
19
  require 'vcr'
20
20
 
21
+ # Support
22
+ Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
23
+
21
24
  SPEC_ROOT = File.dirname __FILE__
22
25
 
23
26
  VCR.configure do |config|