workos 0.11.0 → 1.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/.semaphore/semaphore.yml +2 -2
- data/Gemfile.lock +2 -2
- data/LICENSE +1 -1
- data/README.md +13 -230
- data/lib/workos.rb +2 -0
- data/lib/workos/client.rb +21 -3
- data/lib/workos/connection.rb +5 -1
- data/lib/workos/directory_user.rb +4 -1
- data/lib/workos/organizations.rb +171 -0
- data/lib/workos/passwordless.rb +4 -0
- data/lib/workos/portal.rb +0 -80
- data/lib/workos/profile.rb +8 -10
- data/lib/workos/profile_and_token.rb +28 -0
- data/lib/workos/sso.rb +25 -100
- data/lib/workos/types/connection_struct.rb +1 -0
- data/lib/workos/types/directory_user_struct.rb +1 -0
- data/lib/workos/version.rb +1 -1
- data/spec/lib/workos/organizations_spec.rb +191 -0
- data/spec/lib/workos/portal_spec.rb +0 -113
- data/spec/lib/workos/sso_spec.rb +35 -115
- data/spec/support/fixtures/vcr_cassettes/organization/delete.yml +72 -0
- data/spec/support/fixtures/vcr_cassettes/organization/delete_invalid.yml +72 -0
- data/spec/support/fixtures/vcr_cassettes/organization/get.yml +73 -0
- data/spec/support/fixtures/vcr_cassettes/{sso/create_connection_with_invalid_source.yml → organization/get_invalid.yml} +26 -12
- data/spec/support/fixtures/vcr_cassettes/organization/update.yml +73 -0
- data/spec/support/fixtures/vcr_cassettes/organization/update_invalid.yml +73 -0
- data/spec/support/fixtures/vcr_cassettes/sso/profile.yml +74 -0
- data/workos.gemspec +1 -1
- metadata +23 -9
- data/spec/support/fixtures/vcr_cassettes/sso/create_connection_with_valid_source.yml +0 -63
data/lib/workos/version.rb
CHANGED
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: false
|
3
|
+
|
4
|
+
describe WorkOS::Organizations do
|
5
|
+
describe '.create_organization' do
|
6
|
+
context 'with valid payload' do
|
7
|
+
it 'creates an organization' do
|
8
|
+
VCR.use_cassette 'organization/create' do
|
9
|
+
organization = described_class.create_organization(
|
10
|
+
domains: ['example.com'],
|
11
|
+
name: 'Test Organization',
|
12
|
+
)
|
13
|
+
|
14
|
+
expect(organization.id).to eq('org_01EHT88Z8J8795GZNQ4ZP1J81T')
|
15
|
+
expect(organization.name).to eq('Test Organization')
|
16
|
+
expect(organization.domains.first[:domain]).to eq('example.com')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'with an invalid payload' do
|
22
|
+
it 'returns an error' do
|
23
|
+
VCR.use_cassette 'organization/create_invalid' do
|
24
|
+
expect do
|
25
|
+
described_class.create_organization(
|
26
|
+
domains: ['example.com'],
|
27
|
+
name: 'Test Organization 2',
|
28
|
+
)
|
29
|
+
end.to raise_error(
|
30
|
+
WorkOS::APIError,
|
31
|
+
/An Organization with the domain example.com already exists/,
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '.list_organizations' do
|
39
|
+
context 'with no options' do
|
40
|
+
it 'returns organizations and metadata' do
|
41
|
+
expected_metadata = {
|
42
|
+
'after' => nil,
|
43
|
+
'before' => 'before-id',
|
44
|
+
}
|
45
|
+
|
46
|
+
VCR.use_cassette 'organization/list' do
|
47
|
+
organizations = described_class.list_organizations
|
48
|
+
|
49
|
+
expect(organizations.data.size).to eq(7)
|
50
|
+
expect(organizations.list_metadata).to eq(expected_metadata)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'with the before option' do
|
56
|
+
it 'forms the proper request to the API' do
|
57
|
+
request_args = [
|
58
|
+
'/organizations?before=before-id',
|
59
|
+
'Content-Type' => 'application/json'
|
60
|
+
]
|
61
|
+
|
62
|
+
expected_request = Net::HTTP::Get.new(*request_args)
|
63
|
+
|
64
|
+
expect(Net::HTTP::Get).to receive(:new).with(*request_args).
|
65
|
+
and_return(expected_request)
|
66
|
+
|
67
|
+
VCR.use_cassette 'organization/list', match_requests_on: [:path] do
|
68
|
+
organizations = described_class.list_organizations(
|
69
|
+
before: 'before-id',
|
70
|
+
)
|
71
|
+
|
72
|
+
expect(organizations.data.size).to eq(7)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'with the after option' do
|
78
|
+
it 'forms the proper request to the API' do
|
79
|
+
request_args = [
|
80
|
+
'/organizations?after=after-id',
|
81
|
+
'Content-Type' => 'application/json'
|
82
|
+
]
|
83
|
+
|
84
|
+
expected_request = Net::HTTP::Get.new(*request_args)
|
85
|
+
|
86
|
+
expect(Net::HTTP::Get).to receive(:new).with(*request_args).
|
87
|
+
and_return(expected_request)
|
88
|
+
|
89
|
+
VCR.use_cassette 'organization/list', match_requests_on: [:path] do
|
90
|
+
organizations = described_class.list_organizations(after: 'after-id')
|
91
|
+
|
92
|
+
expect(organizations.data.size).to eq(7)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'with the limit option' do
|
98
|
+
it 'forms the proper request to the API' do
|
99
|
+
request_args = [
|
100
|
+
'/organizations?limit=10',
|
101
|
+
'Content-Type' => 'application/json'
|
102
|
+
]
|
103
|
+
|
104
|
+
expected_request = Net::HTTP::Get.new(*request_args)
|
105
|
+
|
106
|
+
expect(Net::HTTP::Get).to receive(:new).with(*request_args).
|
107
|
+
and_return(expected_request)
|
108
|
+
|
109
|
+
VCR.use_cassette 'organization/list', match_requests_on: [:path] do
|
110
|
+
organizations = described_class.list_organizations(limit: 10)
|
111
|
+
|
112
|
+
expect(organizations.data.size).to eq(7)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '.get_organization' do
|
119
|
+
context 'with a valid id' do
|
120
|
+
it 'gets the organization details' do
|
121
|
+
VCR.use_cassette('organization/get') do
|
122
|
+
organization = described_class.get_organization(
|
123
|
+
id: 'org_01EZDF20TZEJXKPSX2BJRN6TV6',
|
124
|
+
)
|
125
|
+
|
126
|
+
expect(organization.id).to eq('org_01EZDF20TZEJXKPSX2BJRN6TV6')
|
127
|
+
expect(organization.name).to eq('Foo Corp')
|
128
|
+
expect(organization.domains.first[:domain]).to eq('foo-corp.com')
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'with an invalid id' do
|
134
|
+
it 'raises an error' do
|
135
|
+
VCR.use_cassette('organization/get_invalid') do
|
136
|
+
expect do
|
137
|
+
described_class.get_organization(id: 'invalid')
|
138
|
+
end.to raise_error(
|
139
|
+
WorkOS::APIError,
|
140
|
+
'Status 404, Not Found - request ID: ',
|
141
|
+
)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe '.update_organization' do
|
148
|
+
context 'with valid payload' do
|
149
|
+
it 'creates an organization' do
|
150
|
+
VCR.use_cassette 'organization/update' do
|
151
|
+
organization = described_class.update_organization(
|
152
|
+
organization: 'org_01F29YJ068E52HGEB8ZQGC9MJG',
|
153
|
+
domains: ['example.me'],
|
154
|
+
name: 'Test Organization',
|
155
|
+
)
|
156
|
+
|
157
|
+
expect(organization.id).to eq('org_01F29YJ068E52HGEB8ZQGC9MJG')
|
158
|
+
expect(organization.name).to eq('Test Organization')
|
159
|
+
expect(organization.domains.first[:domain]).to eq('example.me')
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe '.delete_organization' do
|
166
|
+
context 'with a valid id' do
|
167
|
+
it 'returns true' do
|
168
|
+
VCR.use_cassette('organization/delete') do
|
169
|
+
response = described_class.delete_organization(
|
170
|
+
id: 'org_01F4A8TD0B4N1Y9SJ8SH635HDB',
|
171
|
+
)
|
172
|
+
|
173
|
+
expect(response).to be(true)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
context 'with an invalid id' do
|
179
|
+
it 'returns false' do
|
180
|
+
VCR.use_cassette('organization/delete_invalid') do
|
181
|
+
expect do
|
182
|
+
described_class.delete_organization(id: 'invalid')
|
183
|
+
end.to raise_error(
|
184
|
+
WorkOS::APIError,
|
185
|
+
'Status 404, Not Found - request ID: ',
|
186
|
+
)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -2,39 +2,6 @@
|
|
2
2
|
# typed: false
|
3
3
|
|
4
4
|
describe WorkOS::Portal do
|
5
|
-
describe '.create_organization' do
|
6
|
-
context 'with valid payload' do
|
7
|
-
it 'creates an organization' do
|
8
|
-
VCR.use_cassette 'organization/create' do
|
9
|
-
organization = described_class.create_organization(
|
10
|
-
domains: ['example.com'],
|
11
|
-
name: 'Test Organization',
|
12
|
-
)
|
13
|
-
|
14
|
-
expect(organization.id).to eq('org_01EHT88Z8J8795GZNQ4ZP1J81T')
|
15
|
-
expect(organization.name).to eq('Test Organization')
|
16
|
-
expect(organization.domains.first[:domain]).to eq('example.com')
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
context 'with an invalid payload' do
|
22
|
-
it 'returns an error' do
|
23
|
-
VCR.use_cassette 'organization/create_invalid' do
|
24
|
-
expect do
|
25
|
-
described_class.create_organization(
|
26
|
-
domains: ['example.com'],
|
27
|
-
name: 'Test Organization 2',
|
28
|
-
)
|
29
|
-
end.to raise_error(
|
30
|
-
WorkOS::APIError,
|
31
|
-
/An Organization with the domain example.com already exists/,
|
32
|
-
)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
5
|
describe '.generate_link' do
|
39
6
|
let(:organization) { 'org_01EHQMYV6MBK39QC5PZXHY59C3' }
|
40
7
|
|
@@ -100,84 +67,4 @@ describe WorkOS::Portal do
|
|
100
67
|
end
|
101
68
|
end
|
102
69
|
end
|
103
|
-
|
104
|
-
describe '.list_organizations' do
|
105
|
-
context 'with no options' do
|
106
|
-
it 'returns organizations and metadata' do
|
107
|
-
expected_metadata = {
|
108
|
-
'after' => nil,
|
109
|
-
'before' => 'before-id',
|
110
|
-
}
|
111
|
-
|
112
|
-
VCR.use_cassette 'organization/list' do
|
113
|
-
organizations = described_class.list_organizations
|
114
|
-
|
115
|
-
expect(organizations.data.size).to eq(7)
|
116
|
-
expect(organizations.list_metadata).to eq(expected_metadata)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
context 'with the before option' do
|
122
|
-
it 'forms the proper request to the API' do
|
123
|
-
request_args = [
|
124
|
-
'/organizations?before=before-id',
|
125
|
-
'Content-Type' => 'application/json'
|
126
|
-
]
|
127
|
-
|
128
|
-
expected_request = Net::HTTP::Get.new(*request_args)
|
129
|
-
|
130
|
-
expect(Net::HTTP::Get).to receive(:new).with(*request_args).
|
131
|
-
and_return(expected_request)
|
132
|
-
|
133
|
-
VCR.use_cassette 'organization/list', match_requests_on: [:path] do
|
134
|
-
organizations = described_class.list_organizations(
|
135
|
-
before: 'before-id',
|
136
|
-
)
|
137
|
-
|
138
|
-
expect(organizations.data.size).to eq(7)
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
context 'with the after option' do
|
144
|
-
it 'forms the proper request to the API' do
|
145
|
-
request_args = [
|
146
|
-
'/organizations?after=after-id',
|
147
|
-
'Content-Type' => 'application/json'
|
148
|
-
]
|
149
|
-
|
150
|
-
expected_request = Net::HTTP::Get.new(*request_args)
|
151
|
-
|
152
|
-
expect(Net::HTTP::Get).to receive(:new).with(*request_args).
|
153
|
-
and_return(expected_request)
|
154
|
-
|
155
|
-
VCR.use_cassette 'organization/list', match_requests_on: [:path] do
|
156
|
-
organizations = described_class.list_organizations(after: 'after-id')
|
157
|
-
|
158
|
-
expect(organizations.data.size).to eq(7)
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
context 'with the limit option' do
|
164
|
-
it 'forms the proper request to the API' do
|
165
|
-
request_args = [
|
166
|
-
'/organizations?limit=10',
|
167
|
-
'Content-Type' => 'application/json'
|
168
|
-
]
|
169
|
-
|
170
|
-
expected_request = Net::HTTP::Get.new(*request_args)
|
171
|
-
|
172
|
-
expect(Net::HTTP::Get).to receive(:new).with(*request_args).
|
173
|
-
and_return(expected_request)
|
174
|
-
|
175
|
-
VCR.use_cassette 'organization/list', match_requests_on: [:path] do
|
176
|
-
organizations = described_class.list_organizations(limit: 10)
|
177
|
-
|
178
|
-
expect(organizations.data.size).to eq(7)
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
70
|
end
|
data/spec/lib/workos/sso_spec.rb
CHANGED
@@ -147,52 +147,40 @@ describe WorkOS::SSO do
|
|
147
147
|
)
|
148
148
|
end
|
149
149
|
end
|
150
|
+
end
|
150
151
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
project_id: 'workos-proj-123',
|
156
|
-
redirect_uri: 'foo.com/auth/callback',
|
157
|
-
state: {
|
158
|
-
next_page: '/dashboard/edit',
|
159
|
-
}.to_s,
|
160
|
-
}
|
161
|
-
end
|
162
|
-
it 'raises a deprecation warning' do
|
163
|
-
expect do
|
164
|
-
described_class.authorization_url(**args)
|
165
|
-
end.to output(
|
166
|
-
"[DEPRECATION] `project_id` is deprecated.
|
167
|
-
Please use `client_id` instead.\n",
|
168
|
-
).to_stderr
|
169
|
-
end
|
170
|
-
|
171
|
-
it 'returns a valid URL' do
|
172
|
-
authorization_url = described_class.authorization_url(**args)
|
173
|
-
|
174
|
-
expect(URI.parse(authorization_url)).to be_a URI
|
175
|
-
end
|
176
|
-
|
177
|
-
it 'returns the expected hostname' do
|
178
|
-
authorization_url = described_class.authorization_url(**args)
|
152
|
+
describe '.get_profile' do
|
153
|
+
it 'returns a profile' do
|
154
|
+
VCR.use_cassette 'sso/profile' do
|
155
|
+
profile = described_class.get_profile(access_token: 'access_token')
|
179
156
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
157
|
+
expectation = {
|
158
|
+
connection_id: 'conn_01E83FVYZHY7DM4S9503JHV0R5',
|
159
|
+
connection_type: 'GoogleOAuth',
|
160
|
+
email: 'bob.loblaw@workos.com',
|
161
|
+
first_name: 'Bob',
|
162
|
+
id: 'prof_01EEJTY9SZ1R350RB7B73SNBKF',
|
163
|
+
idp_id: '116485463307139932699',
|
164
|
+
last_name: 'Loblaw',
|
165
|
+
raw_attributes: {
|
166
|
+
email: 'bob.loblaw@workos.com',
|
167
|
+
family_name: 'Loblaw',
|
168
|
+
given_name: 'Bob',
|
169
|
+
hd: 'workos.com',
|
170
|
+
id: '116485463307139932699',
|
171
|
+
locale: 'en',
|
172
|
+
name: 'Bob Loblaw',
|
173
|
+
picture: 'https://lh3.googleusercontent.com/a-/AOh14GyO2hLlgZvteDQ3Ldi3_-RteZLya0hWH7247Cam=s96-c',
|
174
|
+
verified_email: true,
|
175
|
+
},
|
176
|
+
}
|
185
177
|
|
186
|
-
expect(
|
187
|
-
'client_id=workos-proj-123&redirect_uri=foo.com%2Fauth%2Fcallback' \
|
188
|
-
'&response_type=code&state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2F' \
|
189
|
-
'edit%22%7D&domain=foo.com',
|
190
|
-
)
|
178
|
+
expect(profile.to_json).to eq(expectation)
|
191
179
|
end
|
192
180
|
end
|
193
181
|
end
|
194
182
|
|
195
|
-
describe '.
|
183
|
+
describe '.profile_and_token' do
|
196
184
|
let(:args) do
|
197
185
|
{
|
198
186
|
code: SecureRandom.hex(10),
|
@@ -225,15 +213,15 @@ describe WorkOS::SSO do
|
|
225
213
|
end
|
226
214
|
|
227
215
|
it 'includes the SDK Version header' do
|
228
|
-
described_class.
|
216
|
+
described_class.profile_and_token(**args)
|
229
217
|
|
230
218
|
expect(a_request(:post, 'https://api.workos.com/sso/token').
|
231
219
|
with(headers: headers, body: request_body)).to have_been_made
|
232
220
|
end
|
233
221
|
|
234
|
-
it 'returns a WorkOS::
|
235
|
-
|
236
|
-
expect(
|
222
|
+
it 'returns a WorkOS::ProfileAndToken' do
|
223
|
+
profile_and_token = described_class.profile_and_token(**args)
|
224
|
+
expect(profile_and_token).to be_a(WorkOS::ProfileAndToken)
|
237
225
|
|
238
226
|
expectation = {
|
239
227
|
connection_id: 'conn_01EMH8WAK20T42N2NBMNBCYHAG',
|
@@ -252,7 +240,8 @@ describe WorkOS::SSO do
|
|
252
240
|
},
|
253
241
|
}
|
254
242
|
|
255
|
-
expect(
|
243
|
+
expect(profile_and_token.access_token).to eq('01DVX6QBS3EG6FHY2ESAA5Q65X')
|
244
|
+
expect(profile_and_token.profile.to_json).to eq(expectation)
|
256
245
|
end
|
257
246
|
end
|
258
247
|
|
@@ -269,7 +258,7 @@ describe WorkOS::SSO do
|
|
269
258
|
|
270
259
|
it 'raises an exception with request ID' do
|
271
260
|
expect do
|
272
|
-
described_class.
|
261
|
+
described_class.profile_and_token(**args)
|
273
262
|
end.to raise_error(
|
274
263
|
WorkOS::APIError,
|
275
264
|
'some error message - request ID: request-id',
|
@@ -293,7 +282,7 @@ describe WorkOS::SSO do
|
|
293
282
|
|
294
283
|
it 'raises an exception' do
|
295
284
|
expect do
|
296
|
-
described_class.
|
285
|
+
described_class.profile_and_token(**args)
|
297
286
|
end.to raise_error(
|
298
287
|
WorkOS::APIError,
|
299
288
|
"The code '01DVX3C5Z367SFHR8QNDMK7V24'" \
|
@@ -303,75 +292,6 @@ describe WorkOS::SSO do
|
|
303
292
|
end
|
304
293
|
end
|
305
294
|
|
306
|
-
describe '.create_connection' do
|
307
|
-
context 'with a valid source' do
|
308
|
-
it 'creates a connection' do
|
309
|
-
VCR.use_cassette('sso/create_connection_with_valid_source') do
|
310
|
-
connection = WorkOS::SSO.create_connection(
|
311
|
-
source: 'draft_conn_01E6PK87QP6NQ29RRX0G100YGV',
|
312
|
-
)
|
313
|
-
|
314
|
-
expect(connection.id).to eq('conn_01E4F9T2YWZFD218DN04KVFDSY')
|
315
|
-
expect(connection.connection_type).to eq('GoogleOAuth')
|
316
|
-
expect(connection.name).to eq('Foo Corp')
|
317
|
-
expect(connection.domains.first[:domain]).to eq('example.com')
|
318
|
-
expect(connection.organization_id).to eq('12345')
|
319
|
-
expect(connection.status).to eq('linked')
|
320
|
-
end
|
321
|
-
end
|
322
|
-
end
|
323
|
-
|
324
|
-
context 'with an invalid source' do
|
325
|
-
it 'raises an error' do
|
326
|
-
VCR.use_cassette('sso/create_connection_with_invalid_source') do
|
327
|
-
expect do
|
328
|
-
WorkOS::SSO.create_connection(source: 'invalid')
|
329
|
-
end.to raise_error(
|
330
|
-
WorkOS::APIError,
|
331
|
-
'Status 404, Not Found - request ID: ',
|
332
|
-
)
|
333
|
-
end
|
334
|
-
end
|
335
|
-
end
|
336
|
-
end
|
337
|
-
|
338
|
-
describe '.promote_draft_connection' do
|
339
|
-
let(:token) { 'draft_conn_id' }
|
340
|
-
let(:client_id) { 'proj_0239u590h' }
|
341
|
-
|
342
|
-
context 'with a valid request' do
|
343
|
-
before do
|
344
|
-
stub_request(
|
345
|
-
:post,
|
346
|
-
"https://api.workos.com/draft_connections/#{token}/activate",
|
347
|
-
).to_return(status: 200)
|
348
|
-
end
|
349
|
-
it 'returns true' do
|
350
|
-
response = described_class.promote_draft_connection(
|
351
|
-
token: token,
|
352
|
-
)
|
353
|
-
|
354
|
-
expect(response).to be(true)
|
355
|
-
end
|
356
|
-
end
|
357
|
-
|
358
|
-
context 'with an invalid request' do
|
359
|
-
before do
|
360
|
-
stub_request(
|
361
|
-
:post,
|
362
|
-
"https://api.workos.com/draft_connections/#{token}/activate",
|
363
|
-
).to_return(status: 403)
|
364
|
-
end
|
365
|
-
it 'returns true' do
|
366
|
-
response = described_class.promote_draft_connection(
|
367
|
-
token: token,
|
368
|
-
)
|
369
|
-
|
370
|
-
expect(response).to be(false)
|
371
|
-
end
|
372
|
-
end
|
373
|
-
end
|
374
|
-
|
375
295
|
describe '.list_connections' do
|
376
296
|
context 'with no options' do
|
377
297
|
it 'returns connections and metadata' do
|