mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
- Add `actor=app` parameter to Linear OAuth authorization URL for consistent app-level authorization https://linear.app/developers/oauth-actor-authorization - Implement user attribution for Linear issue creation and linking using `createAsUser` and `displayIconUrl` parameters - Enhance Linear integration to properly attribute actions to specific Chatwoot agents **Note** - The displayIconUrl parameter is being sent correctly to Linear's GraphQL API (verified through testing), but there is an issues with icon is not attaching properly. - We might need to disconnect the integration connect again.
389 lines
14 KiB
Ruby
389 lines
14 KiB
Ruby
require 'rails_helper'
|
|
|
|
describe Linear do
|
|
let(:access_token) { 'valid_access_token' }
|
|
let(:url) { 'https://api.linear.app/graphql' }
|
|
let(:linear_client) { described_class.new(access_token) }
|
|
let(:headers) { { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{access_token}" } }
|
|
|
|
it 'raises an exception if the API key is absent' do
|
|
expect { described_class.new(nil) }.to raise_error(ArgumentError, 'Missing Credentials')
|
|
end
|
|
|
|
context 'when querying teams' do
|
|
context 'when the API response is success' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 200,
|
|
body: { success: true, data: { teams: { nodes: [{ id: 'team1', name: 'Team One' }] } } }.to_json,
|
|
headers: headers)
|
|
end
|
|
|
|
it 'returns team data' do
|
|
response = linear_client.teams
|
|
expect(response).to eq({ 'teams' => { 'nodes' => [{ 'id' => 'team1', 'name' => 'Team One' }] } })
|
|
end
|
|
end
|
|
|
|
context 'when the API response is an error' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 422, body: { errors: [{ message: 'Error retrieving data' }] }.to_json,
|
|
headers: headers)
|
|
end
|
|
|
|
it 'raises an exception' do
|
|
response = linear_client.teams
|
|
expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error retrieving data' }] }, :error_code => 422 })
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when querying team entities' do
|
|
let(:team_id) { 'team1' }
|
|
|
|
context 'when the API response is success' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 200,
|
|
body: { success: true, data: {
|
|
users: { nodes: [{ id: 'user1', name: 'User One' }] },
|
|
projects: { nodes: [{ id: 'project1', name: 'Project One' }] },
|
|
workflowStates: { nodes: [] },
|
|
issueLabels: { nodes: [{ id: 'bug', name: 'Bug' }] }
|
|
} }.to_json,
|
|
headers: headers)
|
|
end
|
|
|
|
it 'returns team entities' do
|
|
response = linear_client.team_entities(team_id)
|
|
expect(response).to eq({
|
|
'users' => { 'nodes' => [{ 'id' => 'user1', 'name' => 'User One' }] },
|
|
'projects' => { 'nodes' => [{ 'id' => 'project1', 'name' => 'Project One' }] },
|
|
'workflowStates' => { 'nodes' => [] },
|
|
'issueLabels' => { 'nodes' => [{ 'id' => 'bug', 'name' => 'Bug' }] }
|
|
})
|
|
end
|
|
end
|
|
|
|
context 'when the API response is an error' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 422, body: { errors: [{ message: 'Error retrieving data' }] }.to_json,
|
|
headers: headers)
|
|
end
|
|
|
|
it 'raises an exception' do
|
|
response = linear_client.team_entities(team_id)
|
|
expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error retrieving data' }] }, :error_code => 422 })
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when creating an issue' do
|
|
let(:params) do
|
|
{
|
|
title: 'Title',
|
|
team_id: 'team1',
|
|
description: 'Description',
|
|
assignee_id: 'user1',
|
|
priority: 1,
|
|
label_ids: ['bug']
|
|
}
|
|
end
|
|
let(:user) { instance_double(User, name: 'John Doe', avatar_url: 'https://example.com/avatar.jpg') }
|
|
|
|
context 'when the API response is success' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 200, body: { success: true, data: { issueCreate: { id: 'issue1', title: 'Title' } } }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'creates an issue' do
|
|
response = linear_client.create_issue(params)
|
|
expect(response).to eq({ 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title' } })
|
|
end
|
|
|
|
context 'when user is provided' do
|
|
it 'includes user attribution in the request' do
|
|
allow(linear_client).to receive(:post) do |payload|
|
|
expect(payload[:query]).to include('createAsUser: "John Doe"')
|
|
expect(payload[:query]).to include('displayIconUrl: "https://example.com/avatar.jpg"')
|
|
instance_double(HTTParty::Response, success?: true,
|
|
parsed_response: { 'data' => { 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title' } } })
|
|
end
|
|
|
|
linear_client.create_issue(params, user)
|
|
end
|
|
end
|
|
|
|
context 'when user has no avatar' do
|
|
let(:user_no_avatar) { instance_double(User, name: 'Jane Doe', avatar_url: '') }
|
|
|
|
it 'includes only user name in the request' do
|
|
allow(linear_client).to receive(:post) do |payload|
|
|
expect(payload[:query]).to include('createAsUser: "Jane Doe"')
|
|
expect(payload[:query]).not_to include('displayIconUrl')
|
|
instance_double(HTTParty::Response, success?: true,
|
|
parsed_response: { 'data' => { 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title' } } })
|
|
end
|
|
|
|
linear_client.create_issue(params, user_no_avatar)
|
|
end
|
|
end
|
|
|
|
context 'when the priority is invalid' do
|
|
let(:params) { { title: 'Title', team_id: 'team1', priority: 5 } }
|
|
|
|
it 'raises an exception' do
|
|
expect { linear_client.create_issue(params) }.to raise_error(ArgumentError, 'Invalid priority value. Priority must be 0, 1, 2, 3, or 4.')
|
|
end
|
|
end
|
|
|
|
context 'when the label_ids are invalid' do
|
|
let(:params) { { title: 'Title', team_id: 'team1', label_ids: 'bug' } }
|
|
|
|
it 'raises an exception' do
|
|
expect { linear_client.create_issue(params) }.to raise_error(ArgumentError, 'label_ids must be an array of strings.')
|
|
end
|
|
end
|
|
|
|
context 'when the title is missing' do
|
|
let(:params) { { team_id: 'team1' } }
|
|
|
|
it 'raises an exception' do
|
|
expect { linear_client.create_issue(params) }.to raise_error(ArgumentError, 'Missing title')
|
|
end
|
|
end
|
|
|
|
context 'when the team_id is missing' do
|
|
let(:params) { { title: 'Title' } }
|
|
|
|
it 'raises an exception' do
|
|
expect { linear_client.create_issue(params) }.to raise_error(ArgumentError, 'Missing team id')
|
|
end
|
|
end
|
|
|
|
context 'when the API key is invalid' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 401, body: { errors: [{ message: 'Invalid API key' }] }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'raises an exception' do
|
|
response = linear_client.create_issue(params)
|
|
expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Invalid API key' }] }, :error_code => 401 })
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the description is markdown' do
|
|
let(:description) { 'Cmd/Ctrl` `K` **is our most powerful feature.** \n\nUse it to search for or take any action in the app' }
|
|
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 200, body: { success: true,
|
|
data: { issueCreate: { id: 'issue1', title: 'Title',
|
|
description: description } } }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'creates an issue' do
|
|
response = linear_client.create_issue(params)
|
|
expect(response).to eq({ 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title',
|
|
'description' => description } })
|
|
end
|
|
end
|
|
|
|
context 'when the API response is an error' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 422, body: { errors: [{ message: 'Error creating issue' }] }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'raises an exception' do
|
|
response = linear_client.create_issue(params)
|
|
expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error creating issue' }] }, :error_code => 422 })
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when linking an issue' do
|
|
let(:link) { 'https://example.com' }
|
|
let(:issue_id) { 'issue1' }
|
|
let(:title) { 'Title' }
|
|
let(:user) { instance_double(User, name: 'John Doe', avatar_url: 'https://example.com/avatar.jpg') }
|
|
|
|
context 'when the API response is success' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 200, body: { success: true, data: { attachmentLinkURL: { id: 'attachment1' } } }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'links an issue' do
|
|
response = linear_client.link_issue(link, issue_id, title)
|
|
expect(response).to eq({ 'attachmentLinkURL' => { 'id' => 'attachment1' } })
|
|
end
|
|
|
|
context 'when user is provided' do
|
|
it 'includes user attribution in the request' do
|
|
expected_params = {
|
|
issue_id: issue_id,
|
|
link: link,
|
|
title: title,
|
|
user_name: 'John Doe',
|
|
user_avatar_url: 'https://example.com/avatar.jpg'
|
|
}
|
|
|
|
expect(Linear::Mutations).to receive(:issue_link).with(expected_params).and_call_original
|
|
allow(linear_client).to receive(:post).and_return(
|
|
instance_double(HTTParty::Response, success?: true, parsed_response: { 'data' => { 'attachmentLinkURL' => { 'id' => 'attachment1' } } })
|
|
)
|
|
|
|
linear_client.link_issue(link, issue_id, title, user)
|
|
end
|
|
end
|
|
|
|
context 'when user has no avatar' do
|
|
let(:user_no_avatar) { instance_double(User, name: 'Jane Doe', avatar_url: '') }
|
|
|
|
it 'includes only user name in the request' do
|
|
expected_params = {
|
|
issue_id: issue_id,
|
|
link: link,
|
|
title: title,
|
|
user_name: 'Jane Doe'
|
|
}
|
|
|
|
expect(Linear::Mutations).to receive(:issue_link).with(expected_params).and_call_original
|
|
allow(linear_client).to receive(:post).and_return(
|
|
instance_double(HTTParty::Response, success?: true, parsed_response: { 'data' => { 'attachmentLinkURL' => { 'id' => 'attachment1' } } })
|
|
)
|
|
|
|
linear_client.link_issue(link, issue_id, title, user_no_avatar)
|
|
end
|
|
end
|
|
|
|
context 'when the link is missing' do
|
|
let(:link) { '' }
|
|
|
|
it 'raises an exception' do
|
|
expect { linear_client.link_issue(link, issue_id, title) }.to raise_error(ArgumentError, 'Missing link')
|
|
end
|
|
end
|
|
|
|
context 'when the issue_id is missing' do
|
|
let(:issue_id) { '' }
|
|
|
|
it 'raises an exception' do
|
|
expect { linear_client.link_issue(link, issue_id, title) }.to raise_error(ArgumentError, 'Missing issue id')
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the API response is an error' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 422, body: { errors: [{ message: 'Error linking issue' }] }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'raises an exception' do
|
|
response = linear_client.link_issue(link, issue_id, title)
|
|
expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error linking issue' }] }, :error_code => 422 })
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when unlinking an issue' do
|
|
let(:link_id) { 'attachment1' }
|
|
|
|
context 'when the API response is success' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 200, body: { success: true, data: { attachmentLinkURL: { id: 'attachment1' } } }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'unlinks an issue' do
|
|
response = linear_client.unlink_issue(link_id)
|
|
expect(response).to eq({ 'attachmentLinkURL' => { 'id' => 'attachment1' } })
|
|
end
|
|
|
|
context 'when the link_id is missing' do
|
|
let(:link_id) { '' }
|
|
|
|
it 'raises an exception' do
|
|
expect { linear_client.unlink_issue(link_id) }.to raise_error(ArgumentError, 'Missing link id')
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the API response is an error' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 422, body: { errors: [{ message: 'Error unlinking issue' }] }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'raises an exception' do
|
|
response = linear_client.unlink_issue(link_id)
|
|
expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error unlinking issue' }] }, :error_code => 422 })
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when querying issues' do
|
|
let(:term) { 'term' }
|
|
|
|
context 'when the API response is success' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 200, body: { success: true,
|
|
data: { searchIssues: { nodes: [{ id: 'issue1', title: 'Title' }] } } }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'returns issues' do
|
|
response = linear_client.search_issue(term)
|
|
expect(response).to eq({ 'searchIssues' => { 'nodes' => [{ 'id' => 'issue1', 'title' => 'Title' }] } })
|
|
end
|
|
end
|
|
|
|
context 'when the API response is an error' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 422, body: { errors: [{ message: 'Error retrieving data' }] }.to_json,
|
|
headers: headers)
|
|
end
|
|
|
|
it 'raises an exception' do
|
|
response = linear_client.search_issue(term)
|
|
expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error retrieving data' }] }, :error_code => 422 })
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when querying linked issues' do
|
|
context 'when the API response is success' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 200, body: { success: true, data: { linkedIssue: { id: 'issue1', title: 'Title' } } }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'returns linked issues' do
|
|
response = linear_client.linked_issues('app.chatwoot.com')
|
|
expect(response).to eq({ 'linkedIssue' => { 'id' => 'issue1', 'title' => 'Title' } })
|
|
end
|
|
end
|
|
|
|
context 'when the API response is an error' do
|
|
before do
|
|
stub_request(:post, url)
|
|
.to_return(status: 422, body: { errors: [{ message: 'Error retrieving data' }] }.to_json,
|
|
headers: headers)
|
|
end
|
|
|
|
it 'raises an exception' do
|
|
response = linear_client.linked_issues('app.chatwoot.com')
|
|
expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error retrieving data' }] }, :error_code => 422 })
|
|
end
|
|
end
|
|
end
|
|
end
|