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,
|
||||
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id)?.id,
|
||||
context: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@ -148,6 +149,7 @@ describe('SubscriberController', () => {
|
||||
subscriber.labels.includes(label.id),
|
||||
),
|
||||
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id),
|
||||
context: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -119,6 +119,7 @@ describe('SubscriberRepository', () => {
|
||||
subscriber.labels.includes(label.id),
|
||||
),
|
||||
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id),
|
||||
context: undefined,
|
||||
};
|
||||
|
||||
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,
|
||||
})),
|
||||
);
|
||||
return await Block.updateOne(
|
||||
await Block.updateOne(
|
||||
{ name: 'hasNextBlocks' },
|
||||
{ $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'][] = [
|
||||
{
|
||||
label: 'Phone',
|
||||
name: 'phone',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
label: 'Country',
|
||||
name: 'country',
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
label: 'test context var 1',
|
||||
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:
|
||||
* 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 { ConversationModel } from '@/chat/schemas/conversation.schema';
|
||||
import { Subscriber } from '@/chat/schemas/subscriber.schema';
|
||||
|
||||
import { getFixturesWithDefaultValues } from '../defaultValues';
|
||||
import { TFixturesDefaultValues } from '../types';
|
||||
@ -45,23 +46,10 @@ const conversations: ConversationCreateDto[] = [
|
||||
},
|
||||
user: {
|
||||
id: '1',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
first_name: 'Jhon',
|
||||
last_name: 'Doe',
|
||||
language: 'fr',
|
||||
locale: 'en_EN',
|
||||
timezone: 0,
|
||||
gender: 'male',
|
||||
country: 'FR',
|
||||
foreign_id: '',
|
||||
labels: [],
|
||||
assignedTo: null,
|
||||
channel: { name: 'messenger-channel' },
|
||||
avatar: null,
|
||||
context: {},
|
||||
assignedAt: new Date(),
|
||||
},
|
||||
} as Subscriber,
|
||||
skip: {},
|
||||
attempt: 0,
|
||||
},
|
||||
@ -131,7 +119,7 @@ export const conversationFixtures =
|
||||
});
|
||||
|
||||
export const installConversationTypeFixtures = async () => {
|
||||
const subscribers = await installSubscriberFixtures();
|
||||
const { subscribers } = await installSubscriberFixtures();
|
||||
const blocks = await installBlockFixtures();
|
||||
|
||||
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),
|
||||
retainedFrom: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
avatar: null,
|
||||
context: {
|
||||
vars: {},
|
||||
},
|
||||
};
|
||||
|
||||
const subscribers: TSubscriberFixtures['values'][] = [
|
||||
|
Loading…
Reference in New Issue
Block a user