workos 0.0.2 → 0.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -2
  3. data/Gemfile.lock +6 -2
  4. data/README.md +179 -8
  5. data/bin/docs +2 -2
  6. data/codecov.yml +1 -0
  7. data/docs/WorkOS.html +22 -26
  8. data/docs/WorkOS/APIError.html +160 -0
  9. data/docs/WorkOS/AuditLog.html +235 -0
  10. data/docs/WorkOS/AuthenticationError.html +160 -0
  11. data/docs/WorkOS/Base.html +27 -32
  12. data/docs/WorkOS/Client.html +493 -0
  13. data/docs/WorkOS/InvalidRequestError.html +160 -0
  14. data/docs/WorkOS/Profile.html +80 -17
  15. data/docs/WorkOS/RequestError.html +6 -6
  16. data/docs/WorkOS/SSO.html +118 -74
  17. data/docs/WorkOS/Types.html +9 -10
  18. data/docs/WorkOS/Types/ProfileStruct.html +6 -6
  19. data/docs/WorkOS/Types/Provider.html +135 -0
  20. data/docs/WorkOS/WorkOSError.html +447 -0
  21. data/docs/class_list.html +3 -3
  22. data/docs/css/style.css +2 -2
  23. data/docs/file.README.html +173 -13
  24. data/docs/file_list.html +2 -2
  25. data/docs/frames.html +2 -2
  26. data/docs/index.html +77 -16
  27. data/docs/js/app.js +14 -3
  28. data/docs/method_list.html +96 -8
  29. data/docs/top-level-namespace.html +6 -6
  30. data/lib/workos.rb +7 -2
  31. data/lib/workos/audit_log.rb +78 -0
  32. data/lib/workos/base.rb +5 -6
  33. data/lib/workos/client.rb +86 -0
  34. data/lib/workos/errors.rb +48 -0
  35. data/lib/workos/sso.rb +49 -27
  36. data/lib/workos/types.rb +2 -1
  37. data/lib/workos/types/provider_enum.rb +14 -0
  38. data/lib/workos/version.rb +2 -2
  39. data/sorbet/rbi/hidden-definitions/errors.txt +22108 -4368
  40. data/sorbet/rbi/hidden-definitions/hidden.rbi +32490 -6059
  41. data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +1 -1
  42. data/sorbet/rbi/todo.rbi +5 -0
  43. data/spec/lib/workos/audit_log_spec.rb +140 -0
  44. data/spec/lib/workos/base_spec.rb +30 -0
  45. data/spec/lib/workos/sso_spec.rb +131 -36
  46. data/spec/spec_helper.rb +21 -1
  47. data/spec/support/fixtures/vcr_cassettes/audit_log/create_event.yml +65 -0
  48. data/spec/support/fixtures/vcr_cassettes/audit_log/create_event_custom_idempotency_key.yml +67 -0
  49. data/spec/support/fixtures/vcr_cassettes/audit_log/create_event_invalid.yml +68 -0
  50. data/spec/support/fixtures/vcr_cassettes/audit_log/create_events_duplicate_idempotency_key_and_payload.yml +131 -0
  51. data/spec/support/fixtures/vcr_cassettes/audit_log/create_events_duplicate_idempotency_key_different_payload.yml +134 -0
  52. data/spec/support/fixtures/vcr_cassettes/base/execute_request_unauthenticated.yml +66 -0
  53. data/workos.gemspec +2 -0
  54. metadata +57 -27
  55. data/lib/workos/request_error.rb +0 -5
  56. data/sorbet/rbi/gems/addressable.rbi +0 -198
  57. data/sorbet/rbi/gems/ast.rbi +0 -47
  58. data/sorbet/rbi/gems/codecov.rbi +0 -19
  59. data/sorbet/rbi/gems/crack.rbi +0 -47
  60. data/sorbet/rbi/gems/docile.rbi +0 -31
  61. data/sorbet/rbi/gems/hashdiff.rbi +0 -65
  62. data/sorbet/rbi/gems/jaro_winkler.rbi +0 -14
  63. data/sorbet/rbi/gems/parallel.rbi +0 -81
  64. data/sorbet/rbi/gems/parser.rbi +0 -856
  65. data/sorbet/rbi/gems/public_suffix.rbi +0 -102
  66. data/sorbet/rbi/gems/rack.rbi +0 -103
  67. data/sorbet/rbi/gems/rainbow.rbi +0 -117
  68. data/sorbet/rbi/gems/rake.rbi +0 -632
  69. data/sorbet/rbi/gems/rspec-core.rbi +0 -1661
  70. data/sorbet/rbi/gems/rspec-expectations.rbi +0 -388
  71. data/sorbet/rbi/gems/rspec-mocks.rbi +0 -823
  72. data/sorbet/rbi/gems/rspec-support.rbi +0 -266
  73. data/sorbet/rbi/gems/rspec.rbi +0 -14
  74. data/sorbet/rbi/gems/rubocop.rbi +0 -7083
  75. data/sorbet/rbi/gems/ruby-progressbar.rbi +0 -304
  76. data/sorbet/rbi/gems/simplecov-html.rbi +0 -30
  77. data/sorbet/rbi/gems/simplecov.rbi +0 -225
  78. data/sorbet/rbi/gems/unicode-display_width.rbi +0 -16
  79. data/sorbet/rbi/gems/webmock.rbi +0 -526
@@ -5,7 +5,7 @@
5
5
  #
6
6
  # https://github.com/sorbet/sorbet-typed/edit/master/lib/rainbow/all/rainbow.rbi
7
7
  #
8
- # typed: false
8
+ # typed: strong
9
9
 
10
10
  module Rainbow
11
11
  sig { returns(T::Boolean) }
@@ -3,5 +3,10 @@
3
3
 
4
4
  # typed: strong
5
5
  module T::Private::Methods::MethodHooks; end
6
+ module T::Private::Methods::MethodHooks; end
7
+ module T::Private::Methods::MethodHooks; end
8
+ module T::Private::Methods::SingletonMethodHooks; end
9
+ module T::Private::Methods::SingletonMethodHooks; end
10
+ module T::Private::Methods::SingletonMethodHooks; end
6
11
  module T::Private::Methods::SingletonMethodHooks; end
7
12
  module T::Private::Methods::SingletonMethodHooks; end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ describe WorkOS::AuditLog do
5
+ before(:all) do
6
+ WorkOS.key = 'key'
7
+ end
8
+
9
+ after(:all) do
10
+ WorkOS.key = nil
11
+ end
12
+
13
+ describe '.create_event' do
14
+ context 'with valid event payload' do
15
+ let(:valid_event) do
16
+ {
17
+ group: 'Terrace House',
18
+ location: '1.1.1.1',
19
+ action: 'house.created',
20
+ action_type: 'C',
21
+ actor_name: 'Daiki Miyagi',
22
+ actor_id: 'user_12345',
23
+ target_name: 'Ryota Yamasato',
24
+ target_id: 'user_67890',
25
+ occurred_at: '2020-01-10T15:30:00-05:00',
26
+ metadata: {
27
+ a: 'b',
28
+ },
29
+ }
30
+ end
31
+
32
+ context 'with idempotency key' do
33
+ context 'when idempotency key is used once' do
34
+ it 'creates an event' do
35
+ VCR.use_cassette('audit_log/create_event_custom_idempotency_key') do
36
+ response = described_class.create_event(
37
+ event: valid_event,
38
+ idempotency_key: 'key',
39
+ )
40
+
41
+ expect(response.code).to eq '201'
42
+ json = JSON.parse(response.body)
43
+ expect(json['success']).to be true
44
+ end
45
+ end
46
+ end
47
+
48
+ context 'when idempotency key is used more than once' do
49
+ context 'with duplicate event payloads' do
50
+ it 'creates an event' do
51
+ VCR.use_cassette('audit_log/create_events_duplicate_idempotency_key_and_payload') do
52
+ response1 = described_class.create_event(
53
+ event: valid_event,
54
+ idempotency_key: 'foo',
55
+ )
56
+ response2 = described_class.create_event(
57
+ event: valid_event,
58
+ idempotency_key: 'foo',
59
+ )
60
+
61
+ expect(response1.code).to eq '201'
62
+ json1 = JSON.parse(response1.body)
63
+ expect(json1['success']).to be true
64
+
65
+ expect(response2.code).to eq '201'
66
+ json2 = JSON.parse(response1.body)
67
+ expect(json2['success']).to be true
68
+ end
69
+ end
70
+ end
71
+
72
+ context 'with different event payloads' do
73
+ it 'raises an error' do
74
+ VCR.use_cassette('audit_log/create_events_duplicate_idempotency_key_different_payload') do
75
+ described_class.create_event(
76
+ event: valid_event,
77
+ idempotency_key: 'bar',
78
+ )
79
+
80
+ payload = valid_event.clone
81
+ payload[:actor_name] = 'Tetsuya Sugaya'
82
+
83
+ expect do
84
+ described_class.create_event(
85
+ event: payload,
86
+ idempotency_key: 'bar',
87
+ )
88
+ end.to raise_error(
89
+ WorkOS::InvalidRequestError,
90
+ /Status 400, Another idempotency key \(bar\) with different request parameters was found. Please use a different idempotency key./,
91
+ )
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ context 'with no idempotency key' do
99
+ it 'creates an event' do
100
+ VCR.use_cassette('audit_log/create_event') do
101
+ response = described_class.create_event(event: valid_event)
102
+
103
+ expect(response.code).to eq '201'
104
+ json = JSON.parse(response.body)
105
+ expect(json['success']).to be true
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ context 'with invalid event payload' do
112
+ let(:invalid_event) do
113
+ {
114
+ group: 'Terrace House',
115
+ location: '1.1.1.1',
116
+ action: 'house.created',
117
+ actor_name: 'Daiki Miyagi',
118
+ actor_id: 'user_12345',
119
+ target_name: 'Ryota Yamasato',
120
+ target_id: 'user_67890',
121
+ occurred_at: '2020-01-10T15:30:00-05:00',
122
+ metadata: {
123
+ a: 'b',
124
+ },
125
+ }
126
+ end
127
+
128
+ it 'raises an error' do
129
+ VCR.use_cassette('audit_log/create_event_invalid') do
130
+ expect do
131
+ described_class.create_event(event: invalid_event)
132
+ end.to raise_error(
133
+ WorkOS::InvalidRequestError,
134
+ /Status 422, Validation failed \(action_type: action_type must be a string\)/,
135
+ )
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ module WorkOS
5
+ module Test
6
+ class << self
7
+ include Base
8
+ include Client
9
+
10
+ def request
11
+ execute_request(request: post_request(path: '/events', body: {}))
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ describe WorkOS::Base do
18
+ describe '.execute_request' do
19
+ context 'when unauthenticated' do
20
+ it 'raises an error' do
21
+ VCR.use_cassette('base/execute_request_unauthenticated') do
22
+ expect { WorkOS::Test.request }.to raise_error(
23
+ WorkOS::AuthenticationError,
24
+ /Status 401, Unauthorized/,
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,41 +1,117 @@
1
- # typed: false
2
1
  # frozen_string_literal: true
2
+ # typed: false
3
3
 
4
4
  require 'securerandom'
5
5
 
6
6
  describe WorkOS::SSO do
7
7
  describe '.authorization_url' do
8
- let(:args) do
9
- {
10
- domain: 'foo.com',
11
- project_id: 'workos-proj-123',
12
- redirect_uri: 'foo.com/auth/callback',
13
- state: {
14
- next_page: '/dashboard/edit',
15
- },
16
- }
17
- end
8
+ context 'with a domain' do
9
+ let(:args) do
10
+ {
11
+ domain: 'foo.com',
12
+ project_id: 'workos-proj-123',
13
+ redirect_uri: 'foo.com/auth/callback',
14
+ state: {
15
+ next_page: '/dashboard/edit',
16
+ },
17
+ }
18
+ end
19
+ it 'returns a valid URL' do
20
+ authorization_url = described_class.authorization_url(**args)
21
+
22
+ expect(URI.parse(authorization_url)).to be_a URI
23
+ end
24
+
25
+ it 'returns the expected hostname' do
26
+ authorization_url = described_class.authorization_url(**args)
27
+
28
+ expect(URI.parse(authorization_url).host).to eq(WorkOS::API_HOSTNAME)
29
+ end
18
30
 
19
- it 'returns a valid URL' do
20
- authorization_url = described_class.authorization_url(**args)
31
+ it 'returns the expected query string' do
32
+ authorization_url = described_class.authorization_url(**args)
21
33
 
22
- expect(URI.parse(authorization_url)).to be_a URI
34
+ expect(URI.parse(authorization_url).query).to eq(
35
+ 'client_id=workos-proj-123&redirect_uri=foo.com%2Fauth%2Fcallback' \
36
+ '&response_type=code&state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2F' \
37
+ 'edit%22%7D&domain=foo.com',
38
+ )
39
+ end
23
40
  end
24
41
 
25
- it 'returns the expected hostname' do
26
- authorization_url = described_class.authorization_url(**args)
42
+ context 'with a provider' do
43
+ let(:args) do
44
+ {
45
+ provider: 'GoogleOAuth',
46
+ project_id: 'workos-proj-123',
47
+ redirect_uri: 'foo.com/auth/callback',
48
+ state: {
49
+ next_page: '/dashboard/edit',
50
+ },
51
+ }
52
+ end
53
+ it 'returns a valid URL' do
54
+ authorization_url = described_class.authorization_url(**args)
55
+
56
+ expect(URI.parse(authorization_url)).to be_a URI
57
+ end
58
+
59
+ it 'returns the expected hostname' do
60
+ authorization_url = described_class.authorization_url(**args)
61
+
62
+ expect(URI.parse(authorization_url).host).to eq(WorkOS::API_HOSTNAME)
63
+ end
64
+
65
+ it 'returns the expected query string' do
66
+ authorization_url = described_class.authorization_url(**args)
27
67
 
28
- expect(URI.parse(authorization_url).host).to eq(WorkOS::API_HOSTNAME)
68
+ expect(URI.parse(authorization_url).query).to eq(
69
+ 'client_id=workos-proj-123&redirect_uri=foo.com%2Fauth%2Fcallback' \
70
+ '&response_type=code&state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2F' \
71
+ 'edit%22%7D&provider=GoogleOAuth',
72
+ )
73
+ end
29
74
  end
30
75
 
31
- it 'returns the expected query string' do
32
- authorization_url = described_class.authorization_url(**args)
76
+ context 'with neither domain or provider' do
77
+ let(:args) do
78
+ {
79
+ project_id: 'workos-proj-123',
80
+ redirect_uri: 'foo.com/auth/callback',
81
+ state: {
82
+ next_page: '/dashboard/edit',
83
+ },
84
+ }
85
+ end
86
+ it 'raises an error' do
87
+ expect do
88
+ described_class.authorization_url(**args)
89
+ end.to raise_error(
90
+ ArgumentError,
91
+ 'Either domain or provider is required.',
92
+ )
93
+ end
94
+ end
33
95
 
34
- expect(URI.parse(authorization_url).query).to eq(
35
- 'domain=foo.com&client_id=workos-proj-123&redirect_uri=' \
36
- 'foo.com%2Fauth%2Fcallback&response_type=code&' \
37
- 'state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2Fedit%22%7D',
38
- )
96
+ context 'with an invalid provider' do
97
+ let(:args) do
98
+ {
99
+ provider: 'Okta',
100
+ project_id: 'workos-proj-123',
101
+ redirect_uri: 'foo.com/auth/callback',
102
+ state: {
103
+ next_page: '/dashboard/edit',
104
+ },
105
+ }
106
+ end
107
+ it 'raises an error' do
108
+ expect do
109
+ described_class.authorization_url(**args)
110
+ end.to raise_error(
111
+ ArgumentError,
112
+ 'Okta is not a valid value. `provider` must be in ["GoogleOAuth"]',
113
+ )
114
+ end
39
115
  end
40
116
  end
41
117
 
@@ -48,7 +124,6 @@ describe WorkOS::SSO do
48
124
  {
49
125
  code: SecureRandom.hex(10),
50
126
  project_id: 'workos-proj-123',
51
- redirect_uri: 'foo.com/auth/callback',
52
127
  }
53
128
  end
54
129
 
@@ -58,19 +133,30 @@ describe WorkOS::SSO do
58
133
  client_secret: WorkOS.key,
59
134
  code: args[:code],
60
135
  grant_type: 'authorization_code',
61
- redirect_uri: args[:redirect_uri],
62
136
  }
63
137
  end
138
+ let(:user_agent) { 'user-agent-string' }
139
+ let(:headers) { { 'User-Agent' => user_agent } }
140
+ before do
141
+ allow(described_class).to receive(:user_agent).and_return(user_agent)
142
+ end
64
143
 
65
144
  context 'with a successful response' do
66
145
  let(:body) { File.read("#{SPEC_ROOT}/support/profile.txt") }
67
146
 
68
147
  before do
69
148
  stub_request(:post, 'https://api.workos.com/sso/token').
70
- with(query: query).
149
+ with(query: query, headers: headers).
71
150
  to_return(status: 200, body: body)
72
151
  end
73
152
 
153
+ it 'includes the SDK Version header' do
154
+ described_class.profile(**args)
155
+
156
+ expect(a_request(:post, 'https://api.workos.com/sso/token').
157
+ with(query: query, headers: headers)).to have_been_made
158
+ end
159
+
74
160
  it 'returns a WorkOS::Profile' do
75
161
  profile = described_class.profile(**args)
76
162
 
@@ -81,17 +167,21 @@ describe WorkOS::SSO do
81
167
  context 'with an unprocessable request' do
82
168
  before do
83
169
  stub_request(:post, 'https://api.workos.com/sso/token').
84
- with(query: query).
170
+ with(query: query, headers: headers).
85
171
  to_return(
172
+ headers: { 'X-Request-ID' => 'request-id' },
86
173
  status: 422,
87
174
  body: { "message": 'some error message' }.to_json,
88
175
  )
89
176
  end
90
177
 
91
- it 'raises an exception' do
178
+ it 'raises an exception with request ID' do
92
179
  expect do
93
180
  described_class.profile(**args)
94
- end.to raise_error(WorkOS::RequestError, 'some error message')
181
+ end.to raise_error(
182
+ WorkOS::APIError,
183
+ 'some error message - request ID: request-id',
184
+ )
95
185
  end
96
186
  end
97
187
 
@@ -99,18 +189,23 @@ describe WorkOS::SSO do
99
189
  before do
100
190
  stub_request(:post, 'https://api.workos.com/sso/token').
101
191
  with(query: query).
102
- to_return(status: 201, body: {
103
- message: "The code '01DVX3C5Z367SFHR8QNDMK7V24'" \
104
- ' has expired or is invalid.',
105
- }.to_json,)
192
+ to_return(
193
+ status: 201,
194
+ headers: { 'X-Request-ID' => 'request-id' },
195
+ body: {
196
+ message: "The code '01DVX3C5Z367SFHR8QNDMK7V24'" \
197
+ ' has expired or is invalid.',
198
+ }.to_json,
199
+ )
106
200
  end
107
201
 
108
202
  it 'raises an exception' do
109
203
  expect do
110
204
  described_class.profile(**args)
111
205
  end.to raise_error(
112
- WorkOS::RequestError,
113
- "The code '01DVX3C5Z367SFHR8QNDMK7V24' has expired or is invalid.",
206
+ WorkOS::APIError,
207
+ "The code '01DVX3C5Z367SFHR8QNDMK7V24'" \
208
+ ' has expired or is invalid. - request ID: request-id',
114
209
  )
115
210
  end
116
211
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- # typed: false
2
+ # typed: true
3
3
 
4
4
  require 'simplecov'
5
5
  SimpleCov.start
@@ -16,9 +16,16 @@ require 'rubygems'
16
16
  require 'rspec'
17
17
  require 'webmock/rspec'
18
18
  require 'workos'
19
+ require 'vcr'
19
20
 
20
21
  SPEC_ROOT = File.dirname __FILE__
21
22
 
23
+ VCR.configure do |config|
24
+ config.cassette_library_dir = 'spec/support/fixtures/vcr_cassettes'
25
+ config.filter_sensitive_data('<API_KEY>') { WorkOS.key }
26
+ config.hook_into :webmock
27
+ end
28
+
22
29
  RSpec.configure do |config|
23
30
  config.expect_with :rspec do |expectations|
24
31
  expectations.include_chain_clauses_in_custom_matcher_descriptions = true
@@ -29,4 +36,17 @@ RSpec.configure do |config|
29
36
  end
30
37
 
31
38
  config.shared_context_metadata_behavior = :apply_to_host_groups
39
+
40
+ WebMock::API.prepend(Module.new do
41
+ extend self
42
+
43
+ # Disable VCR when a WebMock stub is created
44
+ # for clearer spec failure messaging
45
+ def stub_request(*args)
46
+ VCR.turn_off!
47
+ super
48
+ end
49
+ end)
50
+
51
+ config.before(:each) { VCR.turn_on! }
32
52
  end