diff --git a/spec/integration/status_transitions_spec.rb b/spec/integration/status_transitions_spec.rb new file mode 100644 index 0000000..9b0fa22 --- /dev/null +++ b/spec/integration/status_transitions_spec.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +RSpec.describe "Support-ticket status transitions" do + before { enable_current_plugin } + + fab!(:admin) + fab!(:customer) { Fabricate(:user) } + fab!(:category) + + def create_ticket(user) + PostCreator.create!( + user, + title: "A support ticket title for the spec", + raw: "Initial ticket body, comfortably long enough.", + category: category.id, + skip_validations: true, + ).topic + end + + def reply!(user, topic, type: Post.types[:regular]) + PostCreator.create!( + user, + topic_id: topic.id, + raw: "A reply body, comfortably long enough.", + post_type: type, + skip_validations: true, + ) + end + + def set_status!(topic, status, extra = {}) + { status: status }.merge(extra).each { |k, v| topic.custom_fields[k] = v } + topic.save_custom_fields + end + + def rows(topic) + CommunityCustomFields::TopicStatusChange.where(topic_id: topic.id).order(:id) + end + + describe "topic_created" do + it "seeds status=new for an admin-created topic and records no row" do + topic = create_ticket(admin) + + expect(topic.reload.custom_fields["status"]).to eq("new") + expect(rows(topic)).to be_empty + end + + it "sets waiting_* when a non-admin creates the topic" do + topic = create_ticket(customer) + + expect(topic.reload.custom_fields["status"]).to eq("new") + expect(topic.custom_fields["waiting_id"].to_i).to eq(customer.id) + expect(topic.custom_fields["waiting_since"]).to be_present + end + end + + describe "customer reply (non-admin)" do + it "reopens a snoozed topic to open and records a post_creation row" do + topic = create_ticket(admin) + set_status!(topic, "snoozed", assignee_id: customer.id) + + post = reply!(customer, topic) + + expect(topic.reload.custom_fields["status"]).to eq("open") + expect(rows(topic).last).to have_attributes( + from_status: "snoozed", + to_status: "open", + source: "post_creation", + post_id: post.id, + user_id: nil, + assignee_id: customer.id, + ) + end + + it "reopens a recently-closed assigned topic to open and reassigns" do + topic = create_ticket(admin) + set_status!( + topic, + "closed", + last_assigned_to_id: customer.id, + closed_at: Time.current.iso8601, + ) + + reply!(customer, topic) + + topic.reload + expect(topic.custom_fields["status"]).to eq("open") + expect(topic.custom_fields["assignee_id"].to_i).to eq(customer.id) + expect(rows(topic).last.to_status).to eq("open") + end + + it "reopens a closed topic with no last assignee to new" do + topic = create_ticket(admin) + set_status!(topic, "closed", closed_at: Time.current.iso8601) + + reply!(customer, topic) + + expect(topic.reload.custom_fields["status"]).to eq("new") + expect(rows(topic).last.to_status).to eq("new") + end + + it "reopens a >1-month-closed topic to new despite a last assignee" do + topic = create_ticket(admin) + set_status!( + topic, + "closed", + last_assigned_to_id: customer.id, + closed_at: 2.months.ago.iso8601, + ) + + reply!(customer, topic) + + expect(topic.reload.custom_fields["status"]).to eq("new") + end + + it "sets waiting_* and records no row when the status does not change" do + topic = create_ticket(admin) + set_status!(topic, "open") + + reply!(customer, topic) + + topic.reload + expect(topic.custom_fields["status"]).to eq("open") + expect(topic.custom_fields["waiting_id"].to_i).to eq(customer.id) + expect(rows(topic)).to be_empty + end + end + + describe "admin whisper (post_type 4)" do + before { SiteSetting.whispers_allowed_groups = Group::AUTO_GROUPS[:staff].to_s } + + it "reopens a snoozed topic to open" do + topic = create_ticket(admin) + set_status!(topic, "snoozed") + + reply!(admin, topic, type: Post.types[:whisper]) + + expect(topic.reload.custom_fields["status"]).to eq("open") + expect(rows(topic).last).to have_attributes( + from_status: "snoozed", + to_status: "open", + source: "post_creation", + ) + end + + it "reopens a closed assigned topic to open" do + topic = create_ticket(admin) + set_status!(topic, "closed", last_assigned_to_id: customer.id) + + reply!(admin, topic, type: Post.types[:whisper]) + + expect(topic.reload.custom_fields["status"]).to eq("open") + end + + it "reopens a closed topic with no last assignee to new" do + topic = create_ticket(admin) + set_status!(topic, "closed") + + reply!(admin, topic, type: Post.types[:whisper]) + + expect(topic.reload.custom_fields["status"]).to eq("new") + end + end + + describe "admin regular reply (post_type 1)" do + it "clears waiting_* and records no row" do + topic = create_ticket(admin) + set_status!(topic, "open", waiting_since: 1.hour.ago.iso8601, waiting_id: customer.id) + + reply!(admin, topic) + + topic.reload + expect(topic.custom_fields["status"]).to eq("open") + expect(topic.custom_fields["waiting_id"]).to be_blank + expect(rows(topic)).to be_empty + end + end +end diff --git a/spec/requests/community_custom_fields/custom_fields_controller_spec.rb b/spec/requests/community_custom_fields/custom_fields_controller_spec.rb new file mode 100644 index 0000000..91b3c97 --- /dev/null +++ b/spec/requests/community_custom_fields/custom_fields_controller_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +RSpec.describe "CommunityCustomFields::CustomFieldsController", type: :request do + before { enable_current_plugin } + + fab!(:admin) + fab!(:assignee) { Fabricate(:user) } + fab!(:topic) + + STATUSES = %w[new open snoozed closed] + + def set_status(topic, status, extra = {}) + { status: status }.merge(extra).each { |k, v| topic.custom_fields[k] = v } + topic.save_custom_fields + end + + def put_fields(topic, fields) + put "/admin/plugins/community-custom-fields/#{topic.id}.json", params: { custom_field: fields } + end + + def rows(topic) + CommunityCustomFields::TopicStatusChange.where(topic_id: topic.id).order(:id) + end + + context "as an admin" do + before { sign_in(admin) } + + STATUSES.each do |from| + STATUSES.each do |to| + next if from == to + + it "records an api_update row for #{from} -> #{to}" do + set_status(topic, from) + put_fields(topic, status: to) + + expect(response.status).to eq(200) + expect(topic.reload.custom_fields["status"]).to eq(to) + expect(rows(topic).count).to eq(1) + expect(rows(topic).last).to have_attributes( + from_status: from, + to_status: to, + source: "api_update", + user_id: admin.id, + post_id: nil, + ) + expect(rows(topic).last.duration).to be >= 0 + end + end + end + + it "records a from_status=null row when the topic had no prior status" do + expect(topic.custom_fields["status"]).to be_nil + + put_fields(topic, status: "open") + + expect(response.status).to eq(200) + expect(rows(topic).count).to eq(1) + expect(rows(topic).last).to have_attributes(from_status: nil, to_status: "open") + expect(rows(topic).last.duration).to be >= 0 + end + + it "rejects an invalid status with 422 and records nothing" do + set_status(topic, "open") + + put_fields(topic, status: "bogus") + + expect(response.status).to eq(422) + expect(topic.reload.custom_fields["status"]).to eq("open") + expect(rows(topic)).to be_empty + end + + it "records no row when the status does not change" do + set_status(topic, "open") + + put_fields(topic, priority: "high") + + expect(response.status).to eq(200) + expect(rows(topic)).to be_empty + end + + it "attributes the row to the assignee before the change" do + set_status(topic, "open", assignee_id: assignee.id) + + put_fields(topic, status: "closed", assignee_id: "") + + expect(response.status).to eq(200) + expect(rows(topic).last).to have_attributes(to_status: "closed", assignee_id: assignee.id) + end + + it "measures duration from the previous recorded change" do + freeze_time + set_status(topic, "open") + CommunityCustomFields::TopicStatusChange.create!( + topic_id: topic.id, + from_status: "new", + to_status: "open", + source: "api_update", + duration: 0, + created_at: 1.hour.ago, + ) + + put_fields(topic, status: "closed") + + expect(response.status).to eq(200) + expect(rows(topic).last.duration).to eq(1.hour.to_i) + end + end + + it "forbids non-admins and records nothing" do + sign_in(Fabricate(:user)) + + put_fields(topic, status: "open") + + expect(response.status).to eq(403) + expect(rows(topic)).to be_empty + end +end