diff --git a/plugins/channels/telegram/media_voice_test.go b/plugins/channels/telegram/media_voice_test.go new file mode 100644 index 0000000..26a6d6f --- /dev/null +++ b/plugins/channels/telegram/media_voice_test.go @@ -0,0 +1,78 @@ +package telegram + +// 本文件锁定 **Telegram 入站语音**的字节内联(切片 IM-ASR):「渠道零改动」这条承诺要 +// 在**真适配器**上被验证一次 —— 语音只有 `file_id` + `audio/ogg`(没有文件名、没有 +// 扩展名),因此音频白名单必须能靠平台声明的 MIME 命中,否则 telegram 语音会被整条 +// 挡掉。夹具复用 media_test.go 的假 Bot API。 + +import ( + "context" + "encoding/json" + "fmt" + "testing" +) + +// voiceUpdate 构造一条 Telegram 语音消息的原始 update(无 caption:纯语音)。 +func voiceUpdate(fileID, mime string, size int64) []byte { + body := map[string]any{ + "update_id": 3, + "message": map[string]any{ + "message_id": 44, + "date": 1700000002, + "chat": map[string]any{"id": 7, "type": "private"}, + "from": map[string]any{"id": 9, "first_name": "主人"}, + "voice": map[string]any{"file_id": fileID, "file_size": size, "mime_type": mime, "duration": 3}, + }, + } + raw, err := json.Marshal(body) + if err != nil { + panic(fmt.Sprintf("marshal update: %v", err)) + } + return raw +} + +// TestInboundVoiceIsInlined 是正向用例:语音 ⇒ getFile ⇒ 下载 ⇒ Data 被填, +// 且本切片**一行适配器代码都没改**(因此它同时是「渠道零改动」的回归保护)。 +func TestInboundVoiceIsInlined(t *testing.T) { + t.Parallel() + s := newTGAssetServer(t) + a, h, _ := newInlineAdapter(t, s) + + if _, err := a.ProcessUpdate(context.Background(), voiceUpdate("voice-id", "audio/ogg", 1024)); err != nil { + t.Fatalf("ProcessUpdate: %v", err) + } + + msg := h.message(t) + if msg.Content != "" { + t.Fatalf("Content = %q, want 空(纯语音没有 caption,转写由桥接器负责)", msg.Content) + } + if len(msg.Media) != 1 || string(msg.Media[0].Data) != "附件的正文\n" { + t.Fatalf("Media.Data = %q, want 下载到的语音字节", msg.Media[0].Data) + } + if msg.Media[0].URL != "voice-id" || msg.Media[0].MIMEType != "audio/ogg" { + t.Fatalf("内联改动了媒体元数据:%+v", msg.Media[0]) + } + if s.getFile.Load() != 1 || s.fileHits.Load() != 1 { + t.Fatalf("getFile=%d fileHits=%d, want 1/1(MIME 兜底必须命中音频白名单)", s.getFile.Load(), s.fileHits.Load()) + } +} + +// TestInboundVoiceWithoutMIMEIsNotDownloaded 钉住 deny-by-default:平台既没给文件名也 +// 没给类型时**不发起任何请求**(宁可诚实说读不到,也不猜一份类型不明的音频)。 +func TestInboundVoiceWithoutMIMEIsNotDownloaded(t *testing.T) { + t.Parallel() + s := newTGAssetServer(t) + a, h, _ := newInlineAdapter(t, s) + + if _, err := a.ProcessUpdate(context.Background(), voiceUpdate("voice-id", "", 1024)); err != nil { + t.Fatalf("ProcessUpdate: %v", err) + } + + msg := h.message(t) + if len(msg.Media[0].Data) != 0 { + t.Fatalf("类型不明的语音被内联了(%d 字节)", len(msg.Media[0].Data)) + } + if s.allHits.Load() != 0 { + t.Fatalf("类型不明的语音仍发起了请求:all=%d", s.allHits.Load()) + } +}