From a923255a528cb43cae9b4e9565298c1e390daed0 Mon Sep 17 00:00:00 2001 From: manu Date: Tue, 18 Jun 2024 09:58:03 +0200 Subject: [PATCH] Add tests and enhancements for helper functions This update includes comprehensive tests for `PaginationHelper`, `SorterHelper`, and `FilterHelper`. Additionally, error handling has been added to `SorterHelper` to deal with invalid JSON, and `PaginationHelper` has been tweaked to return an empty array if passed null or undefined data. --- server/src/helpers/PaginationHelper.ts | 1 + server/src/helpers/SorterHelper.ts | 37 +++++--- server/src/tests/helpers/FilterHelper.test.ts | 87 +++++++++++++++++++ server/src/tests/helpers/Pagination.test.ts | 62 +++++++++++++ server/src/tests/helpers/SorterHelper.test.ts | 74 ++++++++++++++++ 5 files changed, 248 insertions(+), 13 deletions(-) create mode 100644 server/src/tests/helpers/FilterHelper.test.ts create mode 100644 server/src/tests/helpers/Pagination.test.ts create mode 100644 server/src/tests/helpers/SorterHelper.test.ts diff --git a/server/src/helpers/PaginationHelper.ts b/server/src/helpers/PaginationHelper.ts index 7fba4469..c95056ff 100644 --- a/server/src/helpers/PaginationHelper.ts +++ b/server/src/helpers/PaginationHelper.ts @@ -1,5 +1,6 @@ // Pagination logic export function paginate(data: T[], current = 1, pageSize = 10): T[] { + if (!data) return []; return [...data].slice( ((current as number) - 1) * (pageSize as number), (current as number) * (pageSize as number), diff --git a/server/src/helpers/SorterHelper.ts b/server/src/helpers/SorterHelper.ts index f5b29a0b..f6959d18 100644 --- a/server/src/helpers/SorterHelper.ts +++ b/server/src/helpers/SorterHelper.ts @@ -1,23 +1,34 @@ +import logger from '../logger'; + export function sortByFields(data: T[], params: any) { - if (params.sorter) { - const sorter = JSON.parse(params.sorter); + let sorter: Record | null = null; + try { + sorter = params.sorter ? JSON.parse(params.sorter) : null; + } catch (error) { + logger.error('Could not parse sorter parameter'); + return data; + } + + if (sorter) { return data.sort((prev, next) => { let sortNumber = 0; (Object.keys(sorter) as Array).forEach((key) => { - const nextSort = next[key] as string; - const preSort = prev[key] as string; - if (sorter[key] === 'descend') { + if (typeof prev[key] === 'string' && typeof next[key] === 'string') { + const nextSort = next[key] as string; + const preSort = prev[key] as string; + if (sorter[key] === 'descend') { + if (preSort.localeCompare(nextSort) > 0) { + sortNumber += -1; + } else { + sortNumber += 1; + } + return; + } if (preSort.localeCompare(nextSort) > 0) { - sortNumber += -1; - } else { sortNumber += 1; + } else { + sortNumber += -1; } - return; - } - if (preSort.localeCompare(nextSort) > 0) { - sortNumber += 1; - } else { - sortNumber += -1; } }); return sortNumber; diff --git a/server/src/tests/helpers/FilterHelper.test.ts b/server/src/tests/helpers/FilterHelper.test.ts new file mode 100644 index 00000000..15c7e556 --- /dev/null +++ b/server/src/tests/helpers/FilterHelper.test.ts @@ -0,0 +1,87 @@ +import { filterByFields, filterByQueryParams } from '../../helpers/FilterHelper'; +import { describe, test, expect } from 'vitest'; + +const data = [ + { name: 'John Doe', age: 30, registered: true }, + { name: 'Jane Doe', age: 25, registered: false }, + { name: 'Tom Jerry', age: 32, registered: true }, + { name: 'Mickey Mouse', age: 22, registered: false }, + { name: 'Donald Duck', age: 18, registered: true }, + { name: 'Bugs Bunny', age: 27, registered: false }, + { name: 'Daffy Duck', age: 28, registered: true }, + { name: 'Porky Pig', age: 25, registered: false }, + { name: 'Elmer Fudd', age: 35, registered: true }, + { name: 'Wile E. Coyote', age: 25, registered: false }, +]; + +describe('filterByFields', () => { + test('filters by the given fields', () => { + const params = { + filter: JSON.stringify({ + registered: ['true'], + }), + }; + const result = filterByFields(data, params); + expect(result.length).toBe(5); + result.forEach((r) => expect(r.registered).toBe(true)); + }); + + test('returns all data if no filter is provided', () => { + const result = filterByFields(data, {}); + expect(result.length).toBe(data.length); + }); + + test('throws an error if provided filter format is invalid', () => { + expect(() => filterByFields(data, { filter: '{invalid' })).toThrow(); + }); + + test('returns empty array if no data given', () => { + const result = filterByFields([], { filter: JSON.stringify({ name: ['Doe'] }) }); + expect(result).toEqual([]); + }); +}); + +const authorizedParams = ['name', 'age', 'registered']; + +const params2 = { + name: 'doe', // This should match John Doe and Jane Doe + age: '25', // This should match Jane Doe and Porky Pig +}; + +describe('filterByQueryParams', () => { + test('filters by the given query param', () => { + let params = { name: 'doe' }; + let result = filterByQueryParams(data, params, authorizedParams); + expect(result.length).toBe(2); + result.forEach((r) => expect(r.name.toLowerCase()).toContain(params.name)); + }); + + test('returns all data if no params are provided', () => { + const result = filterByQueryParams(data, {}, authorizedParams); + expect(result.length).toBe(data.length); + }); + + test('ignores unauthorized params', () => { + const result = filterByQueryParams(data, { franchise: 'wb' }, authorizedParams); + expect(result.length).toBe(data.length); + }); + + test('returns empty array if no data given', () => { + const result = filterByQueryParams([], params2, authorizedParams); + expect(result).toEqual([]); + }); + + test('filters by multiple given query params', () => { + let result = filterByQueryParams(data, params2, authorizedParams); + expect(result.length).toBe(1); + expect(result[0].name.toLowerCase()).toContain(params2.name); + expect(result[0].age).toBe(parseInt(params2.age)); + }); + + test('processes numeric params correctly', () => { + const params = { age: '18' }; // This should match Donald Duck + let result = filterByQueryParams(data, params, authorizedParams); + expect(result.length).toBe(1); + expect(result[0].name).toBe('Donald Duck'); + }); +}); diff --git a/server/src/tests/helpers/Pagination.test.ts b/server/src/tests/helpers/Pagination.test.ts new file mode 100644 index 00000000..58d65dee --- /dev/null +++ b/server/src/tests/helpers/Pagination.test.ts @@ -0,0 +1,62 @@ +import { describe, test, expect } from 'vitest'; +import { paginate } from '../../helpers/PaginationHelper'; + +describe('paginate', () => { + // Generate an array of 100 items for test + const data: number[] = Array.from({ length: 100 }, (_, i) => i + 1); // [1, 2, ... , 100] + + test('returns the correct items for the first page', () => { + const result = paginate(data, 1, 10); + expect(result[0]).toBe(1); + expect(result.length).toBe(10); + expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + test('returns the correct items for a middle page', () => { + const result = paginate(data, 5, 10); + expect(result[0]).toBe(41); + expect(result.length).toBe(10); + expect(result).toEqual([41, 42, 43, 44, 45, 46, 47, 48, 49, 50]); + }); + + test('returns the correct items for the last page', () => { + const result = paginate(data, 10, 10); + expect(result[0]).toBe(91); + expect(result.length).toBe(10); + expect(result).toEqual([91, 92, 93, 94, 95, 96, 97, 98, 99, 100]); + }); + + test('returns an empty array if there are no items', () => { + const result = paginate([], 1, 10); + expect(result.length).toBe(0); + expect(result).toEqual([]); + }); + + test('adjusts the number of returned items if there are less items than pageSize', () => { + const result = paginate(data, 11, 10); + expect(result.length).toBe(0); + expect(result).toEqual([]); + }); + + // Generate an array of 100 items for test + const data2: Array = Array.from({ length: 100 }, (_, i) => + i % 3 === 0 ? null : i % 5 === 0 ? undefined : i + 1, + ); + + test('includes null and undefined values in the output', () => { + const result = paginate(data2, 1, 10); + expect(result).toEqual([null, 2, 3, null, 5, undefined, null, 8, 9, null]); + }); + + test('returns an empty array if the given data is undefined', () => { + const nullData = null as unknown as Array; + let result = paginate(nullData, 1, 10); + expect(result).toEqual([]); + }); + + test('returns an empty array if the given data is null', () => { + const undefinedData = undefined as unknown as Array; + const result = paginate(undefinedData, 1, 10); + expect(result).toEqual([]); + }); +}); diff --git a/server/src/tests/helpers/SorterHelper.test.ts b/server/src/tests/helpers/SorterHelper.test.ts new file mode 100644 index 00000000..c49f29e6 --- /dev/null +++ b/server/src/tests/helpers/SorterHelper.test.ts @@ -0,0 +1,74 @@ +import { test, expect, describe } from 'vitest'; +import { sortByFields } from '../../helpers/SorterHelper'; + +describe('SorterHelper', () => { + test('Sorts array of basic objects by given fields in ascending order', () => { + const data = [ + { name: 'Josh', age: 30 }, + { name: 'Alice', age: 25 }, + { name: 'Tom', age: 35 }, + ]; + const params = { sorter: JSON.stringify({ name: 'ascend' }) }; + const result = sortByFields(data, params); + expect(result).toEqual([ + { name: 'Alice', age: 25 }, + { name: 'Josh', age: 30 }, + { name: 'Tom', age: 35 }, + ]); + }); + + test('Sorts array of basic objects by given fields in descending order', () => { + const data = [ + { name: 'Josh', age: 30 }, + { name: 'Alice', age: 25 }, + { name: 'Tom', age: 35 }, + ]; + const params = { sorter: JSON.stringify({ name: 'descend' }) }; + const result = sortByFields(data, params); + expect(result).toEqual([ + { name: 'Tom', age: 35 }, + { name: 'Josh', age: 30 }, + { name: 'Alice', age: 25 }, + ]); + }); + + test('Returns empty array if input data is empty regardless of sorter', () => { + const data: any[] = []; + const params = { sorter: JSON.stringify({ name: 'descend' }) }; + const result = sortByFields(data, params); + expect(result).toEqual([]); + }); + + test('Returns provided data unaltered if sorter is not provided in params', () => { + const data = [ + { name: 'Josh', age: 30 }, + { name: 'Alice', age: 25 }, + { name: 'Tom', age: 35 }, + ]; + const params = {}; + const result = sortByFields(data, params); + expect(result).toEqual(data); + }); + + test('Returns provided data unaltered if sorter is not valid JSON', () => { + const data = [ + { name: 'Josh', age: 30 }, + { name: 'Alice', age: 25 }, + { name: 'Tom', age: 35 }, + ]; + const params = { sorter: "{name:'descend'}" }; + const result = sortByFields(data, params); + expect(result).toEqual(data); // the SorterHelper now handles invalid JSON without throwing errors + }); + + test('Ignores fields not found on data items when sorting', () => { + const data = [ + { name: 'Josh', age: 30 }, + { name: 'Alice', age: 25 }, + { name: 'Tom', age: 35 }, + ]; + const params = { sorter: JSON.stringify({ foobar: 'ascend' }) }; // the field "foobar" does not exist on any data item + const result = sortByFields(data, params); + expect(result).toEqual(data); // the original data is returned, unaltered + }); +});