Files
thebet365/apps/api/src/applications/admin/admin.controller.spec.ts
Mars ce84226219 feat(admin+api): 代理停用默认、结算加固与冒烟配置探针
- 代理层级默认授信比例与停用冻结/禁登全局默认

- 结算预览去重、比分校验、串关当场判负与市场类型校验

- 站内信 Banner/公告自动通知开关;开发环境动态 API 端口

- 扩充 RBAC/结算/认证/返现单元测试与 agent skills
2026-06-23 11:08:41 +08:00

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 }),
}),
);
});
});