Files
MyGoNavi/frontend/src/utils/messagePublish.test.ts
tianqijiuyun-latiao 9364c48ef0 feat(i18n): 完善多模块多语言适配与发版验证
扩展前后端多语言文案与共享词典。增加多模块 i18n 回归测试与 guard。收口外部 SQL 菜单和弹窗多语言文案。
2026-06-17 13:17:33 +08:00

390 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import {
buildMessagePublishCommand,
createDefaultMessagePublishDraft,
getMessagePublishPresentation,
} from './messagePublish';
const messagePublishSource = readFileSync(new URL('./messagePublish.ts', import.meta.url), 'utf8');
describe('messagePublish', () => {
it('localizes presentation copy through the supplied translator while keeping transport names raw', () => {
const t = (key: string) => key;
expect(getMessagePublishPresentation({ type: 'rabbitmq' }, t)).toMatchObject({
transportLabel: 'RabbitMQ Queue',
destinationLabel: 'Queue',
destinationPlaceholder: 'message_publish.presentation.rabbitmq.destination_placeholder',
destinationRequiredMessage: 'message_publish.presentation.rabbitmq.destination_required',
alertMessage: 'message_publish.presentation.rabbitmq.alert',
successHint: 'message_publish.presentation.rabbitmq.success_hint',
keyLabel: 'message_publish.presentation.key_label',
});
expect(getMessagePublishPresentation({ type: 'rocketmq' }, t)).toMatchObject({
transportLabel: 'RocketMQ Topic',
destinationLabel: 'Topic',
destinationPlaceholder: 'message_publish.presentation.rocketmq.destination_placeholder',
destinationRequiredMessage: 'message_publish.presentation.topic_required',
alertMessage: 'message_publish.presentation.rocketmq.alert',
successHint: 'message_publish.presentation.rocketmq.success_hint',
keyLabel: 'message_publish.presentation.keys_label',
keyPlaceholder: 'message_publish.presentation.rocketmq.key_placeholder',
tagPlaceholder: 'message_publish.presentation.rocketmq.tag_placeholder',
});
expect(getMessagePublishPresentation({ type: 'mqtt' }, t)).toMatchObject({
transportLabel: 'MQTT Topic',
destinationLabel: 'Topic',
destinationPlaceholder: 'message_publish.presentation.mqtt.destination_placeholder',
destinationRequiredMessage: 'message_publish.presentation.topic_required',
alertMessage: 'message_publish.presentation.mqtt.alert',
successHint: 'message_publish.presentation.mqtt.success_hint',
keyLabel: 'message_publish.presentation.key_label',
});
expect(getMessagePublishPresentation({ type: 'kafka' }, t)).toMatchObject({
transportLabel: 'Kafka Topic',
destinationLabel: 'Topic',
destinationPlaceholder: 'message_publish.presentation.kafka.destination_placeholder',
destinationRequiredMessage: 'message_publish.presentation.topic_required',
alertMessage: 'message_publish.presentation.kafka.alert',
successHint: 'message_publish.presentation.kafka.success_hint',
keyLabel: 'message_publish.presentation.key_label',
keyPlaceholder: 'message_publish.presentation.kafka.key_placeholder',
});
});
it('keeps presentation copy out of hardcoded Chinese literals', () => {
[
'例如orders.queue',
'请输入 Queue',
'当前表单会自动拼装 RabbitMQ publish JSON 命令',
'留空 Exchange 时会使用默认交换机',
'例如orders.events',
'请输入 Topic',
'当前表单会自动拼装 RocketMQ publish JSON 命令',
'Tag、Keys、Delay Level 与 Properties 会一并写入 RocketMQ 消息属性',
'消息 Keys可选',
'可输入多个 Key使用逗号分隔',
'例如TagA',
'例如devices/device-001/telemetry',
'当前表单会自动拼装 MQTT publish JSON 命令',
'QoS 与 retain 可单独指定',
'当前表单会自动拼装 Kafka publish JSON 命令',
'Headers 会作为 Kafka Record Headers 一并发送',
'消息 Key可选',
'可留空JSON 模式请输入一行合法 JSON',
].forEach((legacyText) => {
expect(messagePublishSource).not.toContain(legacyText);
});
});
it('keeps presentation keys in every locale catalog', () => {
(['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const).forEach((locale) => {
const catalog = JSON.parse(
readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8'),
) as Record<string, string>;
[
'message_publish.presentation.rabbitmq.destination_placeholder',
'message_publish.presentation.rabbitmq.destination_required',
'message_publish.presentation.rabbitmq.alert',
'message_publish.presentation.rabbitmq.success_hint',
'message_publish.presentation.rocketmq.destination_placeholder',
'message_publish.presentation.topic_required',
'message_publish.presentation.rocketmq.alert',
'message_publish.presentation.rocketmq.success_hint',
'message_publish.presentation.keys_label',
'message_publish.presentation.rocketmq.key_placeholder',
'message_publish.presentation.rocketmq.tag_placeholder',
'message_publish.presentation.mqtt.destination_placeholder',
'message_publish.presentation.mqtt.alert',
'message_publish.presentation.mqtt.success_hint',
'message_publish.presentation.kafka.destination_placeholder',
'message_publish.presentation.kafka.alert',
'message_publish.presentation.kafka.success_hint',
'message_publish.presentation.key_label',
'message_publish.presentation.kafka.key_placeholder',
].forEach((key) => {
expect(catalog[key]).toEqual(expect.any(String));
expect(catalog[key].length).toBeGreaterThan(0);
});
[
['message_publish.presentation.rabbitmq.alert', 'RabbitMQ'],
['message_publish.presentation.rabbitmq.alert', 'Management API'],
['message_publish.presentation.rocketmq.alert', 'RocketMQ'],
['message_publish.presentation.rocketmq.alert', 'NameServer'],
['message_publish.presentation.mqtt.alert', 'MQTT'],
['message_publish.presentation.kafka.alert', 'Kafka'],
['message_publish.presentation.kafka.success_hint', 'Headers'],
['message_publish.presentation.key_label', 'Key'],
].forEach(([key, rawTerm]) => {
expect(catalog[key]).toContain(rawTerm);
});
});
});
it('localizes command validation errors while preserving raw protocol details', () => {
const t = (key: string, params?: Record<string, unknown>) => (
params ? `${key} ${JSON.stringify(params)}` : key
);
expect(() => buildMessagePublishCommand(
{ type: 'kafka' },
{
destination: '',
bodyMode: 'json',
body: '{"ok":true}',
},
t,
)).toThrow('message_publish.error.destination_required');
expect(() => buildMessagePublishCommand(
{ type: 'kafka' },
{
destination: 'orders.events',
bodyMode: 'json',
body: '{bad',
},
t,
)).toThrow(/message_publish\.error\.invalid_json_detail .*message_publish\.field\.body/);
expect(() => buildMessagePublishCommand(
{ type: 'kafka' },
{
destination: 'orders.events',
bodyMode: 'json',
body: '{"ok":true}',
headers: '["bad"]',
},
t,
)).toThrow(/message_publish\.error\.json_object_required .*"field":"Headers"/);
expect(() => buildMessagePublishCommand(
{ type: 'mqtt' },
{
destination: 'devices/+/telemetry',
bodyMode: 'json',
body: '{"ok":true}',
},
t,
)).toThrow('message_publish.error.mqtt_wildcard_topic');
expect(() => buildMessagePublishCommand(
{ type: 'not-a-message-bus' },
{
destination: 'orders.events',
bodyMode: 'json',
body: '{"ok":true}',
},
t,
)).toThrow(/message_publish\.error\.unsupported_type .*not-a-message-bus/);
});
it('keeps command validation errors in locale catalogs and out of hardcoded Chinese literals', () => {
[
'请输入目标 Topic / Queue',
'MQTT 发送 Topic 不能包含 + 或 # 通配符',
'请输入${fieldLabel}',
'${fieldLabel}不是合法 JSON',
'${fieldLabel} 必须是 JSON 对象',
'当前数据源暂不支持测试发送消息',
].forEach((legacyText) => {
expect(messagePublishSource).not.toContain(legacyText);
});
(['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const).forEach((locale) => {
const catalog = JSON.parse(
readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8'),
) as Record<string, string>;
[
'message_publish.field.body',
'message_publish.field.message_key',
'message_publish.error.destination_required',
'message_publish.error.required_field',
'message_publish.error.invalid_json_detail',
'message_publish.error.json_object_required',
'message_publish.error.mqtt_wildcard_topic',
'message_publish.error.unsupported_type',
].forEach((key) => {
expect(catalog[key]).toEqual(expect.any(String));
expect(catalog[key].length).toBeGreaterThan(0);
});
expect(catalog['message_publish.error.destination_required']).toContain('Topic / Queue');
expect(catalog['message_publish.error.mqtt_wildcard_topic']).toContain('MQTT');
expect(catalog['message_publish.error.mqtt_wildcard_topic']).toContain('+');
expect(catalog['message_publish.error.mqtt_wildcard_topic']).toContain('#');
});
});
it('builds a Kafka publish JSON command from JSON payload inputs', () => {
const result = buildMessagePublishCommand(
{ type: 'kafka' },
{
destination: 'orders.events',
keyMode: 'json',
key: '{"tenant":"a"}',
bodyMode: 'json',
body: '{"id":1,"event":"created"}',
headers: '{"x-env":"dev"}',
},
);
expect(result.transportLabel).toBe('Kafka Topic');
expect(result.destinationLabel).toBe('orders.events');
expect(result.commandText).toContain('"publish": "orders.events"');
expect(result.commandText).toContain('"tenant": "a"');
expect(result.commandText).toContain('"id": 1');
expect(result.commandText).toContain('"x-env": "dev"');
});
it('keeps Kafka text payloads as plain strings', () => {
const result = buildMessagePublishCommand(
{ type: 'kafka' },
{
destination: 'logs.app',
keyMode: 'text',
key: 'tenant-a',
bodyMode: 'text',
body: 'hello gonavi',
headers: '',
},
);
expect(result.commandText).toContain('"key": "tenant-a"');
expect(result.commandText).toContain('"value": "hello gonavi"');
});
it('rejects non-object Kafka headers', () => {
expect(() => buildMessagePublishCommand(
{ type: 'kafka' },
{
destination: 'logs.app',
bodyMode: 'json',
body: '{"ok":true}',
headers: '["bad"]',
},
)).toThrow(/Headers.*JSON object/);
});
it('seeds Kafka default publish draft with a JSON body example', () => {
expect(createDefaultMessagePublishDraft({ type: 'kafka' }, 'orders.events')).toMatchObject({
destination: 'orders.events',
keyMode: 'text',
bodyMode: 'json',
});
});
it('builds an MQTT publish JSON command with qos and retain flags', () => {
const result = buildMessagePublishCommand(
{ type: 'mqtt' },
{
destination: 'devices/device-001/telemetry',
qos: 1,
retain: true,
bodyMode: 'json',
body: '{"id":1,"event":"created"}',
},
);
expect(result.transportLabel).toBe('MQTT Topic');
expect(result.destinationLabel).toBe('devices/device-001/telemetry');
expect(result.commandText).toContain('"publish": "devices/device-001/telemetry"');
expect(result.commandText).toContain('"qos": 1');
expect(result.commandText).toContain('"retain": true');
});
it('seeds MQTT default publish draft with connection qos and retain defaults', () => {
expect(createDefaultMessagePublishDraft(
{ type: 'mqtt', database: 'devices/+/telemetry', connectionParams: 'qos=1&retain=true' },
'',
)).toMatchObject({
destination: 'devices/+/telemetry',
qos: 1,
retain: true,
bodyMode: 'json',
});
});
it('builds a RabbitMQ publish JSON command with routing and properties', () => {
const result = buildMessagePublishCommand(
{ type: 'rabbitmq', connectionParams: 'defaultQueue=orders.queue&exchange=events.topic' },
{
destination: 'orders.queue',
exchange: '',
routingKey: '',
bodyMode: 'json',
body: '{"id":1,"event":"created"}',
headers: '{"x-env":"dev"}',
properties: '{"content_type":"application/json"}',
},
);
expect(result.transportLabel).toBe('RabbitMQ Queue');
expect(result.destinationLabel).toBe('orders.queue');
expect(result.commandText).toContain('"publish": "orders.queue"');
expect(result.commandText).toContain('"exchange": "events.topic"');
expect(result.commandText).toContain('"routing_key": "orders.queue"');
expect(result.commandText).toContain('"content_type": "application/json"');
});
it('seeds RabbitMQ default publish draft with defaultQueue and exchange', () => {
expect(createDefaultMessagePublishDraft(
{ type: 'rabbitmq', connectionParams: 'defaultQueue=orders.queue&exchange=events.topic' },
'',
)).toMatchObject({
destination: 'orders.queue',
exchange: 'events.topic',
routingKey: 'orders.queue',
bodyMode: 'json',
});
});
it('builds a RocketMQ publish JSON command with tag, keys and delay level', () => {
const result = buildMessagePublishCommand(
{ type: 'rocketmq' },
{
destination: 'orders.events',
key: 'key-a,key-b',
tag: 'TagA',
delayLevel: 3,
bodyMode: 'json',
body: '{"id":1,"event":"created"}',
properties: '{"trace":"trace-1"}',
},
);
const command = JSON.parse(result.commandText);
expect(result.transportLabel).toBe('RocketMQ Topic');
expect(result.destinationLabel).toBe('orders.events');
expect(command).toMatchObject({
publish: 'orders.events',
tag: 'TagA',
delayLevel: 3,
properties: {
trace: 'trace-1',
},
});
expect(command.keys).toEqual(['key-a', 'key-b']);
expect(command.payload).toMatchObject({ id: 1, event: 'created' });
});
it('seeds RocketMQ default publish draft with connection tag and delay level defaults', () => {
expect(createDefaultMessagePublishDraft(
{ type: 'rocketmq', database: 'orders.events', connectionParams: 'tag=TagA&delayLevel=5' },
'',
)).toMatchObject({
destination: 'orders.events',
tag: 'TagA',
delayLevel: 5,
bodyMode: 'json',
});
});
});