/* * (c) Copyright Ascensio System SIA 2010-2024 * * This program is a free software product. You can redistribute it and/or * modify it under the terms of the GNU Affero General Public License (AGPL) * version 3 as published by the Free Software Foundation. In accordance with * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect * that Ascensio System SIA expressly excludes the warranty of non-infringement * of any third-party rights. * * This program is distributed WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html * * You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish * street, Riga, Latvia, EU, LV-1050. * * The interactive user interfaces in modified source and object code versions * of the Program must display Appropriate Legal Notices, as required under * Section 5 of the GNU AGPL version 3. * * Pursuant to Section 7(b) of the License you must retain the original Product * logo when distributing the program. Pursuant to Section 7(e) we decline to * grant you any rights under trademark law for use of our trademarks. * * All the Product's GUI elements, including illustrations and icon sets, as * well as technical writing content are licensed under the terms of the * Creative Commons Attribution-ShareAlike 4.0 International. See the License * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode * */ const {jest, describe, test, expect, beforeAll, afterAll} = require('@jest/globals'); jest.mock('fs/promises', () => ({ ...jest.requireActual('fs/promises'), cp: jest.fn().mockImplementation((from, to) => fs.writeFileSync(to, testFileData3)) })); const mockNeedServeStatic = jest.fn().mockReturnValue(true); jest.mock('../../../Common/sources/storage/storage-base', () => { const originalModule = jest.requireActual('../../../Common/sources/storage/storage-base'); return { ...originalModule, needServeStatic: mockNeedServeStatic }; }); const {cp} = require('fs/promises'); const http = require('http'); const https = require('https'); const fs = require('fs'); const {Readable} = require('stream'); const testFileData1 = 'test1'; const testFileData2 = 'test22'; const testFileData3 = 'test333'; const testFileData4 = testFileData3; const express = require('express'); const operationContext = require('../../../Common/sources/operationContext'); const tenantManager = require('../../../Common/sources/tenantManager'); const storage = require('../../../Common/sources/storage/storage-base'); const utils = require('../../../Common/sources/utils'); const commonDefines = require('../../../Common/sources/commondefines'); const config = require('../../../Common/node_modules/config'); const staticRouter = require('../../../DocService/sources/routes/static'); const cfgCacheStorage = config.get('storage'); const cfgPersistentStorage = utils.deepMergeObjects({}, cfgCacheStorage, config.get('persistentStorage')); const ctx = operationContext.global; const PORT = 3457; const rand = Math.floor(Math.random() * 1000000); const testDir = 'DocService-DocsCoServer-storage-' + rand; const baseUrl = `http://localhost:${PORT}`; const urlType = commonDefines.c_oAscUrlTypes.Session; const testFile1 = testDir + '/test1.txt'; const testFile2 = testDir + '/test2.txt'; const testFile3 = testDir + '/test3.txt'; const testFile4 = testDir + '/test4.txt'; const specialDirCache = ''; const specialDirForgotten = 'forgotten'; console.debug(`testDir: ${testDir}`); let server; beforeAll(async () => { //start server to server static files generated by getSignedUrl const app = express(); app.use('/', staticRouter); server = app.listen(PORT, () => { console.debug('listening on ' + PORT); }); }); afterAll(async () => { if (server) { await new Promise(resolve => server.close(resolve)); } }); function getStorageCfg(specialDir) { return specialDir ? cfgPersistentStorage : cfgCacheStorage; } function request(url) { return new Promise((resolve, reject) => { const module = url.startsWith('https') ? https : http; const req = module.get(url, response => { let data = ''; response.on('data', _data => (data += _data)); response.on('error', error => reject(error)); response.on('end', () => resolve(data)); }); req.on('error', error => reject(error)); }); } function runTestForDir(ctx, isMultitenantMode, specialDir) { const oldMultitenantMode = tenantManager.isMultitenantMode(); test('start listObjects', async () => { //todo set in all tests do not rely on test order tenantManager.setMultitenantMode(isMultitenantMode); const list = await storage.listObjects(ctx, testDir, specialDir); expect(list).toEqual([]); }); test('putObject', async () => { const buffer = Buffer.from(testFileData1); const res = await storage.putObject(ctx, testFile1, buffer, buffer.length, specialDir); expect(res).toEqual(undefined); const list = await storage.listObjects(ctx, testDir, specialDir); expect(list.sort()).toEqual([testFile1].sort()); }); test('putObject-stream', async () => { const buffer = Buffer.from(testFileData2); const stream = Readable.from(buffer); const res = await storage.putObject(ctx, testFile2, stream, buffer.length, specialDir); expect(res).toEqual(undefined); const list = await storage.listObjects(ctx, testDir, specialDir); expect(list.sort()).toEqual([testFile1, testFile2].sort()); }); if ('storage-fs' === getStorageCfg(specialDir).name) { test('UploadObject', async () => { const res = await storage.uploadObject(ctx, testFile3, 'createReadStream.txt', specialDir); expect(res).toEqual(undefined); expect(cp).toHaveBeenCalled(); const list = await storage.listObjects(ctx, testDir, specialDir); expect(list.sort()).toEqual([testFile1, testFile2, testFile3].sort()); }); } else { test('uploadObject', async () => { const readStream = Readable.from(testFileData3); readStream.size = testFileData3.length; const spy = jest.spyOn(fs, 'createReadStream').mockReturnValue(readStream); const res = await storage.uploadObject(ctx, testFile3, 'createReadStream.txt', specialDir); expect(res).toEqual(undefined); const list = await storage.listObjects(ctx, testDir, specialDir); expect(spy).toHaveBeenCalled(); expect(list.sort()).toEqual([testFile1, testFile2, testFile3].sort()); spy.mockRestore(); }); //todo fails with storage-s3 test.skip('uploadObject - stream error handling', async () => { const streamErrorMessage = new Error('Test stream error'); const mockStream = Readable.from( (async function* () { yield 'first chunk\n'; await new Promise(r => setTimeout(r, 5)); throw streamErrorMessage; })() ); mockStream.size = 1024; const spy = jest.spyOn(fs, 'createReadStream').mockReturnValue(mockStream); // Verify that the uploadObject function rejects when the stream emits an error await expect(storage.uploadObject(ctx, 'test-error-file.txt', 'nonexistent.txt', specialDir)).rejects.toThrow(streamErrorMessage); spy.mockRestore(); }); test.skip('uploadObject - non-existent file handling', async () => { const nonExistentFile = 'definitely-does-not-exist-' + Date.now() + '.txt'; // Verify the file actually doesn't exist expect(fs.existsSync(nonExistentFile)).toBe(false); // Verify that uploadObject properly handles and propagates the error await expect(storage.uploadObject(ctx, 'test-error-file.txt', nonExistentFile, specialDir)).rejects.toThrow(/ENOENT/); }); } test('copyObject', async () => { const res = await storage.copyObject(ctx, testFile3, testFile4, specialDir, specialDir); expect(res).toEqual(undefined); // let buffer = Buffer.from(testFileData3); // await storage.putObject(ctx, testFile3, buffer, buffer.length, specialDir); const list = await storage.listObjects(ctx, testDir, specialDir); expect(list.sort()).toEqual([testFile1, testFile2, testFile3, testFile4].sort()); }); test('headObject', async () => { let output; output = await storage.headObject(ctx, testFile1, specialDir); expect(output).toMatchObject({ContentLength: testFileData1.length}); output = await storage.headObject(ctx, testFile2, specialDir); expect(output).toMatchObject({ContentLength: testFileData2.length}); output = await storage.headObject(ctx, testFile3, specialDir); expect(output).toMatchObject({ContentLength: testFileData3.length}); output = await storage.headObject(ctx, testFile4, specialDir); expect(output).toMatchObject({ContentLength: testFileData4.length}); }); test('getObject', async () => { let output; output = await storage.getObject(ctx, testFile1, specialDir); expect(output.toString('utf8')).toEqual(testFileData1); output = await storage.getObject(ctx, testFile2, specialDir); expect(output.toString('utf8')).toEqual(testFileData2); output = await storage.getObject(ctx, testFile3, specialDir); expect(output.toString('utf8')).toEqual(testFileData3); output = await storage.getObject(ctx, testFile4, specialDir); expect(output.toString('utf8')).toEqual(testFileData4); }); test('createReadStream', async () => { let output, outputText; output = await storage.createReadStream(ctx, testFile1, specialDir); expect(output.contentLength).toEqual(testFileData1.length); outputText = await utils.stream2Buffer(output.readStream); expect(outputText.toString('utf8')).toEqual(testFileData1); output = await storage.createReadStream(ctx, testFile2, specialDir); expect(output.contentLength).toEqual(testFileData2.length); outputText = await utils.stream2Buffer(output.readStream); expect(outputText.toString('utf8')).toEqual(testFileData2); output = await storage.createReadStream(ctx, testFile3, specialDir); expect(output.contentLength).toEqual(testFileData3.length); outputText = await utils.stream2Buffer(output.readStream); expect(outputText.toString('utf8')).toEqual(testFileData3); }); test('getSignedUrl', async () => { let url, data; url = await storage.getSignedUrl(ctx, baseUrl, testFile1, urlType, undefined, undefined, specialDir); data = await request(url); expect(data).toEqual(testFileData1); url = await storage.getSignedUrl(ctx, baseUrl, testFile2, urlType, undefined, undefined, specialDir); data = await request(url); expect(data).toEqual(testFileData2); url = await storage.getSignedUrl(ctx, baseUrl, testFile3, urlType, undefined, undefined, specialDir); data = await request(url); expect(data).toEqual(testFileData3); url = await storage.getSignedUrl(ctx, baseUrl, testFile4, urlType, undefined, undefined, specialDir); data = await request(url); expect(data).toEqual(testFileData4); }); test('getSignedUrls', async () => { const urls = await storage.getSignedUrls(ctx, baseUrl, testDir, urlType, undefined, specialDir); const data = []; for (const i in urls) { data.push(await request(urls[i])); } expect(data.sort()).toEqual([testFileData1, testFileData2, testFileData3, testFileData4].sort()); }); test('getSignedUrlsArrayByArray', async () => { const urls = await storage.getSignedUrlsArrayByArray(ctx, baseUrl, [testFile1, testFile2], urlType, specialDir); const data = []; for (let i = 0; i < urls.length; ++i) { data.push(await request(urls[i])); } expect(data.sort()).toEqual([testFileData1, testFileData2].sort()); }); test('getSignedUrlsByArray', async () => { const urls = await storage.getSignedUrlsByArray(ctx, baseUrl, [testFile3, testFile4], undefined, urlType, specialDir); const data = []; for (const i in urls) { data.push(await request(urls[i])); } expect(data.sort()).toEqual([testFileData3, testFileData4].sort()); }); test('getSignedUrl with direct URLs enabled', async () => { const buffer = Buffer.from(testFileData1); const res = await storage.putObject(ctx, testFile1, buffer, buffer.length, specialDirCache); expect(res).toEqual(undefined); const url = await storage.getSignedUrl(ctx, baseUrl, testFile1, urlType, undefined, undefined, specialDirCache, true); const data = await request(url); expect(data).toEqual(testFileData1); if (cfgCacheStorage.name !== 'storage-fs') { expect(url).toContain(cfgCacheStorage.endpoint); expect(url).toContain(cfgCacheStorage.bucketName); } }); test('getSignedUrl with direct URLs disabled', async () => { const buffer = Buffer.from(testFileData1); const res = await storage.putObject(ctx, testFile1, buffer, buffer.length, specialDirCache); expect(res).toEqual(undefined); const url = await storage.getSignedUrl(ctx, baseUrl, testFile1, urlType, undefined, undefined, specialDirCache, false); const data = await request(url); expect(data).toEqual(testFileData1); expect(url).toContain('md5'); expect(url).toContain('expires'); expect(url).toContain(cfgCacheStorage.storageFolderName); }); test('deleteObject', async () => { let list; list = await storage.listObjects(ctx, testDir, specialDir); expect(list.sort()).toEqual([testFile1, testFile2, testFile3, testFile4].sort()); const res = await storage.deleteObject(ctx, testFile1, specialDir); expect(res).toEqual(undefined); list = await storage.listObjects(ctx, testDir, specialDir); expect(list.sort()).toEqual([testFile2, testFile3, testFile4].sort()); }); test('deletePath', async () => { let list; list = await storage.listObjects(ctx, testDir, specialDir); expect(list.sort()).toEqual([testFile2, testFile3, testFile4].sort()); const res = await storage.deletePath(ctx, testDir, specialDir); expect(res).toEqual(undefined); list = await storage.listObjects(ctx, testDir, specialDir); expect(list.sort()).toEqual([].sort()); tenantManager.setMultitenantMode(oldMultitenantMode); }); } // Assumed, that server is already up. describe('storage common dir', () => { runTestForDir(ctx, false, specialDirCache); }); describe('storage forgotten dir', () => { runTestForDir(ctx, false, specialDirForgotten); }); describe('storage common dir with tenants', () => { runTestForDir(ctx, true, specialDirCache); }); describe('storage forgotten dir with tenants', () => { runTestForDir(ctx, true, specialDirForgotten); }); describe('storage mix common and forgotten dir', () => { test('putObject', async () => { tenantManager.setMultitenantMode(false); let buffer = Buffer.from(testFileData1); const res = await storage.putObject(ctx, testFile1, buffer, buffer.length, specialDirCache); expect(res).toEqual(undefined); let list = await storage.listObjects(ctx, testDir, specialDirCache); expect(list.sort()).toEqual([testFile1].sort()); buffer = Buffer.from(testFileData2); const res2 = await storage.putObject(ctx, testFile2, buffer, buffer.length, specialDirForgotten); expect(res2).toEqual(undefined); list = await storage.listObjects(ctx, testDir, specialDirForgotten); expect(list.sort()).toEqual([testFile2].sort()); }); test('copyPath', async () => { const res = await storage.copyPath(ctx, testDir, testDir, specialDirCache, specialDirForgotten); expect(res).toEqual(undefined); const list = await storage.listObjects(ctx, testDir, specialDirForgotten); expect(list.sort()).toEqual([testFile1, testFile2].sort()); }); test('copyObject', async () => { const res = await storage.copyObject(ctx, testFile2, testFile2, specialDirForgotten, specialDirCache); expect(res).toEqual(undefined); const list = await storage.listObjects(ctx, testDir, specialDirCache); expect(list.sort()).toEqual([testFile1, testFile2].sort()); }); test('deletePath', async () => { let list, res; res = await storage.deletePath(ctx, testDir, specialDirCache); expect(res).toEqual(undefined); list = await storage.listObjects(ctx, testDir, specialDirCache); expect(list.sort()).toEqual([].sort()); res = await storage.deletePath(ctx, testDir, specialDirForgotten); expect(res).toEqual(undefined); list = await storage.listObjects(ctx, testDir, specialDirForgotten); expect(list.sort()).toEqual([].sort()); }); });