import type { QqbotMessagePushApi } from '@test-source/apps/web-antdv-next/src/api/qqbot/message-push'; import { resolve } from 'node:path'; import * as messagePushApi from '@test-source/apps/web-antdv-next/src/api/qqbot/message-push'; import { createAccountMessagePushBinding, createMessageSubscription, createMessageTemplate, deleteAccountMessagePushBinding, deleteMessageSubscription, deleteMessageTemplate, getAccountMessagePushBindings, getAccountMessagePushTargets, getMessagePushSourceDetail, getMessagePushSourceOptions, getMessagePushSources, getMessageSubscriptionList, getMessageTemplateList, getStunMappingPortChangedOptions, previewMessageTemplate, setAccountMessagePushBindingEnabled, setMessageSubscriptionEnabled, setMessageTemplateEnabled, updateAccountMessagePushBinding, updateMessageSubscription, updateMessageTemplate, } from '@test-source/apps/web-antdv-next/src/api/qqbot/message-push'; import ts from 'typescript'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { requestClient } from '#/api/request'; type Equal = (() => Value extends Actual ? 1 : 2) extends < Value, >() => Value extends Expected ? 1 : 2 ? true : false; type Assert = Condition; type ExpectedSystemMessageSourceVariableDefinition = { description: string; example: string; key: string; label: string; type: 'boolean' | 'number' | 'string'; }; type ExpectedSystemMessageSourceFieldDefinition = { dependsOn?: string; key: string; label: string; optionCollection: string; required: boolean; type: 'select'; }; type ExpectedSystemMessageSourceDefinition = { description: string; displayName: string; sourceKey: string; subscriptionFields: ExpectedSystemMessageSourceFieldDefinition[]; variables: ExpectedSystemMessageSourceVariableDefinition[]; version: 1; }; type ExpectedStunMappingPortChangedOptionsResponse = { ddnsRecords: Array<{ disabledReasonCode: null | string; eligible: boolean; fqdn: string; id: string; name: string; portForwardId: string; }>; portForwards: Array<{ disabledReasonCode: null | string; eligible: boolean; externalPort: number; id: string; internalPort: number; name: string; protocol: 'tcp' | 'udp'; }>; }; type ExpectedSystemMessageSourceOptionDefinition = { dependsOnValue?: string; disabled: boolean; disabledReasonCode: null | string; label: string; value: string; }; type ExpectedSystemMessageSourceOptionsResponse = Record< string, ExpectedSystemMessageSourceOptionDefinition[] >; type ExpectedMessageSubscriptionView = { createTime: string; enabled: boolean; id: string; invalidReasonCode: null | string; name: string; remark: null | string; sourceConfig: Record; sourceKey: string; sourceName: string; sourceSummary: string; updateTime: string; valid: boolean; }; type ExpectedMessageSubscriptionListQuery = { enabled?: boolean; name?: string; pageNo?: number; pageSize?: number; sourceKey?: string; }; type ExpectedMessageSubscriptionInput = { enabled: boolean; name: string; remark?: string; sourceConfig: Record; sourceKey: string; }; type ExpectedMessageTemplateView = { content: string; createTime: string; enabled: boolean; id: string; name: string; referenceCount: number; remark: null | string; sourceKey: string; sourceName: string; updateTime: string; }; type ExpectedMessageTemplateListQuery = ExpectedMessageSubscriptionListQuery; type ExpectedMessageTemplateInput = { content: string; enabled: boolean; name: string; remark?: string; sourceKey: string; }; type ExpectedMessageTemplatePreview = { renderedMessage: string; variables: Record; }; type ExpectedQqbotMessagePublishBindingInput = { enabled: boolean; subscriptionId: string; targets: Array<{ targetId: string; targetName?: string; targetType: 'group' | 'private'; }>; templateId: string; }; type ExpectedQqbotMessagePushTargetOptionsResponse = { available: boolean; options: Array<{ label: string; targetId: string; targetType: 'group' | 'private'; }>; reasonCode: null | string; }; type ExpectedQqbotMessagePublishBindingView = { available: boolean; createTime: string; enabled: boolean; id: string; invalidReasonCode: null | string; sourceKey: string; sourceName: string; subscriptionId: string; subscriptionName: string; targets: Array<{ enabled: boolean; id: string; targetId: string; targetName: null | string; targetType: 'group' | 'private'; }>; templateId: string; templateName: string; updateTime: string; }; type MessagePushTypeContracts = [ Assert< Equal< QqbotMessagePushApi.SystemMessageScalar, boolean | null | number | string > >, Assert< Equal< QqbotMessagePushApi.SystemMessageSourceVariableDefinition, ExpectedSystemMessageSourceVariableDefinition > >, Assert< Equal< QqbotMessagePushApi.SystemMessageSourceFieldDefinition, ExpectedSystemMessageSourceFieldDefinition > >, Assert< Equal< QqbotMessagePushApi.SystemMessageSourceDefinition, ExpectedSystemMessageSourceDefinition > >, Assert< Equal< QqbotMessagePushApi.StunMappingPortChangedSubscriptionConfig, { ddnsRecordId: string; portForwardId: string } > >, Assert< Equal< QqbotMessagePushApi.StunMappingPortChangedOptionsResponse, ExpectedStunMappingPortChangedOptionsResponse > >, Assert< Equal< QqbotMessagePushApi.SystemMessageSourceOptionDefinition, ExpectedSystemMessageSourceOptionDefinition > >, Assert< Equal< QqbotMessagePushApi.SystemMessageSourceOptionsResponse, ExpectedSystemMessageSourceOptionsResponse > >, Assert< Equal< QqbotMessagePushApi.MessageSubscriptionView, ExpectedMessageSubscriptionView > >, Assert< Equal< QqbotMessagePushApi.MessageSubscriptionListQuery, ExpectedMessageSubscriptionListQuery > >, Assert< Equal< QqbotMessagePushApi.MessageSubscriptionInput, ExpectedMessageSubscriptionInput > >, Assert< Equal >, Assert< Equal< QqbotMessagePushApi.MessageTemplateListQuery, ExpectedMessageTemplateListQuery > >, Assert< Equal< QqbotMessagePushApi.MessageTemplateInput, ExpectedMessageTemplateInput > >, Assert< Equal< QqbotMessagePushApi.MessageTemplatePreviewInput, { content: string; sourceKey: string } > >, Assert< Equal< QqbotMessagePushApi.MessageTemplatePreview, ExpectedMessageTemplatePreview > >, Assert< Equal >, Assert< Equal< QqbotMessagePushApi.QqbotMessagePublishTargetInput, { targetId: string; targetName?: string; targetType: 'group' | 'private' } > >, Assert< Equal< QqbotMessagePushApi.QqbotMessagePublishBindingInput, ExpectedQqbotMessagePublishBindingInput > >, Assert< Equal< QqbotMessagePushApi.QqbotMessagePushTargetOption, { label: string; targetId: string; targetType: 'group' | 'private' } > >, Assert< Equal< QqbotMessagePushApi.QqbotMessagePushTargetOptionsResponse, ExpectedQqbotMessagePushTargetOptionsResponse > >, Assert< Equal< QqbotMessagePushApi.QqbotMessagePublishTargetView, { enabled: boolean; id: string; targetId: string; targetName: null | string; targetType: 'group' | 'private'; } > >, Assert< Equal< QqbotMessagePushApi.QqbotMessagePublishBindingView, ExpectedQqbotMessagePublishBindingView > >, Assert< Equal< QqbotMessagePushApi.PageResult, { items: ExpectedMessageSubscriptionView[]; total: number } > >, Assert, []>>, Assert< Equal< ReturnType, Promise > >, Assert< Equal, [sourceKey: string]> >, Assert< Equal< ReturnType, Promise > >, Assert< Equal, [sourceKey: string]> >, Assert< Equal< ReturnType, Promise > >, Assert, []>>, Assert< Equal< ReturnType, Promise > >, Assert< Equal< Parameters, [params: ExpectedMessageSubscriptionListQuery] > >, Assert< Equal< ReturnType, Promise<{ items: ExpectedMessageSubscriptionView[]; total: number }> > >, Assert< Equal< Parameters, [data: ExpectedMessageSubscriptionInput] > >, Assert< Equal< ReturnType, Promise > >, Assert< Equal< Parameters, [id: string, data: ExpectedMessageSubscriptionInput] > >, Assert< Equal< ReturnType, Promise > >, Assert< Equal< Parameters, [id: string, enabled: boolean] > >, Assert< Equal< ReturnType, Promise > >, Assert, [id: string]>>, Assert, Promise>>, Assert< Equal< Parameters, [params: ExpectedMessageTemplateListQuery] > >, Assert< Equal< ReturnType, Promise<{ items: ExpectedMessageTemplateView[]; total: number }> > >, Assert< Equal< Parameters, [data: ExpectedMessageTemplateInput] > >, Assert< Equal< ReturnType, Promise > >, Assert< Equal< Parameters, [id: string, data: ExpectedMessageTemplateInput] > >, Assert< Equal< ReturnType, Promise > >, Assert< Equal< Parameters, [id: string, enabled: boolean] > >, Assert< Equal< ReturnType, Promise > >, Assert, [id: string]>>, Assert, Promise>>, Assert< Equal< Parameters, [data: { content: string; sourceKey: string }] > >, Assert< Equal< ReturnType, Promise > >, Assert< Equal, [selfId: string]> >, Assert< Equal< ReturnType, Promise > >, Assert< Equal< Parameters, [selfId: string, data: ExpectedQqbotMessagePublishBindingInput] > >, Assert< Equal< ReturnType, Promise > >, Assert< Equal< Parameters, [ selfId: string, id: string, data: ExpectedQqbotMessagePublishBindingInput, ] > >, Assert< Equal< ReturnType, Promise > >, Assert< Equal< Parameters, [selfId: string, id: string, enabled: boolean] > >, Assert< Equal< ReturnType, Promise > >, Assert< Equal< Parameters, [selfId: string, id: string] > >, Assert< Equal, Promise> >, Assert< Equal, [selfId: string]> >, Assert< Equal< ReturnType, Promise > >, ]; const messagePushCallerNames = [ 'createAccountMessagePushBinding', 'createMessageSubscription', 'createMessageTemplate', 'deleteAccountMessagePushBinding', 'deleteMessageSubscription', 'deleteMessageTemplate', 'getAccountMessagePushBindings', 'getAccountMessagePushTargets', 'getMessagePushSourceDetail', 'getMessagePushSourceOptions', 'getMessagePushSources', 'getMessageSubscriptionList', 'getMessageTemplateList', 'getStunMappingPortChangedOptions', 'previewMessageTemplate', 'setAccountMessagePushBindingEnabled', 'setMessageSubscriptionEnabled', 'setMessageTemplateEnabled', 'updateAccountMessagePushBinding', 'updateMessageSubscription', 'updateMessageTemplate', ]; /** 使用工作区 TypeScript 配置检查消息推送公开契约。 */ function getMessagePushContractDiagnostics() { const appRoot = resolve('apps/web-antdv-next'); const configPath = resolve(appRoot, 'tsconfig.json'); const testFilePath = resolve('test/api/qqbot/message-push.spec.ts'); const virtualRequestModulePath = '/__qqbot-message-push-contract__/request.ts'; const virtualRequestModuleSource = ` export declare const requestClient: { delete(url: string, config?: unknown): Promise; get(url: string, config?: unknown): Promise; post(url: string, data?: unknown, config?: unknown): Promise; put(url: string, data?: unknown, config?: unknown): Promise; };`; const config = ts.readConfigFile(configPath, ts.sys.readFile); const parsed = ts.parseJsonConfigFileContent( config.config, ts.sys, appRoot, { incremental: false, noEmit: true }, configPath, ); const baseCompilerHost = ts.createCompilerHost(parsed.options, true); const compilerHost: ts.CompilerHost = { ...baseCompilerHost, fileExists(fileName) { return ( fileName === virtualRequestModulePath || baseCompilerHost.fileExists(fileName) ); }, getSourceFile( fileName, languageVersion, onError, shouldCreateNewSourceFile, ) { if (fileName === virtualRequestModulePath) { return ts.createSourceFile( fileName, virtualRequestModuleSource, languageVersion, true, ); } return baseCompilerHost.getSourceFile( fileName, languageVersion, onError, shouldCreateNewSourceFile, ); }, readFile(fileName) { return fileName === virtualRequestModulePath ? virtualRequestModuleSource : baseCompilerHost.readFile(fileName); }, resolveModuleNames(moduleNames, containingFile) { return moduleNames.map((moduleName) => { if (moduleName === '#/api/request') { return { extension: ts.Extension.Ts, isExternalLibraryImport: false, resolvedFileName: virtualRequestModulePath, }; } if (moduleName.startsWith('@test-source/')) { return ts.resolveModuleName( resolve(moduleName.slice('@test-source/'.length)), containingFile, parsed.options, baseCompilerHost, ).resolvedModule; } return ts.resolveModuleName( moduleName, containingFile, parsed.options, baseCompilerHost, ).resolvedModule; }); }, }; const program = ts.createProgram({ host: compilerHost, options: parsed.options, rootNames: [testFilePath], }); return ts .getPreEmitDiagnostics(program) .map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'), ); } vi.mock('#/api/request', () => ({ requestClient: { delete: vi.fn(), get: vi.fn(), post: vi.fn(), put: vi.fn(), }, })); describe('qqbot message push api', () => { beforeEach(() => { vi.clearAllMocks(); }); it('owns the exact source, subscription, and template callers', async () => { const sourceKey = 'network.stun/a b'; const id = '10000000000000001'; const previewContent = '端口 $' + '{{port}}'; const subscriptionInput = { enabled: true, name: 'STUN 端口变更', sourceConfig: { ddnsRecordId: '10000000000000003', portForwardId: '10000000000000002', }, sourceKey: 'network.stun.mapping-port-changed', }; const templateInput = { content: previewContent, enabled: true, name: '端口通知', sourceKey: 'network.stun.mapping-port-changed', }; const subscriptionQuery = { enabled: true, name: 'STUN', pageNo: 1, pageSize: 10, sourceKey: 'network.stun.mapping-port-changed', }; const templateQuery = { enabled: false, name: '通知', pageNo: 2, pageSize: 20, sourceKey: 'network.stun.mapping-port-changed', }; await getMessagePushSources(); await getMessagePushSourceDetail(sourceKey); await getMessagePushSourceOptions(sourceKey); await getStunMappingPortChangedOptions(); await getMessageSubscriptionList(subscriptionQuery); await createMessageSubscription(subscriptionInput); await updateMessageSubscription('id/a b', subscriptionInput); await setMessageSubscriptionEnabled(id, false); await deleteMessageSubscription('id/a b'); await getMessageTemplateList(templateQuery); await createMessageTemplate(templateInput); await updateMessageTemplate('template/a b', templateInput); await setMessageTemplateEnabled(id, false); await deleteMessageTemplate('template/a b'); await previewMessageTemplate({ content: previewContent, sourceKey: 'network.stun.mapping-port-changed', }); expect(requestClient.get).toHaveBeenNthCalledWith( 1, '/qqbot/message-push/sources', ); expect(requestClient.get).toHaveBeenNthCalledWith( 2, '/qqbot/message-push/sources/network.stun%2Fa%20b', ); expect(requestClient.get).toHaveBeenNthCalledWith( 3, '/qqbot/message-push/sources/network.stun%2Fa%20b/subscription-options', ); expect(requestClient.get).toHaveBeenNthCalledWith( 4, '/qqbot/message-push/sources/network.stun.mapping-port-changed/options', ); expect(requestClient.get).toHaveBeenNthCalledWith( 5, '/qqbot/message-push/subscriptions', { params: subscriptionQuery }, ); expect(requestClient.get).toHaveBeenNthCalledWith( 6, '/qqbot/message-push/templates', { params: templateQuery }, ); expect(requestClient.post).toHaveBeenNthCalledWith( 1, '/qqbot/message-push/subscriptions', subscriptionInput, ); expect(requestClient.post).toHaveBeenNthCalledWith( 2, '/qqbot/message-push/templates', templateInput, ); expect(requestClient.post).toHaveBeenNthCalledWith( 3, '/qqbot/message-push/templates/preview', { content: previewContent, sourceKey: 'network.stun.mapping-port-changed', }, ); expect(requestClient.put).toHaveBeenNthCalledWith( 1, '/qqbot/message-push/subscriptions/id%2Fa%20b', subscriptionInput, ); expect(requestClient.put).toHaveBeenNthCalledWith( 2, '/qqbot/message-push/subscriptions/10000000000000001/enabled', { enabled: false }, ); expect(requestClient.put).toHaveBeenNthCalledWith( 3, '/qqbot/message-push/templates/template%2Fa%20b', templateInput, ); expect(requestClient.put).toHaveBeenNthCalledWith( 4, '/qqbot/message-push/templates/10000000000000001/enabled', { enabled: false }, ); expect(requestClient.delete).toHaveBeenNthCalledWith( 1, '/qqbot/message-push/subscriptions/id%2Fa%20b', ); expect(requestClient.delete).toHaveBeenNthCalledWith( 2, '/qqbot/message-push/templates/template%2Fa%20b', ); }); it('keeps account IDs as strings and encodes every account path segment', async () => { const selfId = '10000000000000001/a b'; const bindingId = 'binding/a b'; const bindingInput = { enabled: true, subscriptionId: '10000000000000002', targets: [ { targetId: '10000000000000003', targetName: '测试群', targetType: 'group' as const, }, ], templateId: '10000000000000004', }; await getAccountMessagePushBindings(selfId); await createAccountMessagePushBinding(selfId, bindingInput); await updateAccountMessagePushBinding(selfId, bindingId, bindingInput); await setAccountMessagePushBindingEnabled(selfId, bindingId, false); await deleteAccountMessagePushBinding(selfId, bindingId); await getAccountMessagePushTargets(selfId); const accountPath = '/qqbot/accounts/10000000000000001%2Fa%20b/message-push'; expect(requestClient.get).toHaveBeenNthCalledWith( 1, `${accountPath}/bindings`, ); expect(requestClient.get).toHaveBeenNthCalledWith( 2, `${accountPath}/targets`, ); expect(requestClient.post).toHaveBeenCalledWith( `${accountPath}/bindings`, bindingInput, ); expect(requestClient.put).toHaveBeenNthCalledWith( 1, `${accountPath}/bindings/binding%2Fa%20b`, bindingInput, ); expect(requestClient.put).toHaveBeenNthCalledWith( 2, `${accountPath}/bindings/binding%2Fa%20b/enabled`, { enabled: false }, ); expect(requestClient.delete).toHaveBeenCalledWith( `${accountPath}/bindings/binding%2Fa%20b`, ); }); it('exports exactly the twenty-one approved callers', () => { expect(Object.keys(messagePushApi).toSorted()).toEqual( messagePushCallerNames, ); }); it('type-checks the complete public namespace and caller contract', () => { const typeContracts: MessagePushTypeContracts = [] as never; void typeContracts; expect(getMessagePushContractDiagnostics()).toEqual([]); }); });