Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7152ee04a |
@@ -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,7 @@ 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
|
||||||
|
create_linear_activity_message(:issue_created, { id: issue[:data]['identifier'] })
|
||||||
render json: issue[:data], status: :ok
|
render json: issue[:data], status: :ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -42,17 +43,20 @@ 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
|
||||||
|
create_linear_activity_message(:issue_linked, { id: issue_id })
|
||||||
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
|
||||||
|
create_linear_activity_message(:issue_unlinked, { id: issue_id })
|
||||||
render json: issue[:data], status: :ok
|
render json: issue[:data], status: :ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -101,4 +105,17 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
|
|||||||
def fetch_hook
|
def fetch_hook
|
||||||
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'linear')
|
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'linear')
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const priorityLabel = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const unlinkIssue = () => {
|
const unlinkIssue = () => {
|
||||||
emit('unlinkIssue', props.linkId);
|
emit('unlinkIssue', props.linkId, props.issue.identifier);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ const loadLinkedIssue = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const unlinkIssue = async linkId => {
|
const unlinkIssue = async (linkId, issueIdentifier) => {
|
||||||
try {
|
try {
|
||||||
isUnlinking.value = true;
|
isUnlinking.value = true;
|
||||||
await LinearAPI.unlinkIssue(linkId);
|
await LinearAPI.unlinkIssue(linkId, issueIdentifier, props.conversationId);
|
||||||
useTrack(LINEAR_EVENTS.UNLINK_ISSUE);
|
useTrack(LINEAR_EVENTS.UNLINK_ISSUE);
|
||||||
linkedIssue.value = null;
|
linkedIssue.value = null;
|
||||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS'));
|
useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS'));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module ActivityMessageHandler
|
|||||||
include LabelActivityMessageHandler
|
include LabelActivityMessageHandler
|
||||||
include SlaActivityMessageHandler
|
include SlaActivityMessageHandler
|
||||||
include TeamActivityMessageHandler
|
include TeamActivityMessageHandler
|
||||||
|
include LinearActivityMessageHandler
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -186,6 +186,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: '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'
|
muted: '%{user_name} has muted the conversation'
|
||||||
unmuted: '%{user_name} has unmuted 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.'
|
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ module Linear::Mutations
|
|||||||
issue {
|
issue {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: 'New Linear issue ENG-123 has been created'
|
||||||
|
})
|
||||||
|
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 is now linked'
|
||||||
|
})
|
||||||
|
|
||||||
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 has been unlinked'
|
||||||
|
})
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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,61 @@ 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 have_key(:data)
|
||||||
|
expect(result[:data]).to have_key(:link_id)
|
||||||
|
expect(result[:data][:link_id]).to eq(link_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user