kt-template-admin/test/api/qqbot/message-push.spec.ts

891 lines
23 KiB
TypeScript

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<Actual, Expected> =
(<Value>() => Value extends Actual ? 1 : 2) extends <
Value,
>() => Value extends Expected ? 1 : 2
? true
: false;
type Assert<Condition extends true> = 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<string, string>;
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<string, string>;
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<string, boolean | number | string>;
};
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<QqbotMessagePushApi.MessageTemplateView, ExpectedMessageTemplateView>
>,
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<QqbotMessagePushApi.QqbotMessagePushTargetType, 'group' | 'private'>
>,
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<ExpectedMessageSubscriptionView>,
{ items: ExpectedMessageSubscriptionView[]; total: number }
>
>,
Assert<Equal<Parameters<typeof getMessagePushSources>, []>>,
Assert<
Equal<
ReturnType<typeof getMessagePushSources>,
Promise<ExpectedSystemMessageSourceDefinition[]>
>
>,
Assert<
Equal<Parameters<typeof getMessagePushSourceDetail>, [sourceKey: string]>
>,
Assert<
Equal<
ReturnType<typeof getMessagePushSourceDetail>,
Promise<ExpectedSystemMessageSourceDefinition>
>
>,
Assert<
Equal<Parameters<typeof getMessagePushSourceOptions>, [sourceKey: string]>
>,
Assert<
Equal<
ReturnType<typeof getMessagePushSourceOptions>,
Promise<ExpectedSystemMessageSourceOptionsResponse>
>
>,
Assert<Equal<Parameters<typeof getStunMappingPortChangedOptions>, []>>,
Assert<
Equal<
ReturnType<typeof getStunMappingPortChangedOptions>,
Promise<ExpectedStunMappingPortChangedOptionsResponse>
>
>,
Assert<
Equal<
Parameters<typeof getMessageSubscriptionList>,
[params: ExpectedMessageSubscriptionListQuery]
>
>,
Assert<
Equal<
ReturnType<typeof getMessageSubscriptionList>,
Promise<{ items: ExpectedMessageSubscriptionView[]; total: number }>
>
>,
Assert<
Equal<
Parameters<typeof createMessageSubscription>,
[data: ExpectedMessageSubscriptionInput]
>
>,
Assert<
Equal<
ReturnType<typeof createMessageSubscription>,
Promise<ExpectedMessageSubscriptionView>
>
>,
Assert<
Equal<
Parameters<typeof updateMessageSubscription>,
[id: string, data: ExpectedMessageSubscriptionInput]
>
>,
Assert<
Equal<
ReturnType<typeof updateMessageSubscription>,
Promise<ExpectedMessageSubscriptionView>
>
>,
Assert<
Equal<
Parameters<typeof setMessageSubscriptionEnabled>,
[id: string, enabled: boolean]
>
>,
Assert<
Equal<
ReturnType<typeof setMessageSubscriptionEnabled>,
Promise<ExpectedMessageSubscriptionView>
>
>,
Assert<Equal<Parameters<typeof deleteMessageSubscription>, [id: string]>>,
Assert<Equal<ReturnType<typeof deleteMessageSubscription>, Promise<boolean>>>,
Assert<
Equal<
Parameters<typeof getMessageTemplateList>,
[params: ExpectedMessageTemplateListQuery]
>
>,
Assert<
Equal<
ReturnType<typeof getMessageTemplateList>,
Promise<{ items: ExpectedMessageTemplateView[]; total: number }>
>
>,
Assert<
Equal<
Parameters<typeof createMessageTemplate>,
[data: ExpectedMessageTemplateInput]
>
>,
Assert<
Equal<
ReturnType<typeof createMessageTemplate>,
Promise<ExpectedMessageTemplateView>
>
>,
Assert<
Equal<
Parameters<typeof updateMessageTemplate>,
[id: string, data: ExpectedMessageTemplateInput]
>
>,
Assert<
Equal<
ReturnType<typeof updateMessageTemplate>,
Promise<ExpectedMessageTemplateView>
>
>,
Assert<
Equal<
Parameters<typeof setMessageTemplateEnabled>,
[id: string, enabled: boolean]
>
>,
Assert<
Equal<
ReturnType<typeof setMessageTemplateEnabled>,
Promise<ExpectedMessageTemplateView>
>
>,
Assert<Equal<Parameters<typeof deleteMessageTemplate>, [id: string]>>,
Assert<Equal<ReturnType<typeof deleteMessageTemplate>, Promise<boolean>>>,
Assert<
Equal<
Parameters<typeof previewMessageTemplate>,
[data: { content: string; sourceKey: string }]
>
>,
Assert<
Equal<
ReturnType<typeof previewMessageTemplate>,
Promise<ExpectedMessageTemplatePreview>
>
>,
Assert<
Equal<Parameters<typeof getAccountMessagePushBindings>, [selfId: string]>
>,
Assert<
Equal<
ReturnType<typeof getAccountMessagePushBindings>,
Promise<ExpectedQqbotMessagePublishBindingView[]>
>
>,
Assert<
Equal<
Parameters<typeof createAccountMessagePushBinding>,
[selfId: string, data: ExpectedQqbotMessagePublishBindingInput]
>
>,
Assert<
Equal<
ReturnType<typeof createAccountMessagePushBinding>,
Promise<ExpectedQqbotMessagePublishBindingView>
>
>,
Assert<
Equal<
Parameters<typeof updateAccountMessagePushBinding>,
[
selfId: string,
id: string,
data: ExpectedQqbotMessagePublishBindingInput,
]
>
>,
Assert<
Equal<
ReturnType<typeof updateAccountMessagePushBinding>,
Promise<ExpectedQqbotMessagePublishBindingView>
>
>,
Assert<
Equal<
Parameters<typeof setAccountMessagePushBindingEnabled>,
[selfId: string, id: string, enabled: boolean]
>
>,
Assert<
Equal<
ReturnType<typeof setAccountMessagePushBindingEnabled>,
Promise<ExpectedQqbotMessagePublishBindingView>
>
>,
Assert<
Equal<
Parameters<typeof deleteAccountMessagePushBinding>,
[selfId: string, id: string]
>
>,
Assert<
Equal<ReturnType<typeof deleteAccountMessagePushBinding>, Promise<boolean>>
>,
Assert<
Equal<Parameters<typeof getAccountMessagePushTargets>, [selfId: string]>
>,
Assert<
Equal<
ReturnType<typeof getAccountMessagePushTargets>,
Promise<ExpectedQqbotMessagePushTargetOptionsResponse>
>
>,
];
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<T>(url: string, config?: unknown): Promise<T>;
get<T>(url: string, config?: unknown): Promise<T>;
post<T>(url: string, data?: unknown, config?: unknown): Promise<T>;
put<T>(url: string, data?: unknown, config?: unknown): Promise<T>;
};`;
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([]);
});
});