tests
This commit is contained in:
parent
3a7254708e
commit
b63e58784c
41 changed files with 5762 additions and 4477 deletions
|
|
@ -1,359 +1,353 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
ConfigManager,
|
||||
initializeServiceConfig,
|
||||
getConfig,
|
||||
resetConfig,
|
||||
createAppConfig,
|
||||
initializeAppConfig,
|
||||
isDevelopment,
|
||||
isProduction,
|
||||
isTest,
|
||||
getDatabaseConfig,
|
||||
getServiceConfig,
|
||||
getLogConfig,
|
||||
getQueueConfig,
|
||||
ConfigError,
|
||||
ConfigValidationError,
|
||||
baseAppSchema,
|
||||
} from '../src';
|
||||
|
||||
// Mock loader for testing
|
||||
class MockLoader {
|
||||
constructor(
|
||||
private data: Record<string, unknown>,
|
||||
public priority: number = 0
|
||||
) {}
|
||||
|
||||
load(): Record<string, unknown> {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ConfigManager', () => {
|
||||
let manager: ConfigManager<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ConfigManager();
|
||||
});
|
||||
|
||||
it('should initialize with default loaders', () => {
|
||||
expect(manager).toBeDefined();
|
||||
});
|
||||
|
||||
it('should detect environment', () => {
|
||||
const env = manager.getEnvironment();
|
||||
expect(['development', 'test', 'production']).toContain(env);
|
||||
});
|
||||
|
||||
it('should throw when getting config before initialization', () => {
|
||||
expect(() => manager.get()).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should initialize config with schema', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize(schema);
|
||||
expect(config).toEqual({ name: 'test', port: 3000 });
|
||||
});
|
||||
|
||||
it('should merge configs from multiple loaders', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [
|
||||
new MockLoader({ name: 'test', port: 3000 }, 1),
|
||||
new MockLoader({ port: 4000, debug: true }, 2),
|
||||
],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize();
|
||||
expect(config).toEqual({ name: 'test', port: 4000, debug: true, environment: 'test' });
|
||||
});
|
||||
|
||||
it('should deep merge nested objects', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [
|
||||
new MockLoader({ db: { host: 'localhost', port: 5432 } }, 1),
|
||||
new MockLoader({ db: { port: 5433, user: 'admin' } }, 2),
|
||||
],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize();
|
||||
expect(config).toEqual({
|
||||
db: { host: 'localhost', port: 5433, user: 'admin' },
|
||||
environment: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should get value by path', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ db: { host: 'localhost', port: 5432 } })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(mockManager.getValue('db.host')).toBe('localhost');
|
||||
expect(mockManager.getValue('db.port')).toBe(5432);
|
||||
});
|
||||
|
||||
it('should throw for non-existent path', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ db: { host: 'localhost' } })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(() => mockManager.getValue('db.password')).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should check if path exists', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ db: { host: 'localhost' } })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(mockManager.has('db.host')).toBe(true);
|
||||
expect(mockManager.has('db.password')).toBe(false);
|
||||
});
|
||||
|
||||
it('should update config at runtime', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
mockManager.set({ port: 4000 });
|
||||
expect(mockManager.get()).toEqual({ name: 'test', port: 4000, environment: 'test' });
|
||||
});
|
||||
|
||||
it('should validate config update with schema', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize(schema);
|
||||
expect(() => mockManager.set({ port: 'invalid' as any })).toThrow(
|
||||
ConfigValidationError
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset config', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test' })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(mockManager.get()).toEqual({ name: 'test', environment: 'test' });
|
||||
|
||||
mockManager.reset();
|
||||
expect(() => mockManager.get()).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should validate against schema', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
const validated = mockManager.validate(schema);
|
||||
expect(validated).toEqual({ name: 'test', port: 3000 });
|
||||
});
|
||||
|
||||
it('should create typed getter', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
const getTypedConfig = mockManager.createTypedGetter(schema);
|
||||
const config = getTypedConfig();
|
||||
expect(config).toEqual({ name: 'test', port: 3000 });
|
||||
});
|
||||
|
||||
it('should add environment if not present', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
environment: 'test',
|
||||
loaders: [new MockLoader({ name: 'test' })],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize();
|
||||
expect(config).toEqual({ name: 'test', environment: 'test' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Config Service Functions', () => {
|
||||
beforeEach(() => {
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
it('should throw when getting config before initialization', () => {
|
||||
expect(() => getConfig()).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should validate config with schema', () => {
|
||||
// Test that a valid config passes schema validation
|
||||
const mockConfig = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
environment: 'test' as const,
|
||||
service: {
|
||||
name: 'test-service',
|
||||
baseUrl: 'http://localhost:3000',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
mongodb: {
|
||||
uri: 'mongodb://localhost',
|
||||
database: 'test-db',
|
||||
},
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'test-db',
|
||||
user: 'test-user',
|
||||
password: 'test-pass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
},
|
||||
},
|
||||
log: {
|
||||
level: 'info' as const,
|
||||
pretty: true,
|
||||
},
|
||||
queue: {
|
||||
redis: { host: 'localhost', port: 6379 },
|
||||
},
|
||||
};
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new MockLoader(mockConfig)],
|
||||
});
|
||||
|
||||
// Should not throw when initializing with valid config
|
||||
expect(() => manager.initialize(baseAppSchema)).not.toThrow();
|
||||
|
||||
// Verify key properties exist
|
||||
const config = manager.get();
|
||||
expect(config.name).toBe('test-app');
|
||||
expect(config.version).toBe('1.0.0');
|
||||
expect(config.environment).toBe('test');
|
||||
expect(config.service.name).toBe('test-service');
|
||||
expect(config.database.mongodb.uri).toBe('mongodb://localhost');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Config Builders', () => {
|
||||
it('should create app config with schema', () => {
|
||||
const schema = z.object({
|
||||
app: z.string(),
|
||||
version: z.number(),
|
||||
});
|
||||
|
||||
const config = createAppConfig(schema, {
|
||||
loaders: [new MockLoader({ app: 'myapp', version: 1 })],
|
||||
});
|
||||
|
||||
expect(config).toBeDefined();
|
||||
});
|
||||
|
||||
it('should initialize app config in one step', () => {
|
||||
const schema = z.object({
|
||||
app: z.string(),
|
||||
version: z.number(),
|
||||
});
|
||||
|
||||
const config = initializeAppConfig(schema, {
|
||||
loaders: [new MockLoader({ app: 'myapp', version: 1 })],
|
||||
});
|
||||
|
||||
expect(config).toEqual({ app: 'myapp', version: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Environment Helpers', () => {
|
||||
beforeEach(() => {
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
it('should detect environments correctly in ConfigManager', () => {
|
||||
// Test with different environments using mock configs
|
||||
const envConfigs = [
|
||||
{ env: 'development' },
|
||||
{ env: 'production' },
|
||||
{ env: 'test' },
|
||||
];
|
||||
|
||||
for (const { env } of envConfigs) {
|
||||
const mockConfig = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
environment: env as 'development' | 'production' | 'test',
|
||||
service: {
|
||||
name: 'test',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
mongodb: {
|
||||
uri: 'mongodb://localhost',
|
||||
database: 'test-db',
|
||||
},
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'test-db',
|
||||
user: 'test-user',
|
||||
password: 'test-pass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
},
|
||||
},
|
||||
log: {
|
||||
level: 'info' as const,
|
||||
pretty: true,
|
||||
},
|
||||
queue: {
|
||||
redis: { host: 'localhost', port: 6379 },
|
||||
},
|
||||
};
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new MockLoader(mockConfig)],
|
||||
environment: env as any,
|
||||
});
|
||||
|
||||
manager.initialize(baseAppSchema);
|
||||
|
||||
// Test the manager's environment detection
|
||||
expect(manager.getEnvironment()).toBe(env);
|
||||
expect(manager.get().environment).toBe(env);
|
||||
}
|
||||
});
|
||||
});
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
baseAppSchema,
|
||||
ConfigError,
|
||||
ConfigManager,
|
||||
ConfigValidationError,
|
||||
createAppConfig,
|
||||
getConfig,
|
||||
getDatabaseConfig,
|
||||
getLogConfig,
|
||||
getQueueConfig,
|
||||
getServiceConfig,
|
||||
initializeAppConfig,
|
||||
initializeServiceConfig,
|
||||
isDevelopment,
|
||||
isProduction,
|
||||
isTest,
|
||||
resetConfig,
|
||||
} from '../src';
|
||||
|
||||
// Mock loader for testing
|
||||
class MockLoader {
|
||||
constructor(
|
||||
private data: Record<string, unknown>,
|
||||
public priority: number = 0
|
||||
) {}
|
||||
|
||||
load(): Record<string, unknown> {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ConfigManager', () => {
|
||||
let manager: ConfigManager<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ConfigManager();
|
||||
});
|
||||
|
||||
it('should initialize with default loaders', () => {
|
||||
expect(manager).toBeDefined();
|
||||
});
|
||||
|
||||
it('should detect environment', () => {
|
||||
const env = manager.getEnvironment();
|
||||
expect(['development', 'test', 'production']).toContain(env);
|
||||
});
|
||||
|
||||
it('should throw when getting config before initialization', () => {
|
||||
expect(() => manager.get()).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should initialize config with schema', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize(schema);
|
||||
expect(config).toEqual({ name: 'test', port: 3000 });
|
||||
});
|
||||
|
||||
it('should merge configs from multiple loaders', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [
|
||||
new MockLoader({ name: 'test', port: 3000 }, 1),
|
||||
new MockLoader({ port: 4000, debug: true }, 2),
|
||||
],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize();
|
||||
expect(config).toEqual({ name: 'test', port: 4000, debug: true, environment: 'test' });
|
||||
});
|
||||
|
||||
it('should deep merge nested objects', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [
|
||||
new MockLoader({ db: { host: 'localhost', port: 5432 } }, 1),
|
||||
new MockLoader({ db: { port: 5433, user: 'admin' } }, 2),
|
||||
],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize();
|
||||
expect(config).toEqual({
|
||||
db: { host: 'localhost', port: 5433, user: 'admin' },
|
||||
environment: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should get value by path', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ db: { host: 'localhost', port: 5432 } })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(mockManager.getValue('db.host')).toBe('localhost');
|
||||
expect(mockManager.getValue('db.port')).toBe(5432);
|
||||
});
|
||||
|
||||
it('should throw for non-existent path', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ db: { host: 'localhost' } })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(() => mockManager.getValue('db.password')).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should check if path exists', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ db: { host: 'localhost' } })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(mockManager.has('db.host')).toBe(true);
|
||||
expect(mockManager.has('db.password')).toBe(false);
|
||||
});
|
||||
|
||||
it('should update config at runtime', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
mockManager.set({ port: 4000 });
|
||||
expect(mockManager.get()).toEqual({ name: 'test', port: 4000, environment: 'test' });
|
||||
});
|
||||
|
||||
it('should validate config update with schema', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize(schema);
|
||||
expect(() => mockManager.set({ port: 'invalid' as any })).toThrow(ConfigValidationError);
|
||||
});
|
||||
|
||||
it('should reset config', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test' })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(mockManager.get()).toEqual({ name: 'test', environment: 'test' });
|
||||
|
||||
mockManager.reset();
|
||||
expect(() => mockManager.get()).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should validate against schema', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
const validated = mockManager.validate(schema);
|
||||
expect(validated).toEqual({ name: 'test', port: 3000 });
|
||||
});
|
||||
|
||||
it('should create typed getter', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
const getTypedConfig = mockManager.createTypedGetter(schema);
|
||||
const config = getTypedConfig();
|
||||
expect(config).toEqual({ name: 'test', port: 3000 });
|
||||
});
|
||||
|
||||
it('should add environment if not present', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
environment: 'test',
|
||||
loaders: [new MockLoader({ name: 'test' })],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize();
|
||||
expect(config).toEqual({ name: 'test', environment: 'test' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Config Service Functions', () => {
|
||||
beforeEach(() => {
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
it('should throw when getting config before initialization', () => {
|
||||
expect(() => getConfig()).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should validate config with schema', () => {
|
||||
// Test that a valid config passes schema validation
|
||||
const mockConfig = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
environment: 'test' as const,
|
||||
service: {
|
||||
name: 'test-service',
|
||||
baseUrl: 'http://localhost:3000',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
mongodb: {
|
||||
uri: 'mongodb://localhost',
|
||||
database: 'test-db',
|
||||
},
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'test-db',
|
||||
user: 'test-user',
|
||||
password: 'test-pass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
},
|
||||
},
|
||||
log: {
|
||||
level: 'info' as const,
|
||||
pretty: true,
|
||||
},
|
||||
queue: {
|
||||
redis: { host: 'localhost', port: 6379 },
|
||||
},
|
||||
};
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new MockLoader(mockConfig)],
|
||||
});
|
||||
|
||||
// Should not throw when initializing with valid config
|
||||
expect(() => manager.initialize(baseAppSchema)).not.toThrow();
|
||||
|
||||
// Verify key properties exist
|
||||
const config = manager.get();
|
||||
expect(config.name).toBe('test-app');
|
||||
expect(config.version).toBe('1.0.0');
|
||||
expect(config.environment).toBe('test');
|
||||
expect(config.service.name).toBe('test-service');
|
||||
expect(config.database.mongodb.uri).toBe('mongodb://localhost');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Config Builders', () => {
|
||||
it('should create app config with schema', () => {
|
||||
const schema = z.object({
|
||||
app: z.string(),
|
||||
version: z.number(),
|
||||
});
|
||||
|
||||
const config = createAppConfig(schema, {
|
||||
loaders: [new MockLoader({ app: 'myapp', version: 1 })],
|
||||
});
|
||||
|
||||
expect(config).toBeDefined();
|
||||
});
|
||||
|
||||
it('should initialize app config in one step', () => {
|
||||
const schema = z.object({
|
||||
app: z.string(),
|
||||
version: z.number(),
|
||||
});
|
||||
|
||||
const config = initializeAppConfig(schema, {
|
||||
loaders: [new MockLoader({ app: 'myapp', version: 1 })],
|
||||
});
|
||||
|
||||
expect(config).toEqual({ app: 'myapp', version: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Environment Helpers', () => {
|
||||
beforeEach(() => {
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
it('should detect environments correctly in ConfigManager', () => {
|
||||
// Test with different environments using mock configs
|
||||
const envConfigs = [{ env: 'development' }, { env: 'production' }, { env: 'test' }];
|
||||
|
||||
for (const { env } of envConfigs) {
|
||||
const mockConfig = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
environment: env as 'development' | 'production' | 'test',
|
||||
service: {
|
||||
name: 'test',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
mongodb: {
|
||||
uri: 'mongodb://localhost',
|
||||
database: 'test-db',
|
||||
},
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'test-db',
|
||||
user: 'test-user',
|
||||
password: 'test-pass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
},
|
||||
},
|
||||
log: {
|
||||
level: 'info' as const,
|
||||
pretty: true,
|
||||
},
|
||||
queue: {
|
||||
redis: { host: 'localhost', port: 6379 },
|
||||
},
|
||||
};
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new MockLoader(mockConfig)],
|
||||
environment: env as any,
|
||||
});
|
||||
|
||||
manager.initialize(baseAppSchema);
|
||||
|
||||
// Test the manager's environment detection
|
||||
expect(manager.getEnvironment()).toBe(env);
|
||||
expect(manager.get().environment).toBe(env);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue