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