Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions spec/dummy/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require_relative "config/application"

Rails.application.load_tasks
3 changes: 3 additions & 0 deletions spec/dummy/app/models/application_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
21 changes: 21 additions & 0 deletions spec/dummy/app/models/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class User < ApplicationRecord
has_many :escalated_tickets,
class_name: "Escalated::Ticket",
as: :requester,
dependent: :nullify

validates :name, presence: true
validates :email, presence: true, uniqueness: true

def escalated_agent?
role == "agent" || role == "admin"
end

def admin?
role == "admin"
end

def to_s
name
end
end
49 changes: 49 additions & 0 deletions spec/dummy/config/application.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require_relative "boot"

require "rails"
require "active_model/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "rails/test_unit/railtie"

Bundler.require(*Rails.groups)

require "escalated"

module Dummy
class Application < Rails::Application
config.load_defaults Rails::VERSION::STRING.to_f
config.eager_load = false

# Minimal configuration for testing
config.active_record.maintain_test_schema = false

# Configure Active Storage (required for Attachment model)
config.active_storage.service = :test if config.respond_to?(:active_storage)

# Action Mailer test config
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
end
end

# Configure Escalated for testing
Escalated.configure do |config|
config.mode = :self_hosted
config.user_class = "User"
config.table_prefix = "escalated_"
config.route_prefix = "support"
config.notification_channels = [] # Disable email notifications in tests
config.sla = {
enabled: true,
business_hours_only: false,
business_hours: {
start: 9,
end: 17,
timezone: "UTC",
working_days: [1, 2, 3, 4, 5]
}
}
end
4 changes: 4 additions & 0 deletions spec/dummy/config/boot.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Set up gems listed in the Gemfile.
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../../Gemfile", __dir__)

require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
11 changes: 11 additions & 0 deletions spec/dummy/config/database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
test:
adapter: sqlite3
database: ":memory:"
pool: 5
timeout: 5000

development:
adapter: sqlite3
database: ":memory:"
pool: 5
timeout: 5000
5 changes: 5 additions & 0 deletions spec/dummy/config/environment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Load the Rails application.
require_relative "application"

# Initialize the Rails application.
Rails.application.initialize!
3 changes: 3 additions & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Rails.application.routes.draw do
mount Escalated::Engine, at: "/support"
end
2 changes: 2 additions & 0 deletions spec/dummy/config/secrets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test:
secret_key_base: "test_secret_key_base_for_escalated_dummy_app_do_not_use_in_production_1234567890abcdef"
13 changes: 13 additions & 0 deletions spec/dummy/db/migrate/20240101000000_create_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false
t.string :role, default: "customer"

t.timestamps
end

add_index :users, :email, unique: true
end
end
15 changes: 15 additions & 0 deletions spec/factories/users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { Faker::Internet.unique.email }
role { "customer" }

trait :agent do
role { "agent" }
end

trait :admin do
role { "admin" }
end
end
end
216 changes: 216 additions & 0 deletions spec/models/escalated/canned_response_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
require "rails_helper"

RSpec.describe Escalated::CannedResponse, type: :model do
# ------------------------------------------------------------------ #
# Associations
# ------------------------------------------------------------------ #
describe "associations" do
it { is_expected.to belong_to(:creator).class_name("User").with_foreign_key(:created_by) }
end

# ------------------------------------------------------------------ #
# Validations
# ------------------------------------------------------------------ #
describe "validations" do
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:body) }

context "shortcode uniqueness" do
subject { create(:escalated_canned_response) }

it { is_expected.to validate_uniqueness_of(:shortcode).case_insensitive }
end

it "allows nil shortcode" do
response = build(:escalated_canned_response, shortcode: nil)
expect(response).to be_valid
end
end

# ------------------------------------------------------------------ #
# Scopes
# ------------------------------------------------------------------ #
describe "scopes" do
let(:agent) { create(:user, :agent) }
let(:other_agent) { create(:user, :agent) }

describe ".shared" do
it "returns only shared responses" do
shared = create(:escalated_canned_response, creator: agent, is_shared: true)
_personal = create(:escalated_canned_response, :personal, creator: agent)

result = described_class.shared
expect(result).to include(shared)
expect(result).not_to include(_personal)
end
end

describe ".personal" do
it "returns only personal responses" do
_shared = create(:escalated_canned_response, creator: agent, is_shared: true)
personal = create(:escalated_canned_response, :personal, creator: agent)

result = described_class.personal
expect(result).to include(personal)
expect(result).not_to include(_shared)
end
end

describe ".for_user" do
it "returns shared responses and user's personal responses" do
shared = create(:escalated_canned_response, creator: other_agent, is_shared: true)
own_personal = create(:escalated_canned_response, :personal, creator: agent)
_other_personal = create(:escalated_canned_response, :personal, creator: other_agent)

result = described_class.for_user(agent.id)
expect(result).to include(shared, own_personal)
expect(result).not_to include(_other_personal)
end
end

describe ".by_category" do
it "returns responses in a specific category" do
greeting = create(:escalated_canned_response, :greeting, creator: agent)
_closing = create(:escalated_canned_response, :closing, creator: agent)

result = described_class.by_category("greeting")
expect(result).to include(greeting)
expect(result).not_to include(_closing)
end
end

describe ".search" do
it "searches by title" do
response = create(:escalated_canned_response, title: "Password Reset Template", creator: agent)
_other = create(:escalated_canned_response, title: "Billing Question", creator: agent)

result = described_class.search("Password")
expect(result).to include(response)
expect(result).not_to include(_other)
end

it "searches by body" do
response = create(:escalated_canned_response, body: "Please reset your credentials", creator: agent)
_other = create(:escalated_canned_response, body: "Your invoice is attached", creator: agent)

result = described_class.search("credentials")
expect(result).to include(response)
expect(result).not_to include(_other)
end

it "searches by shortcode" do
response = create(:escalated_canned_response, shortcode: "pw_reset", creator: agent)
_other = create(:escalated_canned_response, shortcode: "billing_q", creator: agent)

result = described_class.search("pw_reset")
expect(result).to include(response)
expect(result).not_to include(_other)
end
end

describe ".ordered" do
it "returns responses ordered by title" do
beta = create(:escalated_canned_response, title: "Beta Template", creator: agent)
alpha = create(:escalated_canned_response, title: "Alpha Template", creator: agent)

result = described_class.ordered
expect(result.first).to eq(alpha)
expect(result.last).to eq(beta)
end
end
end

# ------------------------------------------------------------------ #
# Instance methods
# ------------------------------------------------------------------ #
describe "#shared?" do
it "returns true when shared" do
response = build(:escalated_canned_response, is_shared: true)
expect(response.shared?).to be(true)
end

it "returns false when not shared" do
response = build(:escalated_canned_response, is_shared: false)
expect(response.shared?).to be(false)
end
end

describe "#personal?" do
it "returns true when not shared" do
response = build(:escalated_canned_response, is_shared: false)
expect(response.personal?).to be(true)
end

it "returns false when shared" do
response = build(:escalated_canned_response, is_shared: true)
expect(response.personal?).to be(false)
end
end

describe "#render" do
let(:agent) { create(:user, :agent) }

it "replaces variables with provided values" do
response = build(:escalated_canned_response,
body: "Hello {{ticket.requester_name}}, regarding {{ticket.subject}}.",
creator: agent)

result = response.render(
"ticket.requester_name" => "John Doe",
"ticket.subject" => "Login Issue"
)

expect(result).to eq("Hello John Doe, regarding Login Issue.")
end

it "removes unmatched variables" do
response = build(:escalated_canned_response,
body: "Hello {{ticket.requester_name}}, {{unknown.variable}} here.",
creator: agent)

result = response.render("ticket.requester_name" => "Jane")
expect(result).to eq("Hello Jane, here.")
end

it "handles empty variables hash" do
response = build(:escalated_canned_response,
body: "Hello {{ticket.requester_name}}!",
creator: agent)

result = response.render({})
expect(result).to eq("Hello !")
end

it "renders body with multiple variables" do
response = build(:escalated_canned_response, :with_variables, creator: agent)

result = response.render(
"ticket.requester_name" => "Alice",
"ticket.subject" => "Password reset",
"agent.name" => "Bob"
)

expect(result).to include("Alice")
expect(result).to include("Password reset")
expect(result).to include("Bob")
end

it "does not modify the original body" do
response = build(:escalated_canned_response,
body: "Hello {{name}}",
creator: agent)

response.render("name" => "Test")
expect(response.body).to eq("Hello {{name}}")
end

it "converts non-string values to strings" do
response = build(:escalated_canned_response,
body: "Ticket #{{ticket.id}}",
creator: agent)

result = response.render("ticket.id" => 42)
expect(result).to eq("Ticket #42")
end
end
end
Loading