-
-
Notifications
You must be signed in to change notification settings - Fork 892
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: PR(1) added centralized helper function for minio-upload using …
…pre-signed url (#3762) * fixed conflict * fixed conflict
- Loading branch information
Showing
6 changed files
with
268 additions
and
1 deletion.
There are no files selected for viewing
9 changes: 9 additions & 0 deletions
9
docs/docs/auto-docs/GraphQl/Mutations/mutations/variables/PRESIGNED_URL.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
[Admin Docs](/) | ||
|
||
*** | ||
|
||
# Variable: PRESIGNED\_URL | ||
|
||
> `const` **PRESIGNED\_URL**: `DocumentNode` | ||
Defined in: [src/GraphQl/Mutations/mutations.ts:786](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/GraphQl/Mutations/mutations.ts#L786) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
docs/docs/auto-docs/utils/MinioUpload/functions/useMinioUpload.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[Admin Docs](/) | ||
|
||
*** | ||
|
||
# Function: useMinioUpload() | ||
|
||
> **useMinioUpload**(): `InterfaceMinioUpload` | ||
Defined in: [src/utils/MinioUpload.ts:11](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/utils/MinioUpload.ts#L11) | ||
|
||
## Returns | ||
|
||
`InterfaceMinioUpload` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
import React from 'react'; | ||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; | ||
import { MockedProvider } from '@apollo/client/testing'; | ||
import { PRESIGNED_URL } from 'GraphQl/Mutations/mutations'; | ||
import { useMinioUpload } from './MinioUpload'; | ||
import { vi } from 'vitest'; | ||
|
||
const TestComponent = ({ | ||
onUploadComplete, | ||
}: { | ||
onUploadComplete: (result: { fileUrl: string; objectName: string }) => void; | ||
}): JSX.Element => { | ||
const { uploadFileToMinio } = useMinioUpload(); | ||
const [status, setStatus] = React.useState('idle'); | ||
|
||
const handleFileChange = async ( | ||
e: React.ChangeEvent<HTMLInputElement>, | ||
): Promise<void> => { | ||
const files = e.target.files; | ||
if (!files || !files[0]) return; | ||
const file = files[0]; | ||
|
||
setStatus('uploading'); | ||
try { | ||
const result = await uploadFileToMinio(file, 'test-org-id'); | ||
setStatus('success'); | ||
onUploadComplete(result); | ||
} catch (error) { | ||
setStatus('error'); | ||
console.error(error); | ||
} | ||
}; | ||
|
||
return ( | ||
<div> | ||
<input type="file" data-testid="file-input" onChange={handleFileChange} /> | ||
<div data-testid="status">{status}</div> | ||
</div> | ||
); | ||
}; | ||
|
||
describe('Minio Upload Integration', (): void => { | ||
const successMocks = [ | ||
{ | ||
request: { | ||
query: PRESIGNED_URL, | ||
variables: { | ||
input: { | ||
fileName: 'test.png', | ||
fileType: 'image/png', | ||
organizationId: 'test-org-id', | ||
}, | ||
}, | ||
}, | ||
result: { | ||
data: { | ||
createPresignedUrl: { | ||
presignedUrl: 'https://minio-test.com/upload/url', | ||
fileUrl: 'https://minio-test.com/file/url', | ||
objectName: 'test-object-name', | ||
}, | ||
}, | ||
}, | ||
}, | ||
]; | ||
|
||
beforeEach((): void => { | ||
global.fetch = vi.fn(() => | ||
Promise.resolve({ | ||
ok: true, | ||
json: () => Promise.resolve({}), | ||
} as Response), | ||
); | ||
}); | ||
|
||
afterEach(() => { | ||
vi.resetAllMocks(); | ||
}); | ||
|
||
it('should upload a file and call onUploadComplete with the expected result', async (): Promise<void> => { | ||
const handleComplete = vi.fn(); | ||
|
||
render( | ||
<MockedProvider mocks={successMocks} addTypename={false}> | ||
<TestComponent onUploadComplete={handleComplete} /> | ||
</MockedProvider>, | ||
); | ||
|
||
const file = new File(['dummy content'], 'test.png', { type: 'image/png' }); | ||
const input = screen.getByTestId('file-input') as HTMLInputElement; | ||
|
||
Object.defineProperty(input, 'files', { | ||
value: [file], | ||
writable: false, | ||
}); | ||
|
||
fireEvent.change(input); | ||
|
||
// Expect initial status to be "uploading" | ||
expect(screen.getByTestId('status').textContent).toBe('uploading'); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByTestId('status').textContent).toBe('success'); | ||
}); | ||
|
||
expect(handleComplete).toHaveBeenCalledWith({ | ||
fileUrl: 'https://minio-test.com/file/url', | ||
objectName: 'test-object-name', | ||
}); | ||
}); | ||
|
||
it('should set status to error if mutation returns no data or missing createPresignedUrl', async () => { | ||
const errorMock = [ | ||
{ | ||
request: { | ||
query: PRESIGNED_URL, | ||
variables: { | ||
input: { | ||
fileName: 'test.png', | ||
fileType: 'image/png', | ||
organizationId: 'test-org-id', | ||
}, | ||
}, | ||
}, | ||
result: { | ||
data: { | ||
createPresignedUrl: null, | ||
}, | ||
}, | ||
}, | ||
]; | ||
|
||
const handleComplete = vi.fn(); | ||
|
||
render( | ||
<MockedProvider mocks={errorMock} addTypename={false}> | ||
<TestComponent onUploadComplete={handleComplete} /> | ||
</MockedProvider>, | ||
); | ||
|
||
const file = new File(['dummy content'], 'test.png', { type: 'image/png' }); | ||
const input = screen.getByTestId('file-input') as HTMLInputElement; | ||
Object.defineProperty(input, 'files', { | ||
value: [file], | ||
writable: false, | ||
}); | ||
|
||
fireEvent.change(input); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByTestId('status').textContent).toBe('error'); | ||
}); | ||
|
||
expect(handleComplete).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should set status to error when file upload fails', async () => { | ||
( | ||
global.fetch as unknown as { | ||
mockImplementationOnce: (fn: () => Promise<Response>) => void; | ||
} | ||
).mockImplementationOnce(() => Promise.resolve({ ok: false } as Response)); | ||
const handleComplete = vi.fn(); | ||
|
||
render( | ||
<MockedProvider mocks={successMocks} addTypename={false}> | ||
<TestComponent onUploadComplete={handleComplete} /> | ||
</MockedProvider>, | ||
); | ||
|
||
const file = new File(['dummy content'], 'test.png', { type: 'image/png' }); | ||
const input = screen.getByTestId('file-input') as HTMLInputElement; | ||
Object.defineProperty(input, 'files', { | ||
value: [file], | ||
writable: false, | ||
}); | ||
fireEvent.change(input); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByTestId('status').textContent).toBe('error'); | ||
}); | ||
|
||
expect(handleComplete).not.toHaveBeenCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { PRESIGNED_URL } from 'GraphQl/Mutations/mutations'; | ||
import { useMutation } from '@apollo/client'; | ||
|
||
interface InterfaceMinioUpload { | ||
uploadFileToMinio: ( | ||
file: File, | ||
organizationId: string, | ||
) => Promise<{ fileUrl: string; objectName: string }>; | ||
} | ||
|
||
export const useMinioUpload = (): InterfaceMinioUpload => { | ||
const [generatePresignedUrl] = useMutation(PRESIGNED_URL); | ||
|
||
const uploadFileToMinio = async ( | ||
file: File, | ||
organizationId: string, | ||
): Promise<{ fileUrl: string; objectName: string }> => { | ||
// 1. Call the mutation to get presignedUrl & fileUrl | ||
const { data } = await generatePresignedUrl({ | ||
variables: { | ||
input: { | ||
fileName: file.name, | ||
fileType: file.type, | ||
organizationId: organizationId, | ||
}, | ||
}, | ||
}); | ||
if (!data || !data.createPresignedUrl) { | ||
throw new Error('Failed to get presigned URL'); | ||
} | ||
const { presignedUrl, fileUrl, objectName } = data.createPresignedUrl; | ||
|
||
// 2. Upload the file directly to MinIO using the presigned URL | ||
const response = await fetch(presignedUrl, { | ||
method: 'PUT', | ||
body: file, | ||
headers: { | ||
'Content-Type': file.type, | ||
}, | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new Error('File upload failed'); | ||
} | ||
|
||
return { fileUrl, objectName }; | ||
}; | ||
|
||
return { uploadFileToMinio }; | ||
}; |