diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb index c66f06909..bfdfff058 100644 --- a/app/controllers/api/v1/accounts/integrations/linear_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/linear_controller.rb @@ -1,5 +1,5 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController - before_action :fetch_conversation, only: [:link_issue, :linked_issues] + before_action :fetch_conversation, only: [:create_issue, :link_issue, :unlink_issue, :linked_issues] before_action :fetch_hook, only: [:destroy] def destroy @@ -31,6 +31,12 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas if issue[:error] render json: { error: issue[:error] }, status: :unprocessable_entity else + Linear::ActivityMessageService.new( + conversation: @conversation, + action_type: :issue_created, + issue_data: { id: issue[:data][:identifier] }, + user: Current.user + ).perform render json: issue[:data], status: :ok end end @@ -42,17 +48,30 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas if issue[:error] render json: { error: issue[:error] }, status: :unprocessable_entity else + Linear::ActivityMessageService.new( + conversation: @conversation, + action_type: :issue_linked, + issue_data: { id: issue_id }, + user: Current.user + ).perform render json: issue[:data], status: :ok end end def unlink_issue link_id = permitted_params[:link_id] + issue_id = permitted_params[:issue_id] issue = linear_processor_service.unlink_issue(link_id) if issue[:error] render json: { error: issue[:error] }, status: :unprocessable_entity else + Linear::ActivityMessageService.new( + conversation: @conversation, + action_type: :issue_unlinked, + issue_data: { id: issue_id }, + user: Current.user + ).perform render json: issue[:data], status: :ok end end diff --git a/app/javascript/dashboard/api/integrations/linear.js b/app/javascript/dashboard/api/integrations/linear.js index 2ac0940aa..bb327b7e8 100644 --- a/app/javascript/dashboard/api/integrations/linear.js +++ b/app/javascript/dashboard/api/integrations/linear.js @@ -33,9 +33,11 @@ class LinearAPI extends ApiClient { ); } - unlinkIssue(linkId) { + unlinkIssue(linkId, issueIdentifier, conversationId) { return axios.post(`${this.url}/unlink_issue`, { link_id: linkId, + issue_id: issueIdentifier, + conversation_id: conversationId, }); } diff --git a/app/javascript/dashboard/api/specs/integrations/linear.spec.js b/app/javascript/dashboard/api/specs/integrations/linear.spec.js index e4bf679a6..3f33e3ed9 100644 --- a/app/javascript/dashboard/api/specs/integrations/linear.spec.js +++ b/app/javascript/dashboard/api/specs/integrations/linear.spec.js @@ -91,6 +91,19 @@ describe('#linearAPI', () => { issueData ); }); + + it('creates a valid request with conversation_id', () => { + const issueData = { + title: 'New Issue', + description: 'Issue description', + conversation_id: 123, + }; + LinearAPIClient.createIssue(issueData); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/integrations/linear/create_issue', + issueData + ); + }); }); describe('link_issue', () => { @@ -120,6 +133,18 @@ describe('#linearAPI', () => { } ); }); + + it('creates a valid request with title', () => { + LinearAPIClient.link_issue(1, 'ENG-123', 'Sample Issue'); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/integrations/linear/link_issue', + { + issue_id: 'ENG-123', + conversation_id: 1, + title: 'Sample Issue', + } + ); + }); }); describe('getLinkedIssue', () => { @@ -164,12 +189,26 @@ describe('#linearAPI', () => { window.axios = originalAxios; }); - it('creates a valid request', () => { - LinearAPIClient.unlinkIssue(1); + it('creates a valid request with link_id only', () => { + LinearAPIClient.unlinkIssue('link123'); expect(axiosMock.post).toHaveBeenCalledWith( '/api/v1/integrations/linear/unlink_issue', { - link_id: 1, + link_id: 'link123', + issue_id: undefined, + conversation_id: undefined, + } + ); + }); + + it('creates a valid request with all parameters', () => { + LinearAPIClient.unlinkIssue('link123', 'ENG-456', 789); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/integrations/linear/unlink_issue', + { + link_id: 'link123', + issue_id: 'ENG-456', + conversation_id: 789, } ); }); diff --git a/app/javascript/dashboard/components/widgets/conversation/linear/CreateIssue.vue b/app/javascript/dashboard/components/widgets/conversation/linear/CreateIssue.vue index 5a276cc0f..9095b1bb8 100644 --- a/app/javascript/dashboard/components/widgets/conversation/linear/CreateIssue.vue +++ b/app/javascript/dashboard/components/widgets/conversation/linear/CreateIssue.vue @@ -183,13 +183,18 @@ const createIssue = async () => { state_id: formState.stateId || undefined, priority: formState.priority || undefined, label_ids: formState.labelId ? [formState.labelId] : undefined, + conversation_id: props.conversationId, }; try { isCreating.value = true; const response = await LinearAPI.createIssue(payload); - const { id: issueId } = response.data; - await LinearAPI.link_issue(props.conversationId, issueId, props.title); + const { identifier: issueIdentifier } = response.data; + await LinearAPI.link_issue( + props.conversationId, + issueIdentifier, + props.title + ); useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS')); useTrack(LINEAR_EVENTS.CREATE_ISSUE); onClose(); diff --git a/app/javascript/dashboard/components/widgets/conversation/linear/IssuesList.vue b/app/javascript/dashboard/components/widgets/conversation/linear/IssuesList.vue index 160394142..a1a2f2e63 100644 --- a/app/javascript/dashboard/components/widgets/conversation/linear/IssuesList.vue +++ b/app/javascript/dashboard/components/widgets/conversation/linear/IssuesList.vue @@ -46,9 +46,9 @@ const loadLinkedIssues = async () => { } }; -const unlinkIssue = async linkId => { +const unlinkIssue = async (linkId, issueIdentifier) => { try { - await LinearAPI.unlinkIssue(linkId); + await LinearAPI.unlinkIssue(linkId, issueIdentifier, props.conversationId); useTrack(LINEAR_EVENTS.UNLINK_ISSUE); linkedIssues.value = linkedIssues.value.filter( issue => issue.id !== linkId @@ -110,7 +110,7 @@ onMounted(() => { diff --git a/app/javascript/dashboard/components/widgets/conversation/linear/LinearIssueItem.vue b/app/javascript/dashboard/components/widgets/conversation/linear/LinearIssueItem.vue index e9d1ca500..10978da39 100644 --- a/app/javascript/dashboard/components/widgets/conversation/linear/LinearIssueItem.vue +++ b/app/javascript/dashboard/components/widgets/conversation/linear/LinearIssueItem.vue @@ -14,6 +14,8 @@ const props = defineProps({ const emit = defineEmits(['unlinkIssue']); +const { linkedIssue } = props; + const priorityMap = { 1: 'Urgent', 2: 'High', @@ -21,7 +23,7 @@ const priorityMap = { 4: 'Low', }; -const issue = computed(() => props.linkedIssue.issue); +const issue = computed(() => linkedIssue.issue); const assignee = computed(() => { const assigneeDetails = issue.value.assignee; @@ -37,7 +39,7 @@ const labels = computed(() => issue.value.labels?.nodes || []); const priorityLabel = computed(() => priorityMap[issue.value.priority]); const unlinkIssue = () => { - emit('unlinkIssue', props.linkedIssue.id); + emit('unlinkIssue', linkedIssue.id, linkedIssue.issue.identifier); }; diff --git a/app/javascript/dashboard/components/widgets/conversation/linear/LinkIssue.vue b/app/javascript/dashboard/components/widgets/conversation/linear/LinkIssue.vue index e1c8e2b6c..e3b69345a 100644 --- a/app/javascript/dashboard/components/widgets/conversation/linear/LinkIssue.vue +++ b/app/javascript/dashboard/components/widgets/conversation/linear/LinkIssue.vue @@ -63,7 +63,7 @@ const onSearch = async value => { isFetching.value = true; const response = await LinearAPI.searchIssues(value); issues.value = response.data.map(issue => ({ - id: issue.id, + id: issue.identifier, name: `${issue.identifier} ${issue.title}`, icon: 'status', iconColor: issue.state.color, diff --git a/app/services/linear/activity_message_service.rb b/app/services/linear/activity_message_service.rb new file mode 100644 index 000000000..671031f37 --- /dev/null +++ b/app/services/linear/activity_message_service.rb @@ -0,0 +1,41 @@ +class Linear::ActivityMessageService + attr_reader :conversation, :action_type, :issue_data, :user + + def initialize(conversation:, action_type:, user:, issue_data: {}) + @conversation = conversation + @action_type = action_type + @issue_data = issue_data + @user = user + end + + def perform + return unless conversation && issue_data[:id] && user + + content = generate_activity_content + return unless content + + ::Conversations::ActivityMessageJob.perform_later(conversation, activity_message_params(content)) + end + + private + + def generate_activity_content + case action_type.to_sym + when :issue_created + I18n.t('conversations.activity.linear.issue_created', user_name: user.name, issue_id: issue_data[:id]) + when :issue_linked + I18n.t('conversations.activity.linear.issue_linked', user_name: user.name, issue_id: issue_data[:id]) + when :issue_unlinked + I18n.t('conversations.activity.linear.issue_unlinked', user_name: user.name, issue_id: issue_data[:id]) + end + end + + def activity_message_params(content) + { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: :activity, + content: content + } + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 20c1391cc..8b392f47f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -190,6 +190,10 @@ en: sla: added: '%{user_name} added SLA policy %{sla_name}' removed: '%{user_name} removed SLA policy %{sla_name}' + linear: + issue_created: 'Linear issue %{issue_id} was created by %{user_name}' + issue_linked: 'Linear issue %{issue_id} was linked by %{user_name}' + issue_unlinked: 'Linear issue %{issue_id} was unlinked by %{user_name}' csat: not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions' muted: '%{user_name} has muted the conversation' diff --git a/lib/integrations/linear/processor_service.rb b/lib/integrations/linear/processor_service.rb index 2dfae28dc..a53d17f4c 100644 --- a/lib/integrations/linear/processor_service.rb +++ b/lib/integrations/linear/processor_service.rb @@ -28,7 +28,8 @@ class Integrations::Linear::ProcessorService { data: { id: response['issueCreate']['issue']['id'], - title: response['issueCreate']['issue']['title'] } + title: response['issueCreate']['issue']['title'], + identifier: response['issueCreate']['issue']['identifier'] } } end diff --git a/lib/linear/mutations.rb b/lib/linear/mutations.rb index 6a9377e13..04774c705 100644 --- a/lib/linear/mutations.rb +++ b/lib/linear/mutations.rb @@ -25,6 +25,7 @@ module Linear::Mutations issue { id title + identifier } } } diff --git a/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb b/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb index 0f27e2bd2..851c5dbaf 100644 --- a/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb @@ -93,6 +93,8 @@ RSpec.describe 'Linear Integration API', type: :request do end describe 'POST /api/v1/accounts/:account_id/integrations/linear/create_issue' do + let(:inbox) { create(:inbox, account: account) } + let(:conversation) { create(:conversation, account: account, inbox: inbox) } let(:issue_params) do { team_id: 'team1', @@ -101,32 +103,56 @@ RSpec.describe 'Linear Integration API', type: :request do assignee_id: 'user1', priority: 'high', state_id: 'state1', - label_ids: ['label1'] + label_ids: ['label1'], + conversation_id: conversation.display_id } end context 'when it is an authenticated user' do context 'when the issue is created successfully' do - let(:created_issue) { { data: { 'id' => 'issue1', 'title' => 'Sample Issue' } } } + let(:created_issue) { { data: { identifier: 'ENG-123', title: 'Sample Issue' } } } it 'returns the created issue' do allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys).and_return(created_issue) + post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue", params: issue_params, headers: agent.create_new_auth_token, as: :json + expect(response).to have_http_status(:ok) expect(response.body).to include('Sample Issue') end + + it 'creates activity message when conversation is provided' do + allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys).and_return(created_issue) + + expect do + post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue", + params: issue_params, + headers: agent.create_new_auth_token, + as: :json + end.to have_enqueued_job(Conversations::ActivityMessageJob) + .with(conversation, { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: :activity, + content: "Linear issue ENG-123 was created by #{agent.name}" + }) + end end context 'when issue creation fails' do - it 'returns error message' do + it 'returns error message and does not create activity message' do allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys).and_return(error: 'error message') - post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue", - params: issue_params, - headers: agent.create_new_auth_token, - as: :json + + expect do + post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue", + params: issue_params, + headers: agent.create_new_auth_token, + as: :json + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + expect(response).to have_http_status(:unprocessable_entity) expect(response.body).to include('error message') end @@ -135,7 +161,7 @@ RSpec.describe 'Linear Integration API', type: :request do end describe 'POST /api/v1/accounts/:account_id/integrations/linear/link_issue' do - let(:issue_id) { 'issue1' } + let(:issue_id) { 'ENG-456' } let(:conversation) { create(:conversation, account: account) } let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" } let(:title) { 'Sample Issue' } @@ -144,24 +170,38 @@ RSpec.describe 'Linear Integration API', type: :request do context 'when the issue is linked successfully' do let(:linked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } } - it 'returns the linked issue' do + it 'returns the linked issue and creates activity message' do allow(processor_service).to receive(:link_issue).with(link, issue_id, title).and_return(linked_issue) - post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue", - params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title }, - headers: agent.create_new_auth_token, - as: :json + + expect do + post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue", + params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title }, + headers: agent.create_new_auth_token, + as: :json + end.to have_enqueued_job(Conversations::ActivityMessageJob) + .with(conversation, { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: :activity, + content: "Linear issue ENG-456 was linked by #{agent.name}" + }) + expect(response).to have_http_status(:ok) expect(response.body).to include('https://linear.app/issue1') end end context 'when issue linking fails' do - it 'returns error message' do + it 'returns error message and does not create activity message' do allow(processor_service).to receive(:link_issue).with(link, issue_id, title).and_return(error: 'error message') - post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue", - params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title }, - headers: agent.create_new_auth_token, - as: :json + + expect do + post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue", + params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title }, + headers: agent.create_new_auth_token, + as: :json + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + expect(response).to have_http_status(:unprocessable_entity) expect(response.body).to include('error message') end @@ -171,29 +211,45 @@ RSpec.describe 'Linear Integration API', type: :request do describe 'POST /api/v1/accounts/:account_id/integrations/linear/unlink_issue' do let(:link_id) { 'attachment1' } + let(:issue_id) { 'ENG-789' } + let(:conversation) { create(:conversation, account: account) } context 'when it is an authenticated user' do context 'when the issue is unlinked successfully' do let(:unlinked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } } - it 'returns the unlinked issue' do + it 'returns the unlinked issue and creates activity message' do allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(unlinked_issue) - post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue", - params: { link_id: link_id }, - headers: agent.create_new_auth_token, - as: :json + + expect do + post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue", + params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id }, + headers: agent.create_new_auth_token, + as: :json + end.to have_enqueued_job(Conversations::ActivityMessageJob) + .with(conversation, { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: :activity, + content: "Linear issue ENG-789 was unlinked by #{agent.name}" + }) + expect(response).to have_http_status(:ok) expect(response.body).to include('https://linear.app/issue1') end end context 'when issue unlinking fails' do - it 'returns error message' do + it 'returns error message and does not create activity message' do allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(error: 'error message') - post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue", - params: { link_id: link_id }, - headers: agent.create_new_auth_token, - as: :json + + expect do + post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue", + params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id }, + headers: agent.create_new_auth_token, + as: :json + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + expect(response).to have_http_status(:unprocessable_entity) expect(response.body).to include('error message') end diff --git a/spec/lib/integrations/linear/processor_service_spec.rb b/spec/lib/integrations/linear/processor_service_spec.rb index 807e93c71..8830e658e 100644 --- a/spec/lib/integrations/linear/processor_service_spec.rb +++ b/spec/lib/integrations/linear/processor_service_spec.rb @@ -82,15 +82,27 @@ describe Integrations::Linear::ProcessorService do end let(:issue_response) do { - 'issueCreate' => { 'issue' => { 'id' => 'issue1', 'title' => 'Issue title' } } + 'issueCreate' => { + 'issue' => { + 'id' => 'issue1', + 'title' => 'Issue title', + 'identifier' => 'ENG-123' + } + } } end context 'when Linear client returns valid data' do - it 'returns parsed issue data' do + it 'returns parsed issue data with identifier' do allow(linear_client).to receive(:create_issue).with(params).and_return(issue_response) result = service.create_issue(params) - expect(result).to eq({ data: { id: 'issue1', title: 'Issue title' } }) + expect(result).to eq({ + data: { + id: 'issue1', + title: 'Issue title', + identifier: 'ENG-123' + } + }) end end @@ -133,13 +145,13 @@ describe Integrations::Linear::ProcessorService do describe '#unlink_issue' do let(:link_id) { 'attachment1' } - let(:unlink_response) { { data: { link_id: link_id } } } + let(:linear_client_response) { { success: true } } context 'when Linear client returns valid data' do - it 'returns parsed unlink data' do - allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(unlink_response) + it 'returns unlink data with link_id' do + allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(linear_client_response) result = service.unlink_issue(link_id) - expect(result).to eq(unlink_response) + expect(result).to eq({ data: { link_id: link_id } }) end end @@ -207,4 +219,59 @@ describe Integrations::Linear::ProcessorService do end end end + + # Tests specifically for activity message integration + describe 'activity message data compatibility' do + let(:linear_client_response) { { success: true } } + + describe '#create_issue' do + it 'includes identifier field needed for activity messages' do + params = { title: 'Test Issue', team_id: 'team1' } + response = { + 'issueCreate' => { + 'issue' => { + 'id' => 'internal_id_123', + 'title' => 'Test Issue', + 'identifier' => 'ENG-456' + } + } + } + + allow(linear_client).to receive(:create_issue).with(params).and_return(response) + result = service.create_issue(params) + + expect(result[:data]).to have_key(:identifier) + expect(result[:data][:identifier]).to eq('ENG-456') + end + end + + describe '#link_issue' do + it 'returns issue_id in response for activity messages' do + link = 'https://example.com' + issue_id = 'ENG-789' + title = 'Test Issue' + response = { + 'attachmentLinkURL' => { + 'attachment' => { 'id' => 'attachment123' } + } + } + + allow(linear_client).to receive(:link_issue).with(link, issue_id, title).and_return(response) + result = service.link_issue(link, issue_id, title) + + expect(result[:data][:id]).to eq(issue_id) + end + end + + describe '#unlink_issue' do + it 'returns structured data for activity messages' do + link_id = 'attachment456' + + allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(linear_client_response) + result = service.unlink_issue(link_id) + + expect(result).to eq({ data: { link_id: link_id } }) + end + end + end end diff --git a/spec/services/linear/activity_message_service_spec.rb b/spec/services/linear/activity_message_service_spec.rb new file mode 100644 index 000000000..4b51f6520 --- /dev/null +++ b/spec/services/linear/activity_message_service_spec.rb @@ -0,0 +1,174 @@ +require 'rails_helper' + +RSpec.describe Linear::ActivityMessageService, type: :service do + let(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:conversation) { create(:conversation, account: account, inbox: inbox) } + let(:user) { create(:user, account: account) } + + describe '#perform' do + context 'when action_type is issue_created' do + let(:service) do + described_class.new( + conversation: conversation, + action_type: :issue_created, + issue_data: { id: 'ENG-123' }, + user: user + ) + end + + it 'enqueues an activity message job' do + expect do + service.perform + end.to have_enqueued_job(Conversations::ActivityMessageJob) + .with(conversation, { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: :activity, + content: "Linear issue ENG-123 was created by #{user.name}" + }) + end + + it 'does not enqueue job when issue data lacks id' do + service = described_class.new( + conversation: conversation, + action_type: :issue_created, + issue_data: { title: 'Some issue' }, + user: user + ) + + expect do + service.perform + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + + it 'does not enqueue job when issue_data is empty' do + service = described_class.new( + conversation: conversation, + action_type: :issue_created, + issue_data: {}, + user: user + ) + + expect do + service.perform + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + + it 'does not enqueue job when conversation is nil' do + service = described_class.new( + conversation: nil, + action_type: :issue_created, + issue_data: { id: 'ENG-123' }, + user: user + ) + + expect do + service.perform + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + + it 'does not enqueue job when user is nil' do + service = described_class.new( + conversation: conversation, + action_type: :issue_created, + issue_data: { id: 'ENG-123' }, + user: nil + ) + + expect do + service.perform + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + end + + context 'when action_type is issue_linked' do + let(:service) do + described_class.new( + conversation: conversation, + action_type: :issue_linked, + issue_data: { id: 'ENG-456' }, + user: user + ) + end + + it 'enqueues an activity message job' do + expect do + service.perform + end.to have_enqueued_job(Conversations::ActivityMessageJob) + .with(conversation, { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: :activity, + content: "Linear issue ENG-456 was linked by #{user.name}" + }) + end + + it 'does not enqueue job when issue data lacks id' do + service = described_class.new( + conversation: conversation, + action_type: :issue_linked, + issue_data: { title: 'Some issue' }, + user: user + ) + + expect do + service.perform + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + end + + context 'when action_type is issue_unlinked' do + let(:service) do + described_class.new( + conversation: conversation, + action_type: :issue_unlinked, + issue_data: { id: 'ENG-789' }, + user: user + ) + end + + it 'enqueues an activity message job' do + expect do + service.perform + end.to have_enqueued_job(Conversations::ActivityMessageJob) + .with(conversation, { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: :activity, + content: "Linear issue ENG-789 was unlinked by #{user.name}" + }) + end + + it 'does not enqueue job when issue data lacks id' do + service = described_class.new( + conversation: conversation, + action_type: :issue_unlinked, + issue_data: { title: 'Some issue' }, + user: user + ) + + expect do + service.perform + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + end + + context 'when action_type is unknown' do + let(:service) do + described_class.new( + conversation: conversation, + action_type: :unknown_action, + issue_data: { id: 'ENG-999' }, + user: user + ) + end + + it 'does not enqueue job for unknown action types' do + expect do + service.perform + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + end + end +end \ No newline at end of file