workos 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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