feat(worker): handleStarReaction returns ApprovalResult with indexDoc
This commit is contained in:
@@ -14,143 +14,111 @@ function makeReaction(overrides: Partial<NormalizedReaction> = {}): NormalizedRe
|
|||||||
|
|
||||||
const adminJids = ['919876543210@s.whatsapp.net'];
|
const adminJids = ['919876543210@s.whatsapp.net'];
|
||||||
|
|
||||||
|
function makeMessage(overrides: object = {}) {
|
||||||
|
return {
|
||||||
|
id: 'msg_1',
|
||||||
|
status: 'PENDING',
|
||||||
|
approval: null,
|
||||||
|
content: 'hello world',
|
||||||
|
senderName: 'Alice',
|
||||||
|
sourceGroupId: 'grp_1',
|
||||||
|
tags: ['#important'],
|
||||||
|
platform: 'whatsapp',
|
||||||
|
sourceGroup: { name: 'UP Parivar Dallas', syncRoutesFrom: [] },
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePrisma(messageOverrides: object = {}, txCount = 1) {
|
||||||
|
return {
|
||||||
|
message: { findUnique: jest.fn().mockResolvedValue(makeMessage(messageOverrides)) },
|
||||||
|
$transaction: jest.fn().mockImplementation(async (fn: any) =>
|
||||||
|
fn({
|
||||||
|
message: { updateMany: jest.fn().mockResolvedValue({ count: txCount }) },
|
||||||
|
approval: { create: jest.fn().mockResolvedValue({}) },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
describe('handleStarReaction', () => {
|
describe('handleStarReaction', () => {
|
||||||
it('returns null for non-star emoji', async () => {
|
it('returns null for non-star emoji', async () => {
|
||||||
const result = await handleStarReaction(makeReaction({ emoji: '👍' }), adminJids, {} as any);
|
expect(await handleStarReaction(makeReaction({ emoji: '👍' }), adminJids, {} as any)).toBeNull();
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null when reactor is not an admin', async () => {
|
it('returns null when reactor is not an admin', async () => {
|
||||||
const result = await handleStarReaction(
|
expect(
|
||||||
makeReaction({ reactorJid: 'stranger@s.whatsapp.net' }),
|
await handleStarReaction(makeReaction({ reactorJid: 'stranger@s.whatsapp.net' }), adminJids, {} as any),
|
||||||
adminJids,
|
).toBeNull();
|
||||||
{} as any,
|
|
||||||
);
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null when message not found', async () => {
|
it('returns null when message not found', async () => {
|
||||||
const prisma = { message: { findUnique: jest.fn().mockResolvedValue(null) } } as any;
|
const prisma = { message: { findUnique: jest.fn().mockResolvedValue(null) } } as any;
|
||||||
const result = await handleStarReaction(makeReaction(), adminJids, prisma);
|
expect(await handleStarReaction(makeReaction(), adminJids, prisma)).toBeNull();
|
||||||
expect(result).toBeNull();
|
|
||||||
expect(prisma.message.findUnique).toHaveBeenCalledWith({
|
expect(prisma.message.findUnique).toHaveBeenCalledWith({
|
||||||
where: {
|
where: { platform_platformMsgId: { platform: 'whatsapp', platformMsgId: 'TARGET_MSG_123' } },
|
||||||
platform_platformMsgId: { platform: 'whatsapp', platformMsgId: 'TARGET_MSG_123' },
|
|
||||||
},
|
|
||||||
include: {
|
include: {
|
||||||
approval: true,
|
approval: true,
|
||||||
sourceGroup: {
|
sourceGroup: {
|
||||||
include: {
|
include: { syncRoutesFrom: { where: { isActive: true }, include: { targetGroup: true } } },
|
||||||
syncRoutesFrom: { where: { isActive: true }, include: { targetGroup: true } },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null when message status is not PENDING', async () => {
|
it('returns null when message status is not PENDING', async () => {
|
||||||
|
const prisma = {
|
||||||
|
message: { findUnique: jest.fn().mockResolvedValue(makeMessage({ status: 'REJECTED' })) },
|
||||||
|
} as any;
|
||||||
|
expect(await handleStarReaction(makeReaction(), adminJids, prisma)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when approval record already exists', async () => {
|
||||||
const prisma = {
|
const prisma = {
|
||||||
message: {
|
message: {
|
||||||
findUnique: jest.fn().mockResolvedValue({
|
findUnique: jest.fn().mockResolvedValue(makeMessage({ status: 'APPROVED', approval: { id: 'appr_1' } })),
|
||||||
id: 'msg_1',
|
|
||||||
status: 'REJECTED',
|
|
||||||
approval: null,
|
|
||||||
sourceGroup: { name: 'Test Group', syncRoutesFrom: [] },
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
expect(await handleStarReaction(makeReaction(), adminJids, prisma)).toBeNull();
|
expect(await handleStarReaction(makeReaction(), adminJids, prisma)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null when message is already approved (approval record exists)', async () => {
|
it('returns null on double-approval race (updateMany count=0)', async () => {
|
||||||
const prisma = {
|
const result = await handleStarReaction(makeReaction(), adminJids, makePrisma({}, 0));
|
||||||
message: {
|
|
||||||
findUnique: jest.fn().mockResolvedValue({
|
|
||||||
id: 'msg_1',
|
|
||||||
status: 'APPROVED',
|
|
||||||
approval: { id: 'appr_1' },
|
|
||||||
sourceGroup: { name: 'Test Group', syncRoutesFrom: [] },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
} as any;
|
|
||||||
expect(await handleStarReaction(makeReaction(), adminJids, prisma)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns null on double-approval race (updateMany returns count=0)', async () => {
|
|
||||||
const prisma = {
|
|
||||||
message: {
|
|
||||||
findUnique: jest.fn().mockResolvedValue({
|
|
||||||
id: 'msg_1',
|
|
||||||
status: 'PENDING',
|
|
||||||
approval: null,
|
|
||||||
content: 'hello',
|
|
||||||
senderName: 'Alice',
|
|
||||||
sourceGroup: { name: 'UP Parivar Dallas', syncRoutesFrom: [] },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
$transaction: jest.fn().mockImplementation(async (fn: any) => fn({
|
|
||||||
message: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) },
|
|
||||||
approval: { create: jest.fn().mockResolvedValue({}) },
|
|
||||||
})),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const result = await handleStarReaction(makeReaction(), adminJids, prisma);
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
expect(prisma.$transaction).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('approves message and returns empty array when no sync routes', async () => {
|
it('returns ApprovalResult with empty forwardJobs and valid indexDoc when no sync routes', async () => {
|
||||||
const prisma = {
|
const result = await handleStarReaction(makeReaction(), adminJids, makePrisma());
|
||||||
message: {
|
expect(result).not.toBeNull();
|
||||||
findUnique: jest.fn().mockResolvedValue({
|
expect(result!.forwardJobs).toEqual([]);
|
||||||
id: 'msg_1',
|
expect(result!.indexDoc).toMatchObject({
|
||||||
status: 'PENDING',
|
messageId: 'msg_1',
|
||||||
approval: null,
|
content: 'hello world',
|
||||||
content: 'hello',
|
senderName: 'Alice',
|
||||||
senderName: 'Alice',
|
sourceGroupId: 'grp_1',
|
||||||
sourceGroup: { name: 'UP Parivar Dallas', syncRoutesFrom: [] },
|
sourceGroupName: 'UP Parivar Dallas',
|
||||||
}),
|
tags: ['#important'],
|
||||||
},
|
platform: 'whatsapp',
|
||||||
approval: { create: jest.fn().mockResolvedValue({}) },
|
});
|
||||||
$transaction: jest.fn().mockImplementation(async (fn: any) => fn({
|
expect(result!.indexDoc.approvedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
||||||
message: { updateMany: jest.fn().mockResolvedValue({ count: 1 }) },
|
|
||||||
approval: { create: jest.fn().mockResolvedValue({}) },
|
|
||||||
})),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const result = await handleStarReaction(makeReaction(), adminJids, prisma);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
expect(prisma.$transaction).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns ForwardJobData for each active sync route', async () => {
|
it('returns ForwardJobData for each active sync route', async () => {
|
||||||
const prisma = {
|
const prisma = makePrisma({
|
||||||
message: {
|
content: 'important announcement',
|
||||||
findUnique: jest.fn().mockResolvedValue({
|
senderName: 'Bob',
|
||||||
id: 'msg_1',
|
sourceGroup: {
|
||||||
status: 'PENDING',
|
name: 'Source Group',
|
||||||
approval: null,
|
syncRoutesFrom: [
|
||||||
content: 'important announcement',
|
{ targetGroup: { platformId: '999@g.us', accountId: 'acc_2' } },
|
||||||
senderName: 'Bob',
|
{ targetGroup: { platformId: '888@g.us', accountId: null } },
|
||||||
sourceGroup: {
|
],
|
||||||
name: 'Source Group',
|
|
||||||
syncRoutesFrom: [
|
|
||||||
{ targetGroup: { platformId: '999@g.us', accountId: 'acc_2' } },
|
|
||||||
{ targetGroup: { platformId: '888@g.us', accountId: null } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
approval: { create: jest.fn().mockResolvedValue({}) },
|
});
|
||||||
$transaction: jest.fn().mockImplementation(async (fn: any) => fn({
|
|
||||||
message: { updateMany: jest.fn().mockResolvedValue({ count: 1 }) },
|
|
||||||
approval: { create: jest.fn().mockResolvedValue({}) },
|
|
||||||
})),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const result = await handleStarReaction(makeReaction(), adminJids, prisma);
|
const result = await handleStarReaction(makeReaction(), adminJids, prisma);
|
||||||
expect(result).toHaveLength(2);
|
expect(result!.forwardJobs).toHaveLength(2);
|
||||||
expect(result![0]).toMatchObject({
|
expect(result!.forwardJobs[0]).toMatchObject({
|
||||||
messageId: 'msg_1',
|
messageId: 'msg_1',
|
||||||
content: 'important announcement',
|
content: 'important announcement',
|
||||||
sourceGroupName: 'Source Group',
|
sourceGroupName: 'Source Group',
|
||||||
@@ -158,8 +126,7 @@ describe('handleStarReaction', () => {
|
|||||||
toGroupJid: '999@g.us',
|
toGroupJid: '999@g.us',
|
||||||
fromAccountId: 'acc_2',
|
fromAccountId: 'acc_2',
|
||||||
});
|
});
|
||||||
// falls back to reaction.accountId when targetGroup.accountId is null
|
expect(result!.forwardJobs[1]).toMatchObject({
|
||||||
expect(result![1]).toMatchObject({
|
|
||||||
toGroupJid: '888@g.us',
|
toGroupJid: '888@g.us',
|
||||||
fromAccountId: 'acc_1',
|
fromAccountId: 'acc_1',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { NormalizedReaction, ForwardJobData } from '@tower/types';
|
import { NormalizedReaction, ForwardJobData, IndexJobData } from '@tower/types';
|
||||||
|
|
||||||
|
export interface ApprovalResult {
|
||||||
|
forwardJobs: ForwardJobData[];
|
||||||
|
indexDoc: IndexJobData;
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleStarReaction(
|
export async function handleStarReaction(
|
||||||
reaction: NormalizedReaction,
|
reaction: NormalizedReaction,
|
||||||
adminJids: string[],
|
adminJids: string[],
|
||||||
prisma: any,
|
prisma: any,
|
||||||
): Promise<ForwardJobData[] | null> {
|
): Promise<ApprovalResult | null> {
|
||||||
if (reaction.emoji !== '⭐') return null;
|
if (reaction.emoji !== '⭐') return null;
|
||||||
if (!adminJids.includes(reaction.reactorJid)) return null;
|
if (!adminJids.includes(reaction.reactorJid)) return null;
|
||||||
|
|
||||||
@@ -36,7 +41,7 @@ export async function handleStarReaction(
|
|||||||
where: { id: message.id, status: 'PENDING' },
|
where: { id: message.id, status: 'PENDING' },
|
||||||
data: { status: 'APPROVED' },
|
data: { status: 'APPROVED' },
|
||||||
});
|
});
|
||||||
if (updated.count === 0) return; // another admin approved first — idempotent
|
if (updated.count === 0) return;
|
||||||
approved = true;
|
approved = true;
|
||||||
await tx.approval.create({
|
await tx.approval.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -49,7 +54,7 @@ export async function handleStarReaction(
|
|||||||
|
|
||||||
if (!approved) return null;
|
if (!approved) return null;
|
||||||
|
|
||||||
const jobs: ForwardJobData[] = message.sourceGroup.syncRoutesFrom
|
const forwardJobs: ForwardJobData[] = message.sourceGroup.syncRoutesFrom
|
||||||
.filter((route: any) => route.targetGroup != null)
|
.filter((route: any) => route.targetGroup != null)
|
||||||
.map((route: any) => ({
|
.map((route: any) => ({
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
@@ -57,9 +62,19 @@ export async function handleStarReaction(
|
|||||||
sourceGroupName: message.sourceGroup.name,
|
sourceGroupName: message.sourceGroup.name,
|
||||||
senderName: message.senderName ?? undefined,
|
senderName: message.senderName ?? undefined,
|
||||||
toGroupJid: route.targetGroup.platformId,
|
toGroupJid: route.targetGroup.platformId,
|
||||||
// fallback: use the account that received the reaction when target group has no assigned account
|
|
||||||
fromAccountId: route.targetGroup.accountId ?? reaction.accountId,
|
fromAccountId: route.targetGroup.accountId ?? reaction.accountId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return jobs;
|
const indexDoc: IndexJobData = {
|
||||||
|
messageId: message.id,
|
||||||
|
content: message.content,
|
||||||
|
senderName: message.senderName ?? null,
|
||||||
|
sourceGroupId: message.sourceGroupId,
|
||||||
|
sourceGroupName: message.sourceGroup.name,
|
||||||
|
tags: message.tags,
|
||||||
|
platform: message.platform,
|
||||||
|
approvedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { forwardJobs, indexDoc };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user