From 0562647ab188214d3dc794e68fc9f58f9929f7a9 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 25 Aug 2025 13:57:06 +0530 Subject: [PATCH] feat: add specs --- .../v1/accounts/saml_settings_controller.rb | 5 +- .../accounts/saml_settings_controller_spec.rb | 245 ++++++++++++++++++ spec/factories/account_saml_settings.rb | 37 +++ 3 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 spec/enterprise/controllers/api/v1/accounts/saml_settings_controller_spec.rb create mode 100644 spec/factories/account_saml_settings.rb diff --git a/enterprise/app/controllers/api/v1/accounts/saml_settings_controller.rb b/enterprise/app/controllers/api/v1/accounts/saml_settings_controller.rb index 1e06919a4..d958c8cd5 100644 --- a/enterprise/app/controllers/api/v1/accounts/saml_settings_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/saml_settings_controller.rb @@ -6,7 +6,7 @@ class Api::V1::Accounts::SamlSettingsController < Api::V1::Accounts::BaseControl def show; end def create - @saml_settings = Current.account.build_saml_settings(saml_settings_params) + @saml_settings = AccountSamlSettings.new(saml_settings_params.merge(account: Current.account)) @saml_settings.save! end @@ -22,7 +22,8 @@ class Api::V1::Accounts::SamlSettingsController < Api::V1::Accounts::BaseControl private def set_saml_settings - @saml_settings = Current.account.saml_settings || Current.account.build_saml_settings + @saml_settings = AccountSamlSettings.find_by(account: Current.account) || + AccountSamlSettings.new(account: Current.account) end def saml_settings_params diff --git a/spec/enterprise/controllers/api/v1/accounts/saml_settings_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/saml_settings_controller_spec.rb new file mode 100644 index 000000000..239bda3fc --- /dev/null +++ b/spec/enterprise/controllers/api/v1/accounts/saml_settings_controller_spec.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::V1::Accounts::SamlSettings', type: :request do + let(:account) { create(:account) } + let(:agent) { create(:user, account: account, role: :agent) } + let(:administrator) { create(:user, account: account, role: :administrator) } + + before do + account.enable_features('saml') + account.save! + end + + def json_response + JSON.parse(response.body, symbolize_names: true) + end + + describe 'GET /api/v1/accounts/{account.id}/saml_settings' do + context 'when unauthenticated' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/saml_settings" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated as administrator' do + context 'when SAML settings exist' do + let!(:saml_settings) do + create(:account_saml_settings, + account: account, + enabled: true, + sso_url: 'https://idp.example.com/saml/sso', + certificate_fingerprint: 'AA:BB:CC:DD', + attribute_mappings: { email: 'emailAddress' }, + role_mappings: { 'Admins' => { 'role' => 1 } }) + end + + it 'returns the SAML settings' do + get "/api/v1/accounts/#{account.id}/saml_settings", + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(json_response[:enabled]).to eq(true) + expect(json_response[:sso_url]).to eq('https://idp.example.com/saml/sso') + expect(json_response[:role_mappings]).to eq({ Admins: { role: 1 } }) + end + end + + context 'when SAML settings do not exist' do + it 'returns default SAML settings' do + get "/api/v1/accounts/#{account.id}/saml_settings", + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(json_response[:enabled]).to eq(false) + expect(json_response[:attribute_mappings]).to eq({}) + expect(json_response[:role_mappings]).to eq({}) + end + end + end + + context 'when authenticated as agent' do + it 'returns unauthorized' do + get "/api/v1/accounts/#{account.id}/saml_settings", + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when SAML feature is not enabled' do + before do + account.disable_features('saml') + account.save! + end + + it 'returns forbidden with feature not enabled message' do + get "/api/v1/accounts/#{account.id}/saml_settings", + headers: administrator.create_new_auth_token + + expect(response).to have_http_status(:forbidden) + expect(json_response[:error]).to eq('SAML feature not enabled for this account') + end + end + end + + describe 'POST /api/v1/accounts/{account.id}/saml_settings' do + let(:valid_params) do + { + saml_settings: { + enabled: true, + sso_url: 'https://idp.example.com/saml/sso', + certificate_fingerprint: 'AA:BB:CC:DD:EE:FF', + sp_entity_id: 'chatwoot-production', + enforced_sso: false, + attribute_mappings: { email: 'emailAddress', name: 'displayName' }, + role_mappings: { 'Admins' => { 'role' => 1 }, 'Users' => { 'role' => 0 } } + } + } + end + + context 'when unauthenticated' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/saml_settings", params: valid_params + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated as administrator' do + context 'with valid parameters' do + it 'creates SAML settings' do + expect do + post "/api/v1/accounts/#{account.id}/saml_settings", + params: valid_params, + headers: administrator.create_new_auth_token, + as: :json + end.to change(AccountSamlSettings, :count).by(1) + + expect(response).to have_http_status(:success) + + saml_settings = AccountSamlSettings.find_by(account: account) + expect(saml_settings.enabled).to eq(true) + expect(saml_settings.sso_url).to eq('https://idp.example.com/saml/sso') + expect(saml_settings.role_mappings).to eq({ 'Admins' => { 'role' => 1 }, 'Users' => { 'role' => 0 } }) + end + end + + context 'with invalid parameters' do + let(:invalid_params) do + valid_params.tap do |params| + params[:saml_settings][:sso_url] = nil + end + end + + it 'returns unprocessable entity' do + post "/api/v1/accounts/#{account.id}/saml_settings", + params: invalid_params, + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unprocessable_entity) + expect(AccountSamlSettings.count).to eq(0) + end + end + end + + context 'when authenticated as agent' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/saml_settings", + params: valid_params, + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:unauthorized) + expect(AccountSamlSettings.count).to eq(0) + end + end + end + + describe 'PUT /api/v1/accounts/{account.id}/saml_settings' do + let!(:saml_settings) do + create(:account_saml_settings, + account: account, + enabled: false, + sso_url: 'https://old.example.com/saml') + end + + let(:update_params) do + { + saml_settings: { + enabled: true, + sso_url: 'https://new.example.com/saml/sso', + role_mappings: { 'NewGroup' => { 'custom_role_id' => 5 } } + } + } + end + + context 'when unauthenticated' do + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/saml_settings", params: update_params + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated as administrator' do + it 'updates SAML settings' do + put "/api/v1/accounts/#{account.id}/saml_settings", + params: update_params, + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + + saml_settings.reload + expect(saml_settings.enabled).to eq(true) + expect(saml_settings.sso_url).to eq('https://new.example.com/saml/sso') + expect(saml_settings.role_mappings).to eq({ 'NewGroup' => { 'custom_role_id' => 5 } }) + end + end + + context 'when authenticated as agent' do + it 'returns unauthorized' do + put "/api/v1/accounts/#{account.id}/saml_settings", + params: update_params, + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'DELETE /api/v1/accounts/{account.id}/saml_settings' do + let!(:saml_settings) { create(:account_saml_settings, account: account) } + + context 'when unauthenticated' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/saml_settings" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when authenticated as administrator' do + it 'destroys SAML settings' do + expect do + delete "/api/v1/accounts/#{account.id}/saml_settings", + headers: administrator.create_new_auth_token + end.to change(AccountSamlSettings, :count).by(-1) + + expect(response).to have_http_status(:no_content) + end + end + + context 'when authenticated as agent' do + it 'returns unauthorized' do + delete "/api/v1/accounts/#{account.id}/saml_settings", + headers: agent.create_new_auth_token + + expect(response).to have_http_status(:unauthorized) + expect(AccountSamlSettings.count).to eq(1) + end + end + end +end diff --git a/spec/factories/account_saml_settings.rb b/spec/factories/account_saml_settings.rb new file mode 100644 index 000000000..ee673bc12 --- /dev/null +++ b/spec/factories/account_saml_settings.rb @@ -0,0 +1,37 @@ +FactoryBot.define do + factory :account_saml_settings do + account + enabled { false } + sso_url { 'https://idp.example.com/saml/sso' } + certificate_fingerprint { 'AA:BB:CC:DD:EE:FF:11:22:33:44:55:66:77:88:99:00:AB:CD:EF:12' } + certificate { nil } + sp_entity_id { 'chatwoot-test' } + enforced_sso { false } + attribute_mappings { {} } + role_mappings { {} } + + trait :enabled do + enabled { true } + end + + trait :with_role_mappings do + role_mappings do + { + 'Administrators' => { 'role' => 1 }, + 'Agents' => { 'role' => 0 }, + 'Custom-Team' => { 'custom_role_id' => 5 } + } + end + end + + trait :with_attribute_mappings do + attribute_mappings do + { + 'email' => 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', + 'name' => 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', + 'first_name' => 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname' + } + end + end + end +end