mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	feat: Add APIs for linear integration (#9346)
This commit is contained in:
		| @@ -0,0 +1,93 @@ | ||||
| class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController | ||||
|   before_action :fetch_conversation, only: [:link_issue, :linked_issues] | ||||
|  | ||||
|   def teams | ||||
|     teams = linear_processor_service.teams | ||||
|     if teams[:error] | ||||
|       render json: { error: teams[:error] }, status: :unprocessable_entity | ||||
|     else | ||||
|       render json: teams[:data], status: :ok | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def team_entities | ||||
|     team_id = permitted_params[:team_id] | ||||
|     team_entities = linear_processor_service.team_entities(team_id) | ||||
|     if team_entities[:error] | ||||
|       render json: { error: team_entities[:error] }, status: :unprocessable_entity | ||||
|     else | ||||
|       render json: team_entities[:data], status: :ok | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def create_issue | ||||
|     issue = linear_processor_service.create_issue(permitted_params) | ||||
|     if issue[:error] | ||||
|       render json: { error: issue[:error] }, status: :unprocessable_entity | ||||
|     else | ||||
|       render json: issue[:data], status: :ok | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def link_issue | ||||
|     issue_id = permitted_params[:issue_id] | ||||
|     title = permitted_params[:title] | ||||
|     issue = linear_processor_service.link_issue(conversation_link, issue_id, title) | ||||
|     if issue[:error] | ||||
|       render json: { error: issue[:error] }, status: :unprocessable_entity | ||||
|     else | ||||
|       render json: issue[:data], status: :ok | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def unlink_issue | ||||
|     link_id = permitted_params[:link_id] | ||||
|     issue = linear_processor_service.unlink_issue(link_id) | ||||
|  | ||||
|     if issue[:error] | ||||
|       render json: { error: issue[:error] }, status: :unprocessable_entity | ||||
|     else | ||||
|       render json: issue[:data], status: :ok | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def linked_issues | ||||
|     issues = linear_processor_service.linked_issues(conversation_link) | ||||
|  | ||||
|     if issues[:error] | ||||
|       render json: { error: issues[:error] }, status: :unprocessable_entity | ||||
|     else | ||||
|       render json: issues[:data], status: :ok | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def search_issue | ||||
|     render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return | ||||
|  | ||||
|     term = params[:q] | ||||
|     issues = linear_processor_service.search_issue(term) | ||||
|     if issues[:error] | ||||
|       render json: { error: issues[:error] }, status: :unprocessable_entity | ||||
|     else | ||||
|       render json: issues[:data], status: :ok | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def conversation_link | ||||
|     "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/conversations/#{@conversation.display_id}" | ||||
|   end | ||||
|  | ||||
|   def fetch_conversation | ||||
|     @conversation = Current.account.conversations.find_by!(display_id: permitted_params[:conversation_id]) | ||||
|   end | ||||
|  | ||||
|   def linear_processor_service | ||||
|     Integrations::Linear::ProcessorService.new(account: Current.account) | ||||
|   end | ||||
|  | ||||
|   def permitted_params | ||||
|     params.permit(:team_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: []) | ||||
|   end | ||||
| end | ||||
| @@ -1,16 +1,16 @@ | ||||
| <template> | ||||
|   <div class="flex-shrink flex-grow overflow-auto p-4"> | ||||
|   <div class="flex-grow flex-shrink p-4 overflow-auto"> | ||||
|     <div class="flex flex-col"> | ||||
|       <div v-if="uiFlags.isFetching" class="my-0 mx-auto"> | ||||
|       <div v-if="uiFlags.isFetching" class="mx-auto my-0"> | ||||
|         <woot-loading-state :message="$t('INTEGRATION_APPS.FETCHING')" /> | ||||
|       </div> | ||||
|  | ||||
|       <div v-else class="w-full"> | ||||
|         <div> | ||||
|           <div | ||||
|             v-for="item in integrationsList" | ||||
|             v-for="item in enabledIntegrations" | ||||
|             :key="item.id" | ||||
|             class="bg-white dark:bg-slate-800 border border-solid border-slate-75 dark:border-slate-700/50 rounded-sm mb-4 p-4" | ||||
|             class="p-4 mb-4 bg-white border border-solid rounded-sm dark:bg-slate-800 border-slate-75 dark:border-slate-700/50" | ||||
|           > | ||||
|             <integration-item | ||||
|               :integration-id="item.id" | ||||
| @@ -25,22 +25,38 @@ | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import IntegrationItem from './IntegrationItem.vue'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     IntegrationItem, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       uiFlags: 'labels/getUIFlags', | ||||
|       integrationsList: 'integrations/getAppIntegrations', | ||||
|     }), | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$store.dispatch('integrations/get'); | ||||
|   }, | ||||
| }; | ||||
| <script setup> | ||||
| import { useStoreGetters, useStore } from 'dashboard/composables/store'; | ||||
| import { computed, onMounted } from 'vue'; | ||||
| import IntegrationItem from './IntegrationItem.vue'; | ||||
| const store = useStore(); | ||||
| const getters = useStoreGetters(); | ||||
|  | ||||
| const uiFlags = getters['integrations/getUIFlags']; | ||||
|  | ||||
| const accountId = getters.getCurrentAccountId; | ||||
|  | ||||
| const integrationList = computed(() => { | ||||
|   return getters['integrations/getAppIntegrations'].value; | ||||
| }); | ||||
|  | ||||
| const isLinearIntegrationEnabled = computed(() => { | ||||
|   return getters['accounts/isFeatureEnabledonAccount'].value( | ||||
|     accountId.value, | ||||
|     'linear_integration' | ||||
|   ); | ||||
| }); | ||||
| const enabledIntegrations = computed(() => { | ||||
|   if (!isLinearIntegrationEnabled.value) { | ||||
|     return integrationList.value.filter( | ||||
|       integration => integration.id !== 'linear' | ||||
|     ); | ||||
|   } | ||||
|   return integrationList.value; | ||||
| }); | ||||
|  | ||||
| onMounted(() => { | ||||
|   store.dispatch('integrations/get'); | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -21,9 +21,13 @@ const state = { | ||||
| }; | ||||
|  | ||||
| const isAValidAppIntegration = integration => { | ||||
|   return ['dialogflow', 'dyte', 'google_translate', 'openai'].includes( | ||||
|     integration.id | ||||
|   ); | ||||
|   return [ | ||||
|     'dialogflow', | ||||
|     'dyte', | ||||
|     'google_translate', | ||||
|     'openai', | ||||
|     'linear', | ||||
|   ].includes(integration.id); | ||||
| }; | ||||
| export const getters = { | ||||
|   getIntegrations($state) { | ||||
|   | ||||
| @@ -83,3 +83,5 @@ | ||||
| - name: help_center_embedding_search | ||||
|   enabled: false | ||||
|   premium: true | ||||
| - name: linear_integration | ||||
|   enabled: false | ||||
|   | ||||
| @@ -159,3 +159,27 @@ openai: | ||||
|     }, | ||||
|   ] | ||||
|   visible_properties: ['api_key', 'label_suggestion'] | ||||
| linear: | ||||
|   id: linear | ||||
|   logo: linear.png | ||||
|   i18n_key: linear | ||||
|   action: /linear | ||||
|   hook_type: account | ||||
|   allow_multiple_hooks: false | ||||
|   settings_json_schema: { | ||||
|     "type": "object", | ||||
|     "properties": { | ||||
|         "api_key": { "type": "string" }, | ||||
|       }, | ||||
|     "required": ["api_key"], | ||||
|     "additionalProperties": false, | ||||
|   } | ||||
|   settings_form_schema: [ | ||||
|     { | ||||
|       "label": "API Key", | ||||
|       "type": "text", | ||||
|       "name": "api_key", | ||||
|       "validation": "required", | ||||
|     }, | ||||
|   ] | ||||
|   visible_properties: [] | ||||
|   | ||||
| @@ -224,6 +224,9 @@ en: | ||||
|     openai: | ||||
|       name: "OpenAI" | ||||
|       description: "Integrate powerful AI features into Chatwoot by leveraging the GPT models from OpenAI." | ||||
|     linear: | ||||
|       name: "Linear" | ||||
|       description: "Create Linear issues from conversations, or link existing ones for seamless tracking." | ||||
|   public_portal: | ||||
|     search: | ||||
|       search_placeholder: Search for article by title or body... | ||||
|   | ||||
| @@ -227,6 +227,17 @@ Rails.application.routes.draw do | ||||
|                 post :add_participant_to_meeting | ||||
|               end | ||||
|             end | ||||
|             resource :linear, controller: 'linear', only: [] do | ||||
|               collection do | ||||
|                 get :teams | ||||
|                 get :team_entities | ||||
|                 post :create_issue | ||||
|                 post :link_issue | ||||
|                 post :unlink_issue | ||||
|                 get :search_issue | ||||
|                 get :linked_issues | ||||
|               end | ||||
|             end | ||||
|           end | ||||
|           resources :working_hours, only: [:update] | ||||
|  | ||||
|   | ||||
							
								
								
									
										82
									
								
								lib/integrations/linear/processor_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								lib/integrations/linear/processor_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| class Integrations::Linear::ProcessorService | ||||
|   pattr_initialize [:account!] | ||||
|  | ||||
|   def teams | ||||
|     response = linear_client.teams | ||||
|     return { error: response[:error] } if response[:error] | ||||
|  | ||||
|     { data: response['teams']['nodes'].map(&:as_json) } | ||||
|   end | ||||
|  | ||||
|   def team_entities(team_id) | ||||
|     response = linear_client.team_entities(team_id) | ||||
|     return response if response[:error] | ||||
|  | ||||
|     { | ||||
|       data: { | ||||
|         users: response['users']['nodes'].map(&:as_json), | ||||
|         projects: response['projects']['nodes'].map(&:as_json), | ||||
|         states: response['workflowStates']['nodes'].map(&:as_json), | ||||
|         labels: response['issueLabels']['nodes'].map(&:as_json) | ||||
|       } | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   def create_issue(params) | ||||
|     response = linear_client.create_issue(params) | ||||
|     return response if response[:error] | ||||
|  | ||||
|     { | ||||
|       data: { id: response['issueCreate']['issue']['id'], | ||||
|               title: response['issueCreate']['issue']['title'] } | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   def link_issue(link, issue_id, title) | ||||
|     response = linear_client.link_issue(link, issue_id, title) | ||||
|     return response if response[:error] | ||||
|  | ||||
|     { | ||||
|       data: { | ||||
|         id: issue_id, | ||||
|         link: link, | ||||
|         link_id: response.with_indifferent_access[:attachmentLinkURL][:attachment][:id] | ||||
|       } | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   def unlink_issue(link_id) | ||||
|     response = linear_client.unlink_issue(link_id) | ||||
|     return response if response[:error] | ||||
|  | ||||
|     { | ||||
|       data: { link_id: link_id } | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   def search_issue(term) | ||||
|     response = linear_client.search_issue(term) | ||||
|  | ||||
|     return response if response[:error] | ||||
|  | ||||
|     { data: response['searchIssues']['nodes'].map(&:as_json) } | ||||
|   end | ||||
|  | ||||
|   def linked_issues(url) | ||||
|     response = linear_client.linked_issues(url) | ||||
|     return response if response[:error] | ||||
|  | ||||
|     { data: response['attachmentsForURL']['nodes'].map(&:as_json) } | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def linear_hook | ||||
|     @linear_hook ||= account.hooks.find_by!(app_id: 'linear') | ||||
|   end | ||||
|  | ||||
|   def linear_client | ||||
|     credentials = linear_hook.settings | ||||
|     @linear_client ||= Linear.new(credentials['api_key']) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										120
									
								
								lib/linear.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								lib/linear.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| class Linear | ||||
|   BASE_URL = 'https://api.linear.app/graphql'.freeze | ||||
|   PRIORITY_LEVELS = (0..4).to_a | ||||
|  | ||||
|   def initialize(api_key) | ||||
|     @api_key = api_key | ||||
|     raise ArgumentError, 'Missing Credentials' if api_key.blank? | ||||
|   end | ||||
|  | ||||
|   def teams | ||||
|     query = { | ||||
|       query: Linear::Queries::TEAMS_QUERY | ||||
|     } | ||||
|     response = post(query) | ||||
|     process_response(response) | ||||
|   end | ||||
|  | ||||
|   def team_entities(team_id) | ||||
|     raise ArgumentError, 'Missing team id' if team_id.blank? | ||||
|  | ||||
|     query = { | ||||
|       query: Linear::Queries.team_entities_query(team_id) | ||||
|     } | ||||
|     response = post(query) | ||||
|     process_response(response) | ||||
|   end | ||||
|  | ||||
|   def search_issue(term) | ||||
|     raise ArgumentError, 'Missing search term' if term.blank? | ||||
|  | ||||
|     query = { | ||||
|       query: Linear::Queries.search_issue(term) | ||||
|     } | ||||
|     response = post(query) | ||||
|     process_response(response) | ||||
|   end | ||||
|  | ||||
|   def linked_issues(url) | ||||
|     raise ArgumentError, 'Missing link' if url.blank? | ||||
|  | ||||
|     query = { | ||||
|       query: Linear::Queries.linked_issues(url) | ||||
|     } | ||||
|     response = post(query) | ||||
|     process_response(response) | ||||
|   end | ||||
|  | ||||
|   def create_issue(params) | ||||
|     validate_team_and_title(params) | ||||
|     validate_priority(params[:priority]) | ||||
|     validate_label_ids(params[:label_ids]) | ||||
|  | ||||
|     variables = { | ||||
|       title: params[:title], | ||||
|       teamId: params[:team_id], | ||||
|       description: params[:description], | ||||
|       assigneeId: params[:assignee_id], | ||||
|       priority: params[:priority], | ||||
|       labelIds: params[:label_ids] | ||||
|     }.compact | ||||
|     mutation = Linear::Mutations.issue_create(variables) | ||||
|     response = post({ query: mutation }) | ||||
|     process_response(response) | ||||
|   end | ||||
|  | ||||
|   def link_issue(link, issue_id, title) | ||||
|     raise ArgumentError, 'Missing link' if link.blank? | ||||
|     raise ArgumentError, 'Missing issue id' if issue_id.blank? | ||||
|  | ||||
|     payload = { | ||||
|       query: Linear::Mutations.issue_link(issue_id, link, title) | ||||
|     } | ||||
|     response = post(payload) | ||||
|     process_response(response) | ||||
|   end | ||||
|  | ||||
|   def unlink_issue(link_id) | ||||
|     raise ArgumentError, 'Missing  link id' if link_id.blank? | ||||
|  | ||||
|     payload = { | ||||
|       query: Linear::Mutations.unlink_issue(link_id) | ||||
|     } | ||||
|     response = post(payload) | ||||
|     process_response(response) | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def validate_team_and_title(params) | ||||
|     raise ArgumentError, 'Missing team id' if params[:team_id].blank? | ||||
|     raise ArgumentError, 'Missing title' if params[:title].blank? | ||||
|   end | ||||
|  | ||||
|   def validate_priority(priority) | ||||
|     return if priority.nil? || PRIORITY_LEVELS.include?(priority) | ||||
|  | ||||
|     raise ArgumentError, 'Invalid priority value. Priority must be 0, 1, 2, 3, or 4.' | ||||
|   end | ||||
|  | ||||
|   def validate_label_ids(label_ids) | ||||
|     return if label_ids.nil? | ||||
|     return if label_ids.is_a?(Array) && label_ids.all?(String) | ||||
|  | ||||
|     raise ArgumentError, 'label_ids must be an array of strings.' | ||||
|   end | ||||
|  | ||||
|   def post(payload) | ||||
|     HTTParty.post( | ||||
|       BASE_URL, | ||||
|       headers: { 'Authorization' => @api_key, 'Content-Type' => 'application/json' }, | ||||
|       body: payload.to_json | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def process_response(response) | ||||
|     return response.parsed_response['data'].with_indifferent_access if response.success? && !response.parsed_response['data'].nil? | ||||
|  | ||||
|     { error: response.parsed_response, error_code: response.code } | ||||
|   end | ||||
| end | ||||
							
								
								
									
										56
									
								
								lib/linear/mutations.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								lib/linear/mutations.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| module Linear::Mutations | ||||
|   def self.graphql_value(value) | ||||
|     case value | ||||
|     when String | ||||
|       # Strings must be enclosed in double quotes | ||||
|       "\"#{value}\"" | ||||
|     when Array | ||||
|       # Arrays need to be recursively converted | ||||
|       "[#{value.map { |v| graphql_value(v) }.join(', ')}]" | ||||
|     else | ||||
|       # Other types (numbers, booleans) can be directly converted to strings | ||||
|       value.to_s | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def self.graphql_input(input) | ||||
|     input.map { |key, value| "#{key}: #{graphql_value(value)}" }.join(', ') | ||||
|   end | ||||
|  | ||||
|   def self.issue_create(input) | ||||
|     <<~GRAPHQL | ||||
|       mutation { | ||||
|         issueCreate(input: { #{graphql_input(input)} }) { | ||||
|           success | ||||
|           issue { | ||||
|             id | ||||
|             title | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     GRAPHQL | ||||
|   end | ||||
|  | ||||
|   def self.issue_link(issue_id, link, title) | ||||
|     <<~GRAPHQL | ||||
|       mutation { | ||||
|         attachmentLinkURL(url: "#{link}", issueId: "#{issue_id}", title: "#{title}") { | ||||
|           success | ||||
|           attachment { | ||||
|             id | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     GRAPHQL | ||||
|   end | ||||
|  | ||||
|   def self.unlink_issue(link_id) | ||||
|     <<~GRAPHQL | ||||
|       mutation { | ||||
|         attachmentDelete(id: "#{link_id}") { | ||||
|           success | ||||
|         } | ||||
|       } | ||||
|     GRAPHQL | ||||
|   end | ||||
| end | ||||
							
								
								
									
										100
									
								
								lib/linear/queries.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								lib/linear/queries.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| module Linear::Queries | ||||
|   TEAMS_QUERY = <<~GRAPHQL.freeze | ||||
|     query { | ||||
|       teams { | ||||
|         nodes { | ||||
|           id | ||||
|           name | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   GRAPHQL | ||||
|  | ||||
|   def self.team_entities_query(team_id) | ||||
|     <<~GRAPHQL | ||||
|       query { | ||||
|         users { | ||||
|           nodes { | ||||
|             id | ||||
|             name | ||||
|           } | ||||
|         } | ||||
|         projects { | ||||
|           nodes { | ||||
|             id | ||||
|             name | ||||
|           } | ||||
|         } | ||||
|         workflowStates( | ||||
|           filter: { team: { id: { eq: "#{team_id}" } } } | ||||
|         ) { | ||||
|           nodes { | ||||
|             id | ||||
|             name | ||||
|           } | ||||
|         } | ||||
|         issueLabels( | ||||
|           filter: { team: { id: { eq: "#{team_id}" } } } | ||||
|         ) { | ||||
|           nodes { | ||||
|             id | ||||
|             name | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     GRAPHQL | ||||
|   end | ||||
|  | ||||
|   def self.search_issue(term) | ||||
|     <<~GRAPHQL | ||||
|       query { | ||||
|         searchIssues(term: "#{term}") { | ||||
|           nodes { | ||||
|             id | ||||
|             title | ||||
|             description | ||||
|             identifier | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     GRAPHQL | ||||
|   end | ||||
|  | ||||
|   def self.linked_issues(url) | ||||
|     <<~GRAPHQL | ||||
|       query { | ||||
|         attachmentsForURL(url: "#{url}") { | ||||
|           nodes { | ||||
|             id | ||||
|             title | ||||
|             issue { | ||||
|               id | ||||
|               identifier | ||||
|               title | ||||
|               description | ||||
|               priority | ||||
|               createdAt | ||||
|               url | ||||
|               assignee { | ||||
|                 name | ||||
|                 avatarUrl | ||||
|               } | ||||
|               state { | ||||
|                 name | ||||
|                 color | ||||
|               } | ||||
|               labels { | ||||
|                 nodes{ | ||||
|                   id | ||||
|                   name | ||||
|                   color | ||||
|                   description | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     GRAPHQL | ||||
|   end | ||||
| end | ||||
							
								
								
									
										
											BIN
										
									
								
								public/dashboard/images/integrations/linear.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/dashboard/images/integrations/linear.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.4 KiB | 
| @@ -0,0 +1,257 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe 'Linear Integration API', type: :request do | ||||
|   let(:account) { create(:account) } | ||||
|   let(:user) { create(:user) } | ||||
|   let(:api_key) { 'valid_api_key' } | ||||
|   let(:agent) { create(:user, account: account, role: :agent) } | ||||
|   let(:processor_service) { instance_double(Integrations::Linear::ProcessorService) } | ||||
|  | ||||
|   before do | ||||
|     create(:integrations_hook, :linear, account: account) | ||||
|     allow(Integrations::Linear::ProcessorService).to receive(:new).with(account: account).and_return(processor_service) | ||||
|   end | ||||
|  | ||||
|   describe 'GET /api/v1/accounts/:account_id/integrations/linear/teams' do | ||||
|     context 'when it is an authenticated user' do | ||||
|       context 'when data is retrieved successfully' do | ||||
|         let(:teams_data) { { data: [{ 'id' => 'team1', 'name' => 'Team One' }] } } | ||||
|  | ||||
|         it 'returns team data' do | ||||
|           allow(processor_service).to receive(:teams).and_return(teams_data) | ||||
|           get "/api/v1/accounts/#{account.id}/integrations/linear/teams", | ||||
|               headers: agent.create_new_auth_token, | ||||
|               as: :json | ||||
|           expect(response).to have_http_status(:ok) | ||||
|           expect(response.body).to include('Team One') | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       context 'when data retrieval fails' do | ||||
|         it 'returns error message' do | ||||
|           allow(processor_service).to receive(:teams).and_return(error: 'error message') | ||||
|           get "/api/v1/accounts/#{account.id}/integrations/linear/teams", | ||||
|               headers: agent.create_new_auth_token, | ||||
|               as: :json | ||||
|           expect(response).to have_http_status(:unprocessable_entity) | ||||
|           expect(response.body).to include('error message') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'GET /api/v1/accounts/:account_id/integrations/linear/team_entities' do | ||||
|     let(:team_id) { 'team1' } | ||||
|  | ||||
|     context 'when it is an authenticated user' do | ||||
|       context 'when data is retrieved successfully' do | ||||
|         let(:team_entities_data) do | ||||
|           { data: { | ||||
|             users: [{ 'id' => 'user1', 'name' => 'User One' }], | ||||
|             projects: [{ 'id' => 'project1', 'name' => 'Project One' }], | ||||
|             states: [{ 'id' => 'state1', 'name' => 'State One' }], | ||||
|             labels: [{ 'id' => 'label1', 'name' => 'Label One' }] | ||||
|           } } | ||||
|         end | ||||
|  | ||||
|         it 'returns team entities data' do | ||||
|           allow(processor_service).to receive(:team_entities).with(team_id).and_return(team_entities_data) | ||||
|           get "/api/v1/accounts/#{account.id}/integrations/linear/team_entities", | ||||
|               params: { team_id: team_id }, | ||||
|               headers: agent.create_new_auth_token, | ||||
|               as: :json | ||||
|           expect(response).to have_http_status(:ok) | ||||
|           expect(response.body).to include('User One') | ||||
|           expect(response.body).to include('Project One') | ||||
|           expect(response.body).to include('State One') | ||||
|           expect(response.body).to include('Label One') | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       context 'when data retrieval fails' do | ||||
|         it 'returns error message' do | ||||
|           allow(processor_service).to receive(:team_entities).with(team_id).and_return(error: 'error message') | ||||
|           get "/api/v1/accounts/#{account.id}/integrations/linear/team_entities", | ||||
|               params: { team_id: team_id }, | ||||
|               headers: agent.create_new_auth_token, | ||||
|               as: :json | ||||
|           expect(response).to have_http_status(:unprocessable_entity) | ||||
|           expect(response.body).to include('error message') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'POST /api/v1/accounts/:account_id/integrations/linear/create_issue' do | ||||
|     let(:issue_params) do | ||||
|       { | ||||
|         team_id: 'team1', | ||||
|         title: 'Sample Issue', | ||||
|         description: 'This is a sample issue.', | ||||
|         assignee_id: 'user1', | ||||
|         priority: 'high', | ||||
|         label_ids: ['label1'] | ||||
|       } | ||||
|     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' } } } | ||||
|  | ||||
|         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 | ||||
|       end | ||||
|  | ||||
|       context 'when issue creation fails' do | ||||
|         it 'returns error 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(response).to have_http_status(:unprocessable_entity) | ||||
|           expect(response.body).to include('error message') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'POST /api/v1/accounts/:account_id/integrations/linear/link_issue' do | ||||
|     let(:issue_id) { 'issue1' } | ||||
|     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' } | ||||
|  | ||||
|     context 'when it is an authenticated user' 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 | ||||
|           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(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 | ||||
|           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(response).to have_http_status(:unprocessable_entity) | ||||
|           expect(response.body).to include('error message') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'POST /api/v1/accounts/:account_id/integrations/linear/unlink_issue' do | ||||
|     let(:link_id) { 'attachment1' } | ||||
|  | ||||
|     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 | ||||
|           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(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 | ||||
|           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(response).to have_http_status(:unprocessable_entity) | ||||
|           expect(response.body).to include('error message') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'GET /api/v1/accounts/:account_id/integrations/linear/search_issue' do | ||||
|     let(:term) { 'issue' } | ||||
|  | ||||
|     context 'when it is an authenticated user' do | ||||
|       context 'when search is successful' do | ||||
|         let(:search_results) { { data: [{ 'id' => 'issue1', 'title' => 'Sample Issue' }] } } | ||||
|  | ||||
|         it 'returns search results' do | ||||
|           allow(processor_service).to receive(:search_issue).with(term).and_return(search_results) | ||||
|           get "/api/v1/accounts/#{account.id}/integrations/linear/search_issue", | ||||
|               params: { q: term }, | ||||
|               headers: agent.create_new_auth_token, | ||||
|               as: :json | ||||
|           expect(response).to have_http_status(:ok) | ||||
|           expect(response.body).to include('Sample Issue') | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       context 'when search fails' do | ||||
|         it 'returns error message' do | ||||
|           allow(processor_service).to receive(:search_issue).with(term).and_return(error: 'error message') | ||||
|           get "/api/v1/accounts/#{account.id}/integrations/linear/search_issue", | ||||
|               params: { q: term }, | ||||
|               headers: agent.create_new_auth_token, | ||||
|               as: :json | ||||
|           expect(response).to have_http_status(:unprocessable_entity) | ||||
|           expect(response.body).to include('error message') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'GET /api/v1/accounts/:account_id/integrations/linear/linked_issues' do | ||||
|     let(:conversation) { create(:conversation, account: account) } | ||||
|     let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" } | ||||
|  | ||||
|     context 'when it is an authenticated user' do | ||||
|       context 'when linked issue is found' do | ||||
|         let(:linked_issue) { { data: [{ 'id' => 'issue1', 'title' => 'Sample Issue' }] } } | ||||
|  | ||||
|         it 'returns linked issue' do | ||||
|           allow(processor_service).to receive(:linked_issues).with(link).and_return(linked_issue) | ||||
|           get "/api/v1/accounts/#{account.id}/integrations/linear/linked_issues", | ||||
|               params: { conversation_id: conversation.display_id }, | ||||
|               headers: agent.create_new_auth_token, | ||||
|               as: :json | ||||
|           expect(response).to have_http_status(:ok) | ||||
|           expect(response.body).to include('Sample Issue') | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       context 'when linked issue is not found' do | ||||
|         it 'returns error message' do | ||||
|           allow(processor_service).to receive(:linked_issues).with(link).and_return(error: 'error message') | ||||
|           get "/api/v1/accounts/#{account.id}/integrations/linear/linked_issues", | ||||
|               params: { conversation_id: conversation.display_id }, | ||||
|               headers: agent.create_new_auth_token, | ||||
|               as: :json | ||||
|           expect(response).to have_http_status(:unprocessable_entity) | ||||
|           expect(response.body).to include('error message') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -26,5 +26,10 @@ FactoryBot.define do | ||||
|       app_id { 'openai' } | ||||
|       settings { { api_key: 'api_key' } } | ||||
|     end | ||||
|  | ||||
|     trait :linear do | ||||
|       app_id { 'linear' } | ||||
|       settings { { api_key: 'api_key' } } | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										209
									
								
								spec/lib/integrations/linear/processor_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								spec/lib/integrations/linear/processor_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| describe Integrations::Linear::ProcessorService do | ||||
|   let(:account) { create(:account) } | ||||
|   let(:api_key) { 'valid_api_key' } | ||||
|   let(:linear_client) { instance_double(Linear) } | ||||
|   let(:service) { described_class.new(account: account) } | ||||
|  | ||||
|   before do | ||||
|     create(:integrations_hook, :linear, account: account) | ||||
|     allow(Linear).to receive(:new).and_return(linear_client) | ||||
|   end | ||||
|  | ||||
|   describe '#teams' do | ||||
|     context 'when Linear client returns valid data' do | ||||
|       let(:teams_response) do | ||||
|         { 'teams' => { 'nodes' => [{ 'id' => 'team1', 'name' => 'Team One' }] } } | ||||
|       end | ||||
|  | ||||
|       it 'returns parsed team data' do | ||||
|         allow(linear_client).to receive(:teams).and_return(teams_response) | ||||
|         result = service.teams | ||||
|         expect(result).to eq({ data: [{ 'id' => 'team1', 'name' => 'Team One' }] }) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns an error' do | ||||
|       let(:error_response) { { error: 'Some error message' } } | ||||
|  | ||||
|       it 'returns the error' do | ||||
|         allow(linear_client).to receive(:teams).and_return(error_response) | ||||
|         result = service.teams | ||||
|         expect(result).to eq(error_response) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe '#team_entities' do | ||||
|     let(:team_id) { 'team1' } | ||||
|     let(:entities_response) do | ||||
|       { | ||||
|         'users' => { 'nodes' => [{ 'id' => 'user1', 'name' => 'User One' }] }, | ||||
|         'projects' => { 'nodes' => [{ 'id' => 'project1', 'name' => 'Project One' }] }, | ||||
|         'workflowStates' => { 'nodes' => [] }, | ||||
|         'issueLabels' => { 'nodes' => [{ 'id' => 'bug', 'name' => 'Bug' }] } | ||||
|       } | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns valid data' do | ||||
|       it 'returns parsed entity data' do | ||||
|         allow(linear_client).to receive(:team_entities).with(team_id).and_return(entities_response) | ||||
|         result = service.team_entities(team_id) | ||||
|         expect(result).to eq({ :data => { :users => | ||||
|         [{ 'id' => 'user1', 'name' => 'User One' }], | ||||
|                                           :projects => [{ 'id' => 'project1', 'name' => 'Project One' }], | ||||
|                                           :states => [], :labels => [{ 'id' => 'bug', 'name' => 'Bug' }] } }) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns an error' do | ||||
|       let(:error_response) { { error: 'Some error message' } } | ||||
|  | ||||
|       it 'returns the error' do | ||||
|         allow(linear_client).to receive(:team_entities).with(team_id).and_return(error_response) | ||||
|         result = service.team_entities(team_id) | ||||
|         expect(result).to eq(error_response) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe '#create_issue' do | ||||
|     let(:params) do | ||||
|       { | ||||
|         title: 'Issue title', | ||||
|         team_id: 'team1', | ||||
|         description: 'Issue description', | ||||
|         assignee_id: 'user1', | ||||
|         priority: 2, | ||||
|         label_ids: %w[bug] | ||||
|       } | ||||
|     end | ||||
|     let(:issue_response) do | ||||
|       { | ||||
|         'issueCreate' => { 'issue' => { 'id' => 'issue1', 'title' => 'Issue title' } } | ||||
|       } | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns valid data' do | ||||
|       it 'returns parsed issue data' 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' } }) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns an error' do | ||||
|       let(:error_response) { { error: 'Some error message' } } | ||||
|  | ||||
|       it 'returns the error' do | ||||
|         allow(linear_client).to receive(:create_issue).with(params).and_return(error_response) | ||||
|         result = service.create_issue(params) | ||||
|         expect(result).to eq(error_response) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe '#link_issue' do | ||||
|     let(:link) { 'https://example.com' } | ||||
|     let(:issue_id) { 'issue1' } | ||||
|     let(:title) { 'Title' } | ||||
|     let(:link_issue_response) { { id: issue_id, link: link, 'attachmentLinkURL': { 'attachment': { 'id': 'attachment1' } } } } | ||||
|     let(:link_response) { { data: { id: issue_id, link: link, link_id: 'attachment1' } } } | ||||
|  | ||||
|     context 'when Linear client returns valid data' do | ||||
|       it 'returns parsed link data' do | ||||
|         allow(linear_client).to receive(:link_issue).with(link, issue_id, title).and_return(link_issue_response) | ||||
|         result = service.link_issue(link, issue_id, title) | ||||
|         expect(result).to eq(link_response) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns an error' do | ||||
|       let(:error_response) { { error: 'Some error message' } } | ||||
|  | ||||
|       it 'returns the error' do | ||||
|         allow(linear_client).to receive(:link_issue).with(link, issue_id, title).and_return(error_response) | ||||
|         result = service.link_issue(link, issue_id, title) | ||||
|         expect(result).to eq(error_response) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe '#unlink_issue' do | ||||
|     let(:link_id) { 'attachment1' } | ||||
|     let(:unlink_response) { { data: { link_id: link_id } } } | ||||
|  | ||||
|     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) | ||||
|         result = service.unlink_issue(link_id) | ||||
|         expect(result).to eq(unlink_response) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns an error' do | ||||
|       let(:error_response) { { error: 'Some error message' } } | ||||
|  | ||||
|       it 'returns the error' do | ||||
|         allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(error_response) | ||||
|         result = service.unlink_issue(link_id) | ||||
|         expect(result).to eq(error_response) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe '#search_issue' do | ||||
|     let(:term) { 'search term' } | ||||
|     let(:search_response) do | ||||
|       { | ||||
|         'searchIssues' => { 'nodes' => [{ 'id' => 'issue1', 'title' => 'Issue title', 'description' => 'Issue description' }] } | ||||
|       } | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns valid data' do | ||||
|       it 'returns parsed search data' do | ||||
|         allow(linear_client).to receive(:search_issue).with(term).and_return(search_response) | ||||
|         result = service.search_issue(term) | ||||
|         expect(result).to eq({ :data => [{ 'description' => 'Issue description', 'id' => 'issue1', 'title' => 'Issue title' }] }) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns an error' do | ||||
|       let(:error_response) { { error: 'Some error message' } } | ||||
|  | ||||
|       it 'returns the error' do | ||||
|         allow(linear_client).to receive(:search_issue).with(term).and_return(error_response) | ||||
|         result = service.search_issue(term) | ||||
|         expect(result).to eq(error_response) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe '#linked_issues' do | ||||
|     let(:url) { 'https://example.com' } | ||||
|     let(:linked_response) do | ||||
|       { | ||||
|         'attachmentsForURL' => { 'nodes' => [{ 'id' => 'attachment1', :issue => { 'id' => 'issue1' } }] } | ||||
|       } | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns valid data' do | ||||
|       it 'returns parsed linked data' do | ||||
|         allow(linear_client).to receive(:linked_issues).with(url).and_return(linked_response) | ||||
|         result = service.linked_issues(url) | ||||
|         expect(result).to eq({ :data => [{ 'id' => 'attachment1', 'issue' => { 'id' => 'issue1' } }] }) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when Linear client returns an error' do | ||||
|       let(:error_response) { { error: 'Some error message' } } | ||||
|  | ||||
|       it 'returns the error' do | ||||
|         allow(linear_client).to receive(:linked_issues).with(url).and_return(error_response) | ||||
|         result = service.linked_issues(url) | ||||
|         expect(result).to eq(error_response) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										302
									
								
								spec/lib/linear_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								spec/lib/linear_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,302 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| describe Linear do | ||||
|   let(:api_key) { 'valid_api_key' } | ||||
|   let(:url) { 'https://api.linear.app/graphql' } | ||||
|   let(:linear_client) { described_class.new(api_key) } | ||||
|   let(:headers) { { 'Content-Type' => 'application/json', 'Authorization' => api_key } } | ||||
|  | ||||
|   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 | ||||
|  | ||||
|     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 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 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' } | ||||
|  | ||||
|     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 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 | ||||
		Reference in New Issue
	
	Block a user
	 Muhsin Keloth
					Muhsin Keloth