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

test(common): improve test coverage for shared utils #14832

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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: 3 additions & 4 deletions packages/common/pipes/file/parse-file.pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable, Optional } from '../../decorators/core';
import { HttpStatus } from '../../enums';
import { PipeTransform } from '../../interfaces/features/pipe-transform.interface';
import { HttpErrorByCode } from '../../utils/http-error-by-code.util';
import { isEmpty, isObject, isUndefined } from '../../utils/shared.utils';
import { isEmptyArray, isObject, isUndefined } from '../../utils/shared.utils';
import { FileValidator } from './file-validator.interface';
import { ParseFileOptions } from './parse-file-options.interface';

Expand Down Expand Up @@ -60,9 +60,8 @@ export class ParseFilePipe implements PipeTransform<any> {
}

private thereAreNoFilesIn(value: any): boolean {
const isEmptyArray = Array.isArray(value) && isEmpty(value);
const isEmptyObject = isObject(value) && isEmpty(Object.keys(value));
return isUndefined(value) || isEmptyArray || isEmptyObject;
const isEmptyObject = isObject(value) && isEmptyArray(Object.keys(value));
return isUndefined(value) || isEmptyArray(value) || isEmptyObject;
}

protected async validate(file: any): Promise<any> {
Expand Down
88 changes: 86 additions & 2 deletions packages/common/test/utils/shared.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
addLeadingSlash,
isConstructor,
isEmpty,
isEmptyArray,
isFunction,
isNil,
isNumber,
Expand All @@ -27,7 +28,13 @@ describe('Shared utils', () => {
it('should return false when object is not undefined', () => {
expect(isUndefined({})).to.be.false;
});
it('should return false for falsy values like false, 0, or empty string', () => {
expect(isUndefined(false)).to.be.false;
expect(isUndefined(0)).to.be.false;
expect(isUndefined('')).to.be.false;
});
});

describe('isFunction', () => {
it('should return true when obj is function', () => {
expect(isFunction(() => ({}))).to.be.true;
Expand All @@ -37,16 +44,19 @@ describe('Shared utils', () => {
expect(isFunction(undefined)).to.be.false;
});
});

describe('isObject', () => {
it('should return true when obj is object', () => {
expect(isObject({})).to.be.true;
});

it('should return false when object is not object', () => {
expect(isObject(3)).to.be.false;
expect(isObject(null)).to.be.false;
expect(isObject(undefined)).to.be.false;
});
});

describe('isPlainObject', () => {
it('should return true when obj is plain object', () => {
expect(isPlainObject({})).to.be.true;
Expand All @@ -66,7 +76,12 @@ describe('Shared utils', () => {
expect(isPlainObject(new Date())).to.be.false;
expect(isPlainObject(new Foo(1))).to.be.false;
});
it('should return false for objects with custom prototypes', () => {
function CustomObject() {}
expect(isPlainObject(new CustomObject())).to.be.false;
});
});

describe('isString', () => {
it('should return true when val is a string', () => {
expect(isString('true')).to.be.true;
Expand All @@ -78,6 +93,7 @@ describe('Shared utils', () => {
expect(isString(undefined)).to.be.false;
});
});

describe('isSymbol', () => {
it('should return true when val is a Symbol', () => {
expect(isSymbol(Symbol())).to.be.true;
Expand All @@ -88,7 +104,11 @@ describe('Shared utils', () => {
expect(isSymbol(null)).to.be.false;
expect(isSymbol(undefined)).to.be.false;
});
it('should return false for invalid Symbol objects', () => {
expect(isSymbol(Object(Symbol()))).to.be.false;
});
});

describe('isNumber', () => {
it('should return true when val is a number or NaN', () => {
expect(isNumber(1)).to.be.true;
Expand All @@ -98,22 +118,32 @@ describe('Shared utils', () => {
expect(isNumber(0b1)).to.be.true; // binary notation
expect(isNumber(0x1)).to.be.true; // hexadecimal notation
expect(isNumber(NaN)).to.be.true;
expect(isNumber(Infinity)).to.be.true;
expect(isNumber(-Infinity)).to.be.true;
});
it('should return false when val is not a number', () => {
// expect(isNumber(1n)).to.be.false; // big int (available on ES2020)
expect(isNumber('1')).to.be.false; // string
expect(isNumber(undefined)).to.be.false; // nullish
expect(isNumber(null)).to.be.false; // nullish
expect(isNumber(new Number(123))).to.be.false; // number
});
});

describe('isConstructor', () => {
it('should return true when string is equal to constructor', () => {
expect(isConstructor('constructor')).to.be.true;
});
it('should return false when string is not equal to constructor', () => {
expect(isConstructor('nope')).to.be.false;
});
it('should return false for non-string values', () => {
expect(isConstructor(null)).to.be.false;
expect(isConstructor(undefined)).to.be.false;
expect(isConstructor(123)).to.be.false;
});
});

describe('addLeadingSlash', () => {
it('should return the validated path ("add / if not exists")', () => {
expect(addLeadingSlash('nope')).to.be.eql('/nope');
Expand All @@ -128,7 +158,13 @@ describe('Shared utils', () => {
expect(addLeadingSlash(null!)).to.be.eql('');
expect(addLeadingSlash(undefined)).to.be.eql('');
});
it('should handle paths with special characters', () => {
expect(addLeadingSlash('path-with-special-chars!@#$%^&*()')).to.eql(
'/path-with-special-chars!@#$%^&*()',
);
});
});

describe('normalizePath', () => {
it('should remove all trailing slashes at the end of the path', () => {
expect(normalizePath('path/')).to.be.eql('/path');
Expand All @@ -146,6 +182,7 @@ describe('Shared utils', () => {
expect(normalizePath(undefined)).to.be.eql('/');
});
});

describe('isNil', () => {
it('should return true when obj is undefined or null', () => {
expect(isNil(undefined)).to.be.true;
Expand All @@ -154,17 +191,64 @@ describe('Shared utils', () => {
it('should return false when object is not undefined and null', () => {
expect(isNil('3')).to.be.false;
});
it('should return false for falsy values like false, 0, or empty string', () => {
expect(isNil(false)).to.be.false;
expect(isNil(0)).to.be.false;
expect(isNil('')).to.be.false;
});
});

describe('isEmpty', () => {
it('should return true when array is empty or not exists', () => {
expect(isEmpty([])).to.be.true;
expect(isEmpty(null)).to.be.true;
expect(isEmpty(undefined)).to.be.true;
});

it('should return false when array is not empty', () => {
expect(isEmpty([1, 2])).to.be.false;
});
it('should return false for non-array values', () => {
expect(isEmpty({})).to.be.false;
expect(isEmpty('')).to.be.false;
expect(isEmpty(0)).to.be.false;
expect(isEmpty(false)).to.be.false;
expect(isEmpty(Symbol())).to.be.false;
expect(isEmpty(() => {})).to.be.false;
});
});

describe('isEmptyArray', () => {
it('should return true when array is empty or not exists', () => {
expect(isEmptyArray([])).to.be.true;
expect(isEmptyArray(null)).to.be.true;
expect(isEmptyArray(undefined)).to.be.true;
});

it('should return false when array is not empty', () => {
expect(isEmptyArray([1, 2])).to.be.false;
expect(isEmptyArray(['a', 'b', 'c'])).to.be.false;
expect(isEmptyArray([{}])).to.be.false;
});

it('should return false for non-array values', () => {
expect(isEmptyArray({})).to.be.false;
expect(isEmptyArray('')).to.be.false;
expect(isEmptyArray(0)).to.be.false;
expect(isEmptyArray(false)).to.be.false;
expect(isEmptyArray(Symbol())).to.be.false;
expect(isEmptyArray(() => {})).to.be.false;
});

it('should return false for array-like objects', () => {
expect(isEmptyArray({ length: 0 })).to.be.false;
expect(isEmptyArray({ length: 1 })).to.be.false;
});

it('should return false for sparse arrays', () => {
const sparseArray = new Array(3);
expect(isEmptyArray(sparseArray)).to.be.false;
});
});

describe('stripEndSlash', () => {
it('should strip end slash if present', () => {
expect(stripEndSlash('/cats/')).to.equal('/cats');
Expand Down
48 changes: 36 additions & 12 deletions packages/common/utils/shared.utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export const isUndefined = (obj: any): obj is undefined =>
export const isUndefined = (obj: unknown): obj is undefined =>
typeof obj === 'undefined';

export const isObject = (fn: any): fn is object =>
export const isObject = (fn: unknown): fn is object =>
!isNil(fn) && typeof fn === 'object';

export const isPlainObject = (fn: any): fn is object => {
export const isPlainObject = (fn: unknown): fn is object => {
if (!isObject(fn)) {
return false;
}
Expand Down Expand Up @@ -37,15 +37,39 @@ export const normalizePath = (path?: string): string =>
: '/' + path.replace(/\/+$/, '')
: '/';

export const stripEndSlash = (path: string) =>
path[path.length - 1] === '/' ? path.slice(0, path.length - 1) : path;
export const stripEndSlash = (path: string): string =>
path.endsWith('/') ? path.slice(0, -1) : path;

export const isFunction = (val: any): val is Function =>
export const isFunction = (val: unknown): val is Function =>
typeof val === 'function';
export const isString = (val: any): val is string => typeof val === 'string';
export const isNumber = (val: any): val is number => typeof val === 'number';
export const isConstructor = (val: any): boolean => val === 'constructor';
export const isNil = (val: any): val is null | undefined =>

export const isString = (val: unknown): val is string =>
typeof val === 'string';

export const isNumber = (val: unknown): val is number =>
typeof val === 'number';

export const isConstructor = (val: unknown): boolean => val === 'constructor';

export const isNil = (val: unknown): val is null | undefined =>
isUndefined(val) || val === null;
export const isEmpty = (array: any): boolean => !(array && array.length > 0);
export const isSymbol = (val: any): val is symbol => typeof val === 'symbol';

export const isEmpty = (value: unknown): boolean => {
if (isNil(value)) {
return true;
}
if (Array.isArray(value)) {
return value.length === 0;
}
return false;
};

export const isEmptyArray = (array: unknown): boolean => {
if (isNil(array)) {
return true;
}
return Array.isArray(array) && array.length === 0;
};

export const isSymbol = (val: unknown): val is symbol =>
typeof val === 'symbol';
4 changes: 2 additions & 2 deletions packages/core/exceptions/base-exception-filter-context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FILTER_CATCH_EXCEPTIONS } from '@nestjs/common/constants';
import { Type } from '@nestjs/common/interfaces';
import { ExceptionFilter } from '@nestjs/common/interfaces/exceptions/exception-filter.interface';
import { isEmpty, isFunction } from '@nestjs/common/utils/shared.utils';
import { isEmptyArray, isFunction } from '@nestjs/common/utils/shared.utils';
import { iterate } from 'iterare';
import { ContextCreator } from '../helpers/context-creator';
import { STATIC_CONTEXT } from '../injector/constants';
Expand All @@ -20,7 +20,7 @@ export class BaseExceptionFilterContext extends ContextCreator {
contextId = STATIC_CONTEXT,
inquirerId?: string,
): R {
if (isEmpty(metadata)) {
if (isEmptyArray(metadata)) {
return [] as any[] as R;
}
return iterate(metadata)
Expand Down
4 changes: 2 additions & 2 deletions packages/core/exceptions/exceptions-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HttpException } from '@nestjs/common';
import { ExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions/exception-filter-metadata.interface';
import { ArgumentsHost } from '@nestjs/common/interfaces/features/arguments-host.interface';
import { selectExceptionFilterMetadata } from '@nestjs/common/utils/select-exception-filter-metadata.util';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { isEmptyArray } from '@nestjs/common/utils/shared.utils';
import { InvalidExceptionFilterException } from '../errors/exceptions/invalid-exception-filter.exception';
import { BaseExceptionFilter } from './base-exception-filter';

Expand All @@ -27,7 +27,7 @@ export class ExceptionsHandler extends BaseExceptionFilter {
exception: T,
ctx: ArgumentsHost,
): boolean {
if (isEmpty(this.filters)) {
if (isEmptyArray(this.filters)) {
return false;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/core/exceptions/external-exception-filter-context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EXCEPTION_FILTERS_METADATA } from '@nestjs/common/constants';
import { Controller } from '@nestjs/common/interfaces';
import { ExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { isEmptyArray } from '@nestjs/common/utils/shared.utils';
import { iterate } from 'iterare';
import { ApplicationConfig } from '../application-config';
import { STATIC_CONTEXT } from '../injector/constants';
Expand Down Expand Up @@ -36,7 +36,7 @@ export class ExternalExceptionFilterContext extends BaseExceptionFilterContext {
contextId,
inquirerId,
);
if (isEmpty(filters)) {
if (isEmptyArray(filters)) {
return exceptionHandler;
}
exceptionHandler.setCustomFilters(filters.reverse());
Expand Down
4 changes: 2 additions & 2 deletions packages/core/exceptions/external-exceptions-handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions';
import { ArgumentsHost } from '@nestjs/common/interfaces/features/arguments-host.interface';
import { selectExceptionFilterMetadata } from '@nestjs/common/utils/select-exception-filter-metadata.util';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { isEmptyArray } from '@nestjs/common/utils/shared.utils';
import { InvalidExceptionFilterException } from '../errors/exceptions/invalid-exception-filter.exception';
import { ExternalExceptionFilter } from './external-exception-filter';

Expand All @@ -27,7 +27,7 @@ export class ExternalExceptionsHandler extends ExternalExceptionFilter {
exception: T,
host: ArgumentsHost,
): Promise<any> | null {
if (isEmpty(this.filters)) {
if (isEmptyArray(this.filters)) {
return null;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/core/guards/guards-consumer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CanActivate } from '@nestjs/common';
import { ContextType, Controller } from '@nestjs/common/interfaces';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { isEmptyArray } from '@nestjs/common/utils/shared.utils';
import { lastValueFrom, Observable } from 'rxjs';
import { ExecutionContextHost } from '../helpers/execution-context-host';

Expand All @@ -12,7 +12,7 @@ export class GuardsConsumer {
callback: (...args: unknown[]) => unknown,
type?: TContext,
): Promise<boolean> {
if (!guards || isEmpty(guards)) {
if (!guards || isEmptyArray(guards)) {
return true;
}
const context = this.createContext(args, instance, callback);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/guards/guards-context-creator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CanActivate } from '@nestjs/common';
import { GUARDS_METADATA } from '@nestjs/common/constants';
import { Controller, Type } from '@nestjs/common/interfaces';
import { isEmpty, isFunction } from '@nestjs/common/utils/shared.utils';
import { isEmptyArray, isFunction } from '@nestjs/common/utils/shared.utils';
import { iterate } from 'iterare';
import { ApplicationConfig } from '../application-config';
import { ContextCreator } from '../helpers/context-creator';
Expand Down Expand Up @@ -41,7 +41,7 @@ export class GuardsContextCreator extends ContextCreator {
contextId = STATIC_CONTEXT,
inquirerId?: string,
): R {
if (isEmpty(metadata)) {
if (isEmptyArray(metadata)) {
return [] as unknown[] as R;
}
return iterate(metadata)
Expand Down
Loading