Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

experimenting with set version #10

Closed
wants to merge 5 commits into from
Closed
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
270 changes: 269 additions & 1 deletion packages/manager/src/utilities/formikErrorUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
getFormikErrorsFromAPIErrors,
handleAPIErrors,
handleVPCAndSubnetErrors,
set,
} from './formikErrorUtils';

const errorWithoutField = [{ reason: 'Internal server error' }];
Expand Down Expand Up @@ -128,7 +129,7 @@ describe('handleVpcAndConvertSubnetErrors', () => {
});

describe('getFormikErrorsFromAPIErrors', () => {
it('should convert APIError[] to errors in the shape formik expects', () => {
it.only('should convert APIError[] to errors in the shape formik expects', () => {
const testCases = [
{
apiErrors: [{ field: 'ip', reason: 'Incorrect IP' }],
Expand Down Expand Up @@ -201,3 +202,270 @@ describe('getFormikErrorsFromAPIErrors', () => {
}
});
});

describe('Tests for set', () => {
// fails (sets object) (probably a quick fix) (jk would need some more thinking)
it("returns the passed in 'object' as is if it's not actually a (non array) object", () => {
expect(set([], 'path not needed', 3)).toEqual([]);
});

// pass
describe('Correctly setting the value at the given path', () => {
it('sets the value for a simple path for both string and array paths', () => {
let object = {};
object = set(object, 'test', 1);
expect(object).toEqual({ test: 1 });

object = set(object, 'test2', 1);
expect(object).toEqual({ test: 1, test2: 1 });
});

// pass
it('sets the value for complex string and array paths (without indexes)', () => {
let object = {};

// the given paths are equivalent in string vs array format
object = set(object, 'a.b.c', 'c');
expect(object).toEqual({ a: { b: { c: 'c' } } });

object = set(object, 'a.b.d', 'd');
expect(object).toEqual({
a: { b: { c: 'c', d: 'd' } },
});

object = set(object, 'e[f][g]', 'g');
expect(object).toEqual({
a: { b: { c: 'c', d: 'd' } },
e: { f: { g: 'g' } },
});
});

it.only('sets the value for complex string paths pt2', () => {
const obj = set({}, 'a[1].b[0].c', 'test');
expect(obj).toEqual({
a: [undefined, { b: [{ c: 'test' }] }],
});

const obj2 = set({}, 'a[1].b.c', 'test');
expect(obj2).toEqual({
a: [undefined, { b: { c: 'test' } }],
});
});

// fails
it.only('sets the value for complex string and array paths (with indexes)', () => {
let object = {};

// the given paths are equivalent in string vs array format
object = set(object, 'a.b[1]', 'b1');
/*
{
a: {
b: { 1: "b1" }
}
}
*/
expect(object).toEqual({ a: { b: [undefined, 'b1'] } });
object = set(object, 'a.b[0]', 5);
expect(object).toEqual({ a: { b: [5] } });

// If path is an array, indexes can be passed in as a string or as a number
object = set(object, 'a.b.2', 'b2');
expect(object).toEqual({
a: { b: [5, 'b1', 'b2'] },
});

object = set(object, 'a.b[3].c', 'c');
expect(object).toEqual({
a: { b: [undefined, undefined, undefined, { c: 'c' }] },
});
});

// fails
it('creates an empty string key', () => {
expect(set({}, '', 'empty string')).toEqual({ '': 'empty string' });
});

// fails
it('only considers valid indexes for setting array values', () => {
let object = {};

object = set(object, 'test[01].test1', 'test');
expect(object).toEqual({
test: { '01': { test1: 'test' } },
});
object = set(object, 'test[01].[02]', 'test2');
expect(object).toEqual({
test: { '01': { '02': 'test2', test1: 'test' } },
});
object = set(object, 'test[-01]', 'test3');
expect(object).toEqual({
test: { '-01': 'test3', '01': { '02': 'test2', test1: 'test' } },
});
object = set(object, 'test[ 02]', 'test4');
expect(object).toEqual({
test: {
' 02': 'test4',
'-01': 'test3',
'01': { '02': 'test2', test1: 'test' },
},
});
object = set(object, 'test[00]', 'test5');
expect(object).toEqual({
test: {
' 02': 'test4',
'-01': 'test3',
'00': 'test5',
'01': { '02': 'test2', test1: 'test' },
},
});
});

// pass
it('considers numbers as keys if they are not followed by another number or if there is an already existing object', () => {
let object = {};
object = set(object, '1', 'test');
expect(object).toEqual({ 1: 'test' });

object = set({ test: { test1: 'test' } }, 'test[1]', 'test2');
expect(object).toEqual({
test: { '1': 'test2', test1: 'test' },
});
});

// fails
it('treats numbers as array indexes if they precede some previous key (if they are valid indexes)', () => {
const obj1 = set({}, '1[1]', 'test');
expect(obj1).toEqual({ 1: [undefined, 'test'] }); // [undefined, { 1: 'test' }]

const obj2 = set({}, '1.2', 'test');
expect(obj2).toEqual({ 1: [undefined, undefined, 'test'] });
});

// fails
it('can replace the value at an already existing key', () => {
let alreadyExisting = { test: 'test' };
alreadyExisting = set(alreadyExisting, 'test', 'changed');

expect(alreadyExisting).toEqual({
test: 'changed',
});

// fails
alreadyExisting = set(
alreadyExisting,
'test[test2][test3]',
'changed x4'
);
expect(alreadyExisting).toEqual({
test: { test2: { test3: 'changed x4' } },
});
});

// pass
it('sets the value for nonstandard paths', () => {
expect(set({}, 'test.[.test]', 'testing 2')).toEqual({
test: { test: 'testing 2' },
});
expect(set({}, 'test.[te[st]', 'testing 3')).toEqual({
test: { te: { st: 'testing 3' } },
});
expect(set({}, 'test.]test', 'testing 4')).toEqual({
test: { test: 'testing 4' },
});
});

// it will take an incredible amount of effort to get set to this level of finesse
it.skip('sets the value for non standard paths - not working currently', () => {
expect(set({}, 'test.[]', 'testing 5')).toEqual({
test: { '': { '': 'testing 5' } },
});
expect(set({}, '[].test', 'testing 6')).toEqual({
'': { test: 'testing 6' },
});
expect(set({}, '.', 'testing 7')).toEqual({
'': { '': 'testing 7' },
});
expect(set({}, '[', 'testing 8')).toEqual({
'[': 'testing 8',
});
expect(set({}, ']', 'testing 9')).toEqual({
']': 'testing 9',
});
expect(set({}, 'test..test', 'testing')).toEqual({
test: { '': { test: 'testing' } },
});
});
});

describe('Ensuring safety against prototype pollution and that the passed in and returned object are the same', () => {
// pass
it.only('protects against the given string path matching a prototype pollution key', () => {
let object = {};
// __proto__
object = set(object, '__proto__', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({});

// constructor
object = set(object, 'constructor', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({});

// prototype
object = set(object, 'prototype', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({});
});

// pass with changes (this is more inline with lodash's set anyway)
it.only('protects against the given string path containing prototype pollution keys that are separated by path delimiters', () => {
let object = {};
// prototype pollution key separated by .
object = set(object, 'test.__proto__.test', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({ test: {} });

object = set(object, 'test.constructor.test', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({ test: {} });

object = set(object, 'test.prototype.test', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({ test: {} });

// prototype pollution key separated by []
object = set(object, 'test.test[__proto__]', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({ test: { test: {} } });

object = set(object, 'test.test[constructor]', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({ test: { test: {} } });

object = set(object, 'test.test[prototype]', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({ test: { test: {} } });
});

it('is not considered prototype pollution if the string paths have a key not separated by delimiters', () => {
let object = {};
// prototype pollution key separated by .
object = set(object, 'test__proto__test', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({ test__proto__test: 1 });

object = set(object, 'constructortest', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({ constructortest: 1, test__proto__test: 1 });

object = set(object, 'testprototype', 1);
// expect(object).toBe(settedObject);
expect(object).toEqual({
constructortest: 1,
test__proto__test: 1,
testprototype: 1,
});
});
});
});
37 changes: 35 additions & 2 deletions packages/manager/src/utilities/formikErrorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { reverse } from 'ramda';

import { getAPIErrorOrDefault } from './errorUtils';
import { isNilOrEmpty } from './isNilOrEmpty';
import { set } from './set';
// import { set } from './set';

import type { APIError } from '@linode/api-v4/lib/types';
import type { FormikErrors } from 'formik';
Expand All @@ -17,12 +17,45 @@ export const getFormikErrorsFromAPIErrors = <T>(
? error.field.replace(prefixToRemoveFromFields, '')
: error.field;

set(acc, field, error.reason);
return set(acc, field, error.reason);
}
return acc;
}, {});
};

export const set = (obj: any, path: string, value: any): any => {
// Split the path into parts, handling both dot notation and array indices
const [head, ...rest] = path.split(/\.|\[|\]/).filter(Boolean);

// protect against prototype pollution
if (head === 'prototype' || head === '__proto__' || head === 'constructor') {
return { ...obj };
}

// Since this is recursive, if there are no more parts in the path, set the value and return
if (rest.length === 0) {
return { ...obj, [head]: value };
}

// Handle array indices
if (head.match(/^\d+$/)) {
const index = parseInt(head, 10);
// Copy the existing one or create a new empty one
const newArray = Array.isArray(obj) ? [...obj] : [];
// Recursively set the value at the specified index
newArray[index] = set(newArray[index] || {}, rest.join('.'), value);

return newArray;
}

// Handle nested objects
return {
...obj,
// Recursively set the value for the nested path
[head]: set(obj[head] || {}, rest.join('.'), value),
};
};

export const handleFieldErrors = (
callback: (error: unknown) => void,
fieldErrors: APIError[] = []
Expand Down
12 changes: 12 additions & 0 deletions packages/manager/src/utilities/set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ describe('Tests for set', () => {
expect(object2).toEqual(object);
});

it('sets the value for complex string paths pt2', () => {
const obj = set({}, 'a[1].b[0].c', 'test');
expect(obj).toEqual({
a: [undefined, { b: [{ c: 'test' }] }],
});

const obj2 = set({}, 'a[1].b.c', 'test');
expect(obj2).toEqual({
a: [undefined, { b: { c: 'test' } }],
});
});

it('creates an empty string key', () => {
expect(set({}, '', 'empty string')).toEqual({ '': 'empty string' });
expect(set({}, [''], 'empty string for array')).toEqual({
Expand Down
Loading