mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
fix: permanent ctx vars update + unit tests
This commit is contained in:
parent
8033eb3099
commit
c9f521a837
@ -128,6 +128,7 @@ describe('SubscriberController', () => {
|
|||||||
),
|
),
|
||||||
labels: labelIDs,
|
labels: labelIDs,
|
||||||
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id)?.id,
|
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id)?.id,
|
||||||
|
context: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -148,6 +149,7 @@ describe('SubscriberController', () => {
|
|||||||
subscriber.labels.includes(label.id),
|
subscriber.labels.includes(label.id),
|
||||||
),
|
),
|
||||||
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id),
|
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id),
|
||||||
|
context: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -119,6 +119,7 @@ describe('SubscriberRepository', () => {
|
|||||||
subscriber.labels.includes(label.id),
|
subscriber.labels.includes(label.id),
|
||||||
),
|
),
|
||||||
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id),
|
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id),
|
||||||
|
context: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(subscriberModel.findById).toHaveBeenCalledWith(
|
expect(subscriberModel.findById).toHaveBeenCalledWith(
|
||||||
|
354
api/src/chat/services/conversation.service.spec.ts
Normal file
354
api/src/chat/services/conversation.service.spec.ts
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2025 Hexastack. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||||
|
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||||
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
|
||||||
|
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||||
|
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||||
|
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||||
|
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||||
|
import { installContextVarFixtures } from '@/utils/test/fixtures/contextvar';
|
||||||
|
import { installConversationTypeFixtures } from '@/utils/test/fixtures/conversation';
|
||||||
|
import {
|
||||||
|
closeInMongodConnection,
|
||||||
|
rootMongooseTestModule,
|
||||||
|
} from '@/utils/test/test';
|
||||||
|
import { buildTestingMocks } from '@/utils/test/utils';
|
||||||
|
|
||||||
|
import { VIEW_MORE_PAYLOAD } from '../helpers/constants';
|
||||||
|
import { ContextVarRepository } from '../repositories/context-var.repository';
|
||||||
|
import { ConversationRepository } from '../repositories/conversation.repository';
|
||||||
|
import { SubscriberRepository } from '../repositories/subscriber.repository';
|
||||||
|
import { Block } from '../schemas/block.schema';
|
||||||
|
import { ContextVarModel } from '../schemas/context-var.schema';
|
||||||
|
import { ConversationModel } from '../schemas/conversation.schema';
|
||||||
|
import { SubscriberModel } from '../schemas/subscriber.schema';
|
||||||
|
import { OutgoingMessageFormat } from '../schemas/types/message';
|
||||||
|
|
||||||
|
import { ContextVarService } from './context-var.service';
|
||||||
|
import { ConversationService } from './conversation.service';
|
||||||
|
import { SubscriberService } from './subscriber.service';
|
||||||
|
|
||||||
|
describe('ConversationService', () => {
|
||||||
|
let conversationService: ConversationService;
|
||||||
|
let subscriberService: SubscriberService;
|
||||||
|
// let labelService: LabelService;
|
||||||
|
// let subscriberRepository: SubscriberRepository;
|
||||||
|
// let allSubscribers: Subscriber[];
|
||||||
|
// let allLabels: Label[];
|
||||||
|
// let labelsWithUsers: LabelFull[];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const { getMocks } = await buildTestingMocks({
|
||||||
|
imports: [
|
||||||
|
rootMongooseTestModule(async () => {
|
||||||
|
await installContextVarFixtures();
|
||||||
|
await installConversationTypeFixtures();
|
||||||
|
}),
|
||||||
|
MongooseModule.forFeature([
|
||||||
|
// LabelModel,
|
||||||
|
SubscriberModel,
|
||||||
|
ConversationModel,
|
||||||
|
AttachmentModel,
|
||||||
|
ContextVarModel,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
// LabelService,
|
||||||
|
// LabelRepository,
|
||||||
|
AttachmentService,
|
||||||
|
SubscriberService,
|
||||||
|
ConversationService,
|
||||||
|
ContextVarService,
|
||||||
|
AttachmentRepository,
|
||||||
|
SubscriberRepository,
|
||||||
|
ConversationRepository,
|
||||||
|
ContextVarRepository,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
[conversationService, subscriberService] = await getMocks([
|
||||||
|
ConversationService,
|
||||||
|
SubscriberService,
|
||||||
|
]);
|
||||||
|
// allSubscribers = await subscriberRepository.findAll();
|
||||||
|
// allLabels = await labelRepository.findAll();
|
||||||
|
// labelsWithUsers = allLabels.map((label) => ({
|
||||||
|
// ...label,
|
||||||
|
// users: allSubscribers,
|
||||||
|
// }));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(jest.clearAllMocks);
|
||||||
|
afterAll(closeInMongodConnection);
|
||||||
|
|
||||||
|
describe('ConversationService.storeContextData', () => {
|
||||||
|
it('should enrich the conversation context and persist conversation + subscriber (permanent)', async () => {
|
||||||
|
const subscriber = (await subscriberService.findOne({
|
||||||
|
foreign_id: 'foreign-id-messenger',
|
||||||
|
}))!;
|
||||||
|
const conversation = (await conversationService.findOne({
|
||||||
|
sender: subscriber.id,
|
||||||
|
}))!;
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
id: 'block-1',
|
||||||
|
capture_vars: [{ entity: -1, context_var: 'phone' }],
|
||||||
|
} as Block;
|
||||||
|
|
||||||
|
const mockPhone = '+1 514 678 9873';
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
getMessageType: jest.fn().mockReturnValue('message'),
|
||||||
|
getText: jest.fn().mockReturnValue(mockPhone),
|
||||||
|
getPayload: jest.fn().mockReturnValue(undefined),
|
||||||
|
getNLP: jest.fn().mockReturnValue(undefined),
|
||||||
|
getMessage: jest.fn().mockReturnValue({
|
||||||
|
text: mockPhone,
|
||||||
|
}),
|
||||||
|
getHandler: jest.fn().mockReturnValue({
|
||||||
|
getName: jest.fn().mockReturnValue('messenger-channel'),
|
||||||
|
}),
|
||||||
|
getSender: jest.fn().mockReturnValue({
|
||||||
|
id: subscriber.id,
|
||||||
|
first_name: subscriber.first_name,
|
||||||
|
last_name: subscriber.last_name,
|
||||||
|
language: subscriber.language,
|
||||||
|
context: {
|
||||||
|
vars: {
|
||||||
|
email: 'john.doe@mail.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
setSender: jest.fn(),
|
||||||
|
} as unknown as EventWrapper<any, any>;
|
||||||
|
|
||||||
|
const result = await conversationService.storeContextData(
|
||||||
|
conversation!,
|
||||||
|
next,
|
||||||
|
event,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---- Assertions ------------------------------------------------------
|
||||||
|
expect(result.context.channel).toBe('messenger-channel');
|
||||||
|
expect(result.context.text).toBe(mockPhone);
|
||||||
|
expect(result.context.vars.phone).toBe(mockPhone);
|
||||||
|
expect(result.context.user).toEqual({
|
||||||
|
id: subscriber.id,
|
||||||
|
first_name: subscriber.first_name,
|
||||||
|
last_name: subscriber.last_name,
|
||||||
|
language: subscriber.language,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedSubscriber = (await subscriberService.findOne({
|
||||||
|
foreign_id: 'foreign-id-messenger',
|
||||||
|
}))!;
|
||||||
|
|
||||||
|
expect(updatedSubscriber.context.vars?.phone).toBe(mockPhone);
|
||||||
|
|
||||||
|
// expect(event.setSender).toHaveBeenCalledWith(updatedSubscriber);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should capture an NLP entity value into context vars (non-permanent)', async () => {
|
||||||
|
const subscriber = (await subscriberService.findOne({
|
||||||
|
foreign_id: 'foreign-id-messenger',
|
||||||
|
}))!;
|
||||||
|
const conversation = (await conversationService.findOne({
|
||||||
|
sender: subscriber.id,
|
||||||
|
}))!;
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
id: 'block-1',
|
||||||
|
capture_vars: [{ entity: 'country_code', context_var: 'country' }],
|
||||||
|
} as Block;
|
||||||
|
|
||||||
|
const mockMessage = 'Are you from the US?';
|
||||||
|
const event = {
|
||||||
|
getMessageType: jest.fn().mockReturnValue('message'),
|
||||||
|
getText: jest.fn().mockReturnValue(mockMessage),
|
||||||
|
getPayload: jest.fn().mockReturnValue(undefined),
|
||||||
|
getNLP: jest.fn().mockReturnValue({
|
||||||
|
entities: [
|
||||||
|
{
|
||||||
|
entity: 'country_code',
|
||||||
|
value: 'US',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
getMessage: jest.fn().mockReturnValue({
|
||||||
|
text: mockMessage,
|
||||||
|
}),
|
||||||
|
getHandler: jest.fn().mockReturnValue({
|
||||||
|
getName: jest.fn().mockReturnValue('messenger-channel'),
|
||||||
|
}),
|
||||||
|
getSender: jest.fn().mockReturnValue({
|
||||||
|
id: subscriber.id,
|
||||||
|
first_name: subscriber.first_name,
|
||||||
|
last_name: subscriber.last_name,
|
||||||
|
language: subscriber.language,
|
||||||
|
context: {
|
||||||
|
vars: {
|
||||||
|
email: 'john.doe@mail.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
setSender: jest.fn(),
|
||||||
|
} as unknown as EventWrapper<any, any>;
|
||||||
|
|
||||||
|
const result = await conversationService.storeContextData(
|
||||||
|
conversation,
|
||||||
|
next,
|
||||||
|
event,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.vars.country).toBe('US');
|
||||||
|
const updatedSubscriber = (await subscriberService.findOne({
|
||||||
|
foreign_id: 'foreign-id-messenger',
|
||||||
|
}))!;
|
||||||
|
expect(updatedSubscriber.context.vars?.country).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should capture user coordinates when message type is "location"', async () => {
|
||||||
|
const subscriber = (await subscriberService.findOne({
|
||||||
|
foreign_id: 'foreign-id-messenger',
|
||||||
|
}))!;
|
||||||
|
const conversation = (await conversationService.findOne({
|
||||||
|
sender: subscriber.id,
|
||||||
|
}))!;
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
id: 'block-1',
|
||||||
|
capture_vars: [{ entity: 'country_code', context_var: 'country' }],
|
||||||
|
} as Block;
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
getMessageType: jest.fn().mockReturnValue('location'),
|
||||||
|
getText: jest.fn().mockReturnValue(''),
|
||||||
|
getPayload: jest.fn().mockReturnValue(undefined),
|
||||||
|
getNLP: jest.fn(),
|
||||||
|
getMessage: jest.fn().mockReturnValue({
|
||||||
|
coordinates: { lat: 36.8065, lon: 10.1815 },
|
||||||
|
}),
|
||||||
|
getHandler: jest.fn().mockReturnValue({
|
||||||
|
getName: jest.fn().mockReturnValue('messenger-channel'),
|
||||||
|
}),
|
||||||
|
getSender: jest.fn().mockReturnValue({
|
||||||
|
id: subscriber.id,
|
||||||
|
first_name: subscriber.first_name,
|
||||||
|
last_name: subscriber.last_name,
|
||||||
|
language: subscriber.language,
|
||||||
|
context: {
|
||||||
|
vars: {
|
||||||
|
email: 'john.doe@mail.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
setSender: jest.fn(),
|
||||||
|
} as unknown as EventWrapper<any, any>;
|
||||||
|
|
||||||
|
const result = await conversationService.storeContextData(
|
||||||
|
conversation,
|
||||||
|
next,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.user_location).toEqual({
|
||||||
|
lat: 36.8065,
|
||||||
|
lon: 10.1815,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should increment skip when VIEW_MORE payload is received for list/carousel blocks', async () => {
|
||||||
|
const subscriber = (await subscriberService.findOne({
|
||||||
|
foreign_id: 'foreign-id-messenger',
|
||||||
|
}))!;
|
||||||
|
const conversation = (await conversationService.findOne({
|
||||||
|
sender: subscriber.id,
|
||||||
|
}))!;
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
id: 'block-1',
|
||||||
|
capture_vars: [],
|
||||||
|
options: {
|
||||||
|
content: {
|
||||||
|
display: OutgoingMessageFormat.list,
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Block;
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
getMessageType: jest.fn().mockReturnValue('message'),
|
||||||
|
getText: jest.fn().mockReturnValue('I would like to see the products'),
|
||||||
|
getPayload: jest.fn().mockReturnValue(undefined),
|
||||||
|
getNLP: jest.fn(),
|
||||||
|
getMessage: jest.fn().mockReturnValue({
|
||||||
|
text: 'I would like to see the products',
|
||||||
|
}),
|
||||||
|
getHandler: jest.fn().mockReturnValue({
|
||||||
|
getName: jest.fn().mockReturnValue('messenger-channel'),
|
||||||
|
}),
|
||||||
|
getSender: jest.fn().mockReturnValue({
|
||||||
|
id: subscriber.id,
|
||||||
|
first_name: subscriber.first_name,
|
||||||
|
last_name: subscriber.last_name,
|
||||||
|
language: subscriber.language,
|
||||||
|
context: {
|
||||||
|
vars: {
|
||||||
|
email: 'john.doe@mail.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
setSender: jest.fn(),
|
||||||
|
} as unknown as EventWrapper<any, any>;
|
||||||
|
|
||||||
|
const result1 = await conversationService.storeContextData(
|
||||||
|
conversation,
|
||||||
|
next,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result1.context.skip['block-1']).toBe(0);
|
||||||
|
|
||||||
|
const event2 = {
|
||||||
|
getMessageType: jest.fn().mockReturnValue('postback'),
|
||||||
|
getText: jest.fn().mockReturnValue('View more'),
|
||||||
|
getPayload: jest.fn().mockReturnValue(VIEW_MORE_PAYLOAD),
|
||||||
|
getNLP: jest.fn(),
|
||||||
|
getMessage: jest.fn().mockReturnValue({
|
||||||
|
coordinates: { lat: 36.8065, lon: 10.1815 },
|
||||||
|
}),
|
||||||
|
getHandler: jest.fn().mockReturnValue({
|
||||||
|
getName: jest.fn().mockReturnValue('messenger-channel'),
|
||||||
|
}),
|
||||||
|
getSender: jest.fn().mockReturnValue({
|
||||||
|
id: subscriber.id,
|
||||||
|
first_name: subscriber.first_name,
|
||||||
|
last_name: subscriber.last_name,
|
||||||
|
language: subscriber.language,
|
||||||
|
context: {
|
||||||
|
vars: {
|
||||||
|
email: 'john.doe@mail.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
setSender: jest.fn(),
|
||||||
|
} as unknown as EventWrapper<any, any>;
|
||||||
|
|
||||||
|
// Second call to ensure the offset keeps growing
|
||||||
|
const result2 = await conversationService.storeContextData(
|
||||||
|
conversation,
|
||||||
|
next,
|
||||||
|
event2,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result2.context.skip['block-1']).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
4
api/src/utils/test/fixtures/block.ts
vendored
4
api/src/utils/test/fixtures/block.ts
vendored
@ -196,8 +196,10 @@ export const installBlockFixtures = async () => {
|
|||||||
category: defaultCategory.id,
|
category: defaultCategory.id,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
return await Block.updateOne(
|
await Block.updateOne(
|
||||||
{ name: 'hasNextBlocks' },
|
{ name: 'hasNextBlocks' },
|
||||||
{ $set: { nextBlocks: blocks[1].id } },
|
{ $set: { nextBlocks: blocks[1].id } },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return blocks;
|
||||||
};
|
};
|
||||||
|
10
api/src/utils/test/fixtures/contextvar.ts
vendored
10
api/src/utils/test/fixtures/contextvar.ts
vendored
@ -21,6 +21,16 @@ export const contentVarDefaultValues: TContentVarFixtures['defaultValues'] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const contextVars: TContentVarFixtures['values'][] = [
|
const contextVars: TContentVarFixtures['values'][] = [
|
||||||
|
{
|
||||||
|
label: 'Phone',
|
||||||
|
name: 'phone',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Country',
|
||||||
|
name: 'country',
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'test context var 1',
|
label: 'test context var 1',
|
||||||
name: 'test1',
|
name: 'test1',
|
||||||
|
20
api/src/utils/test/fixtures/conversation.ts
vendored
20
api/src/utils/test/fixtures/conversation.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024 Hexastack. All rights reserved.
|
* Copyright © 2025 Hexastack. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||||
@ -10,6 +10,7 @@ import mongoose from 'mongoose';
|
|||||||
|
|
||||||
import { ConversationCreateDto } from '@/chat/dto/conversation.dto';
|
import { ConversationCreateDto } from '@/chat/dto/conversation.dto';
|
||||||
import { ConversationModel } from '@/chat/schemas/conversation.schema';
|
import { ConversationModel } from '@/chat/schemas/conversation.schema';
|
||||||
|
import { Subscriber } from '@/chat/schemas/subscriber.schema';
|
||||||
|
|
||||||
import { getFixturesWithDefaultValues } from '../defaultValues';
|
import { getFixturesWithDefaultValues } from '../defaultValues';
|
||||||
import { TFixturesDefaultValues } from '../types';
|
import { TFixturesDefaultValues } from '../types';
|
||||||
@ -45,23 +46,10 @@ const conversations: ConversationCreateDto[] = [
|
|||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
id: '1',
|
id: '1',
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
first_name: 'Jhon',
|
first_name: 'Jhon',
|
||||||
last_name: 'Doe',
|
last_name: 'Doe',
|
||||||
language: 'fr',
|
language: 'fr',
|
||||||
locale: 'en_EN',
|
} as Subscriber,
|
||||||
timezone: 0,
|
|
||||||
gender: 'male',
|
|
||||||
country: 'FR',
|
|
||||||
foreign_id: '',
|
|
||||||
labels: [],
|
|
||||||
assignedTo: null,
|
|
||||||
channel: { name: 'messenger-channel' },
|
|
||||||
avatar: null,
|
|
||||||
context: {},
|
|
||||||
assignedAt: new Date(),
|
|
||||||
},
|
|
||||||
skip: {},
|
skip: {},
|
||||||
attempt: 0,
|
attempt: 0,
|
||||||
},
|
},
|
||||||
@ -131,7 +119,7 @@ export const conversationFixtures =
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const installConversationTypeFixtures = async () => {
|
export const installConversationTypeFixtures = async () => {
|
||||||
const subscribers = await installSubscriberFixtures();
|
const { subscribers } = await installSubscriberFixtures();
|
||||||
const blocks = await installBlockFixtures();
|
const blocks = await installBlockFixtures();
|
||||||
|
|
||||||
const Conversation = mongoose.model(
|
const Conversation = mongoose.model(
|
||||||
|
3
api/src/utils/test/fixtures/subscriber.ts
vendored
3
api/src/utils/test/fixtures/subscriber.ts
vendored
@ -26,6 +26,9 @@ export const subscriberDefaultValues: TSubscriberFixtures['defaultValues'] = {
|
|||||||
lastvisit: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
lastvisit: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||||
retainedFrom: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
retainedFrom: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||||
avatar: null,
|
avatar: null,
|
||||||
|
context: {
|
||||||
|
vars: {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const subscribers: TSubscriberFixtures['values'][] = [
|
const subscribers: TSubscriberFixtures['values'][] = [
|
||||||
|
Loading…
Reference in New Issue
Block a user