Skip to content
Open
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
7 changes: 6 additions & 1 deletion .github/workflows/common-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@ jobs:
- run: npm install --ignore-scripts

- name: Get Previous Tag
# Only consider this SDK's numeric semver tags (e.g. 2.7.9, 2.9.0-beta.1).
# Other packages released from this repo use prefixed tags (e.g. flowvault-v1.0.0),
# which are not valid npm versions and must not be picked up here.
run: |
echo "TAG=$(git describe --abbrev=0 --tags $(git rev-list --tags --max-count=1))" >> $GITHUB_ENV
SEMVER_GLOB='[0-9]*.[0-9]*.[0-9]*'
LAST_SEMVER_COMMIT=$(git rev-list --tags="$SEMVER_GLOB" --max-count=1)
echo "TAG=$(git describe --abbrev=0 --tags --match "$SEMVER_GLOB" "$LAST_SEMVER_COMMIT")" >> $GITHUB_ENV

- name: Set RELEASE_VERSION
run: |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "skyflow-js",
"preferGlobal": true,
"analyze": false,
"version": "2.7.8-dev.819273e",
"version": "2.7.10",
"author": "Skyflow",
"description": "Skyflow JavaScript SDK",
"homepage": "https://github.com/skyflowapi/skyflow-js",
Expand Down
13 changes: 9 additions & 4 deletions src/core/internal/frame-element-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,8 @@ export default class FrameElementInit {
}

if (state.value === undefined || state.value === null || state.value === '') {
rootReject({ error: 'No files selected' });
const noFileError = new SkyflowError(SKYFLOW_ERROR_CODE.NO_FILE_SELECTED, [], true);
rootReject({ errorResponse: [{ error: noFileError.error }] });
return;
}

Expand Down Expand Up @@ -642,9 +643,13 @@ export default class FrameElementInit {
throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true);
}

const isValidFileName = vaildateFileName(file.name);
if (!isValidFileName) {
throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true);
// Only validate the original file name when it will actually be sent to the vault.
// With preserveFileName: false the name is replaced with a generated UUID before upload.
if (fileElement.preserveFileName) {
const isValidFileName = vaildateFileName(file.name);
if (!isValidFileName) {
throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true);
}
}
});
return true;
Expand Down
5 changes: 4 additions & 1 deletion src/core/internal/iframe-form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,10 @@ export default class IFrameFormElement extends EventEmitter {
oversizedFileNames.push(files[i].name);
}
}
if (this.preserveFileName) vaildateFileNames = vaildateFileName(files[i].name);
// Once any file name fails, keep the flag false; do not let a later valid name reset it.
if (this.preserveFileName && vaildateFileNames) {
vaildateFileNames = vaildateFileName(files[i].name);
}
}
if (oversizedFileNames.length > 0) {
if (files.length === 1) {
Expand Down
3 changes: 2 additions & 1 deletion src/libs/element-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,8 @@ export const formatOptions = (
break;
}

case ELEMENTS.FILE_INPUT.name || ELEMENTS.MULTI_FILE_INPUT.name: {
case ELEMENTS.FILE_INPUT.name:
case ELEMENTS.MULTI_FILE_INPUT.name: {
if (!Object.prototype.hasOwnProperty.call(formattedOptions, 'preserveFileName')) {
formattedOptions = { ...formattedOptions, preserveFileName: true };
}
Expand Down
1 change: 0 additions & 1 deletion src/utils/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ const logs = {
NO_ELEMENTS_IN_COLLECT: 'Validation error. No elements found in collect container',
NO_ELEMENTS_IN_COMPOSABLE: 'Validation error. No elements found in composable container',
NO_ELEMENTS_IN_REVEAL: 'Validation error. No elements found in reveal container',
INVALID_FILE_NAMES: 'Invalid File Name. Only alphanumeric characters and !-_.*() are allowed.',
INVALID_FILE_NAME: 'Invalid File Name. Only alphanumeric characters and !-_.*() are allowed.',
CLIENT_CONNECTION: 'client connection not established. client info has not reached iframes',
INVALID_BEARER_TOKEN: 'Token generated from \'getBearerToken\' callback function is invalid. Make sure the implementation of \'getBearerToken\' is correct.',
Expand Down
65 changes: 61 additions & 4 deletions tests/core/internal/frame-element-init.additional.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,22 +330,24 @@ describe('FrameElementInit extended unit tests', () => {
expect(err.errorResponse[0].error).toMatchObject({ code: 400 });
});

test('multipleUploadFiles rejects with { error: "No files selected" } when state.value is empty', async () => {
const noFileSelectedError = { code: 400, description: 'No File Selected' };

test('multipleUploadFiles rejects with NO_FILE_SELECTED in errorResponse when state.value is empty', async () => {
const instance = new FrameElementInit();
const fileElement = makeFileElement({ multiple: true, files: [makeFile('a.txt')] });
fileElement.state.value = '';
const config = { vaultURL: 'https://vault.url', vaultID: 'vault123', authToken: 'token123' };
await expect(instance['multipleUploadFiles'](fileElement, config, undefined))
.rejects.toEqual({ error: 'No files selected' });
.rejects.toEqual({ errorResponse: [{ error: noFileSelectedError }] });
});

test('multipleUploadFiles rejects with { error: "No files selected" } when state.value is null', async () => {
test('multipleUploadFiles rejects with NO_FILE_SELECTED in errorResponse when state.value is null', async () => {
const instance = new FrameElementInit();
const fileElement = makeFileElement({ multiple: true, files: [makeFile('a.txt')] });
fileElement.state.value = null;
const config = { vaultURL: 'https://vault.url', vaultID: 'vault123', authToken: 'token123' };
await expect(instance['multipleUploadFiles'](fileElement, config, undefined))
.rejects.toEqual({ error: 'No files selected' });
.rejects.toEqual({ errorResponse: [{ error: noFileSelectedError }] });
});

test('multipleUploadFiles errorResponse contains error when SkyflowError has .errors[] (plural)', async () => {
Expand Down Expand Up @@ -510,6 +512,46 @@ describe('FrameElementInit extended unit tests', () => {
expect(() => instance['validateFiles'](files, fileElement.state, fileElement)).toThrow(SkyflowError);
});

test('validateFiles skips filename validation when preserveFileName is false', () => {
const instance = new FrameElementInit();
const files = [makeFile('my file.pdf')];
const fileElement = makeFileElement({ multiple: true, files, preserveFileName: false });
helpers.fileValidation = jest.fn(() => true);
helpers.vaildateFileName = jest.fn(() => false); // would reject if consulted
expect(() => instance['validateFiles'](files, fileElement.state, fileElement)).not.toThrow();
expect(helpers.vaildateFileName).not.toHaveBeenCalled();
});

test('validateFiles still throws INVALID_FILE_NAME when preserveFileName is true', () => {
const instance = new FrameElementInit();
const files = [makeFile('my file.pdf')];
const fileElement = makeFileElement({ multiple: true, files, preserveFileName: true });
helpers.fileValidation = jest.fn(() => true);
helpers.vaildateFileName = jest.fn(() => false);
expect(() => instance['validateFiles'](files, fileElement.state, fileElement)).toThrow(SkyflowError);
expect(helpers.vaildateFileName).toHaveBeenCalledTimes(1);
});

test('multipleUploadFiles uploads a file with an invalid original name when preserveFileName is false', async () => {
const instance = new FrameElementInit();
const files = [makeFile('my file.pdf')];
const fileElement = makeFileElement({ multiple: true, files, preserveFileName: false });
instance.iframeFormList = [fileElement];
helpers.fileValidation = jest.fn(() => true);
helpers.vaildateFileName = jest.fn(() => false); // original name is invalid
const originalGenerateUploadFileName = helpers.generateUploadFileName;
helpers.generateUploadFileName = jest.fn(() => 'generated-uuid.pdf');
mockClientRequest.mockResolvedValue({ skyflow_id: 'abc' });
const config = { vaultURL: 'https://vault.url', vaultID: 'vault123', authToken: 'token123' };
await expect(instance['multipleUploadFiles'](fileElement, config, undefined))
.resolves.toEqual({ fileUploadResponse: [{ skyflow_id: 'abc' }] });
expect(mockClientRequest).toHaveBeenCalledTimes(1);
const uploaded = mockClientRequest.mock.calls[0][0].body.get('file');
expect(uploaded.name).toBe('generated-uuid.pdf');
expect(helpers.vaildateFileName).not.toHaveBeenCalled();
helpers.generateUploadFileName = originalGenerateUploadFileName;
});

test('parallelUploadFiles resolves with aggregated responses when all succeed', async () => {
const instance = new FrameElementInit();
const fileElementA = makeFileElement({ multiple: false, files: [makeFile('a.txt')] });
Expand Down Expand Up @@ -588,6 +630,21 @@ describe('FrameElementInit extended unit tests', () => {
genSpy.mockRestore();
});

test('uploadFiles (single FILE_INPUT) skips filename validation and uploads when preserveFileName is false', async () => {
const instance = new FrameElementInit();
const file = makeFile('my file.pdf');
const element = makeSingleFileElement({ file, preserveFileName: false });
helpers.fileValidation = jest.fn(() => true);
helpers.vaildateFileName = jest.fn(() => false); // original name is invalid
const genSpy = jest.spyOn(helpers, 'generateUploadFileName').mockImplementation(() => 'gen_name.pdf');
mockClientRequest.mockResolvedValue({ upload: 'ok' });
const config = { vaultURL: 'https://vault.url', vaultID: 'vault123', authToken: 'tokenXYZ' };
await expect(instance.uploadFiles(element, config)).resolves.toEqual({ upload: 'ok' });
expect(helpers.vaildateFileName).not.toHaveBeenCalled();
expect(mockClientRequest.mock.calls[0][0].body.get('file').name).toBe('gen_name.pdf');
genSpy.mockRestore();
});

test('uploadFiles rejects on invalid file name when preserveFileName true', async () => {
const instance = new FrameElementInit();
const file = makeFile('bad.txt', 5);
Expand Down
2 changes: 1 addition & 1 deletion tests/core/internal/frame-element-init.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ describe('FrameElementInit Additional Test Cases', () => {
expect(postMessageSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:element:MULTI_FILE_INPUT:123`,
data: {"error": "No files selected"}, // Response data (success or error)
data: { errorResponse: [{ error: { code: 400, description: 'No File Selected' } }] }, // Response data (success or error)
}),
'http://localhost.com'
);
Expand Down
38 changes: 38 additions & 0 deletions tests/core/internal/iframe-form/iframe-form.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,44 @@ describe('MULTI_FILE_INPUT validator - specific UI error messages', () => {
return arr;
};

test('invalid file name shows the specific INVALID_FILE_NAME message in a COLLECT container for both file element types', () => {
const expected = logs.errorLogs.INVALID_FILE_NAME;
const badName = new File(['a'], 'my file.pdf', { type: 'application/pdf' });
const cases = [
[file_element, badName],
[multi_file_element, makeFileList(badName)],
];
cases.forEach(([name, value]) => {
const element = new IFrameFormElement(name, 'Passport', { containerType: ContainerType.COLLECT }, context);
element.preserveFileName = true;
element.state.value = value;
expect(element.validator(value)).toBe(false);
expect(element.errorText).toBe(expected);
});
});
test('MULTI_FILE_INPUT with preserveFileName=true fails when an earlier file has an invalid name even if the last one is valid', () => {
const element = new IFrameFormElement(multi_file_element, '', { containerType: ContainerType.COLLECT }, context);
element.preserveFileName = true;
const badName = new File(['a'], 'my file.pdf', { type: 'application/pdf' }); // space is not allowed
const goodName = new File(['b'], 'ok.pdf', { type: 'application/pdf' });
const fileList = makeFileList(badName, goodName); // invalid first, valid last
element.state.value = fileList;
const result = element.validator(fileList);
expect(result).toBe(false);
expect(element.errorText).toBe(parameterizedString(logs.errorLogs.INVALID_FILE_NAME));
});

test('MULTI_FILE_INPUT with preserveFileName=false ignores invalid file names', () => {
const element = new IFrameFormElement(multi_file_element, '', { containerType: ContainerType.COLLECT }, context);
element.preserveFileName = false;
const badName = new File(['a'], 'my file.pdf', { type: 'application/pdf' });
const goodName = new File(['b'], 'ok.pdf', { type: 'application/pdf' });
const fileList = makeFileList(badName, goodName);
element.state.value = fileList;
const result = element.validator(fileList);
expect(result).toBe(true);
});

test('invalid file type in MULTI_FILE_INPUT shows generic error not size-specific message', () => {
const element = new IFrameFormElement(multi_file_element, '', { containerType: ContainerType.COLLECT }, context);
element.maxFileSize = 10_000_000; // 10 MB — file below this so size is not the issue
Expand Down
10 changes: 10 additions & 0 deletions tests/libs/element-options.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ describe('test formatOptions function with format and translation', () => {
expect(formattedOptions).toEqual({required:true,preserveFileName:true});
});

test('should return preserveFileName true for MULTI_FILE_INPUT when not provided in options',()=>{
const formattedOptions = formatOptions(ElementType.MULTI_FILE_INPUT,{required:true},LogLevel.ERROR);
expect(formattedOptions).toEqual({required:true,preserveFileName:true});
});

test('should keep preserveFileName false for MULTI_FILE_INPUT when provided as false',()=>{
const formattedOptions = formatOptions(ElementType.MULTI_FILE_INPUT,{required:true,preserveFileName:false},LogLevel.ERROR);
expect(formattedOptions).toEqual({required:true,preserveFileName:false});
});

test('should return preserveFileName false when not provied as false in options',()=>{
const formattedOptions = formatOptions(ElementType.FILE_INPUT,{required:true,preserveFileName:false},LogLevel.ERROR);
expect(formattedOptions).toEqual({required:true,preserveFileName:false});
Expand Down