v2.2.0
This commit is contained in:
@@ -270,6 +270,50 @@ describe('InboundBuilderService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildHysteria2Inbound', () => {
|
||||
it('creates 3x-ui hysteria v2 inbound with certificate paths', () => {
|
||||
const result = service.buildHysteria2Inbound({
|
||||
port: 34443,
|
||||
uuid: 'test-auth',
|
||||
sni: 'oil.3dp-manager.com',
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
enable: true,
|
||||
listen: '0.0.0.0',
|
||||
port: 34443,
|
||||
protocol: 'hysteria',
|
||||
tag: 'inbound-34443',
|
||||
});
|
||||
|
||||
const settings = JSON.parse(result.settings);
|
||||
const streamSettings = JSON.parse(result.streamSettings);
|
||||
|
||||
expect(settings.clients[0].auth).toBe('test-auth');
|
||||
expect(settings.version).toBe(2);
|
||||
expect(streamSettings.network).toBe('hysteria');
|
||||
expect(streamSettings.hysteriaSettings.version).toBe(2);
|
||||
expect(streamSettings.finalmask.udp[0].type).toBe('salamander');
|
||||
expect(streamSettings.tlsSettings.certificates[0].certificateFile).toBe(
|
||||
'/etc/letsencrypt/live/oil.3dp-manager.com/fullchain.pem',
|
||||
);
|
||||
expect(streamSettings.tlsSettings.certificates[0].keyFile).toBe(
|
||||
'/etc/letsencrypt/live/oil.3dp-manager.com/privkey.pem',
|
||||
);
|
||||
|
||||
const link = service.buildInboundLink(
|
||||
result as any,
|
||||
'relay.example.com',
|
||||
'fallback-auth',
|
||||
'%F0%9F%92%AF',
|
||||
);
|
||||
expect(link).toContain('hy2://test-auth@relay.example.com:34443/');
|
||||
expect(link).toContain('sni=oil.3dp-manager.com');
|
||||
expect(link).toContain('obfs=salamander');
|
||||
expect(link).toContain('obfs-password=abcd1234');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildInboundLink', () => {
|
||||
const baseInbound = {
|
||||
protocol: 'vless',
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Repository } from 'typeorm';
|
||||
import { NodesService } from 'src/nodes/nodes.service';
|
||||
import { Node } from 'src/nodes/entities/node.entity';
|
||||
import { Subscription } from 'src/subscriptions/entities/subscription.entity';
|
||||
import { Tunnel } from 'src/tunnels/entities/tunnel.entity';
|
||||
import { Inbound } from 'src/inbounds/entities/inbound.entity';
|
||||
import { XuiService } from 'src/xui/xui.service';
|
||||
|
||||
describe('NodesService', () => {
|
||||
const createNodeRepo = (getOne: jest.Mock) => ({
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
getOne,
|
||||
})),
|
||||
count: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
save: jest.fn(),
|
||||
});
|
||||
|
||||
const createService = (nodeRepo: ReturnType<typeof createNodeRepo>) => {
|
||||
const subscriptionsRepo = {
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
execute: jest.fn().mockResolvedValue({}),
|
||||
})),
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
save: jest.fn(),
|
||||
};
|
||||
const tunnelsRepo = {
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
execute: jest.fn().mockResolvedValue({}),
|
||||
})),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
const inboundsRepo = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
const xuiService = {
|
||||
deleteInbound: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
return {
|
||||
service: new NodesService(
|
||||
nodeRepo as unknown as Repository<Node>,
|
||||
subscriptionsRepo as unknown as Repository<Subscription>,
|
||||
tunnelsRepo as unknown as Repository<Tunnel>,
|
||||
inboundsRepo as unknown as Repository<Inbound>,
|
||||
xuiService as unknown as XuiService,
|
||||
),
|
||||
subscriptionsRepo,
|
||||
tunnelsRepo,
|
||||
inboundsRepo,
|
||||
xuiService,
|
||||
};
|
||||
};
|
||||
|
||||
it('deletes the main node and makes the next node main', async () => {
|
||||
const mainNode = { id: 'main', isMain: true } as Node;
|
||||
const nextNode = { id: 'next', isMain: false } as Node;
|
||||
const getOne = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mainNode)
|
||||
.mockResolvedValueOnce(null);
|
||||
const nodeRepo = createNodeRepo(getOne);
|
||||
nodeRepo.count.mockResolvedValue(2);
|
||||
nodeRepo.findOne.mockResolvedValue(nextNode);
|
||||
nodeRepo.save.mockResolvedValue(nextNode);
|
||||
const { service } = createService(nodeRepo);
|
||||
|
||||
const result = await service.remove('main');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(nodeRepo.remove).toHaveBeenCalledWith(mainNode);
|
||||
expect(nodeRepo.findOne).toHaveBeenCalledWith({
|
||||
where: {},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
expect(nextNode.isMain).toBe(true);
|
||||
expect(nodeRepo.save).toHaveBeenCalledWith(nextNode);
|
||||
});
|
||||
|
||||
it('uses node credentials when deleting node inbounds', async () => {
|
||||
const mainNode = { id: 'main', isMain: true } as Node;
|
||||
const getOne = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mainNode)
|
||||
.mockResolvedValueOnce(null);
|
||||
const nodeRepo = createNodeRepo(getOne);
|
||||
nodeRepo.count.mockResolvedValue(1);
|
||||
nodeRepo.findOne.mockResolvedValue(null);
|
||||
const { service, inboundsRepo, xuiService } = createService(nodeRepo);
|
||||
inboundsRepo.find.mockResolvedValue([{ id: 1, xuiId: 101, nodeId: 'main' }]);
|
||||
|
||||
await service.remove('main');
|
||||
|
||||
expect(xuiService.deleteInbound).toHaveBeenCalledWith(101, mainNode);
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,8 @@ import { Domain } from 'src/domains/entities/domain.entity';
|
||||
import { Setting } from 'src/settings/entities/setting.entity';
|
||||
import { XuiService } from 'src/xui/xui.service';
|
||||
import { InboundBuilderService } from 'src/inbounds/inbound-builder.service';
|
||||
import { Node } from 'src/nodes/entities/node.entity';
|
||||
import { Tunnel } from 'src/tunnels/entities/tunnel.entity';
|
||||
|
||||
// Mock @nestjs/schedule для тестирования Cron
|
||||
jest.mock('@nestjs/schedule', () => ({
|
||||
@@ -64,6 +66,19 @@ describe('RotationService', () => {
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const mockNodeRepo = {
|
||||
findOne: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
getOne: jest.fn(),
|
||||
})),
|
||||
};
|
||||
|
||||
const mockTunnelRepo = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const mockXuiService = {
|
||||
login: jest.fn(),
|
||||
deleteInbound: jest.fn(),
|
||||
@@ -79,6 +94,7 @@ describe('RotationService', () => {
|
||||
buildVmessTcp: jest.fn(),
|
||||
buildShadowsocksTcp: jest.fn(),
|
||||
buildTrojanRealityTcp: jest.fn(),
|
||||
buildHysteria2Inbound: jest.fn(),
|
||||
buildHysteria2Link: jest.fn(),
|
||||
buildInboundLink: jest.fn(),
|
||||
};
|
||||
@@ -103,6 +119,14 @@ describe('RotationService', () => {
|
||||
provide: getRepositoryToken(Setting),
|
||||
useValue: mockSettingRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Node),
|
||||
useValue: mockNodeRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Tunnel),
|
||||
useValue: mockTunnelRepo,
|
||||
},
|
||||
{
|
||||
provide: XuiService,
|
||||
useValue: mockXuiService,
|
||||
@@ -129,6 +153,10 @@ describe('RotationService', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockXuiService.deleteInbound.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe('onModuleInit', () => {
|
||||
it('должен инициализировать настройки по умолчанию', async () => {
|
||||
mockSettingRepo.findOne.mockResolvedValue(null);
|
||||
@@ -437,7 +465,7 @@ describe('RotationService', () => {
|
||||
{ id: 1, name: 'ya.ru', isEnabled: true },
|
||||
]);
|
||||
|
||||
expect(xuiService.deleteInbound).toHaveBeenCalledWith(101);
|
||||
expect(xuiService.deleteInbound).toHaveBeenCalledWith(101, undefined);
|
||||
expect(inboundRepo.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -468,6 +496,7 @@ describe('RotationService', () => {
|
||||
id: '1',
|
||||
uuid: 'uuid-1',
|
||||
isEnabled: true,
|
||||
node: { id: 'node-1', domain: 'node.example.com' },
|
||||
inbounds: [],
|
||||
inboundsConfig: [{ type: 'hysteria2-udp', sni: 'ya.ru' }],
|
||||
};
|
||||
@@ -475,15 +504,59 @@ describe('RotationService', () => {
|
||||
mockDomainRepo.find.mockResolvedValue([
|
||||
{ id: 1, name: 'ya.ru', isEnabled: true },
|
||||
]);
|
||||
mockInboundBuilder.buildHysteria2Link.mockReturnValue('hy2://link');
|
||||
mockXuiService.getNewX25519Cert.mockResolvedValue({
|
||||
privateKey: 'key',
|
||||
publicKey: 'pub',
|
||||
});
|
||||
mockInboundBuilder.buildHysteria2Inbound.mockReturnValue({
|
||||
protocol: 'hysteria2',
|
||||
remark: 'hysteria2-udp',
|
||||
settings: '{"clients":[{"password":"uuid"}]}',
|
||||
streamSettings: '{"tlsSettings":{"serverName":"ya.ru"}}',
|
||||
sniffing: '{}',
|
||||
});
|
||||
mockXuiService.addInbound.mockResolvedValue(101);
|
||||
mockInboundBuilder.buildInboundLink.mockReturnValue('hy2://link');
|
||||
mockInboundRepo.save.mockResolvedValue({});
|
||||
|
||||
await (service as any).rotateSubscription(mockSub, [
|
||||
{ id: 1, name: 'ya.ru', isEnabled: true },
|
||||
]);
|
||||
|
||||
expect(xuiService.addInbound).not.toHaveBeenCalled();
|
||||
expect(inboundBuilder.buildHysteria2Link).toHaveBeenCalled();
|
||||
expect(xuiService.addInbound).toHaveBeenCalled();
|
||||
expect(inboundBuilder.buildHysteria2Inbound).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sni: 'node.example.com' }),
|
||||
);
|
||||
expect(inboundBuilder.buildInboundLink).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not save hysteria2 when 3x-ui does not create an inbound', async () => {
|
||||
const mockSub = {
|
||||
id: '1',
|
||||
uuid: 'uuid-1',
|
||||
isEnabled: true,
|
||||
inbounds: [],
|
||||
inboundsConfig: [{ type: 'hysteria2-udp', sni: 'ya.ru' }],
|
||||
};
|
||||
|
||||
mockXuiService.getNewX25519Cert.mockResolvedValue({
|
||||
privateKey: 'key',
|
||||
publicKey: 'pub',
|
||||
});
|
||||
mockInboundBuilder.buildHysteria2Inbound.mockReturnValue({
|
||||
protocol: 'hysteria2',
|
||||
remark: 'hysteria2-udp',
|
||||
settings: '{"clients":[{"password":"uuid"}]}',
|
||||
streamSettings: '{"tlsSettings":{"serverName":"ya.ru"}}',
|
||||
sniffing: '{}',
|
||||
});
|
||||
mockXuiService.addInbound.mockResolvedValue(null);
|
||||
|
||||
await (service as any).rotateSubscription(mockSub, [
|
||||
{ id: 1, name: 'ya.ru', isEnabled: true },
|
||||
]);
|
||||
|
||||
expect(inboundRepo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('должен использовать случайный порт, если указано random', async () => {
|
||||
|
||||
@@ -8,6 +8,8 @@ import { SubscriptionsService } from 'src/subscriptions/subscriptions.service';
|
||||
import { Subscription } from 'src/subscriptions/entities/subscription.entity';
|
||||
import { XuiService } from 'src/xui/xui.service';
|
||||
import { CreateSubscriptionDto } from 'src/subscriptions/dto/create-subscription.dto';
|
||||
import { Node } from 'src/nodes/entities/node.entity';
|
||||
import { Tunnel } from 'src/tunnels/entities/tunnel.entity';
|
||||
|
||||
describe('SubscriptionsService', () => {
|
||||
let service: SubscriptionsService;
|
||||
@@ -23,6 +25,19 @@ describe('SubscriptionsService', () => {
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
const mockNodeRepo = {
|
||||
findOne: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
getOne: jest.fn(),
|
||||
})),
|
||||
};
|
||||
|
||||
const mockTunnelRepo = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const mockXuiService = {
|
||||
deleteInbound: jest.fn(),
|
||||
};
|
||||
@@ -35,6 +50,14 @@ describe('SubscriptionsService', () => {
|
||||
provide: getRepositoryToken(Subscription),
|
||||
useValue: mockSubRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Node),
|
||||
useValue: mockNodeRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Tunnel),
|
||||
useValue: mockTunnelRepo,
|
||||
},
|
||||
{
|
||||
provide: XuiService,
|
||||
useValue: mockXuiService,
|
||||
@@ -47,6 +70,7 @@ describe('SubscriptionsService', () => {
|
||||
getRepositoryToken(Subscription),
|
||||
);
|
||||
xuiService = module.get<XuiService>(XuiService);
|
||||
mockXuiService.deleteInbound.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -86,7 +110,7 @@ describe('SubscriptionsService', () => {
|
||||
|
||||
expect(result).toEqual(mockSubs);
|
||||
expect(subRepo.find).toHaveBeenCalledWith({
|
||||
relations: ['inbounds'],
|
||||
relations: ['inbounds', 'node', 'relayServer'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
});
|
||||
@@ -128,6 +152,8 @@ describe('SubscriptionsService', () => {
|
||||
uuid: expect.any(String),
|
||||
inboundsConfig: createDto.inboundsConfig,
|
||||
isAutoRotationEnabled: true,
|
||||
node: null,
|
||||
relayServer: null,
|
||||
});
|
||||
expect(subRepo.save).toHaveBeenCalledWith(mockSubscription);
|
||||
expect(result).toEqual(mockSubscription);
|
||||
@@ -197,7 +223,7 @@ describe('SubscriptionsService', () => {
|
||||
|
||||
expect(subRepo.findOne).toHaveBeenCalledWith({
|
||||
where: { id: 'test-id' },
|
||||
relations: ['inbounds'],
|
||||
relations: ['inbounds', 'node', 'relayServer'],
|
||||
});
|
||||
expect(subRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'New Name' }),
|
||||
@@ -329,8 +355,8 @@ describe('SubscriptionsService', () => {
|
||||
|
||||
await service.remove('test-id');
|
||||
|
||||
expect(xuiService.deleteInbound).toHaveBeenCalledWith(101);
|
||||
expect(xuiService.deleteInbound).toHaveBeenCalledWith(102);
|
||||
expect(xuiService.deleteInbound).toHaveBeenCalledWith(101, undefined);
|
||||
expect(xuiService.deleteInbound).toHaveBeenCalledWith(102, undefined);
|
||||
expect(subRepo.remove).toHaveBeenCalledWith(subWithInbounds);
|
||||
});
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('TunnelsController', () => {
|
||||
|
||||
await controller.remove('1');
|
||||
|
||||
expect(tunnelsService.remove).toHaveBeenCalledWith(1);
|
||||
expect(tunnelsService.remove).toHaveBeenCalledWith(1, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ import { TunnelsService } from 'src/tunnels/tunnels.service';
|
||||
import { Tunnel } from 'src/tunnels/entities/tunnel.entity';
|
||||
import { SshService } from 'src/tunnels/ssh.service';
|
||||
import { Setting } from 'src/settings/entities/setting.entity';
|
||||
import { Node } from 'src/nodes/entities/node.entity';
|
||||
import { Subscription } from 'src/subscriptions/entities/subscription.entity';
|
||||
|
||||
describe('TunnelsService', () => {
|
||||
let service: TunnelsService;
|
||||
@@ -34,6 +36,15 @@ describe('TunnelsService', () => {
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const mockNodeRepo = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const mockSubscriptionRepo = {
|
||||
find: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const mockSshService = {
|
||||
executeCommand: jest.fn(),
|
||||
};
|
||||
@@ -50,6 +61,14 @@ describe('TunnelsService', () => {
|
||||
provide: getRepositoryToken(Setting),
|
||||
useValue: mockSettingRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Node),
|
||||
useValue: mockNodeRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Subscription),
|
||||
useValue: mockSubscriptionRepo,
|
||||
},
|
||||
{
|
||||
provide: SshService,
|
||||
useValue: mockSshService,
|
||||
@@ -64,21 +83,27 @@ describe('TunnelsService', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('должен создать туннель', async () => {
|
||||
const dto = { ip: '192.168.1.1', sshPort: 22, username: 'root' };
|
||||
const mockTunnel = { id: 1, ...dto };
|
||||
const node = { id: 'node-1', isMain: true };
|
||||
const mockTunnel = { id: 1, ...dto, node, nodeId: node.id };
|
||||
|
||||
mockNodeRepo.findOne.mockResolvedValue(node);
|
||||
mockTunnelRepo.create.mockReturnValue(mockTunnel);
|
||||
mockTunnelRepo.save.mockResolvedValue(mockTunnel);
|
||||
|
||||
const result = await service.create(dto);
|
||||
|
||||
expect(result).toEqual(mockTunnel);
|
||||
expect(tunnelRepo.create).toHaveBeenCalledWith(dto);
|
||||
expect(tunnelRepo.create).toHaveBeenCalledWith({
|
||||
...dto,
|
||||
node,
|
||||
nodeId: node.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,12 +119,13 @@ describe('TunnelsService', () => {
|
||||
const result = await service.findAll();
|
||||
|
||||
expect(result).toEqual(mockTunnels);
|
||||
expect(tunnelRepo.find).toHaveBeenCalledTimes(1);
|
||||
expect(tunnelRepo.find).toHaveBeenCalledWith({ relations: ['node'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('должен удалить туннель по ID', async () => {
|
||||
mockSubscriptionRepo.find.mockResolvedValue([]);
|
||||
mockTunnelRepo.delete.mockResolvedValue({ affected: 1 });
|
||||
|
||||
await service.remove(1);
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('XuiService', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
@@ -149,7 +149,16 @@ describe('XuiService', () => {
|
||||
|
||||
describe('deleteInbound', () => {
|
||||
it('должен удалить инбаунд', async () => {
|
||||
mockAxiosInstance.post.mockResolvedValue({ data: { success: true } });
|
||||
mockSettingsRepo.find.mockResolvedValue([
|
||||
{ key: 'xui_url', value: 'http://localhost:3100' },
|
||||
{ key: 'xui_login', value: 'admin' },
|
||||
{ key: 'xui_password', value: 'password' },
|
||||
]);
|
||||
mockAxiosInstance.post
|
||||
.mockResolvedValueOnce({
|
||||
headers: { 'set-cookie': ['session=abc123'] },
|
||||
})
|
||||
.mockResolvedValueOnce({ data: { success: true } });
|
||||
|
||||
await service.deleteInbound(101);
|
||||
|
||||
@@ -159,16 +168,35 @@ describe('XuiService', () => {
|
||||
});
|
||||
|
||||
it('должен обработать ошибку удаления', async () => {
|
||||
mockAxiosInstance.post.mockRejectedValue(new Error('Not found'));
|
||||
mockSettingsRepo.find.mockResolvedValue([
|
||||
{ key: 'xui_url', value: 'http://localhost:3100' },
|
||||
{ key: 'xui_login', value: 'admin' },
|
||||
{ key: 'xui_password', value: 'password' },
|
||||
]);
|
||||
mockAxiosInstance.post
|
||||
.mockResolvedValueOnce({
|
||||
headers: { 'set-cookie': ['session=abc123'] },
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('Not found'));
|
||||
|
||||
await service.deleteInbound(999);
|
||||
|
||||
expect(mockAxiosInstance.post).toHaveBeenCalled();
|
||||
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
|
||||
'/panel/api/inbounds/del/999',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNewX25519Cert', () => {
|
||||
it('должен получить Reality ключи', async () => {
|
||||
mockSettingsRepo.find.mockResolvedValue([
|
||||
{ key: 'xui_url', value: 'http://localhost:3100' },
|
||||
{ key: 'xui_login', value: 'admin' },
|
||||
{ key: 'xui_password', value: 'password' },
|
||||
]);
|
||||
mockAxiosInstance.post.mockResolvedValueOnce({
|
||||
headers: { 'set-cookie': ['session=abc123'] },
|
||||
});
|
||||
mockAxiosInstance.get.mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user