diff --git a/spec/controllers/scim_rails/scim_users_controller_spec.rb b/spec/controllers/scim_rails/scim_users_controller_spec.rb new file mode 100644 index 0000000..fa0379d --- /dev/null +++ b/spec/controllers/scim_rails/scim_users_controller_spec.rb @@ -0,0 +1,445 @@ +require "spec_helper" + +RSpec.describe ScimRails::ScimUsersController, type: :controller do + include AuthHelper + + routes { ScimRails::Engine.routes } + + describe "index" do + let(:company) { create(:company) } + + context "when unauthorized" do + it "fails with no credentials" do + get :index + + expect(response.status).to eq 401 + end + + it "fails with invalid credentials" do + request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials("unauthorized","123456") + + get :index + + expect(response.status).to eq 401 + end + end + + context "when when authorized" do + before :each do + http_login(company) + end + + it "is successful with valid credentials" do + get :index + + expect(response.status).to eq 200 + end + + it "returns all results" do + create_list(:user, 10, company: company) + + get :index + response_body = JSON.parse(response.body) + expect(response_body.dig("schemas", 0)).to eq "urn:ietf:params:scim:api:messages:2.0:ListResponse" + expect(response_body["totalResults"]).to eq 10 + end + + it "defaults to 100 results" do + create_list(:user, 300, company: company) + + get :index + response_body = JSON.parse(response.body) + expect(response_body["totalResults"]).to eq 300 + expect(response_body["Resources"].count).to eq 100 + end + + it "paginates results" do + create_list(:user, 400, company: company) + expect(company.users.first.id).to eq 1 + + get :index, params: { + startIndex: 101, + count: 200, + } + response_body = JSON.parse(response.body) + expect(response_body["totalResults"]).to eq 400 + expect(response_body["Resources"].count).to eq 200 + expect(response_body.dig("Resources", 0, "id")).to eq 101 + end + + it "paginates results by configurable scim_users_list_order" do + ScimRails.config.scim_users_list_order = { created_at: :desc } + + create_list(:user, 400, company: company) + expect(company.users.first.id).to eq 1 + + get :index, params: { + startIndex: 1, + count: 10, + } + response_body = JSON.parse(response.body) + expect(response_body["totalResults"]).to eq 400 + expect(response_body["Resources"].count).to eq 10 + expect(response_body.dig("Resources", 0, "id")).to eq 400 + + ScimRails.config.scim_users_list_order = nil + end + + it "filters results by provided email filter" do + create(:user, email: "test1@example.com", company: company) + create(:user, email: "test2@example.com", company: company) + + get :index, params: { + filter: "email eq test1@example.com" + } + response_body = JSON.parse(response.body) + expect(response_body["totalResults"]).to eq 1 + expect(response_body["Resources"].count).to eq 1 + end + + it "filters results by provided name filter" do + create(:user, first_name: "Chidi", last_name: "Anagonye", company: company) + create(:user, first_name: "Eleanor", last_name: "Shellstrop", company: company) + + get :index, params: { + filter: "familyName eq Shellstrop" + } + response_body = JSON.parse(response.body) + expect(response_body["totalResults"]).to eq 1 + expect(response_body["Resources"].count).to eq 1 + end + + it "returns no results for unfound filter parameters" do + get :index, params: { + filter: "familyName eq fake_not_there" + } + response_body = JSON.parse(response.body) + expect(response_body["totalResults"]).to eq 0 + expect(response_body["Resources"].count).to eq 0 + end + + it "returns no results for undefined filter queries" do + get :index, params: { + filter: "address eq 101 Nowhere USA" + } + expect(response.status).to eq 400 + response_body = JSON.parse(response.body) + expect(response_body.dig("schemas", 0)).to eq "urn:ietf:params:scim:api:messages:2.0:Error" + end + end + end + + + describe "show" do + let(:company) { create(:company) } + + context "when unauthorized" do + it "fails with no credentials" do + get :show, params: { id: 1 } + + expect(response.status).to eq 401 + end + + it "fails with invalid credentials" do + request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials("unauthorized","123456") + + get :show, params: { id: 1 } + + expect(response.status).to eq 401 + end + end + + context "when authorized" do + before :each do + http_login(company) + end + + it "is successful with valid credentials" do + create(:user, id: 1, company: company) + get :show, params: { id: 1 } + + expect(response.status).to eq 200 + end + + it "returns :not_found for id that cannot be found" do + get :show, params: { id: "fake_id" } + + expect(response.status).to eq 404 + end + + it "returns :not_found for a correct id but unauthorized company" do + new_company = create(:company) + create(:user, company: new_company, id: 1) + + get :show, params: { id: 1 } + + expect(response.status).to eq 404 + end + end + end + + + describe "create" do + let(:company) { create(:company) } + + context "when unauthorized" do + it "fails with no credentials" do + post :create + + expect(response.status).to eq 401 + end + + it "fails with invalid credentials" do + request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials("unauthorized","123456") + + post :create + + expect(response.status).to eq 401 + end + end + + context "when authorized" do + before :each do + http_login(company) + end + + it "is successful with valid credentials" do + post :create, params: { + name: { + givenName: "New", + familyName: "User" + }, + emails: [ + { + value: "new@example.com" + } + ] + } + + expect(response.status).to eq 201 + expect(company.users.count).to eq 1 + user = company.users.first + expect(user.persisted?).to eq true + expect(user.first_name).to eq "New" + expect(user.last_name).to eq "User" + expect(user.email).to eq "new@example.com" + end + + it "ignores unconfigured params" do + post :create, params: { + name: { + formattedName: "New User", + givenName: "New", + familyName: "User" + }, + emails: [ + { + value: "new@example.com" + } + ] + } + + expect(response.status).to eq 201 + expect(company.users.count).to eq 1 + end + + it "returns 422 if required params are missing" do + post :create, params: { + name: { + familyName: "User" + }, + emails: [ + { + value: "new@example.com" + } + ] + } + + expect(response.status).to eq 422 + expect(company.users.count).to eq 0 + end + + it "returns 409 if user already exists" do + create(:user, email: "new@example.com", company: company) + + post :create, params: { + name: { + givenName: "New", + familyName: "User" + }, + emails: [ + { + value: "new@example.com" + } + ] + } + + expect(response.status).to eq 409 + expect(company.users.count).to eq 1 + end + + it "creates and archives inactive user" do + post :create, params: { + id: 1, + userName: "test@example.com", + name: { + givenName: "Test", + familyName: "User" + }, + emails: [ + { + value: "test@example.com" + }, + ], + active: "false" + } + + expect(response.status).to eq 201 + expect(company.users.count).to eq 1 + user = company.users.first + expect(user.archived?).to eq true + end + end + end + + + describe "put update" do + let(:company) { create(:company) } + + context "when unauthorized" do + it "fails with no credentials" do + put :put_update, params: { id: 1 } + + expect(response.status).to eq 401 + end + + it "fails with invalid credentials" do + request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials("unauthorized","123456") + + put :put_update, params: { id: 1 } + + expect(response.status).to eq 401 + end + end + + context "when authorized" do + let!(:user) { create(:user, id: 1, company: company) } + + before :each do + http_login(company) + end + + it "is successful with with valid credentials" do + put :put_update, params: { + id: 1, + userName: "test@example.com", + name: { + givenName: "Test", + familyName: "User" + }, + emails: [ + { + value: "test@example.com" + }, + ], + active: "true" + } + + expect(response.status).to eq 200 + end + + it "returns :not_found for id that cannot be found" do + get :put_update, params: { id: "fake_id" } + + expect(response.status).to eq 404 + end + + it "returns :not_found for a correct id but unauthorized company" do + new_company = create(:company) + create(:user, company: new_company, id: 1000) + + get :put_update, params: { id: 1000 } + + expect(response.status).to eq 404 + end + + it "is returns 422 with incomplete request" do + put :put_update, params: { + id: 1, + userName: "test@example.com", + emails: [ + { + value: "test@example.com" + }, + ], + active: "true" + } + + expect(response.status).to eq 422 + end + end + end + + + describe "patch update" do + let(:company) { create(:company) } + + context "when unauthorized" do + it "fails with no credentials" do + patch :patch_update, params: { id: 1 } + + expect(response.status).to eq 401 + end + + it "fails with invalid credentials" do + request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials("unauthorized","123456") + + patch :patch_update, params: { id: 1 } + + expect(response.status).to eq 401 + end + end + + context "when authorized" do + let!(:user) { create(:user, id: 1, company: company) } + + before :each do + http_login(company) + end + + it "is successful with valid credentials" do + patch :patch_update, params: { id: 1 } + + expect(response.status).to eq 200 + end + + it "returns :not_found for id that cannot be found" do + get :patch_update, params: { id: "fake_id" } + + expect(response.status).to eq 404 + end + + it "returns :not_found for a correct id but unauthorized company" do + new_company = create(:company) + create(:user, company: new_company, id: 1000) + + get :patch_update, params: { id: 1000 } + + expect(response.status).to eq 404 + end + + it "successfully archives user" do + expect(company.users.count).to eq 1 + user = company.users.first + expect(user.archived?).to eq false + + patch :patch_update, params: { id: 1 } + + expect(response.status).to eq 200 + expect(company.users.count).to eq 1 + user.reload + expect(user.archived?).to eq true + end + end + end +end diff --git a/spec/dummy/app/models/company.rb b/spec/dummy/app/models/company.rb new file mode 100644 index 0000000..b3f5fc0 --- /dev/null +++ b/spec/dummy/app/models/company.rb @@ -0,0 +1,3 @@ +class Company < ApplicationRecord + has_many :users +end diff --git a/spec/dummy/app/models/user.rb b/spec/dummy/app/models/user.rb new file mode 100644 index 0000000..b617385 --- /dev/null +++ b/spec/dummy/app/models/user.rb @@ -0,0 +1,37 @@ +class User < ApplicationRecord + belongs_to :company + + validates \ + :first_name, + :last_name, + :email, + presence: true + + validates \ + :email, + uniqueness: { + case_insensitive: true + } + + def active? + archived_at.blank? + end + + def archived? + archived_at.present? + end + + def archive! + write_attribute(:archived_at, Time.now) + save! + end + + def unarchived? + archived_at.blank? + end + + def unarchive! + write_attribute(:archived_at, nil) + save! + end +end diff --git a/spec/dummy/config/initializers/scim_rails_config.rb b/spec/dummy/config/initializers/scim_rails_config.rb new file mode 100644 index 0000000..0a012e1 --- /dev/null +++ b/spec/dummy/config/initializers/scim_rails_config.rb @@ -0,0 +1,53 @@ +ScimRails.configure do |config| + config.basic_auth_model = "Company" + config.scim_users_model = "User" + + config.basic_auth_model_searchable_attribute = :subdomain + config.basic_auth_model_authenticatable_attribute = :api_token + config.scim_users_scope = :users + config.scim_users_list_order = :id + + config.user_deprovision_method = :archive! + config.user_reprovision_method = :unarchive! + + config.mutable_user_attributes = [ + :first_name, + :last_name, + :email + ] + + config.queryable_user_attributes = { + userName: :email, + givenName: :first_name, + familyName: :last_name, + email: :email + } + + config.mutable_user_attributes_schema = { + name: { + givenName: :first_name, + familyName: :last_name + }, + emails: [ + { + value: :email + } + ] + } + + config.user_schema = { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], + id: :id, + userName: :email, + name: { + givenName: :first_name, + familyName: :last_name + }, + emails: [ + { + value: :email + }, + ], + active: :unarchived? + } +end diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb new file mode 100644 index 0000000..1af9685 --- /dev/null +++ b/spec/dummy/config/routes.rb @@ -0,0 +1,3 @@ +Rails.application.routes.draw do + mount ScimRails::Engine => "/scim_rails" +end diff --git a/spec/dummy/db/migrate/20181206184304_create_users.rb b/spec/dummy/db/migrate/20181206184304_create_users.rb new file mode 100644 index 0000000..68a00ab --- /dev/null +++ b/spec/dummy/db/migrate/20181206184304_create_users.rb @@ -0,0 +1,15 @@ +class CreateUsers < ActiveRecord::Migration[5.0] + def change + create_table :users do |t| + t.string :first_name, null: false + t.string :last_name, null: false + t.string :email, null: false + + t.integer :company_id + + t.timestamp :archived_at + + t.timestamps null: false + end + end +end diff --git a/spec/dummy/db/migrate/20181206184313_create_companies.rb b/spec/dummy/db/migrate/20181206184313_create_companies.rb new file mode 100644 index 0000000..b997bd0 --- /dev/null +++ b/spec/dummy/db/migrate/20181206184313_create_companies.rb @@ -0,0 +1,11 @@ +class CreateCompanies < ActiveRecord::Migration[5.0] + def change + create_table :companies do |t| + t.string :name, null: false + t.string :subdomain, null: false + t.string :api_token, null: false + + t.timestamps null: false + end + end +end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb new file mode 100644 index 0000000..395e9b8 --- /dev/null +++ b/spec/dummy/db/schema.rb @@ -0,0 +1,33 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 20181206184313) do + + create_table "companies", force: :cascade do |t| + t.string "name", null: false + t.string "subdomain", null: false + t.string "api_token", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "users", force: :cascade do |t| + t.string "first_name", null: false + t.string "last_name", null: false + t.string "email", null: false + t.integer "company_id" + t.datetime "archived_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + +end diff --git a/spec/dummy/db/seeds.rb b/spec/dummy/db/seeds.rb new file mode 100644 index 0000000..d6cc891 --- /dev/null +++ b/spec/dummy/db/seeds.rb @@ -0,0 +1,14 @@ +company = Company.create( + name: "Test Company", + subdomain: "test_company", + api_token: 1 +) + +1.upto(1000) do |n| + User.create( + company: company, + first_name: "Test#{n}", + last_name: "User#{n}", + email: "#{n}@example.com" + ) +end diff --git a/spec/factories/company.rb b/spec/factories/company.rb new file mode 100644 index 0000000..77129fc --- /dev/null +++ b/spec/factories/company.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :company do + name { "Test Company" } + subdomain { "test" } + api_token { "1" } + end +end diff --git a/spec/factories/user.rb b/spec/factories/user.rb new file mode 100644 index 0000000..ce67048 --- /dev/null +++ b/spec/factories/user.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :user do + company + + first_name { "Test" } + last_name { "User" } + sequence(:email) { |n| "#{n}@example.com" } + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..e538fe2 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,17 @@ +ENV['RAILS_ENV'] = 'test' + +require File.expand_path("../dummy/config/environment.rb", __FILE__) +require 'rspec/rails' +require 'factory_bot_rails' + +Rails.backtrace_cleaner.remove_silencers! + +# Load support files +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } + +RSpec.configure do |config| + config.mock_with :rspec + config.use_transactional_fixtures = true + config.infer_base_class_for_anonymous_controllers = false + config.order = "random" +end diff --git a/spec/support/auth_helper.rb b/spec/support/auth_helper.rb new file mode 100644 index 0000000..a329ffe --- /dev/null +++ b/spec/support/auth_helper.rb @@ -0,0 +1,7 @@ +module AuthHelper + def http_login(company) + user = company.subdomain + password = company.api_token + request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user,password) + end +end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 0000000..c7890e4 --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end -- libgit2 0.21.0