feat: add a common upload endpoint (#7806)

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Shivam Mishra
2023-08-31 10:36:02 +07:00
committed by GitHub
parent 25c3fb3c36
commit deec1d213b
11 changed files with 195 additions and 44 deletions

View File

@@ -0,0 +1,13 @@
class Api::V1::UploadController < Api::BaseController
def create
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:attachment].tempfile,
filename: params[:attachment].original_filename,
content_type: params[:attachment].content_type
)
file_blob.save!
render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id }
end
end

View File

@@ -9,14 +9,6 @@ class AutomationsAPI extends ApiClient {
clone(automationId) {
return axios.post(`${this.url}/${automationId}/clone`);
}
attachment(file) {
return axios.post(`${this.url}/attach_file`, file, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
}
export default new AutomationsAPI();

View File

@@ -47,20 +47,6 @@ class ArticlesAPI extends PortalsAPI {
return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`);
}
uploadImage({ portalSlug, file }) {
let formData = new FormData();
formData.append('background_image', file);
return axios.post(
`${this.url}/${portalSlug}/articles/attach_file`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
}
reorderArticles({ portalSlug, reorderedGroup, categorySlug }) {
return axios.post(`${this.url}/${portalSlug}/articles/reorder`, {
positions_hash: reorderedGroup,

View File

@@ -61,11 +61,9 @@ export default {
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
try {
const file = event.target.files[0];
const formData = new FormData();
formData.append('attachment', file, file.name);
const id = await this.$store.dispatch(
'automations/uploadAttachment',
formData
file
);
this.$emit('input', [id]);
this.uploadState = 'uploaded';

View File

@@ -0,0 +1,53 @@
import { uploadFile } from '../uploadHelper';
import axios from 'axios';
// Mocking axios using jest-mock-axios
global.axios = axios;
jest.mock('axios');
describe('#Upload Helpers', () => {
afterEach(() => {
// Cleaning up the mock after each test
axios.post.mockReset();
});
it('should send a POST request with correct data', async () => {
const mockFile = new File(['dummy content'], 'example.png', {
type: 'image/png',
});
const mockResponse = {
data: {
file_url: 'https://example.com/fileUrl',
blob_key: 'blobKey123',
blob_id: 'blobId456',
},
};
axios.post.mockResolvedValueOnce(mockResponse);
const result = await uploadFile(mockFile);
expect(axios.post).toHaveBeenCalledWith(
'/api/v1/upload',
expect.any(FormData),
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
expect(result).toEqual({
fileUrl: 'https://example.com/fileUrl',
blobKey: 'blobKey123',
blobId: 'blobId456',
});
});
it('should handle errors', async () => {
const mockFile = new File(['dummy content'], 'example.png', {
type: 'image/png',
});
const mockError = new Error('Failed to upload');
axios.post.mockRejectedValueOnce(mockError);
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
});
});

View File

@@ -0,0 +1,41 @@
/* global axios */
/**
* Constants and Configuration
*/
// Version for the API endpoint.
const API_VERSION = 'v1';
// Default headers to be used in the axios request.
const HEADERS = {
'Content-Type': 'multipart/form-data',
};
/**
* Uploads a file to the server.
*
* This function sends a POST request to a given API endpoint and uploads the specified file.
* The function uses FormData to wrap the file and axios to send the request.
*
* @param {File} file - The file to be uploaded. It should be a File object (typically coming from a file input element).
* @returns {Promise} A promise that resolves with the server's response when the upload is successful, or rejects if there's an error.
*/
export async function uploadFile(file) {
// Create a new FormData instance.
let formData = new FormData();
// Append the file to the FormData instance under the key 'attachment'.
formData.append('attachment', file);
// Use axios to send a POST request to the upload endpoint.
const { data } = await axios.post(`/api/${API_VERSION}/upload`, formData, {
headers: HEADERS,
});
return {
fileUrl: data.file_url,
blobKey: data.blob_key,
blobId: data.blob_id,
};
}

View File

@@ -1,5 +1,6 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import types from '../mutation-types';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import AutomationAPI from '../../api/automation';
export const state = {
@@ -77,12 +78,8 @@ export const actions = {
}
},
uploadAttachment: async (_, file) => {
try {
const { data } = await AutomationAPI.attachment(file);
return data.blob_id;
} catch (error) {
throw new Error(error);
}
const { blobId } = await uploadFile(file);
return blobId;
},
};

View File

@@ -1,4 +1,5 @@
import articlesAPI from 'dashboard/api/helpCenter/articles';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import { throwErrorMessage } from 'dashboard/store/utils/api';
import types from '../../mutation-types';
@@ -126,19 +127,9 @@ export const actions = {
}
},
attachImage: async (_, { portalSlug, file }) => {
try {
const {
data: { file_url: fileUrl },
} = await articlesAPI.uploadImage({
portalSlug,
file,
});
return fileUrl;
} catch (error) {
throwErrorMessage(error);
}
return '';
attachImage: async (_, { file }) => {
const { fileUrl } = await uploadFile(file);
return fileUrl;
},
reorder: async (_, { portalSlug, categorySlug, reorderedGroup }) => {

View File

@@ -1,6 +1,10 @@
import axios from 'axios';
import { actions } from '../actions';
import * as types from '../../../mutation-types';
import { uploadFile } from 'dashboard/helper/uploadHelper';
jest.mock('dashboard/helper/uploadHelper');
const articleList = [
{
id: 1,
@@ -180,4 +184,36 @@ describe('#actions', () => {
]);
});
});
describe('attachImage', () => {
it('should upload the file and return the fileUrl', async () => {
// Given
const mockFile = new Blob(['test'], { type: 'image/png' });
mockFile.name = 'test.png';
const mockFileUrl = 'https://test.com/test.png';
uploadFile.mockResolvedValueOnce({ fileUrl: mockFileUrl });
// When
const result = await actions.attachImage({}, { file: mockFile });
// Then
expect(uploadFile).toHaveBeenCalledWith(mockFile);
expect(result).toBe(mockFileUrl);
});
it('should throw an error if the upload fails', async () => {
// Given
const mockFile = new Blob(['test'], { type: 'image/png' });
mockFile.name = 'test.png';
const mockError = new Error('Upload failed');
uploadFile.mockRejectedValueOnce(mockError);
// When & Then
await expect(actions.attachImage({}, { file: mockFile })).rejects.toThrow(
'Upload failed'
);
});
});
});

View File

@@ -226,6 +226,8 @@ Rails.application.routes.draw do
# end of account scoped api routes
# ----------------------------------
resources :upload, only: [:create]
namespace :integrations do
resources :webhooks, only: [:create]
end

View File

@@ -0,0 +1,42 @@
require 'rails_helper'
RSpec.describe 'Api::V1::UploadController', type: :request do
describe 'POST /api/v1/upload/' do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
it 'uploads the image when authorized' do
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
post '/api/v1/upload/',
headers: user.create_new_auth_token,
params: { attachment: file }
expect(response).to have_http_status(:success)
blob = response.parsed_body
expect(blob['errors']).to be_nil
expect(blob['file_url']).to be_present
expect(blob['blob_key']).to be_present
expect(blob['blob_id']).to be_present
end
it 'does not upload when un-authorized' do
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
post '/api/v1/upload/',
headers: {},
params: { attachment: file }
expect(response).to have_http_status(:unauthorized)
blob = response.parsed_body
expect(blob['errors']).to be_present
expect(blob['file_url']).to be_nil
expect(blob['blob_key']).to be_nil
expect(blob['blob_id']).to be_nil
end
end
end