Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ describe('util/file', () => {
['filename.txt', 'txt'],
['filename.backup.mp4', 'mp4'],
['filename..temp.pdf', 'pdf'],
[{ name: 'test.pdf' }, ''],
['invalidfilenamepdf', ''],
])('should return extension of file correctly', (filename, extension) => {
expect(getFileExtension(filename)).toBe(extension);
});

test('should return empty string when filename is not a string', () => {
expect(getFileExtension({ name: 'test.pdf' } as unknown as string)).toBe('');
});
});

describe('isGSuiteExtension()', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ const file = new FileAPI({ cache });
const folder = new FolderAPI({ cache });
const weblink = new WebLinkAPI({ cache });

const getThrownError = (callback: () => unknown): Error => {
try {
callback();
} catch (error) {
expect(error).toBeInstanceOf(Error);
return error as Error;
}

throw new Error('Expected callback to throw');
};

describe('util/flatten', () => {
test('should flatten the list and create new cache entries', () => {
const items = flatten(list, folder, file, weblink);
Expand All @@ -45,16 +56,19 @@ describe('util/flatten', () => {

test('should throw with a bad type', () => {
const badList = [{ id: '1', type: 'foo' }];
expect(flatten.bind(flatten, badList, folder, file, weblink)).toThrow(Error, /Unknown Type/);
const error = getThrownError(flatten.bind(flatten, badList, folder, file, weblink));
expect(error.message).toMatch(/Unknown Type/);
});

test('should throw with a bad item when no id', () => {
const badList = [{ type: 'foo' }];
expect(flatten.bind(flatten, badList, folder, file, weblink)).toThrow(Error, /Bad box item/);
const error = getThrownError(flatten.bind(flatten, badList, folder, file, weblink));
expect(error.message).toMatch(/Bad box item/);
});

test('should throw with a bad item when no type', () => {
const badList = [{ id: 'foo' }];
expect(flatten.bind(flatten, badList, folder, file, weblink)).toThrow(Error, /Bad box item/);
const error = getThrownError(flatten.bind(flatten, badList, folder, file, weblink));
expect(error.message).toMatch(/Bad box item/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('util/function', () => {

setTimeout(() => {
promise.catch(sandbox.mock());
expect(inner.callCount).to.equal(2);
expect(inner.callCount).toBe(2);
}, 100);
});

Expand All @@ -62,7 +62,7 @@ describe('util/function', () => {

setTimeout(() => {
promise.catch(sandbox.mock());
expect(inner.callCount).to.equal(1);
expect(inner.callCount).toBe(1);
}, 100);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// @flow

import { domainNameValidator, emailValidator, hostnameValidator, ipv4AddressValidator } from '../validators';

describe('util/validators', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import { digest, getRandomValues } from '../webcrypto';

jest.mock('js-sha1');

type WindowWithMsCrypto = Omit<Window, 'crypto'> & { crypto?: Crypto; msCrypto?: Crypto };
type Sha1WithArrayBuffer = typeof sha1 & { arrayBuffer: jest.Mock };
type CryptoOperation = {
oncomplete?: (event: { target: { result: ArrayBuffer } }) => void;
onerror?: (reason?: unknown) => void;
};

const sha1Mock = sha1 as Sha1WithArrayBuffer;

describe('util/webcrypto', () => {
beforeEach(() => {
// eslint-disable-next-line no-undef
Object.defineProperty(globalThis, 'window', {
value: {
...window,
Expand All @@ -17,107 +25,108 @@ describe('util/webcrypto', () => {
describe('getRandomValues()', () => {
test('should call getRandomValues() to get an array of random values', () => {
const getRandomValuesMock = jest.fn();
window.crypto = {
(window as WindowWithMsCrypto).crypto = {
getRandomValues: getRandomValuesMock,
};
} as unknown as Crypto;

getRandomValues();
getRandomValues(new Uint8Array());
expect(getRandomValuesMock).toHaveBeenCalled();
});
});

describe('digest()', () => {
const algorithm = 'a';
const buffer = new Uint8Array([1, 2]);
const { buffer } = new Uint8Array([1, 2]);
Comment thread
bonchevskyi marked this conversation as resolved.
const digestVal = 'd';

test('should return the return value of digest() when the crypto lib is not msCrypto', () => {
const digestMock = jest.fn().mockReturnValueOnce(digestVal);
window.crypto = {
(window as WindowWithMsCrypto).crypto = {
subtle: {
digest: digestMock,
},
};
} as unknown as Crypto;

expect(digest(algorithm, buffer)).toBe(digestVal);
expect(digestMock).toHaveBeenCalledWith(algorithm, buffer);
});
describe('msCrypto', () => {
test('should return a promise which resolves properly when the crypto lib is msCrypto', () => {
sha1.arrayBuffer = jest.fn().mockImplementation(() => new ArrayBuffer());
const cryptoOperation = {};
test('should return a promise which resolves properly when the crypto lib is msCrypto', async () => {
sha1Mock.arrayBuffer = jest.fn().mockImplementation(() => new ArrayBuffer(0));
const cryptoOperation: CryptoOperation = {};
const digestMock = jest.fn().mockReturnValueOnce(cryptoOperation);
const expectedDigest = new ArrayBuffer(0);

window.crypto = undefined;
window.msCrypto = {
(window as WindowWithMsCrypto).crypto = undefined;
(window as WindowWithMsCrypto).msCrypto = {
subtle: {
digest: digestMock,
},
};
} as unknown as Crypto;

digest(algorithm, buffer);
const digestPromise = digest(algorithm, buffer);

cryptoOperation.oncomplete({
expect(cryptoOperation.oncomplete).toEqual(expect.any(Function));
cryptoOperation.oncomplete!({
target: {
result: 'digest',
result: expectedDigest,
},
});

await expect(digestPromise).resolves.toBe(expectedDigest);
expect(digestMock).toHaveBeenCalledWith({ name: algorithm }, buffer);
expect(sha1.arrayBuffer).not.toHaveBeenCalled();
expect(sha1Mock.arrayBuffer).not.toHaveBeenCalled();
});

test('should return a promise which rejects properly when the crypto lib is msCrypto', () => {
const cryptoOperation = {};
test('should return a promise which rejects properly when the crypto lib is msCrypto', async () => {
const cryptoOperation: CryptoOperation = {};
const digestMock = jest.fn().mockReturnValueOnce(cryptoOperation);

window.crypto = undefined;
window.msCrypto = {
(window as WindowWithMsCrypto).crypto = undefined;
(window as WindowWithMsCrypto).msCrypto = {
subtle: {
digest: digestMock,
},
};
} as unknown as Crypto;

const expectedError = new Error('ERROR');
const digestPromise = digest(algorithm, buffer);

digest(algorithm, buffer).catch(error => {
expect(error).toBe(expectedError);
});

cryptoOperation.onerror(expectedError);
expect.assertions(1);
expect(cryptoOperation.onerror).toEqual(expect.any(Function));
cryptoOperation.onerror!(expectedError);
await expect(digestPromise).rejects.toBe(expectedError);
});
});
describe('js-sha1', () => {
test('should use js-sha1 for calculating hash in IE-11 SHA-1 digest scenarios', async () => {
// ie11 does not support sha-1, so we use a library
sha1.arrayBuffer = jest.fn().mockImplementation(() => new ArrayBuffer());
sha1Mock.arrayBuffer = jest.fn().mockImplementation(() => new ArrayBuffer(0));
const digestMock = jest.fn().mockReturnValueOnce({});
window.crypto = undefined;
(window as WindowWithMsCrypto).crypto = undefined;

window.msCrypto = {
(window as WindowWithMsCrypto).msCrypto = {
subtle: {
digest: digestMock,
},
};
} as unknown as Crypto;

const hash = await digest('SHA-1', buffer);
expect(hash).toBeDefined();
expect(digestMock).not.toHaveBeenCalled();
expect(sha1.arrayBuffer).toHaveBeenCalledWith(buffer);
expect(sha1Mock.arrayBuffer).toHaveBeenCalledWith(buffer);
});
test('should return a promise which rejects properly when js-sha1 fails', () => {
const expectedError = new Error('ERROR');
// ie11 does not support sha-1, so we use a library
sha1.arrayBuffer = jest.fn().mockRejectedValue(expectedError);
sha1Mock.arrayBuffer = jest.fn().mockRejectedValue(expectedError);
const digestMock = jest.fn().mockReturnValueOnce({});
window.crypto = undefined;
(window as WindowWithMsCrypto).crypto = undefined;

window.msCrypto = {
(window as WindowWithMsCrypto).msCrypto = {
subtle: {
digest: digestMock,
},
};
} as unknown as Crypto;

digest('SHA-1', buffer).catch(error => {
expect(error).toBe(expectedError);
Expand Down
File renamed without changes.
30 changes: 30 additions & 0 deletions src/utils/domPolyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Polyfills the .closest method for Element
* Currently being used for scrollIntoView
* For reference: https://github.com/zloirock/core-js/issues/317
*/
window.Element.prototype.closest = function closest(this: Element, s: string): Element | null {
const elementProto = window.Element.prototype as Element & {
msMatchesSelector?: (selector: string) => boolean;
webkitMatchesSelector?: (selector: string) => boolean;
};
if (!window.Element.prototype.matches) {
window.Element.prototype.matches = (elementProto.msMatchesSelector ||
elementProto.webkitMatchesSelector) as typeof Element.prototype.matches;
}

if (this.matches(s)) {
return this;
}

let el: Element | Node | null = this.parentElement || this.parentNode;
while (el !== null && el.nodeType === 1) {
const element = el as Element;
if (element.matches(s)) {
return element;
}
el = element.parentElement || element.parentNode;
}

return null;
};
File renamed without changes.
40 changes: 40 additions & 0 deletions src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
HTTP_STATUS_CODE_CONFLICT,
HTTP_STATUS_CODE_UNAUTHORIZED,
HTTP_STATUS_CODE_RATE_LIMIT,
HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR,
} from '../constants';

const getBadItemError = (): Error => new Error('Bad box item!');

const getBadPermissionsError = (): Error => new Error('Insufficient Permissions!');

const getBadUserError = (): Error => new Error('Bad box user!');

const getMissingItemTextOrStatus = (): Error => new Error('Missing text or status!');

const isUserCorrectableError = (status: number): boolean =>
status === HTTP_STATUS_CODE_RATE_LIMIT ||
status === HTTP_STATUS_CODE_UNAUTHORIZED ||
status === HTTP_STATUS_CODE_CONFLICT ||
status >= HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR;

const getAbortError = (): Error => {
class AbortError extends Error {
constructor(message: string) {
super(message);
this.name = 'AbortError';
}
}

return new AbortError('Aborted');
};

export {
getAbortError,
getBadItemError,
getBadPermissionsError,
getBadUserError,
getMissingItemTextOrStatus,
isUserCorrectableError,
};
2 changes: 1 addition & 1 deletion src/utils/file.js → src/utils/file.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function isGSuiteExtension(extension: string): boolean {
* @param {string} filename a Box file
* @return {string} typed id for file
*/
export function getFileExtension(filename: string | void): string {
export function getFileExtension(filename?: string): string {
if (typeof filename !== 'string') {
return '';
}
Expand Down
47 changes: 47 additions & 0 deletions src/utils/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import getProp from 'lodash/get';
import {
TYPED_ID_FILE_PREFIX,
TYPED_ID_FOLDER_PREFIX,
FILE_EXTENSION_BOX_CANVAS,
FILE_EXTENSION_BOX_NOTE,
FILE_EXTENSION_GOOGLE_DOC,
FILE_EXTENSION_GOOGLE_SHEET,
FILE_EXTENSION_GOOGLE_SLIDE,
FILE_EXTENSION_GOOGLE_SLIDE_LEGACY,
} from '../constants';
import type { BoxItem } from '../common/types/core';

const FILE_EXT_REGEX = /\.([0-9a-z]+)$/i; // Case insensitive regex to extract file extension without "."

/**
* Returns typed id for file. Useful for when
* making file based XHRs where auth token
* can be per file as used by Preview.
*/
export const getTypedFileId = (id: string): string => `${TYPED_ID_FILE_PREFIX}${id}`;

/** Returns typed id for folder. */
export const getTypedFolderId = (id: string): string => `${TYPED_ID_FOLDER_PREFIX}${id}`;

/** Determines if the file is a box note. */
export const isBoxNote = (file: BoxItem): boolean => file.extension === FILE_EXTENSION_BOX_NOTE;

/** Determines if the file is box canvas. */
export const isBoxCanvas = (file: BoxItem): boolean => file.extension === FILE_EXTENSION_BOX_CANVAS;

/** Determines whether a file extension is associated with a G Suite file. */
export const isGSuiteExtension = (extension: string): boolean =>
extension === FILE_EXTENSION_GOOGLE_DOC ||
extension === FILE_EXTENSION_GOOGLE_SHEET ||
extension === FILE_EXTENSION_GOOGLE_SLIDE ||
extension === FILE_EXTENSION_GOOGLE_SLIDE_LEGACY;

/** Returns the extension from the file name. */
export const getFileExtension = (filename?: string): string => {
if (typeof filename !== 'string') {
return '';
}

const result = FILE_EXT_REGEX.exec(filename);
return getProp(result, '[1]', '');
};
File renamed without changes.
Loading
Loading