- 代理层级默认授信比例与停用冻结/禁登全局默认 - 结算预览去重、比分校验、串关当场判负与市场类型校验 - 站内信 Banner/公告自动通知开关;开发环境动态 API 端口 - 扩充 RBAC/结算/认证/返现单元测试与 agent skills
256 lines
7.8 KiB
TypeScript
256 lines
7.8 KiB
TypeScript
import { Decimal } from '@prisma/client/runtime/library';
|
|
import { AdminController } from './admin.controller';
|
|
import { AgentsService } from '../../domains/agent/agents.service';
|
|
import { createPrismaMock } from '../../testing/prisma-mock';
|
|
|
|
function stubDeps() {
|
|
return {
|
|
users: {},
|
|
agents: { updateAgentAdmin: jest.fn() } as AgentsService | { updateAgentAdmin: jest.Mock },
|
|
wallet: {},
|
|
matches: {},
|
|
catalogArchive: {},
|
|
outright: {},
|
|
markets: {},
|
|
settlement: {},
|
|
cashback: {},
|
|
content: { create: jest.fn() },
|
|
i18n: {},
|
|
audit: { log: jest.fn().mockResolvedValue(undefined) },
|
|
bets: {},
|
|
prisma: {},
|
|
dashboardService: {},
|
|
systemConfig: { getInboxNotifySettings: jest.fn() },
|
|
bettingLimits: {},
|
|
databaseReset: {},
|
|
smokeTests: {},
|
|
depositService: {},
|
|
playerMessages: {
|
|
broadcastBannerPromotion: jest.fn(),
|
|
broadcastAnnouncementPromotion: jest.fn(),
|
|
},
|
|
staff: {},
|
|
presence: {},
|
|
depositCleanup: {},
|
|
};
|
|
}
|
|
|
|
function buildController(deps: ReturnType<typeof stubDeps>) {
|
|
return new AdminController(
|
|
deps.users as never,
|
|
deps.agents as never,
|
|
deps.wallet as never,
|
|
deps.matches as never,
|
|
deps.catalogArchive as never,
|
|
deps.outright as never,
|
|
deps.markets as never,
|
|
deps.settlement as never,
|
|
deps.cashback as never,
|
|
deps.content as never,
|
|
deps.i18n as never,
|
|
deps.audit as never,
|
|
deps.bets as never,
|
|
deps.prisma as never,
|
|
deps.dashboardService as never,
|
|
deps.systemConfig as never,
|
|
deps.bettingLimits as never,
|
|
deps.databaseReset as never,
|
|
deps.smokeTests as never,
|
|
deps.depositService as never,
|
|
deps.playerMessages as never,
|
|
deps.staff as never,
|
|
deps.presence as never,
|
|
deps.depositCleanup as never,
|
|
);
|
|
}
|
|
|
|
describe('AdminController createContent inbox notify gating', () => {
|
|
const deps = stubDeps();
|
|
const controller = buildController(deps);
|
|
|
|
const baseDto = {
|
|
contentType: 'BANNER' as const,
|
|
status: 'ACTIVE' as const,
|
|
notifyInbox: true,
|
|
translations: [{ locale: 'zh-CN', title: 'Promo', body: 'Body' }],
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
deps.content.create.mockResolvedValue({ id: '88' });
|
|
deps.playerMessages.broadcastBannerPromotion.mockResolvedValue(3);
|
|
deps.playerMessages.broadcastAnnouncementPromotion.mockResolvedValue(5);
|
|
});
|
|
|
|
it('does not broadcast banner when inbox.notify.banner is false', async () => {
|
|
deps.systemConfig.getInboxNotifySettings.mockResolvedValue({
|
|
inboxEnabled: true,
|
|
deposit: true,
|
|
banner: false,
|
|
announcement: true,
|
|
});
|
|
|
|
const res = await controller.createContent(baseDto);
|
|
|
|
expect(deps.playerMessages.broadcastBannerPromotion).not.toHaveBeenCalled();
|
|
expect((res.data as { notifiedCount?: number }).notifiedCount).toBeUndefined();
|
|
});
|
|
|
|
it('broadcasts banner when inbox.notify.banner is true', async () => {
|
|
deps.systemConfig.getInboxNotifySettings.mockResolvedValue({
|
|
inboxEnabled: true,
|
|
deposit: true,
|
|
banner: true,
|
|
announcement: false,
|
|
});
|
|
|
|
const res = await controller.createContent(baseDto);
|
|
|
|
expect(deps.playerMessages.broadcastBannerPromotion).toHaveBeenCalledWith({
|
|
contentId: 88n,
|
|
translations: [{ locale: 'zh-CN', title: 'Promo', body: 'Body' }],
|
|
});
|
|
expect((res.data as { notifiedCount?: number }).notifiedCount).toBe(3);
|
|
});
|
|
|
|
it('does not broadcast announcement when inbox.notify.announcement is false', async () => {
|
|
deps.systemConfig.getInboxNotifySettings.mockResolvedValue({
|
|
inboxEnabled: true,
|
|
deposit: true,
|
|
banner: true,
|
|
announcement: false,
|
|
});
|
|
|
|
await controller.createContent({
|
|
...baseDto,
|
|
contentType: 'NOTICE',
|
|
});
|
|
|
|
expect(deps.playerMessages.broadcastAnnouncementPromotion).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('skips notify when inbox feature is disabled even if notifyInbox is checked', async () => {
|
|
deps.systemConfig.getInboxNotifySettings.mockResolvedValue({
|
|
inboxEnabled: false,
|
|
deposit: true,
|
|
banner: true,
|
|
announcement: true,
|
|
});
|
|
|
|
await controller.createContent(baseDto);
|
|
|
|
expect(deps.playerMessages.broadcastBannerPromotion).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('AdminController updateAgent suspend wiring', () => {
|
|
const agentId = 10n;
|
|
const operatorId = 1n;
|
|
|
|
const tx = {
|
|
user: {
|
|
create: jest.fn(),
|
|
findUnique: jest.fn(),
|
|
update: jest.fn(),
|
|
},
|
|
userAuth: { create: jest.fn() },
|
|
userPreference: { create: jest.fn() },
|
|
userInvite: { create: jest.fn(), findUnique: jest.fn() },
|
|
agentProfile: { create: jest.fn(), findUnique: jest.fn() },
|
|
agentClosure: { create: jest.fn(), findMany: jest.fn() },
|
|
};
|
|
|
|
const prismaBase = {
|
|
...tx,
|
|
agentProfile: { ...tx.agentProfile, update: jest.fn() },
|
|
user: { ...tx.user, updateMany: jest.fn() },
|
|
};
|
|
const prisma = createPrismaMock(prismaBase) as typeof prismaBase & { $transaction: jest.Mock };
|
|
|
|
const systemConfig = {
|
|
getAgentHierarchySettings: jest.fn(),
|
|
getAgentSuspendSettings: jest.fn(),
|
|
};
|
|
const audit = { log: jest.fn().mockResolvedValue(undefined) };
|
|
|
|
let agentsService: AgentsService;
|
|
let controller: AdminController;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
prisma.$transaction.mockImplementation(async (arg: unknown) => {
|
|
if (Array.isArray(arg)) return Promise.all(arg);
|
|
return (arg as (client: typeof tx) => Promise<unknown>)(tx);
|
|
});
|
|
|
|
systemConfig.getAgentSuspendSettings.mockResolvedValue({
|
|
suspendFreezeDirectPlayers: true,
|
|
suspendBlockPlayerLogin: true,
|
|
});
|
|
|
|
agentsService = new AgentsService(
|
|
prisma as never,
|
|
{ hashPassword: jest.fn() } as never,
|
|
systemConfig as never,
|
|
{} as never,
|
|
{ recalculateUsedCredit: jest.fn() } as never,
|
|
);
|
|
jest.spyOn(agentsService, 'getAgentAdminDetail').mockResolvedValue({ userId: '10' } as never);
|
|
|
|
prisma.agentProfile.findUnique.mockResolvedValue({
|
|
userId: agentId,
|
|
user: { username: 'agent-a', locale: 'zh-CN' },
|
|
parentAgentId: null,
|
|
});
|
|
prisma.user.updateMany.mockResolvedValue({ count: 2 });
|
|
prisma.agentProfile.update.mockResolvedValue({});
|
|
prisma.user.update.mockResolvedValue({});
|
|
|
|
const deps = stubDeps();
|
|
deps.agents = agentsService as never;
|
|
deps.audit = audit;
|
|
controller = buildController(deps);
|
|
});
|
|
|
|
it('forwards bare SUSPENDED body to AgentsService and applies global defaults', async () => {
|
|
await controller.updateAgent(operatorId, agentId.toString(), { status: 'SUSPENDED' });
|
|
|
|
expect(systemConfig.getAgentSuspendSettings).toHaveBeenCalled();
|
|
expect(prisma.user.updateMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({ parentId: agentId, userType: 'PLAYER' }),
|
|
data: { status: 'SUSPENDED' },
|
|
}),
|
|
);
|
|
expect(prisma.agentProfile.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({ blockDirectPlayerLogin: true }),
|
|
}),
|
|
);
|
|
expect(audit.log).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
operatorId,
|
|
action: 'UPDATE_AGENT',
|
|
module: 'AGENTS',
|
|
targetId: agentId.toString(),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('does not freeze players when global default is false and body omits flag', async () => {
|
|
systemConfig.getAgentSuspendSettings.mockResolvedValue({
|
|
suspendFreezeDirectPlayers: false,
|
|
suspendBlockPlayerLogin: false,
|
|
});
|
|
|
|
await controller.updateAgent(operatorId, agentId.toString(), { status: 'SUSPENDED' });
|
|
|
|
expect(prisma.user.updateMany).not.toHaveBeenCalled();
|
|
expect(prisma.agentProfile.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({ blockDirectPlayerLogin: false }),
|
|
}),
|
|
);
|
|
});
|
|
});
|