feat(admin+api): 代理停用默认、结算加固与冒烟配置探针
- 代理层级默认授信比例与停用冻结/禁登全局默认 - 结算预览去重、比分校验、串关当场判负与市场类型校验 - 站内信 Banner/公告自动通知开关;开发环境动态 API 端口 - 扩充 RBAC/结算/认证/返现单元测试与 agent skills
This commit is contained in:
255
apps/api/src/applications/admin/admin.controller.spec.ts
Normal file
255
apps/api/src/applications/admin/admin.controller.spec.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
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 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user