workos 0.4.2 → 0.8.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +8 -11
  3. data/README.md +7 -0
  4. data/lib/workos.rb +3 -0
  5. data/lib/workos/audit_trail.rb +2 -2
  6. data/lib/workos/client.rb +13 -7
  7. data/lib/workos/organization.rb +49 -0
  8. data/lib/workos/passwordless.rb +81 -0
  9. data/lib/workos/portal.rb +149 -0
  10. data/lib/workos/profile.rb +6 -1
  11. data/lib/workos/types.rb +4 -0
  12. data/lib/workos/types/intent_enum.rb +14 -0
  13. data/lib/workos/types/list_struct.rb +13 -0
  14. data/lib/workos/types/organization_struct.rb +14 -0
  15. data/lib/workos/types/passwordless_session_struct.rb +15 -0
  16. data/lib/workos/types/profile_struct.rb +1 -0
  17. data/lib/workos/version.rb +1 -1
  18. data/spec/lib/workos/passwordless_spec.rb +82 -0
  19. data/spec/lib/workos/portal_spec.rb +176 -0
  20. data/spec/lib/workos/sso_spec.rb +18 -0
  21. data/spec/support/fixtures/vcr_cassettes/organization/create.yml +72 -0
  22. data/spec/support/fixtures/vcr_cassettes/organization/create_invalid.yml +72 -0
  23. data/spec/support/fixtures/vcr_cassettes/organization/list.yml +72 -0
  24. data/spec/support/fixtures/vcr_cassettes/passwordless/create_session.yml +72 -0
  25. data/spec/support/fixtures/vcr_cassettes/passwordless/create_session_invalid.yml +73 -0
  26. data/spec/support/fixtures/vcr_cassettes/passwordless/send_session.yml +72 -0
  27. data/spec/support/fixtures/vcr_cassettes/passwordless/send_session_invalid.yml +73 -0
  28. data/spec/support/fixtures/vcr_cassettes/portal/generate_link.yml +72 -0
  29. data/spec/support/fixtures/vcr_cassettes/portal/generate_link_invalid.yml +72 -0
  30. data/spec/support/profile.txt +1 -1
  31. data/workos.gemspec +1 -1
  32. metadata +33 -4
@@ -6,6 +6,10 @@ module WorkOS
6
6
  # so we're using Sorbet throughout this Ruby gem.
7
7
  module Types
8
8
  require_relative 'types/connection_struct'
9
+ require_relative 'types/intent_enum'
10
+ require_relative 'types/list_struct'
11
+ require_relative 'types/organization_struct'
12
+ require_relative 'types/passwordless_session_struct'
9
13
  require_relative 'types/profile_struct'
10
14
  require_relative 'types/provider_enum'
11
15
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module WorkOS
5
+ module Types
6
+ # The IntentEnum is type-safe declarations of a fixed set of values for
7
+ # intents while generating an Admin Portal link.
8
+ class Intent < T::Enum
9
+ enums do
10
+ SSO = new('sso')
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module WorkOS
5
+ module Types
6
+ # ListStruct acts as a typed interface to expose lists of data and related
7
+ # metadata
8
+ class ListStruct < T::Struct
9
+ const :data, T::Array[T.untyped]
10
+ const :list_metadata, T::Hash[String, T.nilable(String)]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module WorkOS
5
+ module Types
6
+ # This OrganizationStruct acts as a typed interface
7
+ # for the Organization class
8
+ class OrganizationStruct < T::Struct
9
+ const :id, String
10
+ const :name, String
11
+ const :domains, T::Array[T.untyped]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module WorkOS
5
+ module Types
6
+ # This PasswordlessSessionStruct acts as a typed interface
7
+ # for the Passwordless class
8
+ class PasswordlessSessionStruct < T::Struct
9
+ const :id, String
10
+ const :email, String
11
+ const :expires_at, Date
12
+ const :link, String
13
+ end
14
+ end
15
+ end
@@ -12,6 +12,7 @@ module WorkOS
12
12
  const :last_name, T.nilable(String)
13
13
  const :connection_type, String
14
14
  const :idp_id, T.nilable(String)
15
+ const :raw_attributes, T::Hash[Symbol, Object]
15
16
  end
16
17
  end
17
18
  end
@@ -2,5 +2,5 @@
2
2
  # typed: strong
3
3
 
4
4
  module WorkOS
5
- VERSION = '0.4.2'
5
+ VERSION = '0.8.1'
6
6
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ describe WorkOS::Passwordless 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_session' do
14
+ context 'with valid options payload' do
15
+ let(:valid_options) do
16
+ {
17
+ email: 'demo@workos-okta.com',
18
+ type: 'MagicLink',
19
+ }
20
+ end
21
+
22
+ it 'creates a session' do
23
+ VCR.use_cassette('passwordless/create_session') do
24
+ response = described_class.create_session(valid_options)
25
+
26
+ expect(response.email).to eq 'demo@workos-okta.com'
27
+ end
28
+ end
29
+ end
30
+
31
+ context 'with invalid event payload' do
32
+ let(:invalid_options) do
33
+ {}
34
+ end
35
+
36
+ it 'raises an error' do
37
+ VCR.use_cassette('passwordless/create_session_invalid') do
38
+ expect do
39
+ described_class.create_session(invalid_options)
40
+ end.to raise_error(
41
+ WorkOS::InvalidRequestError,
42
+ /Status 422, Validation failed \(email: email must be a string; type: type must be a valid enum value\)/,
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ describe '.send_session' do
50
+ context 'with valid session id' do
51
+ let(:valid_options) do
52
+ {
53
+ email: 'demo@workos-okta.com',
54
+ type: 'MagicLink',
55
+ }
56
+ end
57
+
58
+ it 'send a session' do
59
+ VCR.use_cassette('passwordless/send_session') do
60
+ response = described_class.send_session(
61
+ 'passwordless_session_01EJC0F4KH42T11Y2DHPEB09BM',
62
+ )
63
+
64
+ expect(response['success']).to eq true
65
+ end
66
+ end
67
+ end
68
+
69
+ context 'with invalid session id' do
70
+ it 'raises an error' do
71
+ VCR.use_cassette('passwordless/send_session_invalid') do
72
+ expect do
73
+ described_class.send_session('session_123')
74
+ end.to raise_error(
75
+ WorkOS::InvalidRequestError,
76
+ /Status 422, The passwordless session 'session_123' has expired or is invalid./,
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ describe WorkOS::Portal do
5
+ before :all do
6
+ WorkOS.key = 'test'
7
+ end
8
+
9
+ after :all do
10
+ WorkOS.key = nil
11
+ end
12
+
13
+ describe '.create_organization' do
14
+ context 'with valid payload' do
15
+ it 'creates an organization' do
16
+ VCR.use_cassette 'organization/create' do
17
+ organization = described_class.create_organization(
18
+ domains: ['example.com'],
19
+ name: 'Test Organization',
20
+ )
21
+
22
+ expect(organization.id).to eq('org_01EHT88Z8J8795GZNQ4ZP1J81T')
23
+ expect(organization.name).to eq('Test Organization')
24
+ expect(organization.domains.first[:domain]).to eq('example.com')
25
+ end
26
+ end
27
+ end
28
+
29
+ context 'with an invalid payload' do
30
+ it 'returns an error' do
31
+ VCR.use_cassette 'organization/create_invalid' do
32
+ expect do
33
+ described_class.create_organization(
34
+ domains: ['example.com'],
35
+ name: 'Test Organization 2',
36
+ )
37
+ end.to raise_error(
38
+ WorkOS::APIError,
39
+ /An Organization with the domain example.com already exists/,
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ describe '.generate_link' do
47
+ let(:organization) { 'org_01EHQMYV6MBK39QC5PZXHY59C3' }
48
+
49
+ describe 'with a valid organization' do
50
+ describe 'with the minimal params' do
51
+ it 'returns an Admin Portal link' do
52
+ VCR.use_cassette 'portal/generate_link' do
53
+ portal_link = described_class.generate_link(
54
+ intent: 'sso',
55
+ organization: organization,
56
+ )
57
+
58
+ expect(portal_link).to eq(
59
+ 'https://id.workos.com/portal/launch?secret=secret',
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ describe 'with an invalid organization' do
67
+ it 'raises an error' do
68
+ VCR.use_cassette 'portal/generate_link_invalid' do
69
+ expect do
70
+ described_class.generate_link(
71
+ intent: 'sso',
72
+ organization: 'bogus-id',
73
+ )
74
+ end.to raise_error(
75
+ WorkOS::InvalidRequestError,
76
+ /Could not find an organization with the id, bogus-id/,
77
+ )
78
+ end
79
+ end
80
+ end
81
+
82
+ describe 'with an invalid intent' do
83
+ it 'raises an error' do
84
+ expect do
85
+ described_class.generate_link(
86
+ intent: 'bogus-intent',
87
+ organization: organization,
88
+ )
89
+ end.to raise_error(
90
+ ArgumentError,
91
+ 'bogus-intent is not a valid value. `intent` must be in ["sso"]',
92
+ )
93
+ end
94
+ end
95
+ end
96
+
97
+ describe '.list_organizations' do
98
+ context 'with no options' do
99
+ it 'returns organizations and metadata' do
100
+ expected_metadata = {
101
+ 'after' => nil,
102
+ 'before' => 'before-id',
103
+ }
104
+
105
+ VCR.use_cassette 'organization/list' do
106
+ organizations = described_class.list_organizations
107
+
108
+ expect(organizations.data.size).to eq(7)
109
+ expect(organizations.list_metadata).to eq(expected_metadata)
110
+ end
111
+ end
112
+ end
113
+
114
+ context 'with the before option' do
115
+ it 'forms the proper request to the API' do
116
+ request_args = [
117
+ '/organizations?before=before-id',
118
+ 'Content-Type' => 'application/json'
119
+ ]
120
+
121
+ expected_request = Net::HTTP::Get.new(*request_args)
122
+
123
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
124
+ and_return(expected_request)
125
+
126
+ VCR.use_cassette 'organization/list', match_requests_on: [:path] do
127
+ organizations = described_class.list_organizations(
128
+ before: 'before-id',
129
+ )
130
+
131
+ expect(organizations.data.size).to eq(7)
132
+ end
133
+ end
134
+ end
135
+
136
+ context 'with the after option' do
137
+ it 'forms the proper request to the API' do
138
+ request_args = [
139
+ '/organizations?after=after-id',
140
+ 'Content-Type' => 'application/json'
141
+ ]
142
+
143
+ expected_request = Net::HTTP::Get.new(*request_args)
144
+
145
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
146
+ and_return(expected_request)
147
+
148
+ VCR.use_cassette 'organization/list', match_requests_on: [:path] do
149
+ organizations = described_class.list_organizations(after: 'after-id')
150
+
151
+ expect(organizations.data.size).to eq(7)
152
+ end
153
+ end
154
+ end
155
+
156
+ context 'with the limit option' do
157
+ it 'forms the proper request to the API' do
158
+ request_args = [
159
+ '/organizations?limit=10',
160
+ 'Content-Type' => 'application/json'
161
+ ]
162
+
163
+ expected_request = Net::HTTP::Get.new(*request_args)
164
+
165
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
166
+ and_return(expected_request)
167
+
168
+ VCR.use_cassette 'organization/list', match_requests_on: [:path] do
169
+ organizations = described_class.list_organizations(limit: 10)
170
+
171
+ expect(organizations.data.size).to eq(7)
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -161,6 +161,24 @@ describe WorkOS::SSO do
161
161
  it 'returns a WorkOS::Profile' do
162
162
  profile = described_class.profile(**args)
163
163
  expect(profile).to be_a(WorkOS::Profile)
164
+
165
+ expectation = {
166
+ connection_type: 'OktaSAML',
167
+ email: 'demo@workos-okta.com',
168
+ first_name: 'WorkOS',
169
+ id: 'prof_01DRA1XNSJDZ19A31F183ECQW5',
170
+ idp_id: '00u1klkowm8EGah2H357',
171
+ last_name: 'Demo',
172
+ raw_attributes: {
173
+ email: 'demo@workos-okta.com',
174
+ first_name: 'WorkOS',
175
+ id: 'prof_01DRA1XNSJDZ19A31F183ECQW5',
176
+ idp_id: '00u1klkowm8EGah2H357',
177
+ last_name: 'Demo',
178
+ },
179
+ }
180
+
181
+ expect(profile.to_json).to eq(expectation)
164
182
  end
165
183
  end
166
184
 
@@ -0,0 +1,72 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: https://api.workos.com/organizations
6
+ body:
7
+ encoding: UTF-8
8
+ string: '{"domains":["example.com"],"name":"Test Organization"}'
9
+ headers:
10
+ Content-Type:
11
+ - application/json
12
+ Accept-Encoding:
13
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
14
+ Accept:
15
+ - "*/*"
16
+ User-Agent:
17
+ - WorkOS; ruby/2.7.1; x86_64-darwin19; v0.5.0
18
+ Authorization:
19
+ - Bearer <API_KEY>
20
+ response:
21
+ status:
22
+ code: 201
23
+ message: Created
24
+ headers:
25
+ Server:
26
+ - Cowboy
27
+ Connection:
28
+ - keep-alive
29
+ Vary:
30
+ - Origin, Accept-Encoding
31
+ Access-Control-Allow-Credentials:
32
+ - 'true'
33
+ Content-Security-Policy:
34
+ - 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self''
35
+ https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src
36
+ ''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests'
37
+ X-Dns-Prefetch-Control:
38
+ - 'off'
39
+ Expect-Ct:
40
+ - max-age=0
41
+ X-Frame-Options:
42
+ - SAMEORIGIN
43
+ Strict-Transport-Security:
44
+ - max-age=15552000; includeSubDomains
45
+ X-Download-Options:
46
+ - noopen
47
+ X-Content-Type-Options:
48
+ - nosniff
49
+ X-Permitted-Cross-Domain-Policies:
50
+ - none
51
+ Referrer-Policy:
52
+ - no-referrer
53
+ X-Xss-Protection:
54
+ - '0'
55
+ X-Request-Id:
56
+ - bcddfa8b-3a12-4d48-b265-6094a26225a4
57
+ Content-Type:
58
+ - application/json; charset=utf-8
59
+ Content-Length:
60
+ - '203'
61
+ Etag:
62
+ - W/"cb-T4+GTGrJeuAmC0PAjcaSX5ZXL18"
63
+ Date:
64
+ - Wed, 09 Sep 2020 20:17:53 GMT
65
+ Via:
66
+ - 1.1 vegur
67
+ body:
68
+ encoding: UTF-8
69
+ string: '{"name":"Test Organization","object":"organization","id":"org_01EHT88Z8J8795GZNQ4ZP1J81T","domains":[{"domain":"example.com","object":"organization_domain","id":"org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"}]}'
70
+ http_version:
71
+ recorded_at: Wed, 09 Sep 2020 20:17:54 GMT
72
+ recorded_with: VCR 5.0.0