@aj-archipelago/cortex 1.3.55 → 1.3.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.env.sample +3 -1
  2. package/config/default.example.json +2 -2
  3. package/config.js +32 -0
  4. package/helper-apps/mogrt-handler/.env.example +24 -0
  5. package/helper-apps/mogrt-handler/README.md +166 -0
  6. package/helper-apps/mogrt-handler/glossaryHandler.js +218 -0
  7. package/helper-apps/mogrt-handler/index.js +213 -0
  8. package/helper-apps/mogrt-handler/package-lock.json +7106 -0
  9. package/helper-apps/mogrt-handler/package.json +34 -0
  10. package/helper-apps/mogrt-handler/s3Handler.js +444 -0
  11. package/helper-apps/mogrt-handler/start.js +98 -0
  12. package/helper-apps/mogrt-handler/swagger.js +42 -0
  13. package/helper-apps/mogrt-handler/swagger.yaml +436 -0
  14. package/helper-apps/mogrt-handler/tests/integration/api.test.js +226 -0
  15. package/helper-apps/mogrt-handler/tests/integration/glossary.test.js +106 -0
  16. package/helper-apps/mogrt-handler/tests/setup.js +8 -0
  17. package/helper-apps/mogrt-handler/tests/test-files/test.gif +1 -0
  18. package/helper-apps/mogrt-handler/tests/test-files/test.mogrt +1 -0
  19. package/helper-apps/mogrt-handler/tests/test-files/test.mp4 +1 -0
  20. package/helper-apps/mogrt-handler/tests/unit/glossary.unit.test.js +118 -0
  21. package/helper-apps/mogrt-handler/tests/unit/index.test.js +349 -0
  22. package/helper-apps/mogrt-handler/tests/unit/s3Handler.test.js +204 -0
  23. package/helper-apps/mogrt-handler/tests/unit/sample.test.js +28 -0
  24. package/helper-apps/mogrt-handler/vitest.config.js +15 -0
  25. package/lib/entityConstants.js +1 -1
  26. package/lib/requestExecutor.js +1 -1
  27. package/package.json +1 -1
  28. package/pathways/list_translation_models.js +67 -0
  29. package/pathways/system/sys_test_response_reasonableness.js +20 -0
  30. package/pathways/system/workspaces/workspace_applet_edit.js +187 -0
  31. package/pathways/translate_apptek.js +11 -0
  32. package/pathways/translate_google.js +10 -0
  33. package/pathways/translate_groq.js +36 -0
  34. package/pathways/video_seedance.js +17 -0
  35. package/pathways/video_veo.js +31 -0
  36. package/server/modelExecutor.js +16 -0
  37. package/server/plugins/apptekTranslatePlugin.js +189 -0
  38. package/server/plugins/googleTranslatePlugin.js +121 -0
  39. package/server/plugins/groqChatPlugin.js +108 -0
  40. package/server/plugins/replicateApiPlugin.js +22 -0
  41. package/server/plugins/veoVideoPlugin.js +218 -0
  42. package/tests/apptekTranslatePlugin.test.js +228 -0
  43. package/tests/integration/apptekTranslatePlugin.integration.test.js +156 -0
  44. package/tests/translate_apptek.test.js +117 -0
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import request from 'supertest';
3
+ import express from 'express';
4
+ import GlossaryHandler from '../../glossaryHandler.js';
5
+
6
+ // Helper to create app with the handler
7
+ function createApp() {
8
+ const app = express();
9
+ app.use(express.json());
10
+ app.use(express.urlencoded({ extended: true }));
11
+ app.all('/api/glossary/*', (req, res) => {
12
+ const context = { req, res, log: () => {} };
13
+ GlossaryHandler(context, req)
14
+ .then(() => res.status(context.res.status || 200).json(context.res.body))
15
+ .catch(err => res.status(500).json({ error: err.message }));
16
+ });
17
+ return app;
18
+ }
19
+
20
+ describe('Glossary API Proxy', () => {
21
+ let app;
22
+
23
+ beforeAll(() => {
24
+ app = createApp();
25
+ });
26
+
27
+ it('should proxy GET /list to AppTek', async () => {
28
+ // This test expects the real AppTek API or a mock server
29
+ // To run as a unit test, mock fetch in glossaryHandler.js
30
+ // Here we just check the request shape
31
+ await request(app)
32
+ .get('/api/glossary/list')
33
+ .expect(res => {
34
+ // Accept either 200 or 401/403 from upstream
35
+ expect([200, 401, 403]).toContain(res.status);
36
+ });
37
+ });
38
+
39
+ it('should proxy POST /en-es (create glossary)', async () => {
40
+ const body = {
41
+ source_lang_code: 'en',
42
+ target_lang_code: 'es',
43
+ entries: [{ source: 'Roberto', target: 'Berto' }],
44
+ name: 'English to Spanish Glossary'
45
+ };
46
+ const res = await request(app)
47
+ .post('/api/glossary/en-es?name=English%20to%20Spanish%20Glossary')
48
+ .send(body)
49
+ .expect(res => {
50
+ expect([200, 401, 403, 400]).toContain(res.status);
51
+ });
52
+ });
53
+
54
+ it('should proxy DELETE /glossary_id', async () => {
55
+ // First, create a glossary
56
+ const createBody = {
57
+ source_lang_code: 'en',
58
+ target_lang_code: 'es',
59
+ entries: [{ source: 'DeleteMe', target: 'BorrarMe' }],
60
+ name: 'ToDelete'
61
+ };
62
+ const createRes = await request(app)
63
+ .post('/api/glossary/en-es?name=ToDelete')
64
+ .send(createBody)
65
+ .expect(res => {
66
+ expect([200, 201]).toContain(res.status);
67
+ });
68
+ const glossaryId = createRes.body.id || createRes.body._id || createRes.body.glossary_id || createRes.body.name || 'ToDelete';
69
+ // Then, delete the created glossary
70
+ const delRes = await request(app)
71
+ .delete(`/api/glossary/${glossaryId}`)
72
+ .expect(res => {
73
+ expect([200, 204, 401, 403, 404]).toContain(res.status);
74
+ });
75
+ });
76
+
77
+ it('should proxy POST /edit/:id (edit glossary)', async () => {
78
+ // First, create a glossary
79
+ const createBody = {
80
+ source_lang_code: 'en',
81
+ target_lang_code: 'es',
82
+ entries: [{ source: 'EditMe', target: 'EditarMe' }],
83
+ name: 'ToEdit'
84
+ };
85
+ const createRes = await request(app)
86
+ .post('/api/glossary/en-es?name=ToEdit')
87
+ .send(createBody)
88
+ .expect(res => {
89
+ expect([200, 201]).toContain(res.status);
90
+ });
91
+ const glossaryId = createRes.body.id || createRes.body._id || createRes.body.glossary_id || createRes.body.name || 'ToEdit';
92
+ // Now, edit the created glossary
93
+ const editBody = {
94
+ source_lang_code: 'en',
95
+ target_lang_code: 'es',
96
+ entries: [{ source: 'EditMe', target: 'YaEditado' }],
97
+ name: 'ToEditEdited'
98
+ };
99
+ const editRes = await request(app)
100
+ .post(`/api/glossary/edit/${glossaryId}`)
101
+ .send(editBody)
102
+ .expect(res => {
103
+ expect([200, 201, 401, 403, 400]).toContain(res.status);
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,8 @@
1
+ // Set test environment
2
+ process.env.NODE_ENV = 'test';
3
+
4
+ // Mock environment variables
5
+ process.env.AWS_REGION = 'us-east-1';
6
+ process.env.AWS_ACCESS_KEY_ID = 'test-key';
7
+ process.env.AWS_SECRET_ACCESS_KEY = 'test-secret';
8
+ process.env.S3_BUCKET_NAME = 'test-bucket';
@@ -0,0 +1 @@
1
+ test preview gif content
@@ -0,0 +1 @@
1
+ test mogrt content
@@ -0,0 +1 @@
1
+ test preview mp4 content
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ // Helper to call handler
3
+ async function callHandler({ method, url, body = {}, query = {}, headers = {} }) {
4
+ const { default: GlossaryHandler } = await import('../../glossaryHandler.js');
5
+ const context = {};
6
+ const req = { method, url, body, query, headers };
7
+ await GlossaryHandler(context, req);
8
+ return context.res;
9
+ }
10
+
11
+ describe('GlossaryHandler Unit', () => {
12
+ let fetchMock;
13
+ let s3HandlerMock;
14
+
15
+ beforeEach(() => {
16
+ fetchMock = vi.fn();
17
+ s3HandlerMock = {
18
+ saveGlossaryId: vi.fn().mockResolvedValue({ versionId: 'mock-version-id', key: 'mock-key' }),
19
+ getGlossaryVersions: vi.fn(),
20
+ getGlossaryVersion: vi.fn()
21
+ };
22
+
23
+ vi.resetModules();
24
+ vi.doMock('node-fetch', () => ({
25
+ __esModule: true,
26
+ default: fetchMock
27
+ }));
28
+ vi.doMock('../../s3Handler.js', () => ({
29
+ __esModule: true,
30
+ saveGlossaryId: s3HandlerMock.saveGlossaryId,
31
+ getGlossaryVersions: s3HandlerMock.getGlossaryVersions,
32
+ getGlossaryVersion: s3HandlerMock.getGlossaryVersion
33
+ }));
34
+ });
35
+
36
+
37
+ it('should proxy GET /list', async () => {
38
+ fetchMock.mockResolvedValue({ status: 200, json: async () => ({ glossaries: [] }) });
39
+ const res = await callHandler({ method: 'GET', url: '/api/glossary/list', headers: {} });
40
+ expect(fetchMock).toHaveBeenCalledWith(
41
+ expect.stringContaining('/list'),
42
+ expect.objectContaining({ method: 'GET' })
43
+ );
44
+ expect(res.status).toBe(200);
45
+ expect(res.body.glossaries).toEqual([]);
46
+ });
47
+
48
+ it('should proxy POST /en-es (create glossary)', async () => {
49
+ // Mock API response with glossary_id instead of id to match actual API response
50
+ fetchMock.mockResolvedValue({
51
+ status: 200,
52
+ json: async () => ({ glossary_id: 'glossary-id' })
53
+ });
54
+
55
+ const body = {
56
+ source_lang_code: 'en',
57
+ target_lang_code: 'es',
58
+ entries: [{ source: 'a', target: 'b' }],
59
+ name: 'Test Glossary'
60
+ };
61
+
62
+ const res = await callHandler({
63
+ method: 'POST',
64
+ url: '/api/glossary/en-es',
65
+ body
66
+ });
67
+
68
+ // Verify fetch was called with correct URL and method
69
+ expect(fetchMock).toHaveBeenCalledWith(
70
+ expect.stringContaining('/en-es'),
71
+ expect.objectContaining({
72
+ method: 'POST',
73
+ headers: expect.objectContaining({
74
+ 'accept': 'application/json',
75
+ 'content-type': 'application/json'
76
+ }),
77
+ body: expect.any(String)
78
+ })
79
+ );
80
+
81
+ // Verify S3 save was called
82
+ expect(s3HandlerMock.saveGlossaryId).toHaveBeenCalledWith(
83
+ 'glossary-id',
84
+ 'en-es',
85
+ 'Test Glossary'
86
+ );
87
+
88
+ // Verify response
89
+ expect(res.status).toBe(200);
90
+ expect(res.body.glossary_id).toBe('glossary-id');
91
+ expect(res.body.version).toEqual({
92
+ versionId: 'mock-version-id',
93
+ key: 'mock-key'
94
+ });
95
+ });
96
+
97
+ it('should proxy DELETE /glossary_id', async () => {
98
+ fetchMock.mockResolvedValue({ status: 200, json: async () => ({ success: true }) });
99
+ const res = await callHandler({ method: 'DELETE', url: '/api/glossary/test-id' });
100
+ expect(fetchMock).toHaveBeenCalledWith(
101
+ expect.stringContaining('/test-id'),
102
+ expect.objectContaining({ method: 'DELETE' })
103
+ );
104
+ expect(res.status).toBe(200);
105
+ expect(res.body.success).toBe(true);
106
+ });
107
+
108
+ it('should proxy POST /edit/:id (edit glossary)', async () => {
109
+ fetchMock
110
+ .mockResolvedValueOnce({ status: 200, json: async () => ({}) }) // delete
111
+ .mockResolvedValueOnce({ status: 200, json: async () => ({ id: 'new-id' }) }); // create
112
+ const body = { source_lang_code: 'en', target_lang_code: 'es', entries: [{ source: 'a', target: 'b' }], name: 'Test' };
113
+ const res = await callHandler({ method: 'POST', url: '/api/glossary/edit/old-id', body });
114
+ expect(fetchMock).toHaveBeenCalledTimes(2);
115
+ expect(res.status).toBe(200);
116
+ expect(res.body.id).toBe('new-id');
117
+ });
118
+ });
@@ -0,0 +1,349 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import MogrtHandler from '../../index.js';
3
+ import { uploadToS3 as s3Upload, getManifest as s3GetManifest, saveManifest as s3SaveManifest, removeFromMasterManifest as s3RemoveFromMasterManifest } from '../../s3Handler.js';
4
+ import Busboy from 'busboy';
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import stream from 'stream';
7
+
8
+ // Mock dependencies
9
+ vi.mock('../../s3Handler.js', () => ({
10
+ uploadToS3: vi.fn(),
11
+ getManifest: vi.fn(),
12
+ saveManifest: vi.fn(),
13
+ removeFromMasterManifest: vi.fn(),
14
+ }));
15
+ vi.mock('busboy');
16
+ vi.mock('uuid');
17
+
18
+ const mockContext = () => ({
19
+ res: null,
20
+ log: vi.fn(),
21
+ done: vi.fn()
22
+ });
23
+
24
+ const mockReq = (method, query = {}, params = {}, headers = {}, body = null, rawBody = null) => ({
25
+ method,
26
+ query,
27
+ params,
28
+ headers,
29
+ body,
30
+ rawBody,
31
+ pipe: vi.fn()
32
+ });
33
+
34
+ describe('MogrtHandler', () => {
35
+ let context;
36
+ let mockBusboyInstance;
37
+
38
+ beforeEach(() => {
39
+ context = mockContext();
40
+ vi.clearAllMocks();
41
+
42
+ uuidv4.mockReturnValue('test-uuid');
43
+ s3Upload.mockResolvedValue({ key: 'test-uuid/file.mogrt', Location: 's3://bucket/test-uuid/file.mogrt' });
44
+ s3GetManifest.mockResolvedValue({ id: 'master', items: [] });
45
+ s3SaveManifest.mockResolvedValue(undefined);
46
+ s3RemoveFromMasterManifest.mockResolvedValue(true);
47
+
48
+ mockBusboyInstance = {
49
+ on: vi.fn((event, callback) => {
50
+ mockBusboyInstance[`_${event}Callback`] = callback;
51
+ }),
52
+ emit: vi.fn((event, ...args) => {
53
+ if (mockBusboyInstance[`_${event}Callback`]) {
54
+ mockBusboyInstance[`_${event}Callback`](...args);
55
+ }
56
+ }),
57
+ end: vi.fn(),
58
+ };
59
+ Busboy.mockImplementation(() => mockBusboyInstance);
60
+ });
61
+
62
+ afterEach(() => {
63
+ vi.restoreAllMocks();
64
+ });
65
+
66
+ const simulateBusboyEventsAsync = (filesData = [], fieldsData = [], error = null) => {
67
+ return new Promise((resolveImmediate) => {
68
+ process.nextTick(async () => {
69
+ try {
70
+ if (error) {
71
+ mockBusboyInstance.emit('error', error);
72
+ resolveImmediate();
73
+ return;
74
+ }
75
+
76
+ for (const fileData of filesData) {
77
+ const fileStream = new stream.Readable();
78
+ fileStream._read = () => {};
79
+ mockBusboyInstance.emit('file', fileData.fieldname, fileStream, fileData.fileInfo);
80
+
81
+ for (const chunk of fileData.chunks) {
82
+ fileStream.push(chunk);
83
+ }
84
+ fileStream.push(null);
85
+ await new Promise(r => process.nextTick(r));
86
+ }
87
+
88
+ fieldsData.forEach(fieldData => {
89
+ mockBusboyInstance.emit('field', fieldData.fieldname, fieldData.value);
90
+ });
91
+
92
+ mockBusboyInstance.emit('finish');
93
+ } catch (e) {
94
+ console.error('Error in simulateBusboyEventsAsync:', e);
95
+ }
96
+ resolveImmediate();
97
+ });
98
+ });
99
+ };
100
+
101
+ describe('GET requests', () => {
102
+ it('should fetch the master manifest if no manifestId is provided', async () => {
103
+ const req = mockReq('GET');
104
+ s3GetManifest.mockResolvedValue({ id: 'master', data: 'master_manifest_data' });
105
+
106
+ await MogrtHandler(context, req);
107
+
108
+ expect(s3GetManifest).toHaveBeenCalledWith('master');
109
+ expect(context.res.status).toBe(200);
110
+ expect(context.res.body).toEqual({ id: 'master', data: 'master_manifest_data' });
111
+ });
112
+
113
+ it('should fetch a specific manifest if manifestId is provided', async () => {
114
+ const req = mockReq('GET', { manifestId: 'specific-manifest-id' });
115
+ s3GetManifest.mockResolvedValue({ id: 'specific-manifest-id', data: 'specific_data' });
116
+
117
+ await MogrtHandler(context, req);
118
+
119
+ expect(s3GetManifest).toHaveBeenCalledWith('specific-manifest-id');
120
+ expect(context.res.status).toBe(200);
121
+ expect(context.res.body).toEqual({ id: 'specific-manifest-id', data: 'specific_data' });
122
+ });
123
+ });
124
+
125
+ describe('DELETE requests', () => {
126
+ it('should delete a MOGRT item successfully', async () => {
127
+ const req = mockReq('DELETE', { manifestId: 'master' }, { id: 'mogrt-to-delete' });
128
+ s3RemoveFromMasterManifest.mockResolvedValue(true);
129
+
130
+ await MogrtHandler(context, req);
131
+
132
+ expect(s3RemoveFromMasterManifest).toHaveBeenCalledWith('mogrt-to-delete');
133
+ expect(context.res.status).toBe(200);
134
+ expect(context.res.body).toEqual({
135
+ success: true,
136
+ message: 'MOGRT with ID mogrt-to-delete successfully deleted'
137
+ });
138
+ });
139
+
140
+ it('should return 404 if MOGRT item to delete is not found', async () => {
141
+ const req = mockReq('DELETE', { manifestId: 'master' }, { id: 'non-existent-id' });
142
+ s3RemoveFromMasterManifest.mockResolvedValue(false);
143
+
144
+ await MogrtHandler(context, req);
145
+
146
+ expect(s3RemoveFromMasterManifest).toHaveBeenCalledWith('non-existent-id');
147
+ expect(context.res.status).toBe(404);
148
+ expect(context.res.body).toEqual({ error: 'MOGRT with ID non-existent-id not found' });
149
+ });
150
+
151
+ it('should return 500 if removeFromMasterManifest throws an error', async () => {
152
+ const req = mockReq('DELETE', { manifestId: 'master' }, { id: 'mogrt-id' });
153
+ s3RemoveFromMasterManifest.mockRejectedValue(new Error('S3 delete error'));
154
+
155
+ await MogrtHandler(context, req);
156
+
157
+ expect(s3RemoveFromMasterManifest).toHaveBeenCalledWith('mogrt-id');
158
+ expect(context.res.status).toBe(500);
159
+ expect(context.res.body).toEqual({ error: 'Failed to delete MOGRT: S3 delete error' });
160
+ });
161
+
162
+ it('should return 500 if id is not provided for DELETE', async () => {
163
+ const req = mockReq('DELETE', { manifestId: 'master' });
164
+ await MogrtHandler(context, req);
165
+ expect(s3RemoveFromMasterManifest).not.toHaveBeenCalled();
166
+ expect(context.res.status).toBe(500);
167
+ expect(context.res.body).toEqual({ error: 'Method not allowed' });
168
+ });
169
+ });
170
+
171
+ describe('POST requests', () => {
172
+ const getSuccessfulFilesData = () => ([
173
+ { fieldname: 'mogrt', fileInfo: { filename: 'test.mogrt' }, chunks: [Buffer.from('mogrt data')] },
174
+ { fieldname: 'preview', fileInfo: { filename: 'preview.mp4' }, chunks: [Buffer.from('preview data')] }
175
+ ]);
176
+
177
+ it('should upload files and save manifest successfully', async () => {
178
+ const req = mockReq('POST', {}, {}, { 'content-type': 'multipart/form-data' }, 'fake-body');
179
+ s3Upload
180
+ .mockResolvedValueOnce({ key: 'test-uuid/test.mogrt' })
181
+ .mockResolvedValueOnce({ key: 'test-uuid/preview.mp4' });
182
+ s3GetManifest.mockResolvedValueOnce({ id: 'master', items: [] });
183
+
184
+ const mogrtHandlerPromise = MogrtHandler(context, req);
185
+ await simulateBusboyEventsAsync(getSuccessfulFilesData(), [{ fieldname: 'name', value: 'My Awesome Mogrt' }]);
186
+ await mogrtHandlerPromise;
187
+
188
+ expect(s3Upload).toHaveBeenCalledTimes(2);
189
+ expect(s3Upload).toHaveBeenCalledWith('test-uuid/test.mogrt', Buffer.from('mogrt data'), 'application/octet-stream');
190
+ expect(s3Upload).toHaveBeenCalledWith('test-uuid/preview.mp4', Buffer.from('preview data'), 'video/mp4');
191
+ expect(s3SaveManifest).toHaveBeenCalledWith(expect.objectContaining({
192
+ id: 'test-uuid',
193
+ name: 'My Awesome Mogrt',
194
+ mogrtFile: 'test-uuid/test.mogrt',
195
+ previewFile: 'test-uuid/preview.mp4',
196
+ }));
197
+ expect(s3GetManifest).toHaveBeenCalledWith('master');
198
+ expect(context.res.status).toBe(200);
199
+ expect(context.res.body.manifest.id).toBe('test-uuid');
200
+ });
201
+
202
+ it('should use uploadId as name if name field is not provided', async () => {
203
+ const req = mockReq('POST', {}, {}, { 'content-type': 'multipart/form-data' }, 'fake-body');
204
+ s3Upload
205
+ .mockResolvedValueOnce({ key: 'test-uuid/test.mogrt' })
206
+ .mockResolvedValueOnce({ key: 'test-uuid/preview.mp4' });
207
+ s3GetManifest.mockResolvedValueOnce({ id: 'master', items: [] });
208
+
209
+ const mogrtHandlerPromise = MogrtHandler(context, req);
210
+ await simulateBusboyEventsAsync(getSuccessfulFilesData(), []);
211
+ await mogrtHandlerPromise;
212
+
213
+ expect(s3SaveManifest).toHaveBeenCalledWith(expect.objectContaining({
214
+ id: 'test-uuid',
215
+ name: 'test-uuid',
216
+ }));
217
+ expect(context.res.status).toBe(200);
218
+ });
219
+
220
+ it('should return 500 if MOGRT file is missing', async () => {
221
+ const req = mockReq('POST', {}, {}, { 'content-type': 'multipart/form-data' }, 'fake-body');
222
+ const filesData = [
223
+ { fieldname: 'preview', fileInfo: { filename: 'preview.gif' }, chunks: [Buffer.from('gif data')] }
224
+ ];
225
+ const mogrtHandlerPromise = MogrtHandler(context, req);
226
+ await simulateBusboyEventsAsync(filesData, [{ fieldname: 'name', value: 'Test' }]);
227
+ await mogrtHandlerPromise;
228
+
229
+ expect(context.res.status).toBe(500);
230
+ expect(context.res.body).toEqual({ error: 'Both MOGRT and preview files (GIF or MP4) are required' });
231
+ });
232
+
233
+ it('should return 500 if preview file is missing', async () => {
234
+ const req = mockReq('POST', {}, {}, { 'content-type': 'multipart/form-data' }, 'fake-body');
235
+ const filesData = [
236
+ { fieldname: 'mogrt', fileInfo: { filename: 'animation.mogrt' }, chunks: [Buffer.from('mogrt data')] }
237
+ ];
238
+ const mogrtHandlerPromise = MogrtHandler(context, req);
239
+ await simulateBusboyEventsAsync(filesData, [{ fieldname: 'name', value: 'Test' }]);
240
+ await mogrtHandlerPromise;
241
+ expect(context.res.status).toBe(500);
242
+ expect(context.res.body).toEqual({ error: 'Both MOGRT and preview files (GIF or MP4) are required' });
243
+ });
244
+
245
+ it('should reject with error for invalid file type during file event', async () => {
246
+ const req = mockReq('POST', {}, {}, { 'content-type': 'multipart/form-data' }, 'fake-body');
247
+ const promise = MogrtHandler(context, req);
248
+
249
+ const fileStream = new stream.Readable({ read() {} });
250
+ const currentBusboyInstance = Busboy.mock.results[Busboy.mock.results.length - 1].value;
251
+ currentBusboyInstance.emit('file', 'somefield', fileStream, { filename: 'document.txt' });
252
+
253
+ await expect(promise).rejects.toThrow('Invalid file type. Only .mogrt, .gif and .mp4 files are allowed.');
254
+ // If 'file' event rejects, the 'finish' event might not run its async block.
255
+ // Let's ensure the promise itself is rejected.
256
+ // The actual setting of context.res for this specific early rejection needs careful check of MogrtHandler's flow.
257
+ // For now, confirming the promise rejection is key.
258
+ });
259
+
260
+ it('should return 500 if S3 upload fails', async () => {
261
+ const req = mockReq('POST', {}, {}, { 'content-type': 'multipart/form-data' }, 'fake-body');
262
+ s3Upload.mockRejectedValue(new Error('S3 upload failed'));
263
+ // Simulate valid files being processed by Busboy, but S3 upload fails
264
+ // Call MogrtHandler; it sets up Busboy and returns a promise that resolves/rejects after Busboy events.
265
+ const mogrtHandlerPromise = MogrtHandler(context, req);
266
+
267
+ // Simulate valid files being processed by Busboy, which will trigger s3Upload internally.
268
+ const filesData = [
269
+ { fieldname: 'mogrt', fileInfo: { filename: 'test.mogrt' }, chunks: [Buffer.from('m-data')] },
270
+ { fieldname: 'preview', fileInfo: { filename: 'prev.gif' }, chunks: [Buffer.from('p-data')] }
271
+ ];
272
+ // This simulation will cause the mocked s3Upload (which rejects) to be called by MogrtHandler's event listeners.
273
+ await simulateBusboyEventsAsync(filesData, [{ fieldname: 'name', value: 'Fail Upload' }]);
274
+
275
+ // Await the main handler promise after events have been processed.
276
+ await mogrtHandlerPromise;
277
+
278
+ expect(context.res.status).toBe(500);
279
+ expect(context.res.body).toEqual({ error: 'S3 upload failed' });
280
+ });
281
+
282
+ it('should return 500 if saveManifest fails', async () => {
283
+ const req = mockReq('POST', {}, {}, { 'content-type': 'multipart/form-data' }, 'fake-body');
284
+ s3SaveManifest.mockRejectedValue(new Error('Manifest save failed'));
285
+ // Simulate successful S3 uploads, but manifest saving fails
286
+ const filesData = [
287
+ { fieldname: 'mogrt', fileInfo: { filename: 'good.mogrt' }, chunks: [Buffer.from('m-data')] },
288
+ { fieldname: 'preview', fileInfo: { filename: 'good.mp4' }, chunks: [Buffer.from('p-data')] }
289
+ ];
290
+ s3Upload
291
+ .mockResolvedValueOnce({ key: 'test-uuid/good.mogrt' })
292
+ .mockResolvedValueOnce({ key: 'test-uuid/good.mp4' });
293
+
294
+ // Call MogrtHandler; it sets up Busboy and returns a promise.
295
+ const mogrtHandlerPromise = MogrtHandler(context, req);
296
+
297
+ // Simulate events. This will trigger s3Upload (resolve) then s3SaveManifest (reject).
298
+ await simulateBusboyEventsAsync(filesData, [{ fieldname: 'name', value: 'Fail Save' }]);
299
+
300
+ // Await the main handler promise.
301
+ await mogrtHandlerPromise;
302
+
303
+ expect(context.res.status).toBe(500);
304
+ expect(context.res.body).toEqual({ error: 'Manifest save failed' });
305
+ });
306
+
307
+ it('should return 500 if Busboy emits an error', async () => {
308
+ const req = mockReq('POST', {}, {}, { 'content-type': 'multipart/form-data' }, 'fake-body');
309
+ const busboyError = new Error('Busboy processing error');
310
+
311
+ // We need to ensure the main promise from MogrtHandler is awaited
312
+ const mogrtHandlerPromise = MogrtHandler(context, req);
313
+
314
+ // Simulate Busboy emitting an error *after* MogrtHandler has started listening
315
+ process.nextTick(() => {
316
+ const currentBusboyInstance = Busboy.mock.results[Busboy.mock.results.length -1].value;
317
+ currentBusboyInstance.emit('error', busboyError);
318
+ });
319
+
320
+ // The promise should reject, and the catch block in MogrtHandler should set context.res
321
+ // However, the current MogrtHandler's main promise rejection doesn't set context.res directly.
322
+ // The rejection is caught by the Azure Functions runtime or not at all if not handled by main promise.
323
+ // The 'busboy.on('error', reject)' will reject the promise returned by `new Promise(...)`
324
+ // Let's test that the promise is rejected.
325
+ await expect(mogrtHandlerPromise).rejects.toThrow('Busboy processing error');
326
+
327
+ // If the handler were to set context.res in a top-level catch for the promise it returns:
328
+ // expect(context.res.status).toBe(500); // Or appropriate error code
329
+ // expect(context.res.body).toEqual({ error: 'Busboy processing error' });
330
+ // This test highlights that the main promise rejection needs to be handled to set context.res.
331
+ });
332
+
333
+ });
334
+
335
+ describe('Unhandled methods/requests', () => {
336
+ it('should not set a response for an unhandled HTTP method (e.g., PUT)', async () => {
337
+ const req = mockReq('PUT');
338
+ await MogrtHandler(context, req);
339
+ expect(context.res.status).toBe(500);
340
+ });
341
+
342
+ it('should not set a response if DELETE request is missing id param', async () => {
343
+ const req = mockReq('DELETE'); // No id in params
344
+ await MogrtHandler(context, req);
345
+ expect(s3RemoveFromMasterManifest).not.toHaveBeenCalled();
346
+ });
347
+ });
348
+
349
+ });