main
1# frozen_string_literal: true
2
3require 'rails_helper'
4
5describe '/scim/v2/users' do
6 let(:user) { create(:user) }
7 let(:token) { create(:access_token, subject: user).to_jwt }
8 let(:headers) do
9 {
10 'Authorization' => "Bearer #{token}",
11 'Accept' => 'application/scim+json',
12 'Content-Type' => 'application/scim+json',
13 }
14 end
15
16 describe "POST /scim/v2/users" do
17 context "when a valid request is sent" do
18 let(:email) { generate(:email) }
19 let(:locale) { 'en' }
20 let(:timezone) { 'Etc/UTC' }
21 let(:body) { { schemas: [Scim::Kit::V2::Schemas::USER], userName: email, locale: locale, timezone: timezone } }
22 let(:json) { JSON.parse(response.body, symbolize_names: true) }
23
24 before { post '/scim/v2/users', params: body.to_json, headers: headers }
25
26 specify { expect(response).to have_http_status(:created) }
27 specify { expect(response.headers['Content-Type']).to eql('application/scim+json') }
28 specify { expect(response.headers['Location']).to be_present }
29 specify { expect(response.body).to be_present }
30 specify { expect(json[:schemas]).to match_array([Scim::Kit::V2::Schemas::USER]) }
31 specify { expect(json[:id]).to be_present }
32 specify { expect(json[:userName]).to eql(email) }
33 specify { expect(json[:meta][:resourceType]).to eql('User') }
34 specify { expect(json[:meta][:created]).to be_present }
35 specify { expect(json[:meta][:lastModified]).to be_present }
36 specify { expect(json[:meta][:version]).to be_present }
37 specify { expect(json[:meta][:location]).to be_present }
38 specify { expect(json[:locale]).to eql(locale) }
39 specify { expect(json[:timezone]).to eql(timezone) }
40 end
41
42 context "when a duplicate email is specified" do
43 let(:other_user) { create(:user) }
44 let(:request_body) { attributes_for(:scim_user, userName: other_user.email) }
45 let(:json) { JSON.parse(response.body, symbolize_names: true) }
46
47 before { post '/scim/v2/users', params: request_body.to_json, headers: headers }
48
49 specify { expect(response).to have_http_status(:bad_request) }
50 specify { expect(json[:schemas]).to match_array(['urn:ietf:params:scim:api:messages:2.0:Error']) }
51 specify { expect(json[:scimType]).to eql('uniqueness') }
52 specify { expect(json[:detail]).to be_instance_of(String) }
53 specify { expect(json[:status]).to eql('400') }
54 end
55 end
56
57 describe "GET /scim/v2/users/:id" do
58 let(:user) { create(:user) }
59
60 context "when the resource is available" do
61 before { get "/scim/v2/users/#{user.to_param}", headers: headers }
62
63 let(:json) { JSON.parse(response.body, symbolize_names: true) }
64
65 specify { expect(response).to have_http_status(:ok) }
66 specify { expect(response.headers['Content-Type']).to eql('application/scim+json') }
67 specify { expect(response.headers['Location']).to eql(scim_v2_user_url(user)) }
68 specify { expect(response.headers['ETag']).to be_present }
69 specify { expect(response.body).to be_present }
70
71 specify { expect(json[:schemas]).to match_array([Scim::Kit::V2::Schemas::USER]) }
72 specify { expect(json[:id]).to eql(user.to_param) }
73 specify { expect(json[:userName]).to eql(user.email) }
74 specify { expect(json[:meta][:resourceType]).to eql('User') }
75 specify { expect(json[:meta][:created]).to eql(user.created_at.iso8601) }
76 specify { expect(json[:meta][:lastModified]).to eql(user.updated_at.iso8601) }
77 specify { expect(json[:meta][:version]).to eql(response.headers['ETag']) }
78 specify { expect(json[:meta][:location]).to eql(scim_v2_user_url(user)) }
79 specify { expect(json[:name][:formatted]).to eql(user.email) }
80 specify { expect(json[:name][:familyName]).to eql(user.email) }
81 specify { expect(json[:name][:givenName]).to eql(user.email) }
82 end
83
84 context "when the resource does not exist" do
85 let(:json) { JSON.parse(response.body, symbolize_names: true) }
86
87 before { get "/scim/v2/users/#{SecureRandom.uuid}", headers: headers }
88
89 specify { expect(response).to have_http_status(:not_found) }
90 specify { expect(json[:schemas]).to match_array(['urn:ietf:params:scim:api:messages:2.0:Error']) }
91 specify { expect(json[:detail]).to be_present }
92 specify { expect(json[:status]).to eql('404') }
93 end
94
95 context "when a valid Authorization header is not provided" do
96 let(:user) { create(:user) }
97 let(:token) { SecureRandom.uuid }
98 let(:json) { JSON.parse(response.body, symbolize_names: true) }
99
100 before { get "/scim/v2/users/#{user.to_param}", headers: headers }
101
102 specify { expect(response).to have_http_status(:unauthorized) }
103 specify { expect(json[:schemas]).to match_array(['urn:ietf:params:scim:api:messages:2.0:Error']) }
104 specify { expect(json[:detail]).to be_present }
105 specify { expect(json[:detail]).to be_instance_of(String) }
106 specify { expect(json[:status]).to eql('401') }
107 end
108 end
109
110 describe "GET /scim/v2/users" do
111 context "when fetching all users" do
112 let(:json) { JSON.parse(response.body, symbolize_names: true) }
113 let!(:user) { create(:user) }
114
115 before { get "/scim/v2/users", headers: headers }
116
117 specify { expect(response).to have_http_status(:ok) }
118 specify { expect(response.headers['Content-Type']).to eql('application/scim+json') }
119 specify { expect(response.body).to be_present }
120 specify { expect(json[:schemas]).to match_array([Scim::Kit::V2::Messages::LIST_RESPONSE]) }
121 specify { expect(json[:totalResults]).to be(1) }
122 specify { expect(json[:startIndex]).to be(1) }
123 specify { expect(json[:itemsPerPage]).to be(25) }
124 specify { expect(json[:Resources]).not_to be_empty }
125 specify { expect(json[:Resources][0][:id]).to eql(user.to_param) }
126 specify { expect(json[:Resources][0][:active]).to be(true) }
127 specify { expect(json[:Resources][0][:displayName]).to eql(user.email) }
128 specify { expect(json[:Resources][0][:locale]).to eql(user.locale) }
129 specify { expect(json[:Resources][0][:timezone]).to eql(user.timezone) }
130 specify { expect(json[:Resources][0][:emails]).to match_array([{ primary: true, value: user.email }]) }
131 specify { expect(json[:Resources][0][:groups]).to be_empty }
132 specify { expect(json[:Resources][0][:meta]).to be_present }
133 end
134
135 context "when paginating users" do
136 let(:json) { JSON.parse(response.body, symbolize_names: true) }
137 let!(:users) { create_list(:user, 10) }
138
139 context "when requesting the first page of results (1-based index)" do
140 before { get "/scim/v2/users", params: { startIndex: 1, count: 1 }, headers: headers }
141
142 specify { expect(response).to have_http_status(:ok) }
143 specify { expect(response.headers['Content-Type']).to eql('application/scim+json') }
144 specify { expect(response.body).to be_present }
145 specify { expect(json[:schemas]).to match_array([Scim::Kit::V2::Messages::LIST_RESPONSE]) }
146 specify { expect(json[:totalResults]).to be(users.count + 1) }
147 specify { expect(json[:startIndex]).to be(1) }
148 specify { expect(json[:itemsPerPage]).to be(1) }
149 specify { expect(json[:Resources][0][:id]).to eql(User.order(:created_at).offset(0).limit(1).pluck(:id).first) }
150 end
151
152 context "when requesting a negative page of results" do
153 before { get "/scim/v2/users", params: { startIndex: -1 }, headers: headers }
154
155 specify { expect(response).to have_http_status(:ok) }
156 specify { expect(json[:totalResults]).to be(users.count + 1) }
157 specify { expect(json[:startIndex]).to be(1) }
158 specify { expect(json[:itemsPerPage]).to be(25) }
159 specify { expect(json[:Resources]).to be_present }
160 end
161
162 context "when requesting a page 0" do
163 before { get "/scim/v2/users", params: { startIndex: 0 }, headers: headers }
164
165 specify { expect(response).to have_http_status(:ok) }
166 specify { expect(json[:totalResults]).to be(users.count + 1) }
167 specify { expect(json[:startIndex]).to be(1) }
168 specify { expect(json[:itemsPerPage]).to be(25) }
169 specify { expect(json[:Resources]).to be_present }
170 end
171
172 context "when requesting an invalid page" do
173 before { get "/scim/v2/users", params: { startIndex: 'x' }, headers: headers }
174
175 specify { expect(response).to have_http_status(:ok) }
176 specify { expect(json[:totalResults]).to be(users.count + 1) }
177 specify { expect(json[:startIndex]).to be(1) }
178 specify { expect(json[:itemsPerPage]).to be(25) }
179 specify { expect(json[:Resources]).to be_present }
180 end
181
182 context "when requesting a page size of 0 results" do
183 before { get "/scim/v2/users", params: { count: 0 }, headers: headers }
184
185 specify { expect(response).to have_http_status(:ok) }
186 specify { expect(json[:schemas]).to match_array([Scim::Kit::V2::Messages::LIST_RESPONSE]) }
187 specify { expect(json[:totalResults]).to be(users.count + 1) }
188 specify { expect(json[:startIndex]).to be(1) }
189 specify { expect(json[:itemsPerPage]).to be(0) }
190 specify { expect(json[:Resources]).to be_empty }
191 end
192
193 context "when requesting a page size of negative results" do
194 before { get "/scim/v2/users", params: { count: -1 }, headers: headers }
195
196 specify { expect(response).to have_http_status(:ok) }
197 specify { expect(json[:totalResults]).to be(users.count + 1) }
198 specify { expect(json[:startIndex]).to be(1) }
199 specify { expect(json[:itemsPerPage]).to be(0) }
200 specify { expect(json[:Resources]).to be_empty }
201 end
202 end
203
204 context "when searching for users" do
205 let!(:users) { create_list(:user, 10) }
206
207 context "when searching for an exact match on one field" do
208 let(:matching_user) { users.sample }
209
210 before { get "/scim/v2/users", params: { filter: "userName eq \"#{matching_user.email}\"" }, headers: headers }
211
212 specify { expect(response).to have_http_status(:ok) }
213 specify { expect(json[:totalResults]).to be(1) }
214 specify { expect(json[:startIndex]).to be(1) }
215 specify { expect(json[:itemsPerPage]).to be(25) }
216 specify { expect(json[:Resources]).to be_present }
217 specify { expect(json[:Resources][0][:id]).to eql(matching_user.to_param) }
218 end
219
220 context "when searhing for a match using a logical or condition" do
221 let(:first_matching_user) { users.first }
222 let(:second_matching_user) { users.last }
223 let(:filter) do
224 %(userName eq "#{first_matching_user.email}" or userName eq "#{second_matching_user.email}")
225 end
226
227 before { get "/scim/v2/users", params: { filter: filter }, headers: headers }
228
229 specify { expect(response).to have_http_status(:ok) }
230 specify { expect(json[:totalResults]).to be(2) }
231 specify { expect(json[:startIndex]).to be(1) }
232 specify { expect(json[:itemsPerPage]).to be(25) }
233 specify { expect(json[:Resources]).to be_present }
234 specify { expect(json[:Resources].map { |x| x[:id] }.uniq).to match_array([first_matching_user.to_param, second_matching_user.to_param].uniq) }
235 end
236 end
237
238 xcontext "when fetching specific attributes" do
239 let!(:user) { create(:user) }
240 let(:json) { JSON.parse(response.body, symbolize_names: true) }
241
242 before { get "/scim/v2/users?attributes=userName", headers: headers }
243
244 specify { expect(response).to have_http_status(:ok) }
245 specify { expect(response.headers['Content-Type']).to eql('application/scim+json') }
246 specify { expect(response.body).to be_present }
247 specify { expect(json[:schemas]).to match_array([Scim::Kit::V2::Messages::LIST_RESPONSE]) }
248 specify { expect(json[:totalResults]).to be(1) }
249 specify { expect(json[:Resources]).not_to be_empty }
250 specify { expect(json[:Resources][0]).to eql(id: user.to_param, displayName: user.email) }
251 end
252 end
253
254 describe "PUT /scim/v2/users" do
255 let(:user) { create(:user) }
256 let(:new_email) { FFaker::Internet.email }
257 let(:locale) { 'ja' }
258 let(:timezone) { 'America/Denver' }
259 let(:body) { { schemas: [Scim::Kit::V2::Schemas::USER], userName: new_email, locale: locale, timezone: timezone } }
260 let(:json) { JSON.parse(response.body, symbolize_names: true) }
261
262 before { put "/scim/v2/users/#{user.to_param}", headers: headers, params: body.to_json }
263
264 specify { expect(response).to have_http_status(:ok) }
265 specify { expect(response.headers['Content-Type']).to eql('application/scim+json') }
266 specify { expect(response.headers['Location']).to eql(scim_v2_user_url(user)) }
267 specify { expect(response.body).to be_present }
268 specify { expect(json[:schemas]).to match_array([Scim::Kit::V2::Schemas::USER]) }
269 specify { expect(json[:id]).to be_present }
270 specify { expect(json[:userName]).to eql(new_email) }
271 specify { expect(json[:meta][:resourceType]).to eql('User') }
272 specify { expect(json[:meta][:created]).to be_present }
273 specify { expect(json[:meta][:lastModified]).to be_present }
274 specify { expect(json[:meta][:version]).to be_present }
275 specify { expect(json[:meta][:location]).to be_present }
276 specify { expect(json[:emails]).to match_array([value: new_email, primary: true]) }
277 specify { expect(json[:locale]).to eql(locale) }
278 specify { expect(json[:timezone]).to eql(timezone) }
279 end
280
281 describe "DELETE /scim/v2/users/:id" do
282 let(:other_user) { create(:user) }
283
284 context "when the user can be deleted" do
285 before { delete "/scim/v2/users/#{other_user.to_param}", headers: headers }
286
287 specify { expect(response).to have_http_status(:no_content) }
288
289 specify do
290 get "/scim/v2/users/#{other_user.to_param}", headers: headers
291 expect(response).to have_http_status(:not_found)
292 end
293 end
294 end
295end