back tests 80+
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SubscriptionsController } from 'src/subscriptions/subscriptions.controller';
|
||||
import { SubscriptionsService } from 'src/subscriptions/subscriptions.service';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { CreateSubscriptionDto } from 'src/subscriptions/dto/create-subscription.dto';
|
||||
import { UpdateSubscriptionDto } from 'src/subscriptions/dto/update-subscription.dto';
|
||||
|
||||
describe('SubscriptionsController', () => {
|
||||
let controller: SubscriptionsController;
|
||||
let service: SubscriptionsService;
|
||||
|
||||
const mockSubscriptionsService = {
|
||||
findAll: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SubscriptionsController],
|
||||
providers: [
|
||||
{
|
||||
provide: SubscriptionsService,
|
||||
useValue: mockSubscriptionsService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SubscriptionsController>(SubscriptionsController);
|
||||
service = module.get<SubscriptionsService>(SubscriptionsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('должен вернуть все подписки', async () => {
|
||||
const mockSubs = [
|
||||
{ id: '1', name: 'Sub 1', uuid: 'uuid-1' },
|
||||
{ id: '2', name: 'Sub 2', uuid: 'uuid-2' },
|
||||
];
|
||||
mockSubscriptionsService.findAll.mockResolvedValue(mockSubs);
|
||||
|
||||
const result = await controller.findAll();
|
||||
|
||||
expect(result).toEqual(mockSubs);
|
||||
expect(service.findAll).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
const createDto: CreateSubscriptionDto = {
|
||||
name: 'Test Subscription',
|
||||
inboundsConfig: [
|
||||
{ type: 'vless-tcp-reality', port: 443, sni: 'example.com' },
|
||||
],
|
||||
isAutoRotationEnabled: true,
|
||||
};
|
||||
|
||||
it('должен создать подписку', async () => {
|
||||
const mockSubscription = { id: 'new-id', ...createDto };
|
||||
mockSubscriptionsService.create.mockResolvedValue(mockSubscription);
|
||||
|
||||
const result = await controller.create(createDto);
|
||||
|
||||
expect(result).toEqual(mockSubscription);
|
||||
expect(service.create).toHaveBeenCalledWith(createDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkUpdateAutoRotation', () => {
|
||||
it('должен вернуть ошибку, если subscriptionIds не массив', async () => {
|
||||
const result = await controller.bulkUpdateAutoRotation({
|
||||
subscriptionIds: 'not-array' as any,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
message: 'subscriptionIds должен быть массивом',
|
||||
});
|
||||
});
|
||||
|
||||
it('должен вернуть ошибку, если больше 100 ID', async () => {
|
||||
const result = await controller.bulkUpdateAutoRotation({
|
||||
subscriptionIds: Array(101).fill('id'),
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
message: 'Максимум 100 ID за раз',
|
||||
});
|
||||
});
|
||||
|
||||
it('должен обновить подписки и вернуть отчёт', async () => {
|
||||
const ids = ['id1', 'id2', 'id3'];
|
||||
mockSubscriptionsService.update
|
||||
.mockResolvedValueOnce({ id: 'id1' })
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce({ id: 'id3' });
|
||||
|
||||
const result = await controller.bulkUpdateAutoRotation({
|
||||
subscriptionIds: ids,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
message: 'Обновлено 2 подписок',
|
||||
updatedCount: 2,
|
||||
notFound: ['id2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('должен вернуть пустой notFound, если все обновлены успешно', async () => {
|
||||
const ids = ['id1', 'id2'];
|
||||
mockSubscriptionsService.update.mockResolvedValue({ id: 'updated' });
|
||||
|
||||
const result = await controller.bulkUpdateAutoRotation({
|
||||
subscriptionIds: ids,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
message: 'Обновлено 2 подписок',
|
||||
updatedCount: 2,
|
||||
notFound: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('должен обработать пустой массив', async () => {
|
||||
const result = await controller.bulkUpdateAutoRotation({
|
||||
subscriptionIds: [],
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
message: 'Обновлено 0 подписок',
|
||||
updatedCount: 0,
|
||||
notFound: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
const updateDto: UpdateSubscriptionDto = {
|
||||
name: 'Updated Name',
|
||||
isAutoRotationEnabled: false,
|
||||
};
|
||||
|
||||
it('должен обновить подписку', async () => {
|
||||
const mockSub = { id: 'id1', ...updateDto };
|
||||
mockSubscriptionsService.update.mockResolvedValue(mockSub);
|
||||
|
||||
const result = await controller.update('id1', updateDto);
|
||||
|
||||
expect(result).toEqual(mockSub);
|
||||
expect(service.update).toHaveBeenCalledWith('id1', updateDto);
|
||||
});
|
||||
|
||||
it('должен бросить NotFoundException, если подписка не найдена', async () => {
|
||||
mockSubscriptionsService.update.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
controller.update('non-existent', updateDto),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
|
||||
await expect(
|
||||
controller.update('non-existent', updateDto),
|
||||
).rejects.toThrow('Подписка non-existent не найдена');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('должен удалить подписку', async () => {
|
||||
mockSubscriptionsService.remove.mockResolvedValue(undefined);
|
||||
|
||||
await controller.remove('id1');
|
||||
|
||||
expect(service.remove).toHaveBeenCalledWith('id1');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,362 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
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';
|
||||
|
||||
describe('SubscriptionsService', () => {
|
||||
let service: SubscriptionsService;
|
||||
let subRepo: Repository<Subscription>;
|
||||
let xuiService: XuiService;
|
||||
|
||||
const mockSubRepo = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
const mockXuiService = {
|
||||
deleteInbound: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SubscriptionsService,
|
||||
{
|
||||
provide: getRepositoryToken(Subscription),
|
||||
useValue: mockSubRepo,
|
||||
},
|
||||
{
|
||||
provide: XuiService,
|
||||
useValue: mockXuiService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SubscriptionsService>(SubscriptionsService);
|
||||
subRepo = module.get<Repository<Subscription>>(
|
||||
getRepositoryToken(Subscription),
|
||||
);
|
||||
xuiService = module.get<XuiService>(XuiService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('должен вернуть все подписки с relations и сортировкой', async () => {
|
||||
const mockSubs: Subscription[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Sub 1',
|
||||
uuid: 'uuid-1',
|
||||
isEnabled: true,
|
||||
isAutoRotationEnabled: true,
|
||||
inboundsConfig: [],
|
||||
inbounds: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Sub 2',
|
||||
uuid: 'uuid-2',
|
||||
isEnabled: true,
|
||||
isAutoRotationEnabled: false,
|
||||
inboundsConfig: [],
|
||||
inbounds: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
mockSubRepo.find.mockResolvedValue(mockSubs);
|
||||
|
||||
const result = await service.findAll();
|
||||
|
||||
expect(result).toEqual(mockSubs);
|
||||
expect(subRepo.find).toHaveBeenCalledWith({
|
||||
relations: ['inbounds'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
});
|
||||
|
||||
it('должен вернуть пустой массив, если подписок нет', async () => {
|
||||
mockSubRepo.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.findAll();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
const createDto: CreateSubscriptionDto = {
|
||||
name: 'Test Subscription',
|
||||
inboundsConfig: [
|
||||
{ type: 'vless-tcp-reality', port: 443, sni: 'example.com' },
|
||||
],
|
||||
isAutoRotationEnabled: true,
|
||||
};
|
||||
|
||||
it('должен создать подписку с UUID и настройками по умолчанию', async () => {
|
||||
const mockSubscription = {
|
||||
...createDto,
|
||||
id: 'test-id',
|
||||
uuid: 'generated-uuid',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockSubRepo.create.mockReturnValue(mockSubscription);
|
||||
mockSubRepo.save.mockResolvedValue(mockSubscription);
|
||||
|
||||
const result = await service.create(createDto);
|
||||
|
||||
expect(subRepo.create).toHaveBeenCalledWith({
|
||||
name: 'Test Subscription',
|
||||
uuid: expect.any(String),
|
||||
inboundsConfig: createDto.inboundsConfig,
|
||||
isAutoRotationEnabled: true,
|
||||
});
|
||||
expect(subRepo.save).toHaveBeenCalledWith(mockSubscription);
|
||||
expect(result).toEqual(mockSubscription);
|
||||
});
|
||||
|
||||
it('должен использовать inboundsConfig по умолчанию [], если не передан', async () => {
|
||||
const dtoWithoutConfig: CreateSubscriptionDto = {
|
||||
name: 'Test',
|
||||
isAutoRotationEnabled: false,
|
||||
};
|
||||
|
||||
mockSubRepo.create.mockReturnValue({ ...dtoWithoutConfig, uuid: 'uuid' });
|
||||
mockSubRepo.save.mockResolvedValue({ ...dtoWithoutConfig, uuid: 'uuid' });
|
||||
|
||||
await service.create(dtoWithoutConfig);
|
||||
|
||||
expect(subRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
inboundsConfig: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('должен использовать isAutoRotationEnabled=true по умолчанию', async () => {
|
||||
const dtoWithoutRotation: CreateSubscriptionDto = {
|
||||
name: 'Test',
|
||||
inboundsConfig: [],
|
||||
};
|
||||
|
||||
mockSubRepo.create.mockReturnValue({
|
||||
...dtoWithoutRotation,
|
||||
uuid: 'uuid',
|
||||
});
|
||||
mockSubRepo.save.mockResolvedValue({
|
||||
...dtoWithoutRotation,
|
||||
uuid: 'uuid',
|
||||
});
|
||||
|
||||
await service.create(dtoWithoutRotation);
|
||||
|
||||
expect(subRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isAutoRotationEnabled: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
const existingSub: Subscription = {
|
||||
id: 'test-id',
|
||||
name: 'Old Name',
|
||||
uuid: 'uuid',
|
||||
isEnabled: true,
|
||||
isAutoRotationEnabled: true,
|
||||
inboundsConfig: [],
|
||||
inbounds: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
it('должен обновить имя подписки', async () => {
|
||||
mockSubRepo.findOne.mockResolvedValue(existingSub);
|
||||
mockSubRepo.save.mockResolvedValue({ ...existingSub, name: 'New Name' });
|
||||
|
||||
const result = await service.update('test-id', { name: 'New Name' });
|
||||
|
||||
expect(subRepo.findOne).toHaveBeenCalledWith({
|
||||
where: { id: 'test-id' },
|
||||
relations: ['inbounds'],
|
||||
});
|
||||
expect(subRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'New Name' }),
|
||||
);
|
||||
expect(result).toEqual({ ...existingSub, name: 'New Name' });
|
||||
});
|
||||
|
||||
it('должен обновить isAutoRotationEnabled', async () => {
|
||||
mockSubRepo.findOne.mockResolvedValue(existingSub);
|
||||
mockSubRepo.save.mockResolvedValue({
|
||||
...existingSub,
|
||||
isAutoRotationEnabled: false,
|
||||
});
|
||||
|
||||
const result = await service.update('test-id', {
|
||||
isAutoRotationEnabled: false,
|
||||
});
|
||||
|
||||
expect(subRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ isAutoRotationEnabled: false }),
|
||||
);
|
||||
expect(result).toEqual({ ...existingSub, isAutoRotationEnabled: false });
|
||||
});
|
||||
|
||||
it('должен обновить inboundsConfig', async () => {
|
||||
const newConfig = [{ type: 'vmess-tcp', port: 8080 }];
|
||||
mockSubRepo.findOne.mockResolvedValue(existingSub);
|
||||
mockSubRepo.save.mockResolvedValue({
|
||||
...existingSub,
|
||||
inboundsConfig: newConfig,
|
||||
});
|
||||
|
||||
await service.update('test-id', { inboundsConfig: newConfig });
|
||||
|
||||
expect(subRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ inboundsConfig: newConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it('НЕ должен обновлять имя на пустую строку (защита от очистки)', async () => {
|
||||
mockSubRepo.findOne.mockResolvedValue(existingSub);
|
||||
mockSubRepo.save.mockResolvedValue(existingSub);
|
||||
|
||||
await service.update('test-id', { name: '' });
|
||||
|
||||
expect(subRepo.save).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: '' }),
|
||||
);
|
||||
expect(subRepo.save).toHaveBeenCalledWith(existingSub);
|
||||
});
|
||||
|
||||
it('НЕ должен обновлять имя с пробелами (защита от очистки)', async () => {
|
||||
mockSubRepo.findOne.mockResolvedValue(existingSub);
|
||||
mockSubRepo.save.mockResolvedValue(existingSub);
|
||||
|
||||
await service.update('test-id', { name: ' ' });
|
||||
|
||||
expect(subRepo.save).toHaveBeenCalledWith(existingSub);
|
||||
});
|
||||
|
||||
it('должен вернуть null, если подписка не найдена', async () => {
|
||||
mockSubRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await service.update('non-existent-id', { name: 'New' });
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(subRepo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('должен обновить только isAutoRotationEnabled, не трогая имя', async () => {
|
||||
const freshSub: Subscription = {
|
||||
...existingSub,
|
||||
name: 'Old Name',
|
||||
inboundsConfig: [],
|
||||
};
|
||||
mockSubRepo.findOne.mockResolvedValue(freshSub);
|
||||
mockSubRepo.save.mockImplementation((sub) => Promise.resolve(sub));
|
||||
|
||||
await service.update('test-id', { isAutoRotationEnabled: false });
|
||||
|
||||
expect(subRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'Old Name',
|
||||
isAutoRotationEnabled: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
const subWithInbounds: Subscription = {
|
||||
id: 'test-id',
|
||||
name: 'Test',
|
||||
uuid: 'uuid',
|
||||
isEnabled: true,
|
||||
isAutoRotationEnabled: true,
|
||||
inboundsConfig: [],
|
||||
inbounds: [
|
||||
{
|
||||
id: '1',
|
||||
xuiId: 101,
|
||||
port: 443,
|
||||
protocol: 'vless',
|
||||
remark: 'test',
|
||||
link: 'link',
|
||||
subscription: null as any,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
xuiId: 102,
|
||||
port: 8443,
|
||||
protocol: 'vmess',
|
||||
remark: 'test2',
|
||||
link: 'link2',
|
||||
subscription: null as any,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
it('должен удалить инбаунды из 3x-ui перед удалением подписки', async () => {
|
||||
mockSubRepo.findOne.mockResolvedValue(subWithInbounds);
|
||||
mockSubRepo.remove.mockResolvedValue(undefined);
|
||||
|
||||
await service.remove('test-id');
|
||||
|
||||
expect(xuiService.deleteInbound).toHaveBeenCalledWith(101);
|
||||
expect(xuiService.deleteInbound).toHaveBeenCalledWith(102);
|
||||
expect(subRepo.remove).toHaveBeenCalledWith(subWithInbounds);
|
||||
});
|
||||
|
||||
it('должен удалить подписку без инбаундов', async () => {
|
||||
const subWithoutInbounds: Subscription = {
|
||||
...subWithInbounds,
|
||||
inbounds: [],
|
||||
};
|
||||
|
||||
mockSubRepo.findOne.mockResolvedValue(subWithoutInbounds);
|
||||
mockSubRepo.remove.mockResolvedValue(undefined);
|
||||
|
||||
await service.remove('test-id');
|
||||
|
||||
expect(xuiService.deleteInbound).not.toHaveBeenCalled();
|
||||
expect(subRepo.remove).toHaveBeenCalledWith(subWithoutInbounds);
|
||||
});
|
||||
|
||||
it('должен вернуть undefined, если подписка не найдена', async () => {
|
||||
mockSubRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await service.remove('non-existent-id');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(subRepo.remove).not.toHaveBeenCalled();
|
||||
expect(xuiService.deleteInbound).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user