feat: Add activity messages for linear actions (#11654)

This commit is contained in:
Muhsin Keloth 2025-06-13 11:57:11 +05:30 committed by GitHub
parent 58380c6d01
commit f4381e3b5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 460 additions and 49 deletions

View File

@ -1,5 +1,5 @@
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController 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] before_action :fetch_hook, only: [:destroy]
def destroy def destroy
@ -31,6 +31,12 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
if issue[:error] if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity render json: { error: issue[:error] }, status: :unprocessable_entity
else 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 render json: issue[:data], status: :ok
end end
end end
@ -42,17 +48,30 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
if issue[:error] if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity render json: { error: issue[:error] }, status: :unprocessable_entity
else 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 render json: issue[:data], status: :ok
end end
end end
def unlink_issue def unlink_issue
link_id = permitted_params[:link_id] link_id = permitted_params[:link_id]
issue_id = permitted_params[:issue_id]
issue = linear_processor_service.unlink_issue(link_id) issue = linear_processor_service.unlink_issue(link_id)
if issue[:error] if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity render json: { error: issue[:error] }, status: :unprocessable_entity
else 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 render json: issue[:data], status: :ok
end end
end end

View File

@ -33,9 +33,11 @@ class LinearAPI extends ApiClient {
); );
} }
unlinkIssue(linkId) { unlinkIssue(linkId, issueIdentifier, conversationId) {
return axios.post(`${this.url}/unlink_issue`, { return axios.post(`${this.url}/unlink_issue`, {
link_id: linkId, link_id: linkId,
issue_id: issueIdentifier,
conversation_id: conversationId,
}); });
} }

View File

@ -91,6 +91,19 @@ describe('#linearAPI', () => {
issueData 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', () => { 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', () => { describe('getLinkedIssue', () => {
@ -164,12 +189,26 @@ describe('#linearAPI', () => {
window.axios = originalAxios; window.axios = originalAxios;
}); });
it('creates a valid request', () => { it('creates a valid request with link_id only', () => {
LinearAPIClient.unlinkIssue(1); LinearAPIClient.unlinkIssue('link123');
expect(axiosMock.post).toHaveBeenCalledWith( expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/linear/unlink_issue', '/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,
} }
); );
}); });

View File

@ -183,13 +183,18 @@ const createIssue = async () => {
state_id: formState.stateId || undefined, state_id: formState.stateId || undefined,
priority: formState.priority || undefined, priority: formState.priority || undefined,
label_ids: formState.labelId ? [formState.labelId] : undefined, label_ids: formState.labelId ? [formState.labelId] : undefined,
conversation_id: props.conversationId,
}; };
try { try {
isCreating.value = true; isCreating.value = true;
const response = await LinearAPI.createIssue(payload); const response = await LinearAPI.createIssue(payload);
const { id: issueId } = response.data; const { identifier: issueIdentifier } = response.data;
await LinearAPI.link_issue(props.conversationId, issueId, props.title); await LinearAPI.link_issue(
props.conversationId,
issueIdentifier,
props.title
);
useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS')); useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS'));
useTrack(LINEAR_EVENTS.CREATE_ISSUE); useTrack(LINEAR_EVENTS.CREATE_ISSUE);
onClose(); onClose();

View File

@ -46,9 +46,9 @@ const loadLinkedIssues = async () => {
} }
}; };
const unlinkIssue = async linkId => { const unlinkIssue = async (linkId, issueIdentifier) => {
try { try {
await LinearAPI.unlinkIssue(linkId); await LinearAPI.unlinkIssue(linkId, issueIdentifier, props.conversationId);
useTrack(LINEAR_EVENTS.UNLINK_ISSUE); useTrack(LINEAR_EVENTS.UNLINK_ISSUE);
linkedIssues.value = linkedIssues.value.filter( linkedIssues.value = linkedIssues.value.filter(
issue => issue.id !== linkId issue => issue.id !== linkId
@ -110,7 +110,7 @@ onMounted(() => {
<LinearIssueItem <LinearIssueItem
v-for="linkedIssue in linkedIssues" v-for="linkedIssue in linkedIssues"
:key="linkedIssue.id" :key="linkedIssue.id"
class="pt-3 px-4 pb-4 border-b border-n-weak last:border-b-0" class="px-4 pt-3 pb-4 border-b border-n-weak last:border-b-0"
:linked-issue="linkedIssue" :linked-issue="linkedIssue"
@unlink-issue="unlinkIssue" @unlink-issue="unlinkIssue"
/> />

View File

@ -14,6 +14,8 @@ const props = defineProps({
const emit = defineEmits(['unlinkIssue']); const emit = defineEmits(['unlinkIssue']);
const { linkedIssue } = props;
const priorityMap = { const priorityMap = {
1: 'Urgent', 1: 'Urgent',
2: 'High', 2: 'High',
@ -21,7 +23,7 @@ const priorityMap = {
4: 'Low', 4: 'Low',
}; };
const issue = computed(() => props.linkedIssue.issue); const issue = computed(() => linkedIssue.issue);
const assignee = computed(() => { const assignee = computed(() => {
const assigneeDetails = issue.value.assignee; const assigneeDetails = issue.value.assignee;
@ -37,7 +39,7 @@ const labels = computed(() => issue.value.labels?.nodes || []);
const priorityLabel = computed(() => priorityMap[issue.value.priority]); const priorityLabel = computed(() => priorityMap[issue.value.priority]);
const unlinkIssue = () => { const unlinkIssue = () => {
emit('unlinkIssue', props.linkedIssue.id); emit('unlinkIssue', linkedIssue.id, linkedIssue.issue.identifier);
}; };
</script> </script>

View File

@ -63,7 +63,7 @@ const onSearch = async value => {
isFetching.value = true; isFetching.value = true;
const response = await LinearAPI.searchIssues(value); const response = await LinearAPI.searchIssues(value);
issues.value = response.data.map(issue => ({ issues.value = response.data.map(issue => ({
id: issue.id, id: issue.identifier,
name: `${issue.identifier} ${issue.title}`, name: `${issue.identifier} ${issue.title}`,
icon: 'status', icon: 'status',
iconColor: issue.state.color, iconColor: issue.state.color,

View File

@ -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

View File

@ -190,6 +190,10 @@ en:
sla: sla:
added: '%{user_name} added SLA policy %{sla_name}' added: '%{user_name} added SLA policy %{sla_name}'
removed: '%{user_name} removed 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: csat:
not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions' not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
muted: '%{user_name} has muted the conversation' muted: '%{user_name} has muted the conversation'

View File

@ -28,7 +28,8 @@ class Integrations::Linear::ProcessorService
{ {
data: { id: response['issueCreate']['issue']['id'], data: { id: response['issueCreate']['issue']['id'],
title: response['issueCreate']['issue']['title'] } title: response['issueCreate']['issue']['title'],
identifier: response['issueCreate']['issue']['identifier'] }
} }
end end

View File

@ -25,6 +25,7 @@ module Linear::Mutations
issue { issue {
id id
title title
identifier
} }
} }
} }

View File

@ -93,6 +93,8 @@ RSpec.describe 'Linear Integration API', type: :request do
end end
describe 'POST /api/v1/accounts/:account_id/integrations/linear/create_issue' do 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 let(:issue_params) do
{ {
team_id: 'team1', team_id: 'team1',
@ -101,32 +103,56 @@ RSpec.describe 'Linear Integration API', type: :request do
assignee_id: 'user1', assignee_id: 'user1',
priority: 'high', priority: 'high',
state_id: 'state1', state_id: 'state1',
label_ids: ['label1'] label_ids: ['label1'],
conversation_id: conversation.display_id
} }
end end
context 'when it is an authenticated user' do context 'when it is an authenticated user' do
context 'when the issue is created successfully' 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 it 'returns the created issue' do
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys).and_return(created_issue) 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", post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
params: issue_params, params: issue_params,
headers: agent.create_new_auth_token, headers: agent.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(response.body).to include('Sample Issue') expect(response.body).to include('Sample Issue')
end 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 end
context 'when issue creation fails' do 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') 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, expect do
headers: agent.create_new_auth_token, post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
as: :json 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).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message') expect(response.body).to include('error message')
end end
@ -135,7 +161,7 @@ RSpec.describe 'Linear Integration API', type: :request do
end end
describe 'POST /api/v1/accounts/:account_id/integrations/linear/link_issue' do 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(:conversation) { create(:conversation, account: account) }
let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" } let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" }
let(:title) { 'Sample Issue' } let(:title) { 'Sample Issue' }
@ -144,24 +170,38 @@ RSpec.describe 'Linear Integration API', type: :request do
context 'when the issue is linked successfully' do context 'when the issue is linked successfully' do
let(:linked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } } 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) 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 }, expect do
headers: agent.create_new_auth_token, post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
as: :json 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).to have_http_status(:ok)
expect(response.body).to include('https://linear.app/issue1') expect(response.body).to include('https://linear.app/issue1')
end end
end end
context 'when issue linking fails' do 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') 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 }, expect do
headers: agent.create_new_auth_token, post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
as: :json 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).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message') expect(response.body).to include('error message')
end 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 describe 'POST /api/v1/accounts/:account_id/integrations/linear/unlink_issue' do
let(:link_id) { 'attachment1' } 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 it is an authenticated user' do
context 'when the issue is unlinked successfully' do context 'when the issue is unlinked successfully' do
let(:unlinked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } } 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) 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 }, expect do
headers: agent.create_new_auth_token, post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
as: :json 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).to have_http_status(:ok)
expect(response.body).to include('https://linear.app/issue1') expect(response.body).to include('https://linear.app/issue1')
end end
end end
context 'when issue unlinking fails' do 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') 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 }, expect do
headers: agent.create_new_auth_token, post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
as: :json 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).to have_http_status(:unprocessable_entity)
expect(response.body).to include('error message') expect(response.body).to include('error message')
end end

View File

@ -82,15 +82,27 @@ describe Integrations::Linear::ProcessorService do
end end
let(:issue_response) do let(:issue_response) do
{ {
'issueCreate' => { 'issue' => { 'id' => 'issue1', 'title' => 'Issue title' } } 'issueCreate' => {
'issue' => {
'id' => 'issue1',
'title' => 'Issue title',
'identifier' => 'ENG-123'
}
}
} }
end end
context 'when Linear client returns valid data' do 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) allow(linear_client).to receive(:create_issue).with(params).and_return(issue_response)
result = service.create_issue(params) 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
end end
@ -133,13 +145,13 @@ describe Integrations::Linear::ProcessorService do
describe '#unlink_issue' do describe '#unlink_issue' do
let(:link_id) { 'attachment1' } 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 context 'when Linear client returns valid data' do
it 'returns parsed unlink data' do it 'returns unlink data with link_id' do
allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(unlink_response) allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(linear_client_response)
result = service.unlink_issue(link_id) result = service.unlink_issue(link_id)
expect(result).to eq(unlink_response) expect(result).to eq({ data: { link_id: link_id } })
end end
end end
@ -207,4 +219,59 @@ describe Integrations::Linear::ProcessorService do
end end
end 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 end

View File

@ -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