chore: 清理macOS同步产生的重复文件

详细说明:
- 删除了352个带数字后缀的重复文件
- 更新.gitignore防止未来产生此类文件
- 这些文件是由iCloud或其他同步服务冲突产生的
- 不影响项目功能,仅清理冗余文件
This commit is contained in:
Yep_Q
2025-09-08 12:06:01 +08:00
parent 1564396449
commit d6f48d6d14
365 changed files with 2039 additions and 68301 deletions

View File

@@ -1,19 +0,0 @@
abstract class StringArray<T extends string> extends Array<T> {
constructor(str: string, delimiter: string) {
super();
const parsed = str.split(delimiter) as this;
return parsed.filter((i) => typeof i === 'string' && i.length);
}
}
export class CommaSeparatedStringArray<T extends string> extends StringArray<T> {
constructor(str: string) {
super(str, ',');
}
}
export class ColonSeparatedStringArray<T extends string = string> extends StringArray<T> {
constructor(str: string) {
super(str, ':');
}
}

View File

@@ -1,118 +0,0 @@
import 'reflect-metadata';
import { Container, Service } from '@n8n/di';
import { readFileSync } from 'fs';
import { z } from 'zod';
// eslint-disable-next-line @typescript-eslint/no-restricted-types
type Class = Function;
type Constructable<T = unknown> = new (rawValue: string) => T;
type PropertyKey = string | symbol;
type PropertyType = number | boolean | string | Class;
interface PropertyMetadata {
type: PropertyType;
envName?: string;
schema?: z.ZodType<unknown>;
}
const globalMetadata = new Map<Class, Map<PropertyKey, PropertyMetadata>>();
const readEnv = (envName: string) => {
if (envName in process.env) return process.env[envName];
// Read the value from a file, if "_FILE" environment variable is defined
const filePath = process.env[`${envName}_FILE`];
if (filePath) return readFileSync(filePath, 'utf8');
return undefined;
};
export const Config: ClassDecorator = (ConfigClass: Class) => {
const factory = function (...args: unknown[]) {
const config = new (ConfigClass as new (...a: unknown[]) => Record<PropertyKey, unknown>)(
...args,
);
const classMetadata = globalMetadata.get(ConfigClass);
if (!classMetadata) {
throw new Error('Invalid config class: ' + ConfigClass.name);
}
for (const [key, { type, envName, schema }] of classMetadata) {
if (typeof type === 'function' && globalMetadata.has(type)) {
config[key] = Container.get(type as Constructable);
} else if (envName) {
const value = readEnv(envName);
if (value === undefined) continue;
if (schema) {
const result = schema.safeParse(value);
if (result.error) {
console.warn(
`Invalid value for ${envName} - ${result.error.issues[0].message}. Falling back to default value.`,
);
continue;
}
config[key] = result.data;
} else if (type === Number) {
const parsed = Number(value);
if (isNaN(parsed)) {
console.warn(`Invalid number value for ${envName}: ${value}`);
} else {
config[key] = parsed;
}
} else if (type === Boolean) {
if (['true', '1'].includes(value.toLowerCase())) {
config[key] = true;
} else if (['false', '0'].includes(value.toLowerCase())) {
config[key] = false;
} else {
console.warn(`Invalid boolean value for ${envName}: ${value}`);
}
} else if (type === Date) {
const timestamp = Date.parse(value);
if (isNaN(timestamp)) {
console.warn(`Invalid timestamp value for ${envName}: ${value}`);
} else {
config[key] = new Date(timestamp);
}
} else if (type === String) {
config[key] = value.trim().replace(/^(['"])(.*)\1$/, '$2');
} else {
config[key] = new (type as Constructable)(value);
}
}
}
if (typeof config.sanitize === 'function') config.sanitize();
return config;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Service({ factory })(ConfigClass);
};
export const Nested: PropertyDecorator = (target: object, key: PropertyKey) => {
const ConfigClass = target.constructor;
const classMetadata = globalMetadata.get(ConfigClass) ?? new Map<PropertyKey, PropertyMetadata>();
const type = Reflect.getMetadata('design:type', target, key) as PropertyType;
classMetadata.set(key, { type });
globalMetadata.set(ConfigClass, classMetadata);
};
export const Env =
(envName: string, schema?: PropertyMetadata['schema']): PropertyDecorator =>
(target: object, key: PropertyKey) => {
const ConfigClass = target.constructor;
const classMetadata =
globalMetadata.get(ConfigClass) ?? new Map<PropertyKey, PropertyMetadata>();
const type = Reflect.getMetadata('design:type', target, key) as PropertyType;
const isZodSchema = schema instanceof z.ZodType;
if (type === Object && !isZodSchema) {
throw new Error(
`Invalid decorator metadata on key "${key as string}" on ${ConfigClass.name}\n Please use explicit typing on all config fields`,
);
}
classMetadata.set(key, { type, envName, schema });
globalMetadata.set(ConfigClass, classMetadata);
};

View File

@@ -1,211 +0,0 @@
import { z } from 'zod';
import { AiAssistantConfig } from './configs/ai-assistant.config';
import { AiConfig } from './configs/ai.config';
import { AuthConfig } from './configs/auth.config';
import { CacheConfig } from './configs/cache.config';
import { CredentialsConfig } from './configs/credentials.config';
import { DatabaseConfig } from './configs/database.config';
import { DeploymentConfig } from './configs/deployment.config';
import { DiagnosticsConfig } from './configs/diagnostics.config';
import { EndpointsConfig } from './configs/endpoints.config';
import { EventBusConfig } from './configs/event-bus.config';
import { ExecutionsConfig } from './configs/executions.config';
import { ExternalHooksConfig } from './configs/external-hooks.config';
import { GenericConfig } from './configs/generic.config';
import { HiringBannerConfig } from './configs/hiring-banner.config';
import { LicenseConfig } from './configs/license.config';
import { LoggingConfig } from './configs/logging.config';
import { MfaConfig } from './configs/mfa.config';
import { MultiMainSetupConfig } from './configs/multi-main-setup.config';
import { NodesConfig } from './configs/nodes.config';
import { PartialExecutionsConfig } from './configs/partial-executions.config';
import { PersonalizationConfig } from './configs/personalization.config';
import { PublicApiConfig } from './configs/public-api.config';
import { RedisConfig } from './configs/redis.config';
import { TaskRunnersConfig } from './configs/runners.config';
import { ScalingModeConfig } from './configs/scaling-mode.config';
import { SecurityConfig } from './configs/security.config';
import { SentryConfig } from './configs/sentry.config';
import { SsoConfig } from './configs/sso.config';
import { TagsConfig } from './configs/tags.config';
import { TemplatesConfig } from './configs/templates.config';
import { UserManagementConfig } from './configs/user-management.config';
import { VersionNotificationsConfig } from './configs/version-notifications.config';
import { WorkflowHistoryConfig } from './configs/workflow-history.config';
import { WorkflowsConfig } from './configs/workflows.config';
import { Config, Env, Nested } from './decorators';
export { Config, Env, Nested } from './decorators';
export { DatabaseConfig } from './configs/database.config';
export { InstanceSettingsConfig } from './configs/instance-settings-config';
export { TaskRunnersConfig } from './configs/runners.config';
export { SecurityConfig } from './configs/security.config';
export { ExecutionsConfig } from './configs/executions.config';
export { LOG_SCOPES } from './configs/logging.config';
export type { LogScope } from './configs/logging.config';
export { WorkflowsConfig } from './configs/workflows.config';
export * from './custom-types';
export { DeploymentConfig } from './configs/deployment.config';
export { MfaConfig } from './configs/mfa.config';
export { HiringBannerConfig } from './configs/hiring-banner.config';
export { PersonalizationConfig } from './configs/personalization.config';
export { NodesConfig } from './configs/nodes.config';
export { CronLoggingConfig } from './configs/logging.config';
const protocolSchema = z.enum(['http', 'https']);
export type Protocol = z.infer<typeof protocolSchema>;
@Config
export class GlobalConfig {
@Nested
auth: AuthConfig;
@Nested
database: DatabaseConfig;
@Nested
credentials: CredentialsConfig;
@Nested
userManagement: UserManagementConfig;
@Nested
versionNotifications: VersionNotificationsConfig;
@Nested
publicApi: PublicApiConfig;
@Nested
externalHooks: ExternalHooksConfig;
@Nested
templates: TemplatesConfig;
@Nested
eventBus: EventBusConfig;
@Nested
nodes: NodesConfig;
@Nested
workflows: WorkflowsConfig;
@Nested
sentry: SentryConfig;
/** Path n8n is deployed to */
@Env('N8N_PATH')
path: string = '/';
/** Host name n8n can be reached */
@Env('N8N_HOST')
host: string = 'localhost';
/** HTTP port n8n can be reached */
@Env('N8N_PORT')
port: number = 5678;
/** IP address n8n should listen on */
@Env('N8N_LISTEN_ADDRESS')
listen_address: string = '::';
/** HTTP Protocol via which n8n can be reached */
@Env('N8N_PROTOCOL', protocolSchema)
protocol: Protocol = 'http';
@Nested
endpoints: EndpointsConfig;
@Nested
cache: CacheConfig;
@Nested
queue: ScalingModeConfig;
@Nested
logging: LoggingConfig;
@Nested
taskRunners: TaskRunnersConfig;
@Nested
multiMainSetup: MultiMainSetupConfig;
@Nested
generic: GenericConfig;
@Nested
license: LicenseConfig;
@Nested
security: SecurityConfig;
@Nested
executions: ExecutionsConfig;
@Nested
diagnostics: DiagnosticsConfig;
@Nested
aiAssistant: AiAssistantConfig;
@Nested
tags: TagsConfig;
@Nested
partialExecutions: PartialExecutionsConfig;
@Nested
workflowHistory: WorkflowHistoryConfig;
@Nested
deployment: DeploymentConfig;
@Nested
mfa: MfaConfig;
@Nested
hiringBanner: HiringBannerConfig;
@Nested
personalization: PersonalizationConfig;
@Nested
sso: SsoConfig;
/** Default locale for the UI. */
@Env('N8N_DEFAULT_LOCALE')
defaultLocale: string = 'en';
/** Whether to hide the page that shows active workflows and executions count. */
@Env('N8N_HIDE_USAGE_PAGE')
hideUsagePage: boolean = false;
/** Number of reverse proxies n8n is running behind. */
@Env('N8N_PROXY_HOPS')
proxy_hops: number = 0;
/** SSL key for HTTPS protocol. */
@Env('N8N_SSL_KEY')
ssl_key: string = '';
/** SSL cert for HTTPS protocol. */
@Env('N8N_SSL_CERT')
ssl_cert: string = '';
/** Public URL where the editor is accessible. Also used for emails sent from n8n. */
@Env('N8N_EDITOR_BASE_URL')
editorBaseUrl: string = '';
/** URLs to external frontend hooks files, separated by semicolons. */
@Env('EXTERNAL_FRONTEND_HOOKS_URLS')
externalFrontendHooksUrls: string = '';
@Nested
redis: RedisConfig;
@Nested
ai: AiConfig;
}

View File

@@ -1,486 +0,0 @@
import { Container } from '@n8n/di';
import fs from 'fs';
import { mock } from 'jest-mock-extended';
import type { UserManagementConfig } from '../src/configs/user-management.config';
import { GlobalConfig } from '../src/index';
jest.mock('fs');
const mockFs = mock<typeof fs>();
fs.readFileSync = mockFs.readFileSync;
const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
describe('GlobalConfig', () => {
beforeEach(() => {
Container.reset();
jest.clearAllMocks();
});
const originalEnv = process.env;
afterEach(() => {
process.env = originalEnv;
});
const defaultConfig: GlobalConfig = {
path: '/',
host: 'localhost',
port: 5678,
listen_address: '::',
protocol: 'http',
auth: {
cookie: {
samesite: 'lax',
secure: true,
},
},
defaultLocale: 'en',
hideUsagePage: false,
deployment: {
type: 'default',
},
mfa: {
enabled: true,
},
hiringBanner: {
enabled: true,
},
personalization: {
enabled: true,
},
proxy_hops: 0,
ssl_key: '',
ssl_cert: '',
editorBaseUrl: '',
database: {
logging: {
enabled: false,
maxQueryExecutionTime: 0,
options: 'error',
},
mysqldb: {
database: 'n8n',
host: 'localhost',
password: '',
port: 3306,
user: 'root',
},
postgresdb: {
database: 'n8n',
host: 'localhost',
password: '',
poolSize: 2,
port: 5432,
schema: 'public',
connectionTimeoutMs: 20_000,
ssl: {
ca: '',
cert: '',
enabled: false,
key: '',
rejectUnauthorized: true,
},
user: 'postgres',
idleTimeoutMs: 30_000,
},
sqlite: {
database: 'database.sqlite',
enableWAL: false,
executeVacuumOnStartup: false,
poolSize: 0,
},
tablePrefix: '',
type: 'sqlite',
isLegacySqlite: true,
pingIntervalSeconds: 2,
},
credentials: {
defaultName: 'My credentials',
overwrite: {
data: '{}',
endpoint: '',
},
},
userManagement: {
jwtSecret: '',
jwtSessionDurationHours: 168,
jwtRefreshTimeoutHours: 0,
emails: {
mode: 'smtp',
smtp: {
host: '',
port: 465,
secure: true,
sender: '',
startTLS: true,
auth: {
pass: '',
user: '',
privateKey: '',
serviceClient: '',
},
},
template: {
'credentials-shared': '',
'user-invited': '',
'password-reset-requested': '',
'workflow-shared': '',
'project-shared': '',
},
},
} as UserManagementConfig,
eventBus: {
checkUnsentInterval: 0,
crashRecoveryMode: 'extensive',
logWriter: {
keepLogCount: 3,
logBaseName: 'n8nEventLog',
maxFileSizeInKB: 10240,
},
},
externalHooks: {
files: [],
},
nodes: {
errorTriggerType: 'n8n-nodes-base.errorTrigger',
include: [],
exclude: [],
pythonEnabled: true,
},
publicApi: {
disabled: false,
path: 'api',
swaggerUiDisabled: false,
},
templates: {
enabled: true,
host: 'https://api.n8n.io/api/',
},
versionNotifications: {
enabled: true,
endpoint: 'https://api.n8n.io/api/versions/',
whatsNewEnabled: true,
whatsNewEndpoint: 'https://api.n8n.io/api/whats-new',
infoUrl: 'https://docs.n8n.io/hosting/installation/updating/',
},
workflows: {
defaultName: 'My workflow',
callerPolicyDefaultOption: 'workflowsFromSameOwner',
activationBatchSize: 1,
},
endpoints: {
metrics: {
enable: false,
prefix: 'n8n_',
includeWorkflowIdLabel: false,
includeWorkflowNameLabel: false,
includeDefaultMetrics: true,
includeMessageEventBusMetrics: false,
includeNodeTypeLabel: false,
includeCacheMetrics: false,
includeApiEndpoints: false,
includeApiPathLabel: false,
includeApiMethodLabel: false,
includeCredentialTypeLabel: false,
includeApiStatusCodeLabel: false,
includeQueueMetrics: false,
queueMetricsInterval: 20,
activeWorkflowCountInterval: 60,
},
additionalNonUIRoutes: '',
disableProductionWebhooksOnMainProcess: false,
disableUi: false,
form: 'form',
formTest: 'form-test',
formWaiting: 'form-waiting',
mcp: 'mcp',
mcpTest: 'mcp-test',
payloadSizeMax: 16,
formDataFileSizeMax: 200,
rest: 'rest',
webhook: 'webhook',
webhookTest: 'webhook-test',
webhookWaiting: 'webhook-waiting',
},
cache: {
backend: 'auto',
memory: {
maxSize: 3145728,
ttl: 3600000,
},
redis: {
prefix: 'cache',
ttl: 3600000,
},
},
queue: {
health: {
active: false,
port: 5678,
address: '::',
},
bull: {
redis: {
db: 0,
host: 'localhost',
password: '',
port: 6379,
timeoutThreshold: 10_000,
username: '',
clusterNodes: '',
tls: false,
dualStack: false,
},
gracefulShutdownTimeout: 30,
prefix: 'bull',
settings: {
lockDuration: 30_000,
lockRenewTime: 15_000,
stalledInterval: 30_000,
maxStalledCount: 1,
},
},
},
taskRunners: {
enabled: false,
mode: 'internal',
path: '/runners',
authToken: '',
listenAddress: '127.0.0.1',
maxPayload: 1024 * 1024 * 1024,
port: 5679,
maxOldSpaceSize: '',
maxConcurrency: 10,
taskTimeout: 300,
heartbeatInterval: 30,
insecureMode: false,
},
sentry: {
backendDsn: '',
frontendDsn: '',
environment: '',
deploymentName: '',
},
logging: {
level: 'info',
format: 'text',
outputs: ['console'],
file: {
fileCountMax: 100,
fileSizeMax: 16,
location: 'logs/n8n.log',
},
scopes: [],
cron: {
activeInterval: 0,
},
},
multiMainSetup: {
enabled: false,
ttl: 10,
interval: 3,
},
generic: {
timezone: 'America/New_York',
releaseChannel: 'dev',
gracefulShutdownTimeout: 30,
},
license: {
serverUrl: 'https://license.n8n.io/v1',
autoRenewalEnabled: true,
detachFloatingOnShutdown: true,
activationKey: '',
tenantId: 1,
cert: '',
},
security: {
restrictFileAccessTo: '',
blockFileAccessToN8nFiles: true,
daysAbandonedWorkflow: 90,
contentSecurityPolicy: '{}',
contentSecurityPolicyReportOnly: false,
disableWebhookHtmlSandboxing: false,
},
executions: {
pruneData: true,
pruneDataMaxAge: 336,
pruneDataMaxCount: 10_000,
pruneDataHardDeleteBuffer: 1,
pruneDataIntervals: {
hardDelete: 15,
softDelete: 60,
},
concurrency: {
productionLimit: -1,
evaluationLimit: -1,
},
queueRecovery: {
interval: 180,
batchSize: 100,
},
saveDataOnError: 'all',
saveDataOnSuccess: 'all',
saveExecutionProgress: false,
saveDataManualExecutions: true,
},
diagnostics: {
enabled: true,
frontendConfig: '1zPn9bgWPzlQc0p8Gj1uiK6DOTn;https://telemetry.n8n.io',
backendConfig: '1zPn7YoGC3ZXE9zLeTKLuQCB4F6;https://telemetry.n8n.io',
posthogConfig: {
apiKey: 'phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo',
apiHost: 'https://ph.n8n.io',
},
},
aiAssistant: {
baseUrl: '',
},
tags: {
disabled: false,
},
partialExecutions: {
version: 2,
},
workflowHistory: {
enabled: true,
pruneTime: -1,
},
sso: {
justInTimeProvisioning: true,
redirectLoginToSso: true,
saml: {
loginEnabled: false,
loginLabel: '',
},
oidc: {
loginEnabled: false,
},
ldap: {
loginEnabled: false,
loginLabel: '',
},
},
redis: {
prefix: 'n8n',
},
externalFrontendHooksUrls: '',
ai: {
enabled: false,
},
};
it('should use all default values when no env variables are defined', () => {
process.env = {};
const config = Container.get(GlobalConfig);
// Makes sure the objects are structurally equal while respecting getters,
// which `toEqual` and `toBe` does not do.
expect(defaultConfig).toMatchObject(config);
expect(config).toMatchObject(defaultConfig);
expect(mockFs.readFileSync).not.toHaveBeenCalled();
});
it('should use values from env variables when defined', () => {
process.env = {
DB_POSTGRESDB_HOST: 'some-host',
DB_POSTGRESDB_USER: 'n8n',
DB_POSTGRESDB_IDLE_CONNECTION_TIMEOUT: '10000',
DB_TABLE_PREFIX: 'test_',
DB_PING_INTERVAL_SECONDS: '2',
NODES_INCLUDE: '["n8n-nodes-base.hackerNews"]',
DB_LOGGING_MAX_EXECUTION_TIME: '0',
N8N_METRICS: 'TRUE',
N8N_TEMPLATES_ENABLED: '0',
};
const config = Container.get(GlobalConfig);
expect(structuredClone(config)).toEqual({
...defaultConfig,
database: {
logging: defaultConfig.database.logging,
mysqldb: defaultConfig.database.mysqldb,
postgresdb: {
...defaultConfig.database.postgresdb,
host: 'some-host',
user: 'n8n',
idleTimeoutMs: 10_000,
},
sqlite: defaultConfig.database.sqlite,
tablePrefix: 'test_',
type: 'sqlite',
pingIntervalSeconds: 2,
},
endpoints: {
...defaultConfig.endpoints,
metrics: {
...defaultConfig.endpoints.metrics,
enable: true,
},
},
nodes: {
...defaultConfig.nodes,
include: ['n8n-nodes-base.hackerNews'],
},
templates: {
...defaultConfig.templates,
enabled: false,
},
});
expect(mockFs.readFileSync).not.toHaveBeenCalled();
});
it('should read values from files using _FILE env variables', () => {
const passwordFile = '/path/to/postgres/password';
process.env = {
DB_POSTGRESDB_PASSWORD_FILE: passwordFile,
};
mockFs.readFileSync.calledWith(passwordFile, 'utf8').mockReturnValueOnce('password-from-file');
const config = Container.get(GlobalConfig);
const expected = {
...defaultConfig,
database: {
...defaultConfig.database,
postgresdb: {
...defaultConfig.database.postgresdb,
password: 'password-from-file',
},
},
};
// Makes sure the objects are structurally equal while respecting getters,
// which `toEqual` and `toBe` does not do.
expect(config).toMatchObject(expected);
expect(expected).toMatchObject(config);
expect(mockFs.readFileSync).toHaveBeenCalled();
});
it('should handle invalid numbers', () => {
process.env = {
DB_LOGGING_MAX_EXECUTION_TIME: 'abcd',
};
const config = Container.get(GlobalConfig);
expect(config.database.logging.maxQueryExecutionTime).toEqual(0);
expect(consoleWarnMock).toHaveBeenCalledWith(
'Invalid number value for DB_LOGGING_MAX_EXECUTION_TIME: abcd',
);
});
describe('string unions', () => {
it('on invalid value, should warn and fall back to default value', () => {
process.env = {
N8N_RUNNERS_MODE: 'non-existing-mode',
N8N_RUNNERS_ENABLED: 'true',
DB_TYPE: 'postgresdb',
};
const globalConfig = Container.get(GlobalConfig);
expect(globalConfig.taskRunners.mode).toEqual('internal');
expect(consoleWarnMock).toHaveBeenCalledWith(
expect.stringContaining(
"Invalid value for N8N_RUNNERS_MODE - Invalid enum value. Expected 'internal' | 'external', received 'non-existing-mode'. Falling back to default value.",
),
);
expect(globalConfig.taskRunners.enabled).toEqual(true);
expect(globalConfig.database.type).toEqual('postgresdb');
});
});
});

View File

@@ -1,25 +0,0 @@
import { CommaSeparatedStringArray, ColonSeparatedStringArray } from '../src/custom-types';
describe('CommaSeparatedStringArray', () => {
it('should parse comma-separated string into array', () => {
const result = new CommaSeparatedStringArray('a,b,c');
expect(result).toEqual(['a', 'b', 'c']);
});
it('should handle empty strings', () => {
const result = new CommaSeparatedStringArray('a,b,,,');
expect(result).toEqual(['a', 'b']);
});
});
describe('ColonSeparatedStringArray', () => {
it('should parse colon-separated string into array', () => {
const result = new ColonSeparatedStringArray('a:b:c');
expect(result).toEqual(['a', 'b', 'c']);
});
it('should handle empty strings', () => {
const result = new ColonSeparatedStringArray('a::b:::');
expect(result).toEqual(['a', 'b']);
});
});

View File

@@ -1,22 +0,0 @@
import { Container } from '@n8n/di';
import { Config, Env } from '../src/decorators';
describe('decorators', () => {
beforeEach(() => {
Container.reset();
});
it('should throw when explicit typing is missing', () => {
expect(() => {
@Config
class InvalidConfig {
@Env('STRING_VALUE')
value = 'string';
}
Container.get(InvalidConfig);
}).toThrowError(
'Invalid decorator metadata on key "value" on InvalidConfig\n Please use explicit typing on all config fields',
);
});
});

View File

@@ -1,123 +0,0 @@
import { Container } from '@n8n/di';
import { GlobalConfig } from '../src/index';
beforeEach(() => {
Container.reset();
jest.clearAllMocks();
});
const originalEnv = process.env;
afterEach(() => {
process.env = originalEnv;
});
it('should strip double quotes from string values', () => {
process.env = {
GENERIC_TIMEZONE: '"America/Bogota"',
N8N_HOST: '"localhost"',
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe('America/Bogota');
expect(config.host).toBe('localhost');
});
it('should strip single quotes from string values', () => {
process.env = {
GENERIC_TIMEZONE: "'America/Bogota'",
N8N_HOST: "'localhost'",
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe('America/Bogota');
expect(config.host).toBe('localhost');
});
it('should trim whitespace from quoted values', () => {
process.env = {
GENERIC_TIMEZONE: ' "America/Bogota" ',
N8N_HOST: " 'localhost' ",
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe('America/Bogota');
expect(config.host).toBe('localhost');
});
it('should trim whitespace from unquoted values', () => {
process.env = {
GENERIC_TIMEZONE: ' America/Bogota ',
N8N_HOST: ' localhost ',
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe('America/Bogota');
expect(config.host).toBe('localhost');
});
it('should leave mismatched quotes unchanged', () => {
process.env = {
GENERIC_TIMEZONE: '"America/Bogota\'',
N8N_HOST: '\'localhost"',
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe('"America/Bogota\'');
expect(config.host).toBe('\'localhost"');
});
it('should handle empty quotes', () => {
process.env = {
GENERIC_TIMEZONE: '""',
N8N_HOST: "''",
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe('');
expect(config.host).toBe('');
});
it('should handle single character in quotes', () => {
process.env = {
GENERIC_TIMEZONE: '"A"',
N8N_HOST: "'B'",
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe('A');
expect(config.host).toBe('B');
});
it('should handle values with spaces in quotes', () => {
process.env = {
GENERIC_TIMEZONE: '"America/New York"',
N8N_HOST: "'my host name'",
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe('America/New York');
expect(config.host).toBe('my host name');
});
it('should handle nested quotes', () => {
process.env = {
GENERIC_TIMEZONE: '"America/\'Bogota\'"',
N8N_HOST: '\'"localhost"\'',
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe("America/'Bogota'");
expect(config.host).toBe('"localhost"');
});
it('should handle only opening or closing quotes', () => {
process.env = {
GENERIC_TIMEZONE: '"America/Bogota',
N8N_HOST: 'localhost"',
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe('"America/Bogota');
expect(config.host).toBe('localhost"');
});
it('should handle multiple quote pairs', () => {
process.env = {
GENERIC_TIMEZONE: '""America/Bogota""',
N8N_HOST: "''localhost''",
};
const config = Container.get(GlobalConfig);
expect(config.generic.timezone).toBe('"America/Bogota"'); // should strip only outer quotes
expect(config.host).toBe("'localhost'");
});

View File

@@ -1,14 +0,0 @@
{
"extends": "@n8n/typescript-config/tsconfig.common.json",
"compilerOptions": {
"rootDir": ".",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"types": ["node", "jest"],
"baseUrl": "src",
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
},
"include": ["src/**/*.ts", "test/**/*.ts"],
"references": [{ "path": "../di/tsconfig.build.json" }]
}