feat: add controller for custom tools
This commit is contained in:
@@ -67,6 +67,7 @@ Rails.application.routes.draw do
|
||||
resources :copilot_threads, only: [:index, :create] do
|
||||
resources :copilot_messages, only: [:index, :create]
|
||||
end
|
||||
resources :custom_tools
|
||||
resources :documents, only: [:index, :show, :create, :destroy]
|
||||
end
|
||||
resource :saml_settings, only: [:show, :create, :update, :destroy]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
class Api::V1::Accounts::Captain::CustomToolsController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action -> { check_authorization(Captain::CustomTool) }
|
||||
before_action :set_custom_tool, only: [:show, :update, :destroy]
|
||||
|
||||
def index
|
||||
@custom_tools = account_custom_tools.enabled
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@custom_tool = account_custom_tools.create!(custom_tool_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@custom_tool.update!(custom_tool_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@custom_tool.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_custom_tool
|
||||
@custom_tool = account_custom_tools.find(params[:id])
|
||||
end
|
||||
|
||||
def account_custom_tools
|
||||
@account_custom_tools ||= Current.account.captain_custom_tools
|
||||
end
|
||||
|
||||
def custom_tool_params
|
||||
params.require(:custom_tool).permit(
|
||||
:title,
|
||||
:description,
|
||||
:endpoint_url,
|
||||
:http_method,
|
||||
:request_template,
|
||||
:response_template,
|
||||
:auth_type,
|
||||
:enabled,
|
||||
auth_config: {},
|
||||
param_schema: [:name, :type, :description, :required]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -23,7 +23,10 @@ module Concerns::SafeEndpointValidatable
|
||||
end
|
||||
|
||||
def parse_endpoint_uri
|
||||
URI.parse(endpoint_url)
|
||||
# Strip Liquid template syntax for validation
|
||||
# Replace {{ variable }} with a placeholder value
|
||||
sanitized_url = endpoint_url.gsub(/\{\{[^}]+\}\}/, 'placeholder')
|
||||
URI.parse(sanitized_url)
|
||||
rescue URI::InvalidURIError
|
||||
nil
|
||||
end
|
||||
|
||||
@@ -9,6 +9,7 @@ module Enterprise::Concerns::Account
|
||||
|
||||
has_many :captain_assistants, dependent: :destroy_async, class_name: 'Captain::Assistant'
|
||||
has_many :captain_assistant_responses, dependent: :destroy_async, class_name: 'Captain::AssistantResponse'
|
||||
has_many :captain_custom_tools, dependent: :destroy_async, class_name: 'Captain::CustomTool'
|
||||
has_many :captain_documents, dependent: :destroy_async, class_name: 'Captain::Document'
|
||||
|
||||
has_many :copilot_threads, dependent: :destroy_async
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
class Captain::CustomToolPolicy < ApplicationPolicy
|
||||
def index?
|
||||
true
|
||||
end
|
||||
|
||||
def show?
|
||||
true
|
||||
end
|
||||
|
||||
def create?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def update?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! 'api/v1/models/captain/custom_tool', custom_tool: @custom_tool
|
||||
@@ -0,0 +1,10 @@
|
||||
json.payload do
|
||||
json.array! @custom_tools do |custom_tool|
|
||||
json.partial! 'api/v1/models/captain/custom_tool', custom_tool: custom_tool
|
||||
end
|
||||
end
|
||||
|
||||
json.meta do
|
||||
json.total_count @custom_tools.count
|
||||
json.page 1
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! 'api/v1/models/captain/custom_tool', custom_tool: @custom_tool
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! 'api/v1/models/captain/custom_tool', custom_tool: @custom_tool
|
||||
@@ -0,0 +1,15 @@
|
||||
json.id custom_tool.id
|
||||
json.slug custom_tool.slug
|
||||
json.title custom_tool.title
|
||||
json.description custom_tool.description
|
||||
json.endpoint_url custom_tool.endpoint_url
|
||||
json.http_method custom_tool.http_method
|
||||
json.request_template custom_tool.request_template
|
||||
json.response_template custom_tool.response_template
|
||||
json.auth_type custom_tool.auth_type
|
||||
json.auth_config custom_tool.auth_config
|
||||
json.param_schema custom_tool.param_schema
|
||||
json.enabled custom_tool.enabled
|
||||
json.account_id custom_tool.account_id
|
||||
json.created_at custom_tool.created_at
|
||||
json.updated_at custom_tool.updated_at
|
||||
@@ -0,0 +1,281 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Accounts::Captain::CustomTools', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
def json_response
|
||||
JSON.parse(response.body, symbolize_names: true)
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/captain/custom_tools' do
|
||||
context 'when it is an un-authenticated user' do
|
||||
it 'returns unauthorized status' do
|
||||
get "/api/v1/accounts/#{account.id}/captain/custom_tools"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
it 'returns success status' do
|
||||
create_list(:captain_custom_tool, 3, account: account)
|
||||
get "/api/v1/accounts/#{account.id}/captain/custom_tools",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(json_response[:payload].length).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an admin' do
|
||||
it 'returns success status and custom tools' do
|
||||
create_list(:captain_custom_tool, 5, account: account)
|
||||
get "/api/v1/accounts/#{account.id}/captain/custom_tools",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(json_response[:payload].length).to eq(5)
|
||||
end
|
||||
|
||||
it 'returns only enabled custom tools' do
|
||||
create(:captain_custom_tool, account: account, enabled: true)
|
||||
create(:captain_custom_tool, account: account, enabled: false)
|
||||
get "/api/v1/accounts/#{account.id}/captain/custom_tools",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(json_response[:payload].length).to eq(1)
|
||||
expect(json_response[:payload].first[:enabled]).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/captain/custom_tools/{id}' do
|
||||
let(:custom_tool) { create(:captain_custom_tool, account: account) }
|
||||
|
||||
context 'when it is an un-authenticated user' do
|
||||
it 'returns unauthorized status' do
|
||||
get "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
it 'returns success status and custom tool' do
|
||||
get "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(json_response[:id]).to eq(custom_tool.id)
|
||||
expect(json_response[:title]).to eq(custom_tool.title)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when custom tool does not exist' do
|
||||
it 'returns not found status' do
|
||||
get "/api/v1/accounts/#{account.id}/captain/custom_tools/999999",
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/captain/custom_tools' do
|
||||
let(:valid_attributes) do
|
||||
{
|
||||
custom_tool: {
|
||||
title: 'Fetch Order Status',
|
||||
description: 'Fetches order status from external API',
|
||||
endpoint_url: 'https://api.example.com/orders/{{ order_id }}',
|
||||
http_method: 'GET',
|
||||
enabled: true,
|
||||
param_schema: [
|
||||
{ name: 'order_id', type: 'string', description: 'The order ID', required: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
context 'when it is an un-authenticated user' do
|
||||
it 'returns unauthorized status' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/custom_tools",
|
||||
params: valid_attributes
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
it 'returns unauthorized status' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/custom_tools",
|
||||
params: valid_attributes,
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an admin' do
|
||||
it 'creates a new custom tool and returns success status' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/custom_tools",
|
||||
params: valid_attributes,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(json_response[:title]).to eq('Fetch Order Status')
|
||||
expect(json_response[:description]).to eq('Fetches order status from external API')
|
||||
expect(json_response[:enabled]).to be(true)
|
||||
expect(json_response[:slug]).to eq('custom_fetch-order-status')
|
||||
expect(json_response[:param_schema]).to eq([
|
||||
{ name: 'order_id', type: 'string', description: 'The order ID', required: true }
|
||||
])
|
||||
end
|
||||
|
||||
context 'with invalid parameters' do
|
||||
let(:invalid_attributes) do
|
||||
{
|
||||
custom_tool: {
|
||||
title: '',
|
||||
endpoint_url: ''
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns unprocessable entity status' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/custom_tools",
|
||||
params: invalid_attributes,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid endpoint URL' do
|
||||
let(:invalid_url_attributes) do
|
||||
{
|
||||
custom_tool: {
|
||||
title: 'Test Tool',
|
||||
endpoint_url: 'http://localhost/api',
|
||||
http_method: 'GET'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns unprocessable entity status' do
|
||||
post "/api/v1/accounts/#{account.id}/captain/custom_tools",
|
||||
params: invalid_url_attributes,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /api/v1/accounts/{account.id}/captain/custom_tools/{id}' do
|
||||
let(:custom_tool) { create(:captain_custom_tool, account: account) }
|
||||
let(:update_attributes) do
|
||||
{
|
||||
custom_tool: {
|
||||
title: 'Updated Tool Title',
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
context 'when it is an un-authenticated user' do
|
||||
it 'returns unauthorized status' do
|
||||
patch "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
|
||||
params: update_attributes
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
it 'returns unauthorized status' do
|
||||
patch "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
|
||||
params: update_attributes,
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an admin' do
|
||||
it 'updates the custom tool and returns success status' do
|
||||
patch "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
|
||||
params: update_attributes,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(json_response[:title]).to eq('Updated Tool Title')
|
||||
expect(json_response[:enabled]).to be(false)
|
||||
end
|
||||
|
||||
context 'with invalid parameters' do
|
||||
let(:invalid_attributes) do
|
||||
{
|
||||
custom_tool: {
|
||||
title: ''
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns unprocessable entity status' do
|
||||
patch "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
|
||||
params: invalid_attributes,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/captain/custom_tools/{id}' do
|
||||
let!(:custom_tool) { create(:captain_custom_tool, account: account) }
|
||||
|
||||
context 'when it is an un-authenticated user' do
|
||||
it 'returns unauthorized status' do
|
||||
delete "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}"
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
it 'returns unauthorized status' do
|
||||
delete "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an admin' do
|
||||
it 'deletes the custom tool and returns no content status' do
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/captain/custom_tools/#{custom_tool.id}",
|
||||
headers: admin.create_new_auth_token
|
||||
end.to change(Captain::CustomTool, :count).by(-1)
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
end
|
||||
|
||||
context 'when custom tool does not exist' do
|
||||
it 'returns not found status' do
|
||||
delete "/api/v1/accounts/#{account.id}/captain/custom_tools/999999",
|
||||
headers: admin.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user