11// Qqbot tests cover unified sender behavior.
2- import { beforeEach , describe , expect , it , vi } from "vitest" ;
2+ import { mkdtemp , rm , writeFile } from "node:fs/promises" ;
3+ import { tmpdir } from "node:os" ;
4+ import { join } from "node:path" ;
5+ import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
36import { ChunkedMediaApi } from "../api/media-chunked.js" ;
47import { MediaApi } from "../api/media.js" ;
58import { MediaFileType , type MessageResponse , type UploadMediaResponse } from "../types.js" ;
9+ import type { RawMediaSource } from "./media-source.js" ;
610import { registerAccount , sendMedia } from "./sender.js" ;
711
812const fetchWithSsrFGuardMock = vi . hoisted ( ( ) => vi . fn ( ) ) ;
@@ -43,6 +47,16 @@ const logger = {
4347 debug : vi . fn ( ) ,
4448} ;
4549
50+ const tempDirs : string [ ] = [ ] ;
51+
52+ async function createLocalMediaFile ( name : string , bytes : Buffer ) : Promise < string > {
53+ const dir = await mkdtemp ( join ( tmpdir ( ) , "qqbot-sender-test-" ) ) ;
54+ tempDirs . push ( dir ) ;
55+ const filePath = join ( dir , name ) ;
56+ await writeFile ( filePath , bytes ) ;
57+ return filePath ;
58+ }
59+
4660function mockGuardedDownload ( ) : void {
4761 fetchWithSsrFGuardMock . mockResolvedValueOnce ( {
4862 response : new Response ( MEDIA_BYTES ) ,
@@ -62,6 +76,10 @@ describe("qqbot unified sender media upload dispatch", () => {
6276 logger . debug . mockReset ( ) ;
6377 } ) ;
6478
79+ afterEach ( async ( ) => {
80+ await Promise . all ( tempDirs . splice ( 0 ) . map ( ( dir ) => rm ( dir , { recursive : true , force : true } ) ) ) ;
81+ } ) ;
82+
6583 it . each ( [
6684 {
6785 label : "image" ,
@@ -134,4 +152,82 @@ describe("qqbot unified sender media upload dispatch", () => {
134152 } ) ;
135153 } ,
136154 ) ;
155+
156+ it . each ( [
157+ {
158+ label : "base64" ,
159+ source : async ( ) : Promise < RawMediaSource > => ( { base64 : MEDIA_BYTES . toString ( "base64" ) } ) ,
160+ expectedSource : { kind : "buffer" as const , buffer : Buffer . from ( MEDIA_BYTES ) } ,
161+ } ,
162+ {
163+ label : "buffer" ,
164+ source : async ( ) : Promise < RawMediaSource > => ( {
165+ buffer : Buffer . from ( MEDIA_BYTES ) ,
166+ fileName : "buffer-proof.png" ,
167+ } ) ,
168+ expectedSource : {
169+ kind : "buffer" as const ,
170+ buffer : Buffer . from ( MEDIA_BYTES ) ,
171+ fileName : "buffer-proof.png" ,
172+ } ,
173+ } ,
174+ {
175+ label : "localPath" ,
176+ source : async ( ) : Promise < RawMediaSource > => ( {
177+ localPath : await createLocalMediaFile ( "local-proof.png" , MEDIA_BYTES ) ,
178+ } ) ,
179+ expectedSource : { kind : "localPath" as const } ,
180+ } ,
181+ ] ) (
182+ "uploads C2C image $label byte sources through chunked upload instead of one-shot file_data" ,
183+ async ( { source, expectedSource } ) => {
184+ const uploadMediaSpy = vi
185+ . spyOn ( MediaApi . prototype , "uploadMedia" )
186+ . mockResolvedValue ( UPLOAD_RESPONSE ) ;
187+ const uploadChunkedSpy = vi
188+ . spyOn ( ChunkedMediaApi . prototype , "uploadChunked" )
189+ . mockResolvedValue ( UPLOAD_RESPONSE ) ;
190+ vi . spyOn ( MediaApi . prototype , "sendMediaMessage" ) . mockResolvedValue ( MESSAGE_RESPONSE ) ;
191+ const appId = `sender-test-${ expectedSource . kind } ` ;
192+ const creds = { appId, clientSecret : "client-secret" } ;
193+ registerAccount ( appId , { logger } ) ;
194+
195+ const result = await sendMedia ( {
196+ target : { type : "c2c" , id : "user-openid" } ,
197+ creds,
198+ kind : "image" ,
199+ source : await source ( ) ,
200+ content : "caption" ,
201+ } ) ;
202+
203+ expect ( result ) . toBe ( MESSAGE_RESPONSE ) ;
204+ expect ( uploadMediaSpy ) . not . toHaveBeenCalled ( ) ;
205+ expect ( uploadChunkedSpy ) . toHaveBeenCalledOnce ( ) ;
206+ expect ( uploadChunkedSpy ) . toHaveBeenCalledWith (
207+ expect . objectContaining ( {
208+ scope : "c2c" ,
209+ targetId : "user-openid" ,
210+ fileType : MediaFileType . IMAGE ,
211+ creds,
212+ } ) ,
213+ ) ;
214+ const chunkedSource = uploadChunkedSpy . mock . calls [ 0 ] ?. [ 0 ] ?. source ;
215+ expect ( chunkedSource ?. kind ) . toBe ( expectedSource . kind ) ;
216+ if ( expectedSource . kind === "buffer" ) {
217+ expect ( chunkedSource ) . toMatchObject ( {
218+ kind : "buffer" ,
219+ fileName : expectedSource . fileName ,
220+ } ) ;
221+ expect ( chunkedSource ?. kind === "buffer" ? chunkedSource . buffer : undefined ) . toEqual (
222+ expectedSource . buffer ,
223+ ) ;
224+ }
225+ if ( expectedSource . kind === "localPath" ) {
226+ expect ( chunkedSource ) . toMatchObject ( {
227+ kind : "localPath" ,
228+ size : MEDIA_BYTES . length ,
229+ } ) ;
230+ }
231+ } ,
232+ ) ;
137233} ) ;
0 commit comments