added jest config and tests for util classes

This commit is contained in:
biersoeckli
2024-12-11 08:49:44 +00:00
parent e6d202129d
commit 55a8b38459
16 changed files with 3041 additions and 39 deletions

210
jest.config.ts Normal file
View File

@@ -0,0 +1,210 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import type {Config} from 'jest';
import nextJest from 'next/jest.js'
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
})
const config: Config = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
testEnvironment: 'jsdom',
moduleNameMapper: {
// ...
'^@/(.*)$': '<rootDir>/src/$1',
}
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
export default createJestConfig(config)

View File

@@ -12,7 +12,8 @@
"prisma-generate-build": "npx prisma generate && node ./fix-wrong-zod-imports.js",
"prisma-migrate": "bunx prisma migrate dev --name migration",
"prisma-deploy": "bunx prisma migrate deploy",
"lint": "next lint"
"lint": "next lint",
"test": "jest"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
@@ -74,6 +75,11 @@
"zustand": "^5.0.1"
},
"devDependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@types/jest": "^29.5.14",
"@types/mocha": "^10.0.10",
"@types/node": "^22.7.9",
"@types/nodemailer": "^6.4.16",
"@types/react": "^18.3.12",
@@ -81,6 +87,8 @@
"@types/xml2js": "^0.4.14",
"eslint": "^9.13.0",
"eslint-config-next": "14.2.15",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",

View File

@@ -0,0 +1,51 @@
import { FormUtils } from "@/frontend/utils/form.utilts";
import { ServerActionResult } from "@/shared/model/server-action-error-return.model";
import { UseFormReturn } from "react-hook-form";
import { z } from "zod";
describe('FormUtils', () => {
describe('mapValidationErrorsToForm', () => {
it('should clear existing errors and set new errors from state', () => {
const mockForm: UseFormReturn<any> = {
clearErrors: jest.fn(),
setError: jest.fn(),
} as any;
const state = {
data: undefined,
message: undefined,
status: 'error',
errors: {
name: ['Name is required'],
email: ['Email is invalid']
}
} as ServerActionResult<any, any>;
FormUtils.mapValidationErrorsToForm(state, mockForm);
expect(mockForm.clearErrors).toHaveBeenCalled();
expect(mockForm.setError).toHaveBeenCalledWith('name', { type: 'manual', message: 'Name is required' });
expect(mockForm.setError).toHaveBeenCalledWith('email', { type: 'manual', message: 'Email is invalid' });
});
it('should not set errors if state has no errors', () => {
const mockForm: UseFormReturn<any> = {
clearErrors: jest.fn(),
setError: jest.fn(),
} as any;
const state = {
data: undefined,
message: undefined,
status: 'error',
errors: {}
} as ServerActionResult<any, any>;
FormUtils.mapValidationErrorsToForm(state, mockForm);
expect(mockForm.clearErrors).toHaveBeenCalled();
expect(mockForm.setError).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,78 @@
import { formatDate, formatDateTime, formatTime } from '@/frontend/utils/format.utils';
import { formatInTimeZone } from 'date-fns-tz';
jest.mock('date-fns-tz', () => ({
formatInTimeZone: jest.fn(),
}));
describe('format.utils', () => {
const mockDate = new Date('2023-10-10T10:10:10Z');
beforeEach(() => {
jest.clearAllMocks();
});
describe('formatDate', () => {
it('should return formatted date string for valid date', () => {
(formatInTimeZone as jest.Mock).mockReturnValue('10.10.2023');
const result = formatDate(mockDate);
expect(result).toBe('10.10.2023');
expect(formatInTimeZone).toHaveBeenCalledWith(mockDate, 'Europe/Zurich', 'dd.MM.yyyy');
});
it('should return empty string for undefined date', () => {
const result = formatDate(undefined);
expect(result).toBe('');
expect(formatInTimeZone).not.toHaveBeenCalled();
});
it('should return empty string for null date', () => {
const result = formatDate(null);
expect(result).toBe('');
expect(formatInTimeZone).not.toHaveBeenCalled();
});
});
describe('formatDateTime', () => {
it('should return formatted date-time string for valid date', () => {
(formatInTimeZone as jest.Mock).mockReturnValue('10.10.2023 12:10');
const result = formatDateTime(mockDate);
expect(result).toBe('10.10.2023 12:10');
expect(formatInTimeZone).toHaveBeenCalledWith(mockDate, 'Europe/Zurich', 'dd.MM.yyyy HH:mm');
});
it('should return empty string for undefined date', () => {
const result = formatDateTime(undefined);
expect(result).toBe('');
expect(formatInTimeZone).not.toHaveBeenCalled();
});
it('should return empty string for null date', () => {
const result = formatDateTime(null);
expect(result).toBe('');
expect(formatInTimeZone).not.toHaveBeenCalled();
});
});
describe('formatTime', () => {
it('should return formatted time string for valid date', () => {
(formatInTimeZone as jest.Mock).mockReturnValue('12:10');
const result = formatTime(mockDate);
expect(result).toBe('12:10');
expect(formatInTimeZone).toHaveBeenCalledWith(mockDate, 'Europe/Zurich', 'HH:mm');
});
it('should return empty string for undefined date', () => {
const result = formatTime(undefined);
expect(result).toBe('');
expect(formatInTimeZone).not.toHaveBeenCalled();
});
it('should return empty string for null date', () => {
const result = formatTime(null);
expect(result).toBe('');
expect(formatInTimeZone).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,73 @@
import { Toast } from '../../../frontend/utils/toast.utils';
import { toast } from 'sonner';
import { ServerActionResult } from "@/shared/model/server-action-error-return.model";
jest.mock('sonner', () => ({
toast: {
promise: jest.fn()
}
}));
describe('Toast', () => {
describe('fromAction', () => {
it('should resolve with success message when action is successful', async () => {
const action = jest.fn().mockResolvedValue({ status: 'success', message: 'Success' } as ServerActionResult<any, any>);
const defaultSuccessMessage = 'Operation successful';
(toast.promise as jest.Mock).mockImplementation(async (actionFn, { success }) => {
const result = await actionFn();
return success(result);
});
const result = await Toast.fromAction(action, defaultSuccessMessage);
expect(result).toEqual({ status: 'success', message: 'Success' });
expect(toast.promise).toHaveBeenCalled();
});
it('should reject with error message when action fails', async () => {
const action = jest.fn().mockResolvedValue({ status: 'error', message: 'Failure' } as ServerActionResult<any, any>);
(toast.promise as jest.Mock).mockImplementation(async (actionFn, { error }) => {
try {
await actionFn();
} catch (err) {
return error(err);
}
});
await expect(Toast.fromAction(action)).rejects.toThrow('Failure');
expect(toast.promise).toHaveBeenCalled();
});
it('should reject with unknown error message when action throws an error', async () => {
const action = jest.fn().mockRejectedValue(new Error('Some error'));
(toast.promise as jest.Mock).mockImplementation(async (actionFn, { error }) => {
try {
await actionFn();
} catch (err) {
return error(err);
}
});
await expect(Toast.fromAction(action)).rejects.toThrow('Some error');
expect(toast.promise).toHaveBeenCalled();
});
it('should use default success message when action is successful and no message is provided', async () => {
const action = jest.fn().mockResolvedValue({ status: 'success' } as ServerActionResult<any, any>);
const defaultSuccessMessage = 'Operation successful';
(toast.promise as jest.Mock).mockImplementation(async (actionFn, { success }) => {
const result = await actionFn();
return success(result);
});
const result = await Toast.fromAction(action, defaultSuccessMessage);
expect(result).toEqual({ status: 'success' });
expect(toast.promise).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,106 @@
import { promises } from 'dns';
import { FsUtils } from '../../../server/utils/fs.utils';
import fs from 'fs';
jest.mock('fs', () => ({
existsSync: jest.fn(),
mkdirSync: jest.fn(),
mkdir: jest.fn(),
promises: {
access: jest.fn(),
readdir: jest.fn(),
mkdir: jest.fn(),
rm: jest.fn()
}
}));
describe('FsUtils', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('fileExists', () => {
it('should return true if file exists', async () => {
(fs.promises.access as jest.Mock).mockResolvedValue(undefined);
(fs.constants as any) = { F_OK: 0 };
const result = await FsUtils.fileExists('path/to/file');
expect(result).toBe(true);
});
it('should return false if file does not exist', async () => {
(fs.promises.access as jest.Mock).mockRejectedValue(new Error('File not found'));
const result = await FsUtils.fileExists('path/to/file');
expect(result).toBe(false);
});
});
describe('directoryExists', () => {
it('should return true if directory exists', () => {
(fs.existsSync as jest.Mock).mockReturnValue(true);
const result = FsUtils.directoryExists('path/to/dir');
expect(result).toBe(true);
});
it('should return false if directory does not exist', () => {
(fs.existsSync as jest.Mock).mockReturnValue(false);
const result = FsUtils.directoryExists('path/to/dir');
expect(result).toBe(false);
});
});
describe('isFolderEmpty', () => {
it('should return true if folder is empty', async () => {
(fs.promises.readdir as jest.Mock).mockResolvedValue([]);
const result = await FsUtils.isFolderEmpty('path/to/dir');
expect(result).toBe(true);
});
it('should return false if folder is not empty', async () => {
(fs.promises.readdir as jest.Mock).mockResolvedValue(['file1', 'file2']);
const result = await FsUtils.isFolderEmpty('path/to/dir');
expect(result).toBe(false);
});
});
describe('createDirIfNotExists', () => {
it('should create directory if it does not exist', () => {
(fs.existsSync as jest.Mock).mockReturnValue(false);
FsUtils.createDirIfNotExists('path/to/dir');
expect(fs.mkdirSync).toHaveBeenCalledWith('path/to/dir', { recursive: false });
});
it('should not create directory if it exists', () => {
(fs.existsSync as jest.Mock).mockReturnValue(true);
FsUtils.createDirIfNotExists('path/to/dir');
expect(fs.mkdirSync).not.toHaveBeenCalled();
});
});
describe('createDirIfNotExistsAsync', () => {
it('should create directory if it does not exist', async () => {
(fs.existsSync as jest.Mock).mockReturnValue(false);
await FsUtils.createDirIfNotExistsAsync('path/to/dir');
expect(fs.promises.mkdir).toHaveBeenCalledWith('path/to/dir', { recursive: false });
});
it('should not create directory if it exists', async () => {
(fs.existsSync as jest.Mock).mockReturnValue(true);
await FsUtils.createDirIfNotExistsAsync('path/to/dir');
expect(fs.promises.mkdir).not.toHaveBeenCalled();
});
});
describe('deleteDirIfExistsAsync', () => {
it('should delete directory if it exists', async () => {
(fs.existsSync as jest.Mock).mockReturnValue(true);
await FsUtils.deleteDirIfExistsAsync('path/to/dir');
expect(fs.promises.rm).toHaveBeenCalledWith('path/to/dir', { recursive: false });
});
it('should not delete directory if it does not exist', async () => {
(fs.existsSync as jest.Mock).mockReturnValue(false);
await FsUtils.deleteDirIfExistsAsync('path/to/dir');
expect(fs.promises.rm).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,93 @@
import { KubeObjectNameUtils } from '../../../server/utils/kube-object-name.utils';
describe('KubeObjectNameUtils', () => {
describe('toSnakeCase', () => {
it('should convert camelCase to snake_case', () => {
expect(KubeObjectNameUtils.toSnakeCase('camelCaseString')).toBe('camel_case_string');
});
it('should replace spaces with underscores', () => {
expect(KubeObjectNameUtils.toSnakeCase('string with spaces')).toBe('string_with_spaces');
});
it('should remove non-alphanumeric characters', () => {
expect(KubeObjectNameUtils.toSnakeCase('string with-special#chars!')).toBe('string_withspecialchars');
});
it('should return the same string if it is already in snake_case', () => {
expect(KubeObjectNameUtils.toSnakeCase('already_snake_case')).toBe('already_snake_case');
});
it('should return empty string if input is empty', () => {
expect(KubeObjectNameUtils.toSnakeCase('')).toBe('');
});
});
describe('toObjectId', () => {
it('should convert string to object ID format', () => {
const result = KubeObjectNameUtils.toObjectId('TestString');
expect(result).toMatch(/^test-string-[a-f0-9]{8}$/);
});
});
describe('toProjectId', () => {
it('should convert string to project ID format', () => {
const result = KubeObjectNameUtils.toProjectId('TestProject');
expect(result).toMatch(/^proj-test-project-[a-f0-9]{8}$/);
});
it('should trim the string to the max length', () => {
const longString = 'a'.repeat(100);
const result = KubeObjectNameUtils.toProjectId(longString);
expect(result).toMatch(/^proj-a{30}-[a-f0-9]{8}$/);
});
});
describe('toAppId', () => {
it('should convert string to app ID format', () => {
const result = KubeObjectNameUtils.toAppId('TestApp');
expect(result).toMatch(/^app-test-app-[a-f0-9]{8}$/);
});
it('should trim the string to the max length', () => {
const longString = 'b'.repeat(100);
const result = KubeObjectNameUtils.toAppId(longString);
expect(result).toMatch(/^app-b{30}-[a-f0-9]{8}$/);
});
});
describe('toJobName', () => {
it('should convert app ID to job name format', () => {
const result = KubeObjectNameUtils.toJobName('test_app');
expect(result).toBe('build-test_app');
});
});
describe('addRandomSuffix', () => {
it('should add a random suffix to the string', () => {
const result = KubeObjectNameUtils.addRandomSuffix('baseString');
expect(result).toMatch(/^baseString-[a-f0-9]{8}$/);
});
});
describe('toServiceName', () => {
it('should convert app ID to service name format', () => {
const result = KubeObjectNameUtils.toServiceName('test_app');
expect(result).toBe('svc-test_app');
});
});
describe('toPvcName', () => {
it('should convert volume ID to PVC name format', () => {
const result = KubeObjectNameUtils.toPvcName('volume123');
expect(result).toBe('pvc-volume123');
});
});
describe('getIngressName', () => {
it('should convert domain ID to ingress name format', () => {
const result = KubeObjectNameUtils.getIngressName('domain123');
expect(result).toBe('ingress-domain123');
});
});
});

View File

@@ -0,0 +1,20 @@
import { MemoryCalcUtils } from '../../../server/utils/memory-caluclation.utils';
describe('MemoryCalcUtils', () => {
describe('formatSize', () => {
it('should format size in Gi when megabytes is a multiple of 1024', () => {
expect(MemoryCalcUtils.formatSize(2048)).toBe('2Gi');
expect(MemoryCalcUtils.formatSize(1024)).toBe('1Gi');
});
it('should format size in Mi when megabytes is not a multiple of 1024', () => {
expect(MemoryCalcUtils.formatSize(1500)).toBe('1500Mi');
expect(MemoryCalcUtils.formatSize(512)).toBe('512Mi');
});
it('should handle edge cases', () => {
expect(MemoryCalcUtils.formatSize(0)).toBe('0Gi');
expect(MemoryCalcUtils.formatSize(1)).toBe('1Mi');
});
});
});

View File

@@ -0,0 +1,92 @@
import { PathUtils } from '../../../server/utils/path.utils';
describe('PathUtils', () => {
const originalEnv = (process.env as any);
beforeEach(() => {
jest.resetModules();
(process.env as any) = { ...originalEnv };
});
afterEach(() => {
(process.env as any) = originalEnv;
PathUtils.isProduction = false;
});
describe('internalDataRoot', () => {
it('should return production path when NODE_ENV is production', () => {
PathUtils.isProduction = true;
expect(PathUtils.internalDataRoot).toBe('/app/storage');
});
it('should return development path when NODE_ENV is not production', () => {
expect(PathUtils.internalDataRoot).toBe('/workspace/storage/internal');
});
});
describe('tempDataRoot', () => {
it('should return production path when NODE_ENV is production', () => {
PathUtils.isProduction = true;
expect(PathUtils.tempDataRoot).toBe('/app/tmp-storage');
});
it('should return development path when NODE_ENV is not production', () => {
expect(PathUtils.tempDataRoot).toBe('/workspace/storage/tmp');
});
});
describe('gitRootPath', () => {
it('should return the correct git root path', () => {
expect(PathUtils.gitRootPath).toBe('/workspace/storage/tmp/git');
});
});
describe('tempVolumeDownloadPath', () => {
it('should return the correct temp volume download path', () => {
expect(PathUtils.tempVolumeDownloadPath).toBe('/workspace/storage/tmp/volume-downloads');
});
});
describe('gitRootPathForApp', () => {
it('should return the correct git root path for app', () => {
(process.env as any).NODE_ENV = 'development';
const appId = 'testApp';
expect(PathUtils.gitRootPathForApp(appId)).toBe('/workspace/storage/tmp/git/testApp');
});
});
describe('deploymentLogsPath', () => {
it('should return the correct deployment logs path', () => {
(process.env as any).NODE_ENV = 'development';
expect(PathUtils.deploymentLogsPath).toBe('/workspace/storage/internal/deployment-logs');
});
});
describe('appDeploymentLogFile', () => {
it('should return the correct app deployment log file path', () => {
const deploymentId = 'deploy123';
expect(PathUtils.appDeploymentLogFile(deploymentId)).toBe('/workspace/storage/internal/deployment-logs/deploy123.log');
});
});
describe('volumeDownloadFolder', () => {
it('should return the correct volume download folder path', () => {
const volumeId = 'volume123';
expect(PathUtils.volumeDownloadFolder(volumeId)).toBe('/workspace/storage/tmp/volume-downloads/volume123-data');
});
});
describe('volumeDownloadZipPath', () => {
it('should return the correct volume download zip path', () => {
const volumeId = 'volume123';
expect(PathUtils.volumeDownloadZipPath(volumeId)).toBe('/workspace/storage/tmp/volume-downloads/volume123.tar.gz');
});
});
describe('convertIdToFolderFriendlyName', () => {
it('should convert id to folder friendly name', () => {
const id = 'test@123!';
expect(PathUtils['convertIdToFolderFriendlyName'](id)).toBe('test_123_');
});
});
});

View File

@@ -0,0 +1,89 @@
import { ListUtils } from '../../../shared/utils/list.utils';
describe('ListUtils', () => {
describe('removeDuplicates', () => {
it('should remove duplicates from an array', () => {
const array = [1, 2, 2, 3, 4, 4, 5];
const result = ListUtils.removeDuplicates(array);
expect(result).toEqual([1, 2, 3, 4, 5]);
});
});
describe('sortByDate', () => {
it('should sort an array by date in ascending order', () => {
const array = [
{ date: new Date('2021-01-01') },
{ date: new Date('2020-01-01') },
{ date: new Date('2022-01-01') }
];
const result = ListUtils.sortByDate(array, item => item.date);
expect(result).toEqual([
{ date: new Date('2020-01-01') },
{ date: new Date('2021-01-01') },
{ date: new Date('2022-01-01') }
]);
});
it('should sort an array by date in descending order', () => {
const array = [
{ date: new Date('2021-01-01') },
{ date: new Date('2020-01-01') },
{ date: new Date('2022-01-01') }
];
const result = ListUtils.sortByDate(array, item => item.date, true);
expect(result).toEqual([
{ date: new Date('2022-01-01') },
{ date: new Date('2021-01-01') },
{ date: new Date('2020-01-01') }
]);
});
});
describe('distinctBy', () => {
it('should return distinct elements by key', () => {
const array = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }
];
const result = ListUtils.distinctBy(array, item => item.id);
expect(result).toEqual([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]);
});
});
describe('groupBy', () => {
it('should group elements by key', () => {
const array = [
{ category: 'A', value: 1 },
{ category: 'B', value: 2 },
{ category: 'A', value: 3 }
];
const result = ListUtils.groupBy(array, item => item.category);
expect(result).toEqual(new Map([
['A', [{ category: 'A', value: 1 }, { category: 'A', value: 3 }]],
['B', [{ category: 'B', value: 2 }]]
]));
});
});
describe('chunk', () => {
it('should split an array into chunks of specified size', () => {
const array = [1, 2, 3, 4, 5, 6, 7];
const result = ListUtils.chunk(array, 3);
expect(result).toEqual([[1, 2, 3], [4, 5, 6], [7]]);
});
});
describe('removeNulls', () => {
it('should remove null values from an array', () => {
const array = [1, null, 2, null, 3];
const result = ListUtils.removeNulls(array);
expect(result).toEqual([1, 2, 3]);
});
});
});

View File

@@ -0,0 +1,22 @@
import { StreamUtils } from '../../../shared/utils/stream.utils';
import { TerminalSetupInfoModel } from '../../../shared/model/terminal-setup-info.model';
describe('StreamUtils', () => {
const terminalInfo: TerminalSetupInfoModel = {
terminalSessionKey: 'testSessionKey'
} as TerminalSetupInfoModel;
describe('getInputStreamName', () => {
it('should return the correct input stream name', () => {
const inputStreamName = StreamUtils.getInputStreamName(terminalInfo);
expect(inputStreamName).toBe('testSessionKey_input');
});
});
describe('getOutputStreamName', () => {
it('should return the correct output stream name', () => {
const outputStreamName = StreamUtils.getOutputStreamName(terminalInfo);
expect(outputStreamName).toBe('testSessionKey_output');
});
});
});

View File

@@ -0,0 +1,138 @@
import { z } from "zod";
import { ZodUtils, stringToNumber, stringToOptionalNumber, stringToOptionalDate, stringToDate, stringToOptionalBoolean, stringToBoolean } from "../../../shared/utils/zod.utils";
describe("ZodUtils", () => {
describe("getFieldNamesAndTypes", () => {
it("should return field names and types from schema", () => {
const schema = z.object({
name: z.string(),
age: z.number(),
isActive: z.boolean(),
});
const result = ZodUtils.getFieldNamesAndTypes(schema);
expect(result.get("name")).toBe("ZodString");
expect(result.get("age")).toBe("ZodNumber");
expect(result.get("isActive")).toBe("ZodBoolean");
});
});
});
describe("stringToNumber", () => {
it("should transform string to number", () => {
expect(stringToNumber.parse("123")).toBe(123);
});
it("should return null for invalid number string", () => {
expect(stringToNumber.safeParse("abc").success).toBe(false);
});
it("should return number as is", () => {
expect(stringToNumber.parse(123)).toBe(123);
});
});
describe("stringToOptionalNumber", () => {
it("should transform string to number", () => {
expect(stringToOptionalNumber.parse("123")).toBe(123);
});
it("should return null for invalid number string", () => {
expect(stringToOptionalNumber.parse("abc")).toBeNull();
});
it("should return number as is", () => {
expect(stringToOptionalNumber.parse(123)).toBe(123);
});
it("should return null for null or undefined", () => {
expect(stringToOptionalNumber.parse(null)).toBeNull();
expect(stringToOptionalNumber.parse(undefined)).toBeNull();
});
});
describe("stringToOptionalDate", () => {
it("should transform string to date", () => {
const date = new Date("2023-01-01");
expect(stringToOptionalDate.parse("2023-01-01")).toEqual(date);
});
it("should return null for invalid date string", () => {
expect(stringToOptionalDate.parse("invalid-date")).toBeNull();
});
it("should return date as is", () => {
const date = new Date("2023-01-01");
expect(stringToOptionalDate.parse(date)).toEqual(date);
});
it("should return null for null or undefined", () => {
expect(stringToOptionalDate.parse(null)).toBeNull();
expect(stringToOptionalDate.parse(undefined)).toBeNull();
});
});
describe("stringToDate", () => {
it("should transform string to date", () => {
const date = new Date("2023-01-01");
expect(stringToDate.parse("2023-01-01")).toEqual(date);
});
it("should return null for invalid date string", () => {
expect(stringToDate.safeParse("invalid-date").success).toBe(false);
});
it("should return date as is", () => {
const date = new Date("2023-01-01");
expect(stringToDate.parse(date)).toEqual(date);
});
});
describe("stringToOptionalBoolean", () => {
it("should transform 'true' string to true", () => {
expect(stringToOptionalBoolean.parse("true")).toBe(true);
});
it("should transform 'false' string to false", () => {
expect(stringToOptionalBoolean.parse("false")).toBe(false);
});
it("should return null for invalid boolean string", () => {
expect(stringToOptionalBoolean.parse("invalid")).toBeNull();
});
it("should return boolean as is", () => {
expect(stringToOptionalBoolean.parse(true)).toBe(true);
expect(stringToOptionalBoolean.parse(false)).toBe(false);
});
it("should return null for null or undefined", () => {
expect(stringToOptionalBoolean.parse(null)).toBeNull();
expect(stringToOptionalBoolean.parse(undefined)).toBeNull();
});
});
describe("stringToBoolean", () => {
it("should transform 'true' string to true", () => {
expect(stringToBoolean.parse("true")).toBe(true);
});
it("should transform 'false' string to false", () => {
expect(stringToBoolean.parse("false")).toBe(false);
});
it("should return false for invalid boolean string", () => {
expect(stringToBoolean.parse("invalid")).toBe(false);
});
it("should return boolean as is", () => {
expect(stringToBoolean.parse(true)).toBe(true);
expect(stringToBoolean.parse(false)).toBe(false);
});
it("should return false for null or undefined", () => {
expect(stringToBoolean.parse(null)).toBe(false);
expect(stringToBoolean.parse(undefined)).toBe(false);
});
});

View File

@@ -7,6 +7,7 @@ export type FormZodErrorValidationCallback<T> = {
};
export class FormUtils {
static mapValidationErrorsToForm<T extends ZodType<any, any, any>>(
state: ServerActionResult<z.infer<T>, undefined>,
form: UseFormReturn<z.infer<T>, any, undefined>) {

View File

@@ -1,11 +1,10 @@
import fs from "fs"
import fsPromises from "fs/promises"
export class FsUtils {
static async fileExists(pathName: string) {
try {
await fsPromises.access(pathName, fs.constants.F_OK);
await fs.promises.access(pathName, fs.constants.F_OK);
return true;
} catch (ex) {
return false;
@@ -22,7 +21,7 @@ export class FsUtils {
static async isFolderEmpty(pathName: string) {
try {
const files = await fsPromises.readdir(pathName);
const files = await fs.promises.readdir(pathName);
return files.length === 0;
} catch (ex) {
return true;
@@ -45,7 +44,7 @@ export class FsUtils {
}
if (!exists) {
await fsPromises.mkdir(pathName, {
await fs.promises.mkdir(pathName, {
recursive
});
}
@@ -60,7 +59,7 @@ export class FsUtils {
if (!exists) {
return;
}
await fsPromises.rm(pathName, {
await fs.promises.rm(pathName, {
recursive
});
}

View File

@@ -2,8 +2,14 @@ import path from 'path';
export class PathUtils {
static internalDataRoot = process.env.NODE_ENV === 'production' ? '/app/storage' : '/workspace/storage/internal';
static tempDataRoot = process.env.NODE_ENV === 'production' ? '/app/tmp-storage' : '/workspace/storage/tmp';
static isProduction = process.env.NODE_ENV === 'production';
static get internalDataRoot() {
return this.isProduction ? '/app/storage' : '/workspace/storage/internal';
}
static get tempDataRoot() {
return this.isProduction ? '/app/tmp-storage' : '/workspace/storage/tmp';
}
static get gitRootPath() {
return path.join(this.tempDataRoot, 'git');

2078
yarn.lock

File diff suppressed because it is too large Load Diff