546 lines
15 KiB
TypeScript
546 lines
15 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||
|
||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||
|
||
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { InboundBuilderService } from 'src/inbounds/inbound-builder.service';
|
||
|
||
// Mock crypto.randomBytes для детерминированных тестов
|
||
jest.mock('crypto', () => ({
|
||
randomBytes: jest.fn().mockReturnValue(Buffer.from('abcd1234', 'hex')),
|
||
randomFillSync: jest.fn((buffer: Buffer) => {
|
||
for (let i = 0; i < buffer.length; i++) {
|
||
buffer[i] = i;
|
||
}
|
||
return buffer;
|
||
}),
|
||
}));
|
||
|
||
describe('InboundBuilderService', () => {
|
||
let service: InboundBuilderService;
|
||
|
||
beforeEach(async () => {
|
||
const module: TestingModule = await Test.createTestingModule({
|
||
providers: [InboundBuilderService],
|
||
}).compile();
|
||
|
||
service = module.get<InboundBuilderService>(InboundBuilderService);
|
||
});
|
||
|
||
afterEach(() => {
|
||
jest.clearAllMocks();
|
||
});
|
||
|
||
describe('buildVlessRealityTcp', () => {
|
||
const params = {
|
||
port: 443,
|
||
uuid: 'test-uuid-123',
|
||
sni: 'ya.ru',
|
||
privateKey: 'private-key',
|
||
publicKey: 'public-key',
|
||
};
|
||
|
||
it('должен создать конфиг vless-tcp-reality', () => {
|
||
const result = service.buildVlessRealityTcp(params);
|
||
|
||
expect(result).toMatchObject({
|
||
enable: true,
|
||
port: 443,
|
||
protocol: 'vless',
|
||
remark: 'vless-tcp-reality',
|
||
});
|
||
|
||
const settings = JSON.parse(result.settings);
|
||
expect(settings.clients[0].id).toBe('test-uuid-123');
|
||
expect(settings.clients[0].flow).toBe('xtls-rprx-vision');
|
||
});
|
||
|
||
it('должен установить Reality настройки', () => {
|
||
const result = service.buildVlessRealityTcp(params);
|
||
|
||
const streamSettings = JSON.parse(result.streamSettings);
|
||
expect(streamSettings.security).toBe('reality');
|
||
expect(streamSettings.realitySettings.target).toBe('ya.ru:443');
|
||
expect(streamSettings.realitySettings.serverNames).toContain('ya.ru');
|
||
expect(streamSettings.realitySettings.privateKey).toBe('private-key');
|
||
});
|
||
|
||
it('должен сгенерировать shortIds', () => {
|
||
const result = service.buildVlessRealityTcp(params);
|
||
|
||
const streamSettings = JSON.parse(result.streamSettings);
|
||
expect(streamSettings.realitySettings.shortIds).toHaveLength(2);
|
||
});
|
||
});
|
||
|
||
describe('buildVlessRealityXhttp', () => {
|
||
const params = {
|
||
port: 8443,
|
||
uuid: 'test-uuid-456',
|
||
sni: 'vk.com',
|
||
privateKey: 'private-key',
|
||
publicKey: 'public-key',
|
||
};
|
||
|
||
it('должен создать конфиг vless-xhttp-reality', () => {
|
||
const result = service.buildVlessRealityXhttp(params);
|
||
|
||
expect(result).toMatchObject({
|
||
enable: true,
|
||
port: 8443,
|
||
protocol: 'vless',
|
||
remark: 'vless-xhttp-reality',
|
||
});
|
||
|
||
const settings = JSON.parse(result.settings);
|
||
expect(settings.clients[0].id).toBe('test-uuid-456');
|
||
expect(settings.clients[0].flow).toBe('');
|
||
});
|
||
|
||
it('должен установить xhttp настройки', () => {
|
||
const result = service.buildVlessRealityXhttp(params);
|
||
|
||
const streamSettings = JSON.parse(result.streamSettings);
|
||
expect(streamSettings.security).toBe('reality');
|
||
expect(streamSettings.network).toBe('xhttp');
|
||
});
|
||
});
|
||
|
||
describe('buildVlessRealityGrpc', () => {
|
||
const params = {
|
||
port: 2053,
|
||
uuid: 'test-uuid-789',
|
||
sni: 'ok.ru',
|
||
privateKey: 'private-key',
|
||
publicKey: 'public-key',
|
||
};
|
||
|
||
it('должен создать конфиг vless-grpc-reality', () => {
|
||
const result = service.buildVlessRealityGrpc(params);
|
||
|
||
expect(result).toMatchObject({
|
||
enable: true,
|
||
port: 2053,
|
||
protocol: 'vless',
|
||
remark: 'vless-grpc-reality',
|
||
});
|
||
|
||
const streamSettings = JSON.parse(result.streamSettings);
|
||
expect(streamSettings.network).toBe('grpc');
|
||
expect(streamSettings.grpcSettings?.serviceName).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
describe('buildVlessWs', () => {
|
||
const params = {
|
||
port: 10000,
|
||
uuid: 'test-uuid-ws',
|
||
sni: 'ozon.ru',
|
||
};
|
||
|
||
it('должен создать конфиг vless-ws', () => {
|
||
const result = service.buildVlessWs(params);
|
||
|
||
expect(result).toMatchObject({
|
||
enable: true,
|
||
port: 10000,
|
||
protocol: 'vless',
|
||
remark: 'vless-ws',
|
||
});
|
||
|
||
const streamSettings = JSON.parse(result.streamSettings);
|
||
expect(streamSettings.network).toBe('ws');
|
||
});
|
||
|
||
it('должен установить ws настройки', () => {
|
||
const result = service.buildVlessWs(params);
|
||
|
||
const streamSettings = JSON.parse(result.streamSettings);
|
||
expect(streamSettings.wsSettings?.path).toBe('/');
|
||
});
|
||
});
|
||
|
||
describe('buildVmessTcp', () => {
|
||
const params = {
|
||
port: 20000,
|
||
uuid: 'test-uuid-vmess',
|
||
};
|
||
|
||
it('должен создать конфиг vmess-tcp', () => {
|
||
const result = service.buildVmessTcp(params);
|
||
|
||
expect(result).toMatchObject({
|
||
enable: true,
|
||
port: 20000,
|
||
protocol: 'vmess',
|
||
remark: 'vmess-tcp',
|
||
});
|
||
|
||
const settings = JSON.parse(result.settings);
|
||
expect(settings.clients[0].id).toBe('test-uuid-vmess');
|
||
});
|
||
});
|
||
|
||
describe('buildShadowsocksTcp', () => {
|
||
const params = {
|
||
port: 30000,
|
||
uuid: 'test-uuid-ss',
|
||
};
|
||
|
||
it('должен создать конфиг shadowsocks-tcp', () => {
|
||
const result = service.buildShadowsocksTcp(params);
|
||
|
||
expect(result).toMatchObject({
|
||
enable: true,
|
||
port: 30000,
|
||
protocol: 'shadowsocks',
|
||
remark: 'shadowsocks-tcp',
|
||
});
|
||
|
||
const settings = JSON.parse(result.settings);
|
||
expect(settings.method).toBe('2022-blake3-aes-256-gcm');
|
||
expect(settings.password).toBeTruthy(); // Генерируется из uuid
|
||
});
|
||
});
|
||
|
||
describe('buildTrojanRealityTcp', () => {
|
||
const params = {
|
||
port: 443,
|
||
uuid: 'test-uuid-trojan',
|
||
sni: 'ya.ru',
|
||
privateKey: 'private-key',
|
||
publicKey: 'public-key',
|
||
};
|
||
|
||
it('должен создать конфиг trojan-tcp-reality', () => {
|
||
const result = service.buildTrojanRealityTcp(params);
|
||
|
||
expect(result).toMatchObject({
|
||
enable: true,
|
||
port: 443,
|
||
protocol: 'trojan',
|
||
remark: 'trojan-tcp-reality',
|
||
});
|
||
|
||
const settings = JSON.parse(result.settings);
|
||
expect(settings.clients[0].password).toBeTruthy(); // Генерируется из uuid
|
||
});
|
||
|
||
it('должен установить Reality настройки для trojan', () => {
|
||
const result = service.buildTrojanRealityTcp(params);
|
||
|
||
const streamSettings = JSON.parse(result.streamSettings);
|
||
expect(streamSettings.security).toBe('reality');
|
||
expect(streamSettings.realitySettings.target).toBe('ya.ru:443');
|
||
});
|
||
});
|
||
|
||
describe('buildHysteria2Link', () => {
|
||
beforeEach(() => {
|
||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||
});
|
||
|
||
afterEach(() => {
|
||
jest.restoreAllMocks();
|
||
});
|
||
|
||
it('должен создать ссылку hysteria2', () => {
|
||
const result = service.buildHysteria2Link(
|
||
'192.168.1.1',
|
||
'ya.ru',
|
||
'%F0%9F%92%AF%20hysteria2',
|
||
);
|
||
|
||
expect(result).toContain('hy2://');
|
||
expect(result).toContain('192.168.1.1');
|
||
// SNI может быть IP, если конфиг не найден
|
||
});
|
||
|
||
it('должен создать ссылку hysteria2 с портом', () => {
|
||
const result = service.buildHysteria2Link(
|
||
'192.168.1.1',
|
||
'ya.ru',
|
||
'%F0%9F%92%AF%20hysteria2',
|
||
443,
|
||
);
|
||
|
||
expect(result).toContain(':443');
|
||
});
|
||
});
|
||
|
||
describe('buildInboundLink', () => {
|
||
const baseInbound = {
|
||
protocol: 'vless',
|
||
port: 443,
|
||
remark: 'vless-tcp-reality',
|
||
settings: JSON.stringify({
|
||
clients: [{ id: 'test-uuid', flow: 'xtls-rprx-vision' }],
|
||
}),
|
||
streamSettings: JSON.stringify({
|
||
network: 'tcp',
|
||
security: 'reality',
|
||
realitySettings: {
|
||
serverNames: ['ya.ru'],
|
||
publicKey: 'public-key',
|
||
},
|
||
}),
|
||
};
|
||
|
||
it('должен создать ссылку vless reality', () => {
|
||
const result = service.buildInboundLink(
|
||
baseInbound as any,
|
||
'192.168.1.1',
|
||
'test-uuid',
|
||
'%F0%9F%92%AF',
|
||
);
|
||
|
||
expect(result).toContain('vless://');
|
||
expect(result).toContain('192.168.1.1');
|
||
expect(result).toContain('443');
|
||
});
|
||
|
||
it('должен создать ссылку vless reality с xhttp', () => {
|
||
const inbound = {
|
||
protocol: 'vless',
|
||
port: 8443,
|
||
remark: 'vless-xhttp-reality',
|
||
settings: JSON.stringify({
|
||
clients: [{ id: 'test-uuid', flow: '' }],
|
||
}),
|
||
streamSettings: JSON.stringify({
|
||
network: 'xhttp',
|
||
security: 'reality',
|
||
realitySettings: {
|
||
serverNames: ['ya.ru'],
|
||
publicKey: 'public-key',
|
||
settings: { publicKey: 'pk', fingerprint: 'random' },
|
||
shortIds: ['abc123'],
|
||
},
|
||
xhttpSettings: {
|
||
path: '/path',
|
||
host: 'ya.ru',
|
||
mode: 'auto',
|
||
},
|
||
}),
|
||
};
|
||
|
||
const result = service.buildInboundLink(
|
||
inbound as any,
|
||
'192.168.1.1',
|
||
'test-uuid',
|
||
'%F0%9F%92%AF',
|
||
);
|
||
|
||
expect(result).toContain('vless://');
|
||
expect(result).toContain('type=xhttp');
|
||
expect(result).toContain('path=%2Fpath');
|
||
});
|
||
|
||
it('должен создать ссылку vless reality с grpc', () => {
|
||
const inbound = {
|
||
protocol: 'vless',
|
||
port: 8443,
|
||
remark: 'vless-grpc-reality',
|
||
settings: JSON.stringify({
|
||
clients: [{ id: 'test-uuid', flow: '' }],
|
||
}),
|
||
streamSettings: JSON.stringify({
|
||
network: 'grpc',
|
||
security: 'reality',
|
||
realitySettings: {
|
||
serverNames: ['ya.ru'],
|
||
publicKey: 'public-key',
|
||
settings: { publicKey: 'pk', fingerprint: 'random' },
|
||
shortIds: ['abc123'],
|
||
},
|
||
grpcSettings: {
|
||
serviceName: 'grpc-service',
|
||
authority: 'ya.ru',
|
||
},
|
||
}),
|
||
};
|
||
|
||
const result = service.buildInboundLink(
|
||
inbound as any,
|
||
'192.168.1.1',
|
||
'test-uuid',
|
||
'%F0%9F%92%AF',
|
||
);
|
||
|
||
expect(result).toContain('vless://');
|
||
expect(result).toContain('type=grpc');
|
||
expect(result).toContain('serviceName=grpc-service');
|
||
});
|
||
|
||
it('должен создать ссылку vless с ws', () => {
|
||
const inbound = {
|
||
protocol: 'vless',
|
||
port: 443,
|
||
remark: 'vless-ws',
|
||
settings: JSON.stringify({
|
||
clients: [{ id: 'test-uuid' }],
|
||
}),
|
||
streamSettings: JSON.stringify({
|
||
network: 'ws',
|
||
security: 'tls',
|
||
wsSettings: {
|
||
path: '/ws',
|
||
headers: { Host: 'example.com' },
|
||
},
|
||
}),
|
||
};
|
||
|
||
const result = service.buildInboundLink(
|
||
inbound as any,
|
||
'192.168.1.1',
|
||
'test-uuid',
|
||
'%F0%9F%92%AF',
|
||
);
|
||
|
||
expect(result).toContain('vless://');
|
||
expect(result).toContain('type=ws');
|
||
expect(result).toContain('path=%2Fws');
|
||
});
|
||
|
||
it('должен создать ссылку vmess', () => {
|
||
const vmessInbound = {
|
||
protocol: 'vmess',
|
||
port: 20000,
|
||
remark: 'vmess-tcp',
|
||
settings: JSON.stringify({
|
||
clients: [{ id: 'test-uuid' }],
|
||
}),
|
||
streamSettings: JSON.stringify({
|
||
network: 'tcp',
|
||
}),
|
||
};
|
||
|
||
const result = service.buildInboundLink(
|
||
vmessInbound as any,
|
||
'192.168.1.1',
|
||
'test-uuid',
|
||
'%F0%9F%92%AF',
|
||
);
|
||
|
||
expect(result).toContain('vmess://');
|
||
});
|
||
|
||
it('должен создать ссылку shadowsocks', () => {
|
||
const ssInbound = {
|
||
protocol: 'shadowsocks',
|
||
port: 30000,
|
||
remark: 'shadowsocks-tcp',
|
||
settings: JSON.stringify({
|
||
method: '2022-blake3-aes-256-gcm',
|
||
password: 'test-password',
|
||
clients: [{ password: 'client-pass' }],
|
||
}),
|
||
streamSettings: JSON.stringify({
|
||
network: 'tcp',
|
||
}),
|
||
};
|
||
|
||
const result = service.buildInboundLink(
|
||
ssInbound as any,
|
||
'192.168.1.1',
|
||
'',
|
||
'%F0%9F%92%AF',
|
||
);
|
||
|
||
expect(result).toContain('ss://');
|
||
});
|
||
|
||
it('должен создать ссылку trojan', () => {
|
||
const trojanInbound = {
|
||
protocol: 'trojan',
|
||
port: 443,
|
||
remark: 'trojan-reality',
|
||
settings: JSON.stringify({
|
||
clients: [{ password: 'trojan-pass' }],
|
||
}),
|
||
streamSettings: JSON.stringify({
|
||
network: 'tcp',
|
||
security: 'reality',
|
||
realitySettings: {
|
||
serverNames: ['ya.ru'],
|
||
publicKey: 'public-key',
|
||
settings: { publicKey: 'pk', fingerprint: 'random' },
|
||
shortIds: ['abc123'],
|
||
},
|
||
}),
|
||
};
|
||
|
||
const result = service.buildInboundLink(
|
||
trojanInbound as any,
|
||
'192.168.1.1',
|
||
'trojan-pass',
|
||
'%F0%9F%92%AF',
|
||
);
|
||
|
||
expect(result).toContain('trojan://');
|
||
expect(result).toContain('security=reality');
|
||
});
|
||
|
||
it('должен вернуть пустую строку для trojan без reality', () => {
|
||
const trojanInbound = {
|
||
protocol: 'trojan',
|
||
port: 443,
|
||
remark: 'trojan',
|
||
settings: JSON.stringify({
|
||
clients: [{ password: 'pass' }],
|
||
}),
|
||
streamSettings: JSON.stringify({
|
||
network: 'tcp',
|
||
security: 'none',
|
||
}),
|
||
};
|
||
|
||
const result = service.buildInboundLink(
|
||
trojanInbound as any,
|
||
'192.168.1.1',
|
||
'pass',
|
||
'%F0%9F%92%AF',
|
||
);
|
||
|
||
expect(result).toBe('');
|
||
});
|
||
|
||
it('должен вернуть пустую строку для vless без reality settings', () => {
|
||
const vlessInbound = {
|
||
protocol: 'vless',
|
||
port: 443,
|
||
remark: 'vless',
|
||
settings: JSON.stringify({
|
||
clients: [{ id: 'uuid' }],
|
||
}),
|
||
streamSettings: JSON.stringify({
|
||
network: 'tcp',
|
||
security: 'reality',
|
||
realitySettings: null,
|
||
}),
|
||
};
|
||
|
||
const result = service.buildInboundLink(
|
||
vlessInbound as any,
|
||
'192.168.1.1',
|
||
'uuid',
|
||
'%F0%9F%92%AF',
|
||
);
|
||
|
||
expect(result).toBe('');
|
||
});
|
||
});
|
||
|
||
describe('generateUuid', () => {
|
||
it('должен сгенерировать UUID', () => {
|
||
const uuid = service.generateUuid();
|
||
|
||
expect(uuid).toMatch(
|
||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||
);
|
||
});
|
||
});
|
||
});
|