mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2025-12-31 00:10:47 -06:00
added jest config and tests for util classes
This commit is contained in:
210
jest.config.ts
Normal file
210
jest.config.ts
Normal 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)
|
||||
10
package.json
10
package.json
@@ -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",
|
||||
|
||||
51
src/__tests__/frontend/utils/form.utils.test.ts
Normal file
51
src/__tests__/frontend/utils/form.utils.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
78
src/__tests__/frontend/utils/format.utils.test.ts
Normal file
78
src/__tests__/frontend/utils/format.utils.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
73
src/__tests__/frontend/utils/toast.utils.test.ts
Normal file
73
src/__tests__/frontend/utils/toast.utils.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
106
src/__tests__/server/utils/fs.utils.test.ts
Normal file
106
src/__tests__/server/utils/fs.utils.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
93
src/__tests__/server/utils/kube-object-name.utils.test.ts
Normal file
93
src/__tests__/server/utils/kube-object-name.utils.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
20
src/__tests__/server/utils/memory-caluclation.utils.test.ts
Normal file
20
src/__tests__/server/utils/memory-caluclation.utils.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
92
src/__tests__/server/utils/path.utils.test.ts
Normal file
92
src/__tests__/server/utils/path.utils.test.ts
Normal 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_');
|
||||
});
|
||||
});
|
||||
});
|
||||
89
src/__tests__/shared/utils/list.utils.test.ts
Normal file
89
src/__tests__/shared/utils/list.utils.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
22
src/__tests__/shared/utils/stream.utils.test.ts
Normal file
22
src/__tests__/shared/utils/stream.utils.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
138
src/__tests__/shared/utils/zod.utils.test.ts
Normal file
138
src/__tests__/shared/utils/zod.utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user