From e7152ee04a607bd3482b37a4761c86fb777523ac Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 28 May 2025 18:37:27 +0530 Subject: [PATCH] feat: add activity messages for linear actions --- .../integrations/linear_controller.rb | 19 ++- .../dashboard/api/integrations/linear.js | 4 +- .../api/specs/integrations/linear.spec.js | 45 ++++++- .../conversation/linear/CreateIssue.vue | 9 +- .../widgets/conversation/linear/Issue.vue | 2 +- .../widgets/conversation/linear/LinkIssue.vue | 2 +- .../widgets/conversation/linear/index.vue | 4 +- .../concerns/activity_message_handler.rb | 1 + .../linear_activity_message_handler.rb | 24 ++++ config/locales/en.yml | 4 + lib/integrations/linear/processor_service.rb | 3 +- lib/linear/mutations.rb | 1 + .../integrations/linear_controller_spec.rb | 112 +++++++++++++----- .../linear/processor_service_spec.rb | 83 +++++++++++-- .../linear_activity_message_handler_spec.rb | 97 +++++++++++++++ 15 files changed, 363 insertions(+), 47 deletions(-) create mode 100644 app/models/concerns/linear_activity_message_handler.rb create mode 100644 spec/models/concerns/linear_activity_message_handler_spec.rb diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb index c66f06909..2cc791cda 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,7 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas if issue[:error] render json: { error: issue[:error] }, status: :unprocessable_entity else + create_linear_activity_message(:issue_created, { id: issue[:data]['identifier'] }) render json: issue[:data], status: :ok end end @@ -42,17 +43,20 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas if issue[:error] render json: { error: issue[:error] }, status: :unprocessable_entity else + create_linear_activity_message(:issue_linked, { id: issue_id }) 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 + create_linear_activity_message(:issue_unlinked, { id: issue_id }) render json: issue[:data], status: :ok end end @@ -101,4 +105,17 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas def fetch_hook @hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'linear') end + + def create_linear_activity_message(action_type, issue_data = {}) + return unless @conversation + + case action_type + when :issue_created + @conversation.create_linear_issue_created_activity(issue_data) + when :issue_linked + @conversation.create_linear_issue_linked_activity(issue_data) + when :issue_unlinked + @conversation.create_linear_issue_unlinked_activity(issue_data) + end + 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/Issue.vue b/app/javascript/dashboard/components/widgets/conversation/linear/Issue.vue index c1d472964..3d0a89cdb 100644 --- a/app/javascript/dashboard/components/widgets/conversation/linear/Issue.vue +++ b/app/javascript/dashboard/components/widgets/conversation/linear/Issue.vue @@ -50,7 +50,7 @@ const priorityLabel = computed(() => { }); const unlinkIssue = () => { - emit('unlinkIssue', props.linkId); + emit('unlinkIssue', props.linkId, props.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/javascript/dashboard/components/widgets/conversation/linear/index.vue b/app/javascript/dashboard/components/widgets/conversation/linear/index.vue index 012e43a67..e78e78e41 100644 --- a/app/javascript/dashboard/components/widgets/conversation/linear/index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/linear/index.vue @@ -55,10 +55,10 @@ const loadLinkedIssue = async () => { } }; -const unlinkIssue = async linkId => { +const unlinkIssue = async (linkId, issueIdentifier) => { try { isUnlinking.value = true; - await LinearAPI.unlinkIssue(linkId); + await LinearAPI.unlinkIssue(linkId, issueIdentifier, props.conversationId); useTrack(LINEAR_EVENTS.UNLINK_ISSUE); linkedIssue.value = null; useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS')); diff --git a/app/models/concerns/activity_message_handler.rb b/app/models/concerns/activity_message_handler.rb index 54e58b4d9..4f21ba869 100644 --- a/app/models/concerns/activity_message_handler.rb +++ b/app/models/concerns/activity_message_handler.rb @@ -5,6 +5,7 @@ module ActivityMessageHandler include LabelActivityMessageHandler include SlaActivityMessageHandler include TeamActivityMessageHandler + include LinearActivityMessageHandler private diff --git a/app/models/concerns/linear_activity_message_handler.rb b/app/models/concerns/linear_activity_message_handler.rb new file mode 100644 index 000000000..2251e1fdd --- /dev/null +++ b/app/models/concerns/linear_activity_message_handler.rb @@ -0,0 +1,24 @@ +module LinearActivityMessageHandler + extend ActiveSupport::Concern + + def create_linear_issue_created_activity(issue_data) + return unless issue_data[:id] + + content = I18n.t('conversations.activity.linear.issue_created', issue_id: issue_data[:id]) + ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content + end + + def create_linear_issue_linked_activity(issue_data) + return unless issue_data[:id] + + content = I18n.t('conversations.activity.linear.issue_linked', issue_id: issue_data[:id]) + ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content + end + + def create_linear_issue_unlinked_activity(issue_data) + return unless issue_data[:id] + + content = I18n.t('conversations.activity.linear.issue_unlinked', issue_id: issue_data[:id]) + ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content + end +end \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 9a35197ad..0b492e4a6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -186,6 +186,10 @@ en: sla: added: '%{user_name} added SLA policy %{sla_name}' removed: '%{user_name} removed SLA policy %{sla_name}' + linear: + issue_created: 'New Linear issue %{issue_id} has been created' + issue_linked: 'Linear issue %{issue_id} is now linked' + issue_unlinked: 'Linear issue %{issue_id} has been unlinked' muted: '%{user_name} has muted the conversation' unmuted: '%{user_name} has unmuted the conversation' auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.' 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..484fdab32 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: 'New Linear issue ENG-123 has been created' + }) + 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 is now linked' + }) + 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 has been unlinked' + }) + 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..7b00d3e2f 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,61 @@ 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 have_key(:data) + expect(result[:data]).to have_key(:link_id) + expect(result[:data][:link_id]).to eq(link_id) + end + end + end end diff --git a/spec/models/concerns/linear_activity_message_handler_spec.rb b/spec/models/concerns/linear_activity_message_handler_spec.rb new file mode 100644 index 000000000..b40f02b0b --- /dev/null +++ b/spec/models/concerns/linear_activity_message_handler_spec.rb @@ -0,0 +1,97 @@ +require 'rails_helper' + +RSpec.describe LinearActivityMessageHandler, type: :model do + let(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:conversation) { create(:conversation, account: account, inbox: inbox) } + + describe '#create_linear_issue_created_activity' do + it 'enqueues an activity message job when issue data contains id' do + issue_data = { id: 'ENG-123' } + + expect do + conversation.create_linear_issue_created_activity(issue_data) + end.to have_enqueued_job(Conversations::ActivityMessageJob) + .with(conversation, { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: :activity, + content: 'New Linear issue ENG-123 has been created' + }) + end + + it 'does not enqueue job when issue data lacks id' do + issue_data = { title: 'Some issue' } + + expect do + conversation.create_linear_issue_created_activity(issue_data) + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + + it 'does not enqueue job when issue_data is empty' do + expect do + conversation.create_linear_issue_created_activity({}) + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + end + + describe '#create_linear_issue_linked_activity' do + it 'enqueues an activity message job when issue data contains id' do + issue_data = { id: 'ENG-456' } + + expect do + conversation.create_linear_issue_linked_activity(issue_data) + 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 is now linked' + }) + end + + it 'does not enqueue job when issue data lacks id' do + issue_data = { title: 'Some issue' } + + expect do + conversation.create_linear_issue_linked_activity(issue_data) + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + + it 'does not enqueue job when issue_data is empty' do + expect do + conversation.create_linear_issue_linked_activity({}) + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + end + + describe '#create_linear_issue_unlinked_activity' do + it 'enqueues an activity message job when issue data contains id' do + issue_data = { id: 'ENG-789' } + + expect do + conversation.create_linear_issue_unlinked_activity(issue_data) + 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 has been unlinked' + }) + end + + it 'does not enqueue job when issue data lacks id' do + issue_data = { title: 'Some issue' } + + expect do + conversation.create_linear_issue_unlinked_activity(issue_data) + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + + it 'does not enqueue job when issue_data is empty' do + expect do + conversation.create_linear_issue_unlinked_activity({}) + end.not_to have_enqueued_job(Conversations::ActivityMessageJob) + end + end +end \ No newline at end of file