import { afterEach, expect, test, vi } from 'vitest'; import { createAdminAccount, executeAdminRechargeRefund, getAdminFeatureGateConfig, getAdminUserDetail, listAdminRechargeOrders, reconcileAdminUserConsumption, resolveAdminRechargeRefundManualReview, updateAdminAccount, uploadAdminEditorShowcaseCampaignImage, upsertAdminFeatureGateConfig, upsertProfileWalletConfig, } from './adminApiClient'; afterEach(() => { vi.unstubAllGlobals(); }); test('后台账号创建和更新同时携带 Tab 与独立操作权限', async () => { const fetchMock = vi.fn().mockImplementation(() => Promise.resolve( new Response(JSON.stringify({ account: { accountId: 'member-1' } }), { status: 200, }), ), ); vi.stubGlobal('fetch', fetchMock); await createAdminAccount('owner-token', { username: 'operator', displayName: '运营', password: 'secret123', tabPermissions: ['dashboard', 'tracking'], actionPermissions: ['profile-wallet-consumption-reconcile'], enabled: true, }); await updateAdminAccount('owner-token', 'member/1', { displayName: '运营二组', tabPermissions: ['tracking'], actionPermissions: [], enabled: false, }); expect(fetchMock.mock.calls[0]?.[0]).toBe('/admin/api/accounts'); expect(fetchMock.mock.calls[0]?.[1]).toEqual( expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer owner-token' }), body: JSON.stringify({ username: 'operator', displayName: '运营', password: 'secret123', tabPermissions: ['dashboard', 'tracking'], actionPermissions: ['profile-wallet-consumption-reconcile'], enabled: true, }), }), ); expect(fetchMock.mock.calls[1]?.[0]).toBe('/admin/api/accounts/member%2F1'); expect(fetchMock.mock.calls[1]?.[1]).toEqual( expect.objectContaining({ method: 'PUT', body: JSON.stringify({ displayName: '运营二组', tabPermissions: ['tracking'], actionPermissions: [], enabled: false, }), }), ); }); test('账号配置一次提交初始和每日免费泥点', async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({configId: 'profile_wallet'}), { status: 200, headers: {'content-type': 'application/json'}, }), ); vi.stubGlobal('fetch', fetchMock); await upsertProfileWalletConfig('owner-token', { initialMudPoints: 100, dailyFreePointsPerDay: 35, }); expect(fetchMock).toHaveBeenCalledWith( '/admin/api/profile/wallet-config', expect.objectContaining({ method: 'POST', headers: expect.objectContaining({Authorization: 'Bearer owner-token'}), body: JSON.stringify({ initialMudPoints: 100, dailyFreePointsPerDay: 35, }), }), ); }); test('灰度配置读写只使用通用 feature-gates 管理接口', async () => { const fetchMock = vi.fn().mockImplementation(() => Promise.resolve( new Response(JSON.stringify({ gates: [] }), { status: 200, headers: { 'content-type': 'application/json' }, }), ), ); vi.stubGlobal('fetch', fetchMock); await getAdminFeatureGateConfig('gray-token'); await upsertAdminFeatureGateConfig('gray-token', { gateKey: 'image-editor:agent-sidebar', enabled: true, rolloutPercent: 25, allowUserIds: ['user-1'], allowUserTags: ['beta'], denyUserIds: ['blocked-1'], description: '画布 Agent 入口灰度', }); expect(fetchMock.mock.calls[0]?.[0]).toBe('/admin/api/feature-gates'); expect(fetchMock.mock.calls[0]?.[1]).toEqual( expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer gray-token' }), }), ); expect(fetchMock.mock.calls[1]?.[0]).toBe('/admin/api/feature-gates'); expect(fetchMock.mock.calls[1]?.[1]).toEqual( expect.objectContaining({ method: 'PUT', headers: expect.objectContaining({ Authorization: 'Bearer gray-token' }), body: JSON.stringify({ gateKey: 'image-editor:agent-sidebar', enabled: true, rolloutPercent: 25, allowUserIds: ['user-1'], allowUserTags: ['beta'], denyUserIds: ['blocked-1'], description: '画布 Agent 入口灰度', }), }), ); }); test('活动卡图片上传成功后先确认正式私有对象再返回图片引用', async () => { const closeBitmap = vi.fn(); vi.stubGlobal( 'createImageBitmap', vi .fn() .mockResolvedValue({ width: 1024, height: 1536, close: closeBitmap }), ); const fetchMock = vi .fn() .mockResolvedValueOnce( new Response( JSON.stringify({ upload: { bucket: 'genarrative-release', host: 'https://genarrative-release.oss.example.com', objectKey: 'generated-character-drafts/editor/showcase-campaign/current/card.png', legacyPublicPath: '/generated-character-drafts/editor/showcase-campaign/current/card.png', contentType: 'image/png', formFields: { key: 'campaign-key', policy: 'signed-policy' }, }, }), { status: 200, headers: { 'content-type': 'application/json' } }, ), ) .mockResolvedValueOnce(new Response('', { status: 200 })) .mockResolvedValueOnce( new Response( JSON.stringify({ assetObject: { assetObjectId: 'assetobj-1' } }), { status: 200, headers: { 'content-type': 'application/json' }, }, ), ); vi.stubGlobal('fetch', fetchMock); const file = new File(['image-bytes'], 'card.png', { type: 'image/png' }); const uploaded = await uploadAdminEditorShowcaseCampaignImage( 'admin-token', file, ); expect(fetchMock.mock.calls[0]?.[0]).toBe( '/admin/api/editor-showcase/campaign/image-upload-ticket', ); expect(fetchMock.mock.calls[1]?.[0]).toBe( 'https://genarrative-release.oss.example.com', ); expect(fetchMock.mock.calls[2]?.[0]).toBe( '/admin/api/editor-showcase/campaign/image-upload-confirm', ); expect(fetchMock.mock.calls[2]?.[1]).toEqual( expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer admin-token' }), body: JSON.stringify({ bucket: 'genarrative-release', objectKey: 'generated-character-drafts/editor/showcase-campaign/current/card.png', contentType: 'image/png', contentLength: file.size, }), }), ); expect(uploaded).toEqual({ imageSrc: '/generated-character-drafts/editor/showcase-campaign/current/card.png', imageObjectKey: 'generated-character-drafts/editor/showcase-campaign/current/card.png', imageWidth: 1024, imageHeight: 1536, legacyPublicPath: '/generated-character-drafts/editor/showcase-campaign/current/card.png', }); expect(closeBitmap).toHaveBeenCalledOnce(); }); test('充值订单查询按后台契约序列化筛选参数', async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ entries: [] }), { status: 200, headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); await listAdminRechargeOrders('token-1', { orderId: 'order 1', userId: 'user-1', providerTransactionId: 'wx-1', paymentChannel: 'wechat_native', status: 'paid', createdAfter: '2026-07-01T00:00:00.000Z', createdBefore: '2026-07-13T23:59:00.000Z', limit: 50, }); const requestUrl = String(fetchMock.mock.calls[0]?.[0]); const parsed = new URL(requestUrl, 'http://admin.local'); expect(parsed.pathname).toBe('/admin/api/profile/recharge-orders'); expect(Object.fromEntries(parsed.searchParams)).toEqual({ orderId: 'order 1', providerTransactionId: 'wx-1', userId: 'user-1', paymentChannel: 'wechat_native', status: 'paid', createdAfter: '2026-07-01T00:00:00.000Z', createdBefore: '2026-07-13T23:59:00.000Z', limit: '50', }); }); test('用户详情只发送实际提供的用户定位字段', async () => { const fetchMock = vi .fn() .mockResolvedValue( new Response(JSON.stringify({ userId: 'user-1' }), { status: 200 }), ); vi.stubGlobal('fetch', fetchMock); await getAdminUserDetail('token-1', { publicUserCode: 'TN1001' }); const requestUrl = String(fetchMock.mock.calls[0]?.[0]); const parsed = new URL(requestUrl, 'http://admin.local'); expect(parsed.pathname).toBe('/admin/api/profile/users/detail'); expect(Object.fromEntries(parsed.searchParams)).toEqual({ publicUserCode: 'TN1001', }); }); test('历史花费手动对账使用独立管理员写接口', async () => { const fetchMock = vi.fn().mockResolvedValue( new Response( JSON.stringify({ userId: 'user-1', historicalConsumedPoints: 1300, changed: true, }), { status: 200 }, ), ); vi.stubGlobal('fetch', fetchMock); await reconcileAdminUserConsumption('token-1', { userId: 'user-1' }); expect(String(fetchMock.mock.calls[0]?.[0])).toBe( '/admin/api/profile/users/reconcile-consumption', ); expect(fetchMock.mock.calls[0]?.[1]).toEqual( expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer token-1' }), body: JSON.stringify({ userId: 'user-1' }), }), ); }); test('退款执行使用独立 execute 管理员路由', async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ outRefundNo: 'refund-1' }), { status: 200, }), ); vi.stubGlobal('fetch', fetchMock); await executeAdminRechargeRefund('token-1', { orderId: 'order-1', refundAmountCents: 300, requestId: 'request-1', reason: '用户申请', }); expect(String(fetchMock.mock.calls[0]?.[0])).toBe( '/admin/api/profile/recharge-refunds/execute', ); expect(fetchMock.mock.calls[0]?.[1]).toEqual( expect.objectContaining({ method: 'POST', body: JSON.stringify({ orderId: 'order-1', refundAmountCents: 300, requestId: 'request-1', reason: '用户申请', }), }), ); }); test('退款人工复核使用独立 resolve 管理员路由', async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ outRefundNo: 'refund-1' }), { status: 200, }), ); vi.stubGlobal('fetch', fetchMock); await resolveAdminRechargeRefundManualReview('token-1', { outRefundNo: 'refund-1', reason: '已核对微信商户平台原始账单', expectedErrorCode: 'provider_transaction_id_mismatch', }); expect(String(fetchMock.mock.calls[0]?.[0])).toBe( '/admin/api/profile/recharge-refunds/manual-review/resolve', ); expect(fetchMock.mock.calls[0]?.[1]).toEqual( expect.objectContaining({ method: 'POST', body: JSON.stringify({ outRefundNo: 'refund-1', reason: '已核对微信商户平台原始账单', expectedErrorCode: 'provider_transaction_id_mismatch', }), }), ); });