diff --git a/spec/factories/roles.rb b/spec/factories/roles.rb new file mode 100644 index 0000000..8903531 --- /dev/null +++ b/spec/factories/roles.rb @@ -0,0 +1,35 @@ +FactoryBot.define do + factory :role do + sequence(:key) { |n| "role_#{n}" } + sequence(:name) { |n| "Role #{n}" } + + trait :administrator do + key { 'administrator' } + name { 'Administrator' } + end + + trait :super_admin do + key { 'super_admin' } + name { 'Super Admin' } + end + + trait :account_owner do + key { 'account_owner' } + name { 'Account Owner' } + end + + trait :agent do + key { 'agent' } + name { 'Agent' } + end + end + + factory :user_role do + association :user + association :role + + trait :granted_by do + association :granted_by, factory: :user + end + end +end diff --git a/spec/mailers/administrator_notifications/base_mailer_spec.rb b/spec/mailers/administrator_notifications/base_mailer_spec.rb new file mode 100644 index 0000000..a0c76b7 --- /dev/null +++ b/spec/mailers/administrator_notifications/base_mailer_spec.rb @@ -0,0 +1,69 @@ +RSpec.describe AdministratorNotifications::BaseMailer, type: :mailer do + describe '#admin_emails' do + let!(:admin_user) { create(:user, email: 'admin@example.com') } + let!(:super_admin_user) { create(:user, email: 'super_admin@example.com') } + let!(:agent_user) { create(:user, email: 'agent@example.com') } + let!(:admin_role) { create(:role, key: 'administrator') } + let!(:super_admin_role) { create(:role, key: 'super_admin') } + let!(:agent_role) { create(:role, key: 'agent') } + + before do + admin_user.roles << admin_role + super_admin_user.roles << super_admin_role + agent_user.roles << agent_role + end + + it 'returns emails of admin users' do + mailer_class = described_class.new + admin_emails = mailer_class.send(:admin_emails) + + expect(admin_emails).to match_array(['admin@example.com', 'super_admin@example.com']) + end + + it 'does not include agent users' do + mailer_class = described_class.new + admin_emails = mailer_class.send(:admin_emails) + + expect(admin_emails).not_to include('agent@example.com') + end + end + + describe '#send_notification' do + let!(:admin_user) { create(:user, email: 'admin@example.com') } + let!(:admin_role) { create(:role, key: 'administrator') } + + before do + admin_user.roles << admin_role + allow(ENV).to receive(:fetch).with('FRONTEND_URL', nil).and_return('http://example.com') + end + + it 'sends notification to admin emails' do + mail = described_class.send_notification( + 'Test Subject', + action_url: 'http://example.com/action', + meta: { key: 'value' } + ) + + expect(mail.to).to include('admin@example.com') + end + + it 'includes action URL in action_url variable' do + mail = described_class.send_notification( + 'Test Subject', + action_url: 'http://example.com/action' + ) + + expect(mail.body.encoded).to include('http://example.com/action') + end + + it 'includes meta data' do + mail = described_class.send_notification( + 'Test Subject', + action_url: 'http://example.com/action', + meta: { key: 'value' } + ) + + expect(mail.body.encoded).to include('value') + end + end +end diff --git a/spec/migrations/20241020000100_optimize_contacts_performance_spec.rb b/spec/migrations/20241020000100_optimize_contacts_performance_spec.rb new file mode 100644 index 0000000..3585e27 --- /dev/null +++ b/spec/migrations/20241020000100_optimize_contacts_performance_spec.rb @@ -0,0 +1,87 @@ +require 'rails_helper' +require_relative '../../db/migrate/20241020000100_optimize_contacts_performance' + +RSpec.describe OptimizeContactsPerformance, type: :migration do + let(:migration) { described_class.new } + + describe '#up' do + it 'creates idx_contact_inboxes_contact_id partial index' do + migration.up + + expect(ActiveRecord::Base.connection.index_exists?( + :contact_inboxes, + :contact_id, + name: 'idx_contact_inboxes_contact_id', + where: 'contact_id IS NOT NULL' + )).to be true + end + + it 'creates idx_contacts_with_identity partial index' do + migration.up + + expect(ActiveRecord::Base.connection.index_exists?( + :contacts, + :id, + name: 'idx_contacts_with_identity', + where: "(email <> '' OR phone_number <> '' OR identifier <> '')" + )).to be true + end + + context 'when contacts.type column does not exist' do + before do + # Ensure type column doesn't exist + ActiveRecord::Base.connection.remove_column(:contacts, :type) if ActiveRecord::Base.connection.column_exists?(:contacts, :type) + end + + it 'skips creating idx_contacts_name_type_resolved without error' do + expect { migration.up }.not_to raise_error + end + end + + context 'when contacts.type column exists' do + before do + # Add type column if it doesn't exist + unless ActiveRecord::Base.connection.column_exists?(:contacts, :type) + ActiveRecord::Base.connection.add_column(:contacts, :type, :string) + end + end + + after do + ActiveRecord::Base.connection.remove_column(:contacts, :type) if ActiveRecord::Base.connection.column_exists?(:contacts, :type) + end + + it 'creates idx_contacts_name_type_resolved composite index' do + migration.up + + expect(ActiveRecord::Base.connection.index_exists?( + :contacts, + %i[name type id], + name: 'idx_contacts_name_type_resolved', + where: "(email <> '' OR phone_number <> '' OR identifier <> '')" + )).to be true + end + end + end + + describe '#down' do + before do + migration.up + end + + it 'removes all created indexes' do + migration.down + + expect(ActiveRecord::Base.connection.index_exists?( + :contact_inboxes, + :contact_id, + name: 'idx_contact_inboxes_contact_id' + )).to be false + + expect(ActiveRecord::Base.connection.index_exists?( + :contacts, + :id, + name: 'idx_contacts_with_identity' + )).to be false + end + end +end diff --git a/spec/migrations/20251117132621_add_type_to_contacts_spec.rb b/spec/migrations/20251117132621_add_type_to_contacts_spec.rb new file mode 100644 index 0000000..4879986 --- /dev/null +++ b/spec/migrations/20251117132621_add_type_to_contacts_spec.rb @@ -0,0 +1,65 @@ +require 'rails_helper' +require_relative '../../db/migrate/20251117132621_add_type_to_contacts' + +RSpec.describe AddTypeToContacts, type: :migration do + let(:migration) { described_class.new } + + describe '#up' do + it 'adds type column to contacts' do + migration.up + + expect(ActiveRecord::Base.connection.column_exists?(:contacts, :type)).to be true + end + + it 'creates contact_type_enum type' do + migration.up + + types = ActiveRecord::Base.connection.query('SELECT unnest(enum_range(NULL::contact_type_enum))') + expect(types.flatten).to match_array(['person', 'company']) + end + + it 'sets default value to person' do + migration.up + + column = ActiveRecord::Base.connection.columns(:contacts).find { |c| c.name == 'type' } + expect(column.default).to eq('person') + end + + it 'creates index on type column' do + migration.up + + expect(ActiveRecord::Base.connection.index_exists?(:contacts, :type)).to be true + end + + it 'backfills idx_contacts_name_type_resolved index' do + migration.up + + expect(ActiveRecord::Base.connection.index_exists?( + :contacts, + %i[name type id], + name: 'idx_contacts_name_type_resolved', + where: "(email <> '' OR phone_number <> '' OR identifier <> '')" + )).to be true + end + end + + describe '#down' do + before do + migration.up + end + + it 'removes type column' do + migration.down + + expect(ActiveRecord::Base.connection.column_exists?(:contacts, :type)).to be false + end + + it 'drops contact_type_enum type' do + migration.down + + expect { + ActiveRecord::Base.connection.query('SELECT unnest(enum_range(NULL::contact_type_enum))') + }.to raise_error(ActiveRecord::StatementInvalid, /type "contact_type_enum" does not exist/) + end + end +end diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb new file mode 100644 index 0000000..0d32a2f --- /dev/null +++ b/spec/models/role_spec.rb @@ -0,0 +1,80 @@ +RSpec.describe Role, type: :model do + describe 'associations' do + it { should have_many(:user_roles).dependent(:destroy_async) } + it { should have_many(:users).through(:user_roles) } + end + + describe 'validations' do + it { should validate_presence_of(:key) } + it { should validate_presence_of(:name) } + it { should validate_uniqueness_of(:key) } + end + + describe '.ADMIN_ROLE_KEYS' do + it 'includes super_admin, account_owner, administrator, admin' do + expect(Role::ADMIN_ROLE_KEYS).to match_array(%w[super_admin account_owner administrator admin]) + end + end + + describe '#administrator?' do + context 'when role key is in ADMIN_ROLE_KEYS' do + it 'returns true for super_admin' do + role = build(:role, key: 'super_admin') + expect(role.administrator?).to be true + end + + it 'returns true for administrator' do + role = build(:role, key: 'administrator') + expect(role.administrator?).to be true + end + + it 'returns true for account_owner' do + role = build(:role, key: 'account_owner') + expect(role.administrator?).to be true + end + + it 'returns true for admin' do + role = build(:role, key: 'admin') + expect(role.administrator?).to be true + end + end + + context 'when role key is not in ADMIN_ROLE_KEYS' do + it 'returns false for agent role' do + role = build(:role, key: 'agent') + expect(role.administrator?).to be false + end + end + end + + describe '.administrator_role' do + let!(:admin_role) { create(:role, key: 'administrator') } + let!(:agent_role) { create(:role, key: 'agent') } + + it 'returns first matching admin role' do + expect(Role.administrator_role).to eq(admin_role) + end + end + + describe '.administrator_users' do + let!(:admin_role) { create(:role, key: 'administrator') } + let!(:super_admin_role) { create(:role, key: 'super_admin') } + let!(:admin_user) { create(:user) } + let!(:super_admin_user) { create(:user) } + let!(:agent_user) { create(:user) } + + before do + admin_user.roles << admin_role + super_admin_user.roles << super_admin_role + agent_user.roles << create(:role, key: 'agent') + end + + it 'returns all users with admin roles' do + expect(Role.administrator_users).to match_array([admin_user, super_admin_user]) + end + + it 'does not include agent users' do + expect(Role.administrator_users).not_to include(agent_user) + end + end +end diff --git a/spec/models/user_role_spec.rb b/spec/models/user_role_spec.rb new file mode 100644 index 0000000..9f935a5 --- /dev/null +++ b/spec/models/user_role_spec.rb @@ -0,0 +1,14 @@ +RSpec.describe UserRole, type: :model do + describe 'associations' do + it { should belong_to(:user) } + it { should belong_to(:role) } + it { should belong_to(:granted_by).class_name('User').optional } + end + + describe 'validations' do + subject { build(:user_role) } + + it { should validate_presence_of(:user) } + it { should validate_presence_of(:role) } + end +end