workos 2.1.1 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.semaphore/semaphore.yml +13 -39
  3. data/Gemfile.lock +2 -2
  4. data/README.md +4 -0
  5. data/lib/workos/audit_trail.rb +1 -0
  6. data/lib/workos/challenge.rb +55 -0
  7. data/lib/workos/client.rb +2 -0
  8. data/lib/workos/connection.rb +1 -0
  9. data/lib/workos/deprecated_hash_wrapper.rb +76 -0
  10. data/lib/workos/directory.rb +1 -0
  11. data/lib/workos/directory_group.rb +22 -2
  12. data/lib/workos/directory_sync.rb +12 -8
  13. data/lib/workos/directory_user.rb +9 -1
  14. data/lib/workos/errors.rb +3 -1
  15. data/lib/workos/factor.rb +54 -0
  16. data/lib/workos/hash_provider.rb +19 -0
  17. data/lib/workos/mfa.rb +165 -0
  18. data/lib/workos/organization.rb +1 -0
  19. data/lib/workos/organizations.rb +1 -0
  20. data/lib/workos/profile.rb +1 -0
  21. data/lib/workos/profile_and_token.rb +1 -0
  22. data/lib/workos/sso.rb +1 -0
  23. data/lib/workos/types/challenge_struct.rb +18 -0
  24. data/lib/workos/types/directory_group_struct.rb +5 -0
  25. data/lib/workos/types/factor_struct.rb +18 -0
  26. data/lib/workos/types/passwordless_session_struct.rb +2 -0
  27. data/lib/workos/types/verify_factor_struct.rb +13 -0
  28. data/lib/workos/types.rb +3 -0
  29. data/lib/workos/verify_factor.rb +40 -0
  30. data/lib/workos/version.rb +1 -1
  31. data/lib/workos/webhook.rb +1 -0
  32. data/lib/workos/webhooks.rb +1 -1
  33. data/lib/workos.rb +7 -0
  34. data/spec/lib/workos/directory_sync_spec.rb +26 -18
  35. data/spec/lib/workos/directory_user_spec.rb +36 -0
  36. data/spec/lib/workos/mfa_spec.rb +262 -0
  37. data/spec/lib/workos/sso_spec.rb +0 -2
  38. data/spec/support/fixtures/vcr_cassettes/directory_sync/get_group.yml +48 -28
  39. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_groups/with_after.yml +46 -32
  40. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_groups/with_before.yml +47 -31
  41. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_groups/with_directory.yml +46 -34
  42. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_groups/with_limit.yml +41 -31
  43. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_groups/with_no_options.yml +36 -26
  44. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_groups/with_user.yml +38 -28
  45. data/spec/support/fixtures/vcr_cassettes/mfa/challenge_factor_generic_valid.yml +82 -0
  46. data/spec/support/fixtures/vcr_cassettes/mfa/challenge_factor_sms_valid.yml +82 -0
  47. data/spec/support/fixtures/vcr_cassettes/mfa/challenge_factor_totp_valid.yml +82 -0
  48. data/spec/support/fixtures/vcr_cassettes/mfa/delete_factor.yml +80 -0
  49. data/spec/support/fixtures/vcr_cassettes/mfa/enroll_factor_generic_valid.yml +82 -0
  50. data/spec/support/fixtures/vcr_cassettes/mfa/enroll_factor_sms_valid.yml +82 -0
  51. data/spec/support/fixtures/vcr_cassettes/mfa/enroll_factor_totp_valid.yml +82 -0
  52. data/spec/support/fixtures/vcr_cassettes/mfa/get_factor_invalid.yml +82 -0
  53. data/spec/support/fixtures/vcr_cassettes/mfa/get_factor_valid.yml +82 -0
  54. data/spec/support/fixtures/vcr_cassettes/mfa/verify_factor_generic_expired.yml +84 -0
  55. data/spec/support/fixtures/vcr_cassettes/mfa/verify_factor_generic_invalid.yml +84 -0
  56. data/spec/support/fixtures/vcr_cassettes/mfa/verify_factor_generic_valid.yml +82 -0
  57. data/spec/support/fixtures/vcr_cassettes/mfa/verify_factor_generic_valid_is_false.yml +82 -0
  58. metadata +42 -3
data/lib/workos/mfa.rb ADDED
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+
5
+ require 'net/http'
6
+ require 'uri'
7
+
8
+ module WorkOS
9
+ # The MFA module provides convenience methods for working with the WorkOS
10
+ # MFA platform. You'll need a valid API key
11
+ module MFA
12
+ class << self
13
+ extend T::Sig
14
+ include Base
15
+ include Client
16
+ sig { params(id: String).returns(T::Boolean) }
17
+ def delete_factor(id:)
18
+ response = execute_request(
19
+ request: delete_request(
20
+ path: "/auth/factors/#{id}",
21
+ auth: true,
22
+ ),
23
+ )
24
+ response.is_a? Net::HTTPSuccess
25
+ end
26
+
27
+ sig do
28
+ params(id: String).returns(WorkOS::Factor)
29
+ end
30
+ def get_factor(
31
+ id:
32
+ )
33
+ response = execute_request(
34
+ request: get_request(
35
+ path: "/auth/factors/#{id}",
36
+ auth: true,
37
+ ),
38
+ )
39
+ WorkOS::Factor.new(response.body)
40
+ end
41
+
42
+ sig do
43
+ params(
44
+ type: String,
45
+ totp_issuer: T.nilable(String),
46
+ totp_user: T.nilable(String),
47
+ phone_number: T.nilable(String),
48
+ ).void
49
+ end
50
+ # rubocop:disable Metrics/CyclomaticComplexity
51
+ # rubocop:disable Metrics/PerceivedComplexity
52
+
53
+ def validate_args(
54
+ type:,
55
+ totp_issuer: nil,
56
+ totp_user: nil,
57
+ phone_number: nil
58
+ )
59
+ if type != 'sms' && type != 'totp' && type != 'generic_otp'
60
+ raise ArgumentError, "Type argument must be either 'sms' or 'totp'"
61
+ end
62
+ if (type == 'totp' && totp_issuer.nil?) || (type == 'totp' && totp_user.nil?)
63
+ raise ArgumentError, 'Incomplete arguments. Need to specify both totp_issuer and totp_user when type is totp'
64
+ end
65
+ return unless type == 'sms' && phone_number.nil?
66
+
67
+ raise ArgumentError, 'Incomplete arguments. Need to specify phone_number when type is sms'
68
+ end
69
+ # rubocop:enable Metrics/CyclomaticComplexity
70
+ # rubocop:enable Metrics/PerceivedComplexity
71
+
72
+ sig do
73
+ params(
74
+ type: String,
75
+ totp_issuer: T.nilable(String),
76
+ totp_user: T.nilable(String),
77
+ phone_number: T.nilable(String),
78
+ ).returns(WorkOS::Factor)
79
+ end
80
+ # rubocop:disable Metrics/MethodLength
81
+ def enroll_factor(
82
+ type:,
83
+ totp_issuer: nil,
84
+ totp_user: nil,
85
+ phone_number: nil
86
+ )
87
+ validate_args(
88
+ type: type,
89
+ totp_issuer: totp_issuer,
90
+ totp_user: totp_user,
91
+ phone_number: phone_number,
92
+ )
93
+ response = execute_request(request: post_request(
94
+ auth: true,
95
+ body: {
96
+ type: type,
97
+ totp_issuer: totp_issuer,
98
+ totp_user: totp_user,
99
+ phone_number: phone_number,
100
+ },
101
+ path: '/auth/factors/enroll',
102
+ ))
103
+ WorkOS::Factor.new(response.body)
104
+ end
105
+ # rubocop:enable Metrics/MethodLength
106
+
107
+ sig do
108
+ params(
109
+ authentication_factor_id: T.nilable(String),
110
+ sms_template: T.nilable(String),
111
+ ).returns(WorkOS::Challenge)
112
+ end
113
+ def challenge_factor(
114
+ authentication_factor_id: nil,
115
+ sms_template: nil
116
+ )
117
+ if authentication_factor_id.nil?
118
+ raise ArgumentError, "Incomplete arguments: 'authentication_factor_id' is a required argument"
119
+ end
120
+
121
+ request = post_request(
122
+ auth: true,
123
+ body: {
124
+ sms_template: sms_template,
125
+ authentication_factor_id: authentication_factor_id,
126
+ },
127
+ path: '/auth/factors/challenge',
128
+ )
129
+
130
+ response = execute_request(request: request)
131
+ WorkOS::Challenge.new(response.body)
132
+ end
133
+
134
+ sig do
135
+ params(
136
+ authentication_challenge_id: T.nilable(String),
137
+ code: T.nilable(String),
138
+ ).returns(WorkOS::VerifyFactor)
139
+ end
140
+ def verify_factor(
141
+ authentication_challenge_id: nil,
142
+ code: nil
143
+ )
144
+
145
+ if authentication_challenge_id.nil? || code.nil?
146
+ raise ArgumentError, "Incomplete arguments: 'authentication_challenge_id' and 'code' are required arguments"
147
+ end
148
+
149
+ options = {
150
+ "authentication_challenge_id": authentication_challenge_id,
151
+ "code": code,
152
+ }
153
+
154
+ response = execute_request(
155
+ request: post_request(
156
+ path: '/auth/factors/verify',
157
+ auth: true,
158
+ body: options,
159
+ ),
160
+ )
161
+ WorkOS::VerifyFactor.new(response.body)
162
+ end
163
+ end
164
+ end
165
+ end
@@ -6,6 +6,7 @@ module WorkOS
6
6
  # a WorkOS Organization resource. This class is not meant to be instantiated
7
7
  # in user space, and is instantiated internally but exposed.
8
8
  class Organization
9
+ include HashProvider
9
10
  extend T::Sig
10
11
 
11
12
  attr_accessor :id, :domains, :name, :allow_profiles_outside_organization, :created_at, :updated_at
@@ -21,6 +21,7 @@ module WorkOS
21
21
  # @param [String] after A pagination argument used to request
22
22
  # organizations after the provided Organization ID.
23
23
  # @param [Integer] limit A pagination argument used to limit the number
24
+ # @param [String] order The order in which to paginate records
24
25
  # of listed Organizations that are returned.
25
26
  sig do
26
27
  params(
@@ -8,6 +8,7 @@ module WorkOS
8
8
  # is not meant to be instantiated in user space, and
9
9
  # is instantiated internally but exposed.
10
10
  class Profile
11
+ include HashProvider
11
12
  extend T::Sig
12
13
 
13
14
  sig { returns(String) }
@@ -6,6 +6,7 @@ module WorkOS
6
6
  # Access Token. This class is not meant to be instantiated in user space, and
7
7
  # is instantiated internally but exposed.
8
8
  class ProfileAndToken
9
+ include HashProvider
9
10
  extend T::Sig
10
11
 
11
12
  attr_accessor :access_token, :profile
data/lib/workos/sso.rb CHANGED
@@ -163,6 +163,7 @@ module WorkOS
163
163
  # @option options [String] organization_id The id of the organization
164
164
  # of the connections to be retrieved.
165
165
  # @option options [String] limit Maximum number of records to return.
166
+ # @option options [String] order The order in which to paginate records
166
167
  # @option options [String] before Pagination cursor to receive records
167
168
  # before a provided Connection ID.
168
169
  # @option options [String] after Pagination cursor to receive records
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module WorkOS
5
+ module Types
6
+ # This ChallgengeStruct acts as a typed interface
7
+ # for the Factor class
8
+ class ChallengeStruct < T::Struct
9
+ const :id, String
10
+ const :object, String
11
+ const :expires_at, String
12
+ const :code, T.nilable(String)
13
+ const :authentication_factor_id, String
14
+ const :created_at, String
15
+ const :updated_at, String
16
+ end
17
+ end
18
+ end
@@ -7,7 +7,12 @@ module WorkOS
7
7
  # for the DirectoryGroup class
8
8
  class DirectoryGroupStruct < T::Struct
9
9
  const :id, String
10
+ const :directory_id, String
11
+ const :idp_id, String
10
12
  const :name, String
13
+ const :created_at, String
14
+ const :updated_at, String
15
+ const :raw_attributes, T::Hash[Symbol, Object]
11
16
  end
12
17
  end
13
18
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module WorkOS
5
+ module Types
6
+ # This FactorStruct acts as a typed interface
7
+ # for the Factor class
8
+ class FactorStruct < T::Struct
9
+ const :id, String
10
+ const :object, String
11
+ const :type, String
12
+ const :totp, T.nilable(T::Hash[Symbol, Object])
13
+ const :sms, T.nilable(T::Hash[Symbol, Object])
14
+ const :created_at, String
15
+ const :updated_at, String
16
+ end
17
+ end
18
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # typed: strict
3
3
 
4
+ require 'date'
5
+
4
6
  module WorkOS
5
7
  module Types
6
8
  # This PasswordlessSessionStruct acts as a typed interface
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module WorkOS
5
+ module Types
6
+ # This VerifyFactorStruct acts as a typed interface
7
+ # for the Factor class
8
+ class VerifyFactorStruct < T::Struct
9
+ const :challenge, T.nilable(T::Hash[Symbol, Object])
10
+ const :valid, T::Boolean
11
+ end
12
+ end
13
+ end
data/lib/workos/types.rb CHANGED
@@ -16,5 +16,8 @@ module WorkOS
16
16
  require_relative 'types/provider_enum'
17
17
  require_relative 'types/directory_user_struct'
18
18
  require_relative 'types/webhook_struct'
19
+ require_relative 'types/factor_struct'
20
+ require_relative 'types/challenge_struct'
21
+ require_relative 'types/verify_factor_struct'
19
22
  end
20
23
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ module WorkOS
5
+ # The VerifyFactor class provides a lightweight wrapper around
6
+ # a WorkOS DirectoryUser resource. This class is not meant to be instantiated
7
+ # in DirectoryUser space, and is instantiated internally but exposed.
8
+ class VerifyFactor
9
+ include HashProvider
10
+ extend T::Sig
11
+
12
+ attr_accessor :challenge, :valid
13
+
14
+ sig { params(json: String).void }
15
+ def initialize(json)
16
+ raw = parse_json(json)
17
+ @challenge = T.let(raw.challenge, Hash)
18
+ @valid = raw.valid
19
+ end
20
+
21
+ def to_json(*)
22
+ {
23
+ challenge: challenge,
24
+ valid: valid,
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ sig { params(json_string: String).returns(WorkOS::Types::VerifyFactorStruct) }
31
+ def parse_json(json_string)
32
+ hash = JSON.parse(json_string, symbolize_names: true)
33
+
34
+ WorkOS::Types::VerifyFactorStruct.new(
35
+ challenge: hash[:challenge],
36
+ valid: hash[:valid],
37
+ )
38
+ end
39
+ end
40
+ end
@@ -2,5 +2,5 @@
2
2
  # typed: strong
3
3
 
4
4
  module WorkOS
5
- VERSION = '2.1.1'
5
+ VERSION = '2.3.0'
6
6
  end
@@ -6,6 +6,7 @@ module WorkOS
6
6
  # a WorkOS Webhook resource. This class is not meant to be instantiated
7
7
  # in user space, and is instantiated internally but exposed.
8
8
  class Webhook
9
+ include HashProvider
9
10
  extend T::Sig
10
11
 
11
12
  attr_accessor :id, :event, :data
@@ -108,7 +108,7 @@ module WorkOS
108
108
  sig do
109
109
  params(
110
110
  sig_header: String,
111
- ).returns(T::Array[T.untyped])
111
+ ).returns([String, String])
112
112
  end
113
113
  def get_timestamp_and_signature_hash(
114
114
  sig_header:
data/lib/workos.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  require 'workos/version'
5
5
  require 'sorbet-runtime'
6
6
  require 'json'
7
+ require 'workos/hash_provider'
7
8
 
8
9
  # Use the WorkOS module to authenticate your
9
10
  # requests to the WorkOS API. The gem will read
@@ -44,6 +45,12 @@ module WorkOS
44
45
  autoload :DirectoryUser, 'workos/directory_user'
45
46
  autoload :Webhook, 'workos/webhook'
46
47
  autoload :Webhooks, 'workos/webhooks'
48
+ autoload :MFA, 'workos/mfa'
49
+ autoload :Factor, 'workos/factor'
50
+ autoload :Challenge, 'workos/challenge'
51
+ autoload :VerifyFactor, 'workos/verify_factor'
52
+ autoload :DeprecatedHashWrapper, 'workos/deprecated_hash_wrapper'
53
+
47
54
 
48
55
  # Errors
49
56
  autoload :APIError, 'workos/errors'
@@ -189,7 +189,7 @@ describe WorkOS::DirectorySync do
189
189
  context 'with directory option' do
190
190
  it 'forms the proper request to the API' do
191
191
  request_args = [
192
- '/directory_groups?directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
192
+ '/directory_groups?directory=directory_01G2Z8ADK5NPMVTWF48MVVE4HT',
193
193
  'Content-Type' => 'application/json'
194
194
  ]
195
195
 
@@ -200,10 +200,11 @@ describe WorkOS::DirectorySync do
200
200
 
201
201
  VCR.use_cassette 'directory_sync/list_groups/with_directory' do
202
202
  groups = described_class.list_groups(
203
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
203
+ directory: 'directory_01G2Z8ADK5NPMVTWF48MVVE4HT',
204
204
  )
205
205
 
206
206
  expect(groups.data.size).to eq(10)
207
+ expect(groups.data[0].name).to eq(groups.data[0]['name'])
207
208
  end
208
209
  end
209
210
  end
@@ -211,7 +212,7 @@ describe WorkOS::DirectorySync do
211
212
  context 'with user option' do
212
213
  it 'forms the proper request to the API' do
213
214
  request_args = [
214
- '/directory_groups?user=directory_user_01EK2YFBC3R10MPB4W49G5QDXG',
215
+ '/directory_groups?user=directory_user_01G2Z8D4FDB28ZNSRRBVCF2E0P',
215
216
  'Content-Type' => 'application/json'
216
217
  ]
217
218
 
@@ -222,7 +223,7 @@ describe WorkOS::DirectorySync do
222
223
 
223
224
  VCR.use_cassette 'directory_sync/list_groups/with_user' do
224
225
  groups = described_class.list_groups(
225
- user: 'directory_user_01EK2YFBC3R10MPB4W49G5QDXG',
226
+ user: 'directory_user_01G2Z8D4FDB28ZNSRRBVCF2E0P',
226
227
  )
227
228
 
228
229
  expect(groups.data.size).to eq(3)
@@ -233,8 +234,8 @@ describe WorkOS::DirectorySync do
233
234
  context 'with the before option' do
234
235
  it 'forms the proper request to the API' do
235
236
  request_args = [
236
- '/directory_groups?before=before-id&' \
237
- 'directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
237
+ '/directory_groups?before=directory_group_01G2Z8D4ZR8RJ03Y1W7P9K8NMG&' \
238
+ 'directory=directory_01G2Z8ADK5NPMVTWF48MVVE4HT',
238
239
  'Content-Type' => 'application/json'
239
240
  ]
240
241
 
@@ -245,11 +246,11 @@ describe WorkOS::DirectorySync do
245
246
 
246
247
  VCR.use_cassette 'directory_sync/list_groups/with_before' do
247
248
  groups = described_class.list_groups(
248
- before: 'before-id',
249
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
249
+ before: 'directory_group_01G2Z8D4ZR8RJ03Y1W7P9K8NMG',
250
+ directory: 'directory_01G2Z8ADK5NPMVTWF48MVVE4HT',
250
251
  )
251
252
 
252
- expect(groups.data.size).to eq(2)
253
+ expect(groups.data.size).to eq(10)
253
254
  end
254
255
  end
255
256
  end
@@ -257,8 +258,8 @@ describe WorkOS::DirectorySync do
257
258
  context 'with the after option' do
258
259
  it 'forms the proper request to the API' do
259
260
  request_args = [
260
- '/directory_groups?after=after-id&' \
261
- 'directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
261
+ '/directory_groups?after=directory_group_01G2Z8D4ZR8RJ03Y1W7P9K8NMG&' \
262
+ 'directory=directory_01G2Z8ADK5NPMVTWF48MVVE4HT',
262
263
  'Content-Type' => 'application/json'
263
264
  ]
264
265
 
@@ -269,11 +270,11 @@ describe WorkOS::DirectorySync do
269
270
 
270
271
  VCR.use_cassette 'directory_sync/list_groups/with_after' do
271
272
  groups = described_class.list_groups(
272
- after: 'after-id',
273
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
273
+ after: 'directory_group_01G2Z8D4ZR8RJ03Y1W7P9K8NMG',
274
+ directory: 'directory_01G2Z8ADK5NPMVTWF48MVVE4HT',
274
275
  )
275
276
 
276
- expect(groups.data.size).to eq(10)
277
+ expect(groups.data.size).to eq(9)
277
278
  end
278
279
  end
279
280
  end
@@ -282,7 +283,7 @@ describe WorkOS::DirectorySync do
282
283
  it 'forms the proper request to the API' do
283
284
  request_args = [
284
285
  '/directory_groups?limit=2&' \
285
- 'directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
286
+ 'directory=directory_01G2Z8ADK5NPMVTWF48MVVE4HT',
286
287
  'Content-Type' => 'application/json'
287
288
  ]
288
289
 
@@ -294,7 +295,7 @@ describe WorkOS::DirectorySync do
294
295
  VCR.use_cassette 'directory_sync/list_groups/with_limit' do
295
296
  groups = described_class.list_groups(
296
297
  limit: 2,
297
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
298
+ directory: 'directory_01G2Z8ADK5NPMVTWF48MVVE4HT',
298
299
  )
299
300
 
300
301
  expect(groups.data.size).to eq(2)
@@ -332,6 +333,7 @@ describe WorkOS::DirectorySync do
332
333
  )
333
334
 
334
335
  expect(users.data.size).to eq(4)
336
+ expect(users.data[0].first_name).to eq(users.data[0]['first_name'])
335
337
  end
336
338
  end
337
339
  end
@@ -436,10 +438,15 @@ describe WorkOS::DirectorySync do
436
438
  it 'returns a group' do
437
439
  VCR.use_cassette('directory_sync/get_group') do
438
440
  group = WorkOS::DirectorySync.get_group(
439
- 'directory_grp_01E64QTDNS0EGJ0FMCVY9BWGZT',
441
+ 'directory_group_01G2Z8D4ZR8RJ03Y1W7P9K8NMG',
440
442
  )
441
443
 
442
- expect(group['name']).to eq('Walrus')
444
+ expect(group['directory_id']).to eq('directory_01G2Z8ADK5NPMVTWF48MVVE4HT')
445
+ expect(group['idp_id']).to eq('01jlao4614two3d')
446
+ expect(group['name']).to eq('Sales')
447
+ expect(group.name).to eq('Sales')
448
+ expect(group['created_at']).to eq('2022-05-13T17:45:31.732Z')
449
+ expect(group['updated_at']).to eq('2022-06-07T17:45:35.739Z')
443
450
  end
444
451
  end
445
452
  end
@@ -464,6 +471,7 @@ describe WorkOS::DirectorySync do
464
471
  )
465
472
 
466
473
  expect(user['first_name']).to eq('Logan')
474
+ expect(user.first_name).to eq('Logan')
467
475
  end
468
476
  end
469
477
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ describe WorkOS::DirectoryUser do
5
+ # rubocop:disable Layout/LineLength
6
+ describe '.get_primary_email' do
7
+ context 'with one primary email' do
8
+ it 'returns the primary email' do
9
+ user = WorkOS::DirectoryUser.new('{"object":"directory_user","id":"directory_user_01FAZYNPC8M0HRYTKFP2GNX852","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","idp_id":"6092c280a3f1e19ef6d8cef8","username":"logan@workos.com","emails":[{"primary":true,"value":"logan@workos.com"}, {"primary":false,"value":"logan@gmail.com"}],"first_name":"Logan","last_name":"Gingerich","state":"active","raw_attributes":{},"custom_attributes":{},"groups":[]}')
10
+ expect(user.primary_email).to eq('logan@workos.com')
11
+ end
12
+ end
13
+
14
+ context 'with multiple primary emails' do
15
+ it 'returns the first email marked as primary' do
16
+ user = WorkOS::DirectoryUser.new('{"object":"directory_user","id":"directory_user_01FAZYNPC8M0HRYTKFP2GNX852","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","idp_id":"6092c280a3f1e19ef6d8cef8","username":"logan@workos.com","emails":[{"primary":true,"value":"logan@workos.com"}, {"primary":true,"value":"logan@gmail.com"}],"first_name":"Logan","last_name":"Gingerich","state":"active","raw_attributes":{},"custom_attributes":{},"groups":[]}')
17
+ expect(user.primary_email).to eq('logan@workos.com')
18
+ end
19
+ end
20
+
21
+ context 'with no primary emails' do
22
+ it 'returns nil' do
23
+ user = WorkOS::DirectoryUser.new('{"object":"directory_user","id":"directory_user_01FAZYNPC8M0HRYTKFP2GNX852","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","idp_id":"6092c280a3f1e19ef6d8cef8","username":"logan@workos.com","emails":[{"primary":false,"value":"logan@gmail.com"}],"first_name":"Logan","last_name":"Gingerich","state":"active","raw_attributes":{},"custom_attributes":{},"groups":[]}')
24
+ expect(user.primary_email).to eq(nil)
25
+ end
26
+ end
27
+
28
+ context 'with an empty email array' do
29
+ it 'returns nil' do
30
+ user = WorkOS::DirectoryUser.new('{"object":"directory_user","id":"directory_user_01FAZYNPC8M0HRYTKFP2GNX852","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","idp_id":"6092c280a3f1e19ef6d8cef8","username":"logan@workos.com","emails":[],"first_name":"Logan","last_name":"Gingerich","state":"active","raw_attributes":{},"custom_attributes":{},"groups":[]}')
31
+ expect(user.primary_email).to eq(nil)
32
+ end
33
+ end
34
+ end
35
+ # rubocop:enable Layout/LineLength
36
+ end