Skip to content

Commit

Permalink
Merge branch 'devel' into fix/cb-4344/alternate
Browse files Browse the repository at this point in the history
  • Loading branch information
dariamarutkina authored Jan 9, 2024
2 parents 967059d + e08f6b3 commit bde9d5b
Show file tree
Hide file tree
Showing 29 changed files with 757 additions and 33 deletions.
31 changes: 6 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io

## Changelog

### 23.3.1, 2023-12-25
### 23.3.2. 2024-01-08
- Added the ability to view decoded binary-type data in the Value panel;
- Enhanced security for unauthorized access;
- Different bug fixes and enhancements have been made.

### 23.3.1. 2023-12-25
- Performance:
- Upgraded to Jetty 11, delivering improved performance, enhanced features, and better alignment with the latest Java specifications.
- Resource management:
Expand All @@ -33,30 +38,6 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io
- Apache Derby driver has been removed because of the vulnerability issues.
- Many small bug fixes, enhancements, and improvements have been made

### Changes since 23.2.0:

- Security:
- Unauthorized access vulnerability was fixed;
- All embedded drivers are disabled by default. Administrators can re-enable them in the Server configuration.
- Access Management:
- Administrators have gained the ability to permanently delete users and their data.
- Authorization:
- The SSL option is available for establishing a connection in SQL Server.
- Connections:
- The 'Save credentials' checkbox has been removed from a template creating form as credentials are not stored in templates.
- SQL Editor:
- Support for using custom delimiters has been added in MySQL;
- The Output tab has been implemented, which includes warnings, info, and notices generated by the database when executing user queries;
- Fixed an issue in the SQL editor where it was impossible to switch the active schema when working with Oracle databases;
- Added ability to select shared connections for private scripts;
- Private connections can be chosen for shared scripts, but this change won’t be saved to the script file.
- Data Editor:
- Scrollbars have been made theme-independent;
- Added the ability to edit binary values in a table;
- Added the ability to count the total number of entries in the table.
- Driver management:
- Updated the version of the Clickhouse driver to 0.4.6.
- Many small bug fixes, enhancements, and improvements have been made

### Old CloudBeaver releases

Expand Down
50 changes: 50 additions & 0 deletions webapp/packages/core-utils/src/LoadingError.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { LoadingError } from './LoadingError';

describe('LoadingError', () => {
it('should be instance of Error', () => {
const error = new LoadingError(() => {}, 'test');

expect(error instanceof Error).toBeTruthy();
});

it('should trigger onRefresh', () => {
const onRefresh = jest.fn();
const error = new LoadingError(onRefresh, 'test');

error.refresh();

expect(onRefresh).toHaveBeenCalledTimes(1);
});

it('should refresh cause of the cause', () => {
const onRefresh = jest.fn();
const cause = new LoadingError(onRefresh, 'test');
const causeCause = new LoadingError(onRefresh, 'test', { cause });
const error = new LoadingError(onRefresh, 'test', { cause: causeCause });

jest.spyOn(causeCause, 'refresh');
jest.spyOn(cause, 'refresh');

error.refresh();

expect(causeCause.refresh).toHaveBeenCalledTimes(1);
expect(cause.refresh).toHaveBeenCalledTimes(1);

expect(onRefresh).toHaveBeenCalledTimes(3);
expect(error.cause).toBe(causeCause);
});

it('should pass cause through the regular error', () => {
const onRefresh = jest.fn();
const cause = new LoadingError(onRefresh, 'test', { cause: 'unit test' });
const regularError = new Error('test', { cause });
const error = new LoadingError(onRefresh, 'test', { cause: regularError });

jest.spyOn(cause, 'refresh');

error.refresh();

expect(cause.refresh).toHaveBeenCalledTimes(1);
expect(onRefresh).toHaveBeenCalledTimes(2);
});
});
33 changes: 33 additions & 0 deletions webapp/packages/core-utils/src/base64ToBlob.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { base64ToBlob } from './base64ToBlob';

const BASE_64_STRING =
'iVBORw0KGgoAAAANSUhEUgAAAhAAAAEWCAIAAAC40zleAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAACydSURBVHhe7Z1rsF1VtecnkAcQkhgsjVJlW3xQEcWkeOV5njknhHhCUCAVIOQFQQIHbocgJCGPaum6RXjdEMWU3WmVRxQhXLttbt+riAKBW1QJlerYbW4CX/jiLZUqbvmlu/ph9xhzzNeaa+199pn7PPZa6/+rUTlzjTnmWHPD3uO/51p776n6lq';

describe('base64ToBlob', () => {
it('should return a blob', () => {
const blob = base64ToBlob(BASE_64_STRING);

expect(blob).toBeInstanceOf(Blob);
expect(blob.type).toBe('application/octet-stream');
expect(blob.size).not.toBe(0);
});

it('should return a blob with the given mime type', () => {
const blob = base64ToBlob(BASE_64_STRING, 'image/jpeg');

expect(blob).toBeInstanceOf(Blob);
expect(blob.type).toBe('image/jpeg');
expect(blob.size).not.toBe(0);
});

it('should create empty blob', () => {
const blob = base64ToBlob('');

expect(blob).toBeInstanceOf(Blob);
expect(blob.size).toBe(0);
});

it('should throw an error if the base64 string is invalid', () => {
expect(() => base64ToBlob('-10')).toThrow();
});
});
20 changes: 20 additions & 0 deletions webapp/packages/core-utils/src/base64ToHex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { base64ToHex } from './base64ToHex';

const BASE_64_STRING =
'iVBORw0KGgoAAAANSUhEUgAAAhAAAAEWCAIAAAC40zleAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAACydSURBVHhe7Z1rsF1VtecnkAcQkhgsjVJlW3xQEcWkeOV5njknhHhCUCAVIOQFQQIHbocgJCGPaum6RXjdEMWU3WmVRxQhXLttbt+riAKBW1QJlerYbW4CX/jiLZUqbvmlu/ph9xhzzNeaa+199pn7PPZa6/+rUTlzjTnmWHPD3uO/51p776n6lq';

describe('base64ToHex', () => {
it('should return a hex string', () => {
expect(base64ToHex(BASE_64_STRING)).toBe(
'89504E470D0A1A0A0000000D4948445200000210000001160802000000B8D3395E000000017352474200AECE1CE90000000467414D410000B18F0BFC6105000000097048597300000EC300000EC301C76FA86400002C9D49444154785EED9D6BB05D55B5E72790071092182C8D52655B7C5011C5A478E5799E392784784250201520E4054102076E872024218F6AE9BA4578DD10C594DD69954714215CBB6D6EDFAB8802815B540995EAD86D6E025FF8E22D952A6EF9A5BBFA61F71873CCD79A6BED7DF699FB3CF65AEBFFAB5139738D39E65873C3DEE3BFE75A7BEFA9FA96',
);
});

it('should return an empty string', () => {
expect(base64ToHex('')).toBe('');
});

it('should throw an error if the base64 string is invalid', () => {
expect(() => base64ToHex('-10')).toThrow();
});
});
39 changes: 39 additions & 0 deletions webapp/packages/core-utils/src/combineITerableIterators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { combineITerableIterators } from './combineITerableIterators';

describe('combineIterableIterators', () => {
it('should return an iterator that combines the values of the given iterators', () => {
const iterator1 = [1, 2, 3][Symbol.iterator]();
const iterator2 = [4, 5, 6][Symbol.iterator]();
const iterator3 = [7, 8, 9][Symbol.iterator]();

const combinedIterator = combineITerableIterators(iterator1, iterator2, iterator3);

expect(Array.from(combinedIterator)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
});

it('should return an empty iterator if no iterators are given', () => {
const combinedIterator = combineITerableIterators();

expect(Array.from(combinedIterator)).toEqual([]);
});

it('should return an empty iterator if all iterators are empty', () => {
const iterator1 = [][Symbol.iterator]();
const iterator2 = [][Symbol.iterator]();
const iterator3 = [][Symbol.iterator]();

const combinedIterator = combineITerableIterators(iterator1, iterator2, iterator3);

expect(Array.from(combinedIterator)).toEqual([]);
});

it('should return an iterator that combines the values of the given iterators, even if some are empty', () => {
const iterator1 = [1, 2, 3][Symbol.iterator]();
const iterator2 = [][Symbol.iterator]();
const iterator3 = [7, 8, 9][Symbol.iterator]();

const combinedIterator = combineITerableIterators(iterator1, iterator2, iterator3);

expect(Array.from(combinedIterator)).toEqual([1, 2, 3, 7, 8, 9]);
});
});
28 changes: 28 additions & 0 deletions webapp/packages/core-utils/src/copyToClipboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { copyToClipboard } from './copyToClipboard';

describe('copyToClipboard', () => {
beforeAll(() => {
document.execCommand = jest.fn();
});

it('should copy data to clipboard', () => {
copyToClipboard('test');

expect(document.execCommand).toHaveBeenCalledWith('copy');
});

it('should focus on active element after copy', () => {
document.body.focus = jest.fn();

copyToClipboard('test');

expect(document.activeElement).toBe(document.body);
expect(document.body.focus).toHaveBeenCalled();
});

it('should have no children after copy', () => {
copyToClipboard('test');

expect(document.body.children.length).toBe(0);
});
});
11 changes: 11 additions & 0 deletions webapp/packages/core-utils/src/createLastPromiseGetter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createLastPromiseGetter } from './createLastPromiseGetter';

describe('createLastPromiseGetter', () => {
const getter = createLastPromiseGetter<number>();

it('should return the result of the given getter', async () => {
const result = await getter([1, 2, 3], () => Promise.resolve(42));

expect(result).toBe(42);
});
});
33 changes: 33 additions & 0 deletions webapp/packages/core-utils/src/formatNumber.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { formatNumber } from './formatNumber';

describe('formatNumber', () => {
it('should not format number', () => {
expect(formatNumber(999, 2)).toBe('999');
});

it('should format number with no extra decimals', () => {
expect(formatNumber(1000, 2)).toBe('1k');
expect(formatNumber(1000000, 2)).toBe('1M');
expect(formatNumber(1000000000, 2)).toBe('1B');
expect(formatNumber(1000000000000, 2)).toBe('1T');
expect(formatNumber(1000000000000000, 2)).toBe('1P');
expect(formatNumber(1000000000000000000, 2)).toBe('1E');
});

it('should format number with extra decimals', () => {
expect(formatNumber(1230, 2)).toBe('1.23k');
expect(formatNumber(1230000, 2)).toBe('1.23M');
expect(formatNumber(1230000000, 2)).toBe('1.23B');
expect(formatNumber(1230000000000, 2)).toBe('1.23T');
expect(formatNumber(1230000000000000, 2)).toBe('1.23P');
expect(formatNumber(1230000000000000000, 2)).toBe('1.23E');
});

it('should round formatted number', () => {
expect(formatNumber(1234, 2)).toBe('1.23k');
expect(formatNumber(1234567, 2)).toBe('1.23M');
expect(formatNumber(1234567890, 2)).toBe('1.23B');
expect(formatNumber(1234567890123, 2)).toBe('1.23T');
expect(formatNumber(1234567890123456, 2)).toBe('1.23P');
});
});
32 changes: 32 additions & 0 deletions webapp/packages/core-utils/src/getMIME.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getMIME } from './getMIME';

describe('getMIME', () => {
it('should return null if binary is empty', () => {
expect(getMIME('')).toBe(null);
});

it('should return image/jpeg if binary starts with /', () => {
const jpegBase64Image =
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCABAAEADAREAAhEBAxEB/8QAGgAAAgMBAQAAAAAAAAAAAAAABgcDBAUIAv/EADcQAAEDAwIFAQUHAgcAAAAAAAECAwQABREGIQcSEzFBYQgUIlFxFSMygZGhsUJiJCVScsHR4f/EABsBAAIDAQEBAAAAAAAAAAAAAAMEAgUGAQcA/8QALhEAAQQBAwMCBQMFAAAAAAAAAQACAxEEEiExBUFhE1EicYGx8ZGh0RQzQ8Hh/9oADAMBAAIRAxEAPwBtP24jOBXrBWZpZ70LvkZrodSEWqmuD3x+9FDwgliiEE/6anqCFoXtMNXgV8HBfaSpWoJ+VTtcorSh2wq8VIFdDbW21ATCiuyHNm2kFaifAAJqJlA2CK2LukVw59p+2TGEwNTlQfBCUzWkjf8A3j/kVRRZrXincq8kgbyxO6GIl6gom2+Q3MiuDKHWVcyTTtpIt7FeF2lRzhP7VDWAuena8JsxPcftUfWAXPRUqLJtsk1Jsy4YVOxaMAkgJSO6j4pgP7lC9MKRV2striKfeuMUYQpYBdG4COf+N/zFCfkgDZEbGAuaPaQ9paLEsVytGnJqpPvSiy44jKQhBZGQkj+7z61lc/qzWOEMJs9/Ht91dYWHqkY6QU0rlWNr6Bul+I4ypWMnGdsbfTeq1mc0H4m0ibEI/wBI8apWjFMvWG8vw1Pn42ivLeQexSdsf91cwdQY0CjYKXkYCKC6J4ce2xZrg+zE1DbUFSkpCpENeDnyeVWx7dgas4pIMs6Y5NLvPH6pbUYt3NsLovTGudCa1cSizajiSHjjLC1dNxG2d0q8bHeqzJfLikiQfoQfsrOBkM9Bh3KV/Hj2jtMcPtNyGdO3KNdr851Gm0MnqBhaduZYHiq13V2MadJt3ZRmxtJpcYai9qbXmo7PLYfuSmIk1bjjiGBykBSAnlBG+POKrJeqZMj3xXt/KGyCMboPf1Rdp0xD8mZIcfU0ltKio7JDYQlIx4wmqB08sz9d+Pp+E6GtDaHusK4dadDWhZUptpZOO5PgfxUoiGzWfYJiyfonebXw1lhTiml9RKcEE7/zXogjw3DcLE3lN4KpPad4ezGULZVKbSk4IS3nxUdGKAA1EYcjfUqjOldATZMXoLuLrpVyqS22MjfvjNfNbjOO26nqnFkppae4J2h5aEWu33ufLeQFISxHKie55dvOAaDLFjQAlwICkwzzmmLLm6Z0dFbfamtTYUwFSVtSGeQkjII381XD+jLxTv2RTHlMHxBCV60/oZDTZbLobBBA5dqDlQ42s6XbnwiY7sitwpxpzTUmKlTTDighKS24hRG5OMmsZlZwaC2I8X2VzDG4C3Ly3YLJBjh4RfgcSXVJVkYwM5/XxWWycycyFoduCnmsA5QZDsltjLz03XHCMnnO2flXrTQy7tJlgCbHDjhLfNdRA5BtyINrYyo3GUrkbBHcjbKiPTNWsOLLM3UwUB3KTfLDGdL+Snjw64S6E0y1BdkvI1HdeRt1TanA0wU53I5RkjfyTmraCCP/ABEWqqYuH9wbJ/Xfiza7XY2vcExbCuIrmjPRGQOVWOXHKn8edhjv8t6Hk9OZp9Uvo+d78V3tSxcsh/p6Laea2+t9q/KV2torPHzQzLGoIDVhvTTL6mJDaB+JSkkOEbK3x2O4zSY6ScyMOe3Q+ipSZjMaYiJ2ti5h1n7PV308zJYiOKlNpJRGJPxS8J5stg9sAnPqKzOf07Kgka0i7487b0reDLglaCw/PxuhRu23izxEtvRVhpop35NiebJH6AVg5Mcmbjkn9grmGiRQtXbkl4W5yQ80Hm0tEKUnwCR+lKT9OmYXOI21BOyxxi9+6aeg+Eds0m39o6pDVyl4yiEoAtN+qj/UfTt9a93wemMj+PI3Pssdl5rnW2EUPdG1w4qIjR0sRy3HjtpwhpICUpHoPlWk1MaNPZUBY9x1d0qeIfFx+1XCNc2HHFtKYW2ltpv7tKv7iO24FY7LzJMTM1R70OPC12LhxZmJpk2s8qxbOMQnxI0vA63IFoSr8ScgZHpWtx8uHIYJWCz9isdkYsuO8xPNf7CJIPFN37RYcUStoIUF5PcZH/tNukD3AX2SrYywE8rVv2s2B7m91hzRldZK1b5QEkHA8bHxSGaYpQyQ8tB/hTx4pWPewHZ35QYZrV3tDEdt3Kpbj/SfaOFAYR8e+xGSdjtWRh6PjZMDRMNyXEG/A3v5q+jzZ8WUuYeALH1OyXmsJlxsrMm2yHHLgywhbbruClLvKdlYG2+MV531TFkw5pMbXtq28hegQyjKxWT1u6j8j3CJdWcV2m47qTIKnVDCUJPxE16iyZ73bcLJSRsY03yl3cdVTbmtbxd6Sl4CWz/SB6miOy2s+G0q3FdJup9G3pu9PSrDcG2nw8OqkOq3wndQH5ZP1FZzqErXOE4+RWj6azSDA7vusOXMZjtyYsRZEqO4SWCQVbAdsHtjG3r6UzgzticSza+Qls/H9Rul/bgqW260lPI93CUtvBOyDsv8vmK0LMhth5Kzbsd9FgCIGNXicy3EclJblJStCOZY+FKhjf8AeuSSXJtwQQVKJg0fFsQRSsaBlPRY1sS8C+qI+8h5POSAM7AehwDQOmtboi1UdJIvwi5t3IGmrA2WfxN/w1tXLt0h5t+VKPWSvJ2ySEowRgYJHrVb17Dx9AneLJd+APZO9HyZifRGwAH/AEpdxJyQjrOqy+od1+KXlyOQOE9DBqr3KtLnq6Tanl8ylDCG84Ur6VXOl5pONjNC1kR9Ru2G5tTIiv8AMEK5+sjH3fySPpUX3JGW9kSIthlDu6xpWvG3ozqRHeYm8p51tKQkKJUM82Eg+Bt86Pj4zxTn0lsnLY4uDQfkvEK63a69JJKumNk5G5+pHerVpZHe6p9Uku3ZFsSK5ZQxIU3jGAoFec0M5Gs1abjg0C6V6HqVdjnHkdUY76ytlR3zknIP0pjBksOaebtBzYwNLmo0nGHf4zAduEVlttxK0BzmPOcHJGAe2360DNcZdLX8DdTw6i1OZydl/9k=';
expect(getMIME(jpegBase64Image)).toBe('image/jpeg');
});

it('should return image/png if binary starts with i', () => {
const pngBase64Image = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';
expect(getMIME(pngBase64Image)).toBe('image/png');
});

it('should return image/gif if binary starts with R', () => {
const gifBase64Image = 'R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
expect(getMIME(gifBase64Image)).toBe('image/gif');
});

it('should return image/webp if binary starts with U', () => {
const webpBase64Image = 'UklGRmh2AABXRUJQVlA4IFx2AADSvgGdASom';
expect(getMIME(webpBase64Image)).toBe('image/webp');
});

it('should return null if binary starts with anything else', () => {
expect(getMIME('aasdqwe')).toBe(null);
});
});
33 changes: 33 additions & 0 deletions webapp/packages/core-utils/src/getOS.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getOS, OperatingSystem } from './getOS';

describe('getOS', () => {
it('should return windowsOS', () => {
jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Windows 11');
expect(getOS()).toBe(OperatingSystem.windowsOS);
});

it('should return macOS', () => {
jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('MacOS Sonoma');
expect(getOS()).toBe(OperatingSystem.macOS);
});

it('should return linuxOS', () => {
jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Linux Ubuntu');
expect(getOS()).toBe(OperatingSystem.linuxOS);
});

it('should return unixOS', () => {
jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('X11');
expect(getOS()).toBe(OperatingSystem.unixOS);
});

it('should return iOS', () => {
jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('like Mac');
expect(getOS()).toBe(OperatingSystem.iOS);
});

it('should return Windows for unknown OS', () => {
jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('zzzz');
expect(getOS()).toBe(OperatingSystem.windowsOS);
});
});
3 changes: 2 additions & 1 deletion webapp/packages/core-utils/src/getOS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function getOS(): OperatingSystem {
];

const userAgent = window.navigator.userAgent;
const OS = operatingSystemOptions.find(([testString]) => userAgent.includes(testString))?.[1] ?? OperatingSystem.windowsOS;
const OS =
operatingSystemOptions.find(([testString]) => userAgent.toLowerCase().includes(testString.toLowerCase()))?.[1] ?? OperatingSystem.windowsOS;
return OS;
}
23 changes: 23 additions & 0 deletions webapp/packages/core-utils/src/getPathName.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getPathName } from './getPathName';

jest.mock('./getPathParts', () => ({
getPathParts: (path: string) => path.split('/'),
}));

describe('getPathName', () => {
it('should return the last part of the path', () => {
expect(getPathName('/a/b/c')).toBe('c');
});

it('should return the path if it has no parts', () => {
expect(getPathName('')).toBe('');
});

it('should return the path if it has only one part', () => {
expect(getPathName('/a')).toBe('a');
});

it('should return same string if cannot divide it to full path', () => {
expect(getPathName('abc')).toBe('abc');
});
});
24 changes: 24 additions & 0 deletions webapp/packages/core-utils/src/getPathParent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getPathParent } from './getPathParent';

jest.mock('./getPathParts', () => ({
getPathParts: (path: string) => path.split('/'),
createPath: (...parts: string[]) => parts.join('/'),
}));

describe('getPathParent', () => {
it('should return the parent path', () => {
expect(getPathParent('/a/b/c')).toBe('a/b');
});

it('should return the parent path if it has no parts', () => {
expect(getPathParent('')).toBe('');
});

it('should return the parent path if it has only one part', () => {
expect(getPathParent('/a')).toBe('');
});

it('should return same string if cannot divide it to full path', () => {
expect(getPathParent('abc')).toBe('');
});
});
Loading

0 comments on commit bde9d5b

Please sign in to comment.