11import http , { RefinedResponse , ResponseType } from "k6/http" ;
22import { getHeaders } from "./user.utils" ;
3- import { check , fail } from "k6" ;
3+ import { bytes , check , fail } from "k6" ;
44//@ts -ignore
55import { FormData } from "https://jslib.k6.io/formdata/0.0.2/index.js" ;
6+ import {
7+ AppImportAnalysis ,
8+ AppImportResult ,
9+ ImportAnalysisResult ,
10+ } from "./models" ;
611
712const rootUrl = __ENV . ROOT_URL ;
813
9- export function launchExport ( apps : string [ ] ) : RefinedResponse < ResponseType | undefined > {
14+ export const EXPORT_TIMEOUT = 30 * 60 * 1000 ; // 30 minutes
15+
16+ export function importArchive (
17+ fileData : bytes ,
18+ ) : RefinedResponse < ResponseType | undefined > {
19+ let headers = getHeaders ( ) ;
20+ const fd = new FormData ( ) ;
21+ const contentType = "application/zip" ;
22+ //@ts -ignore
23+ fd . append ( "file" , http . file ( fileData , "archive.zip" , contentType ) ) ;
24+ //@ts -ignore
25+ headers [ "Content-Type" ] = "multipart/form-data; boundary=" + fd . boundary ;
26+ return http . post ( `${ rootUrl } /archive/import/upload` , fd . body ( ) , { headers } ) ;
27+ }
28+
29+ export function importArchiveOrFail ( fileData : bytes ) : string {
30+ const res = importArchive ( fileData ) ;
31+ if ( res . status !== 200 ) {
32+ fail ( `Failed to import archive. Response: ${ res . status } - ${ res . body } ` ) ;
33+ }
34+ return res . json ( "importId" ) as string ;
35+ }
36+
37+ export function analyzeImport (
38+ importId : string ,
39+ ) : RefinedResponse < ResponseType | undefined > {
40+ return http . get ( `${ rootUrl } /archive/import/analyze/${ importId } ` , {
41+ headers : getHeaders ( ) ,
42+ } ) ;
43+ }
44+
45+ export function analyzeImportOrFail ( importId : string ) : ImportAnalysisResult {
46+ const res = analyzeImport ( importId ) ;
47+ if ( res . status !== 200 ) {
48+ fail (
49+ `Failed to analyze import with id ${ importId } . Response: ${ res . status } - ${ res . body } ` ,
50+ ) ;
51+ }
52+ return res . json ( ) as ImportAnalysisResult ;
53+ }
54+
55+ export function launchImport (
56+ importId : string ,
57+ details : { apps : Record < string , AppImportAnalysis > } ,
58+ ) : RefinedResponse < ResponseType | undefined > {
59+ const payload = JSON . stringify ( details ) ;
60+ return http . post ( `${ rootUrl } /archive/import/${ importId } /launch` , payload , {
61+ headers : getHeaders ( "application/json" ) ,
62+ } ) ;
63+ }
64+
65+ export function launchImportOrFail (
66+ importId : string ,
67+ details : { apps : Record < string , AppImportAnalysis > } ,
68+ ) : Record < string , AppImportResult > {
69+ const res = launchImport ( importId , details ) ;
70+ if ( res . status !== 200 ) {
71+ fail (
72+ `Failed to launch import with id ${ importId } . Response: ${ res . status } - ${ res . body } ` ,
73+ ) ;
74+ }
75+ return res . json ( ) as Record < string , AppImportResult > ;
76+ }
77+
78+ export function launchExport (
79+ apps : string [ ] ,
80+ ) : RefinedResponse < ResponseType | undefined > {
1081 const payload = JSON . stringify ( { apps } ) ;
11- const res = http . post (
12- `${ rootUrl } /archive/export` ,
13- payload ,
14- { headers : getHeaders ( "application/json" ) } ,
15- ) ;
82+ const res = http . post ( `${ rootUrl } /archive/export` , payload , {
83+ headers : getHeaders ( "application/json" ) ,
84+ } ) ;
1685 return res ;
1786}
1887
1988export function launchExportOrFail ( apps : string [ ] ) : string {
2089 const res = launchExport ( apps ) ;
2190 const ok = check ( res , {
2291 "should have exportId in response" : ( r ) => r . json ( "exportId" ) !== undefined ,
23- "should have message in response" : ( r ) => r . json ( "message" ) === "export.in.progress" ,
92+ "should have message in response" : ( r ) =>
93+ r . json ( "message" ) === "export.in.progress" ,
2494 } ) ;
2595 if ( ! ok ) {
26- fail ( `Failed to launch export for apps ${ apps . join ( ", " ) } . Response: ${ res . status } - ${ res . body } ` ) ;
96+ fail (
97+ `Failed to launch export for apps ${ apps . join ( ", " ) } . Response: ${ res . status } - ${ res . body } ` ,
98+ ) ;
2799 }
28100 return res . json ( "exportId" ) as string ;
29101}
30102
103+ export function verifyExportFiles (
104+ exportId : string ,
105+ ) : RefinedResponse < ResponseType | undefined > {
106+ const res = http . get ( `${ rootUrl } /archive/export/verify/${ exportId } ` , {
107+ headers : getHeaders ( ) ,
108+ } ) ;
109+ return res ;
110+ }
31111
32- export function verifyExportFiles ( exportId : string ) : RefinedResponse < ResponseType | undefined > {
33- const res = http . get (
34- `${ rootUrl } /archive/export/verify/${ exportId } ` ,
35- { headers : getHeaders ( ) } ,
36- ) ;
112+ export function downloadExportFile (
113+ exportId : string ,
114+ ) : RefinedResponse < ResponseType | undefined > {
115+ const res = http . get ( `${ rootUrl } /archive/export/${ exportId } ` , {
116+ headers : getHeaders ( ) ,
117+ responseType : "binary" , // Ensure we get binary data
118+ } ) ;
37119 return res ;
38120}
39121
40- export function downloadExportFile ( exportId : string ) : RefinedResponse < ResponseType | undefined > {
41- const res = http . get (
42- ` ${ rootUrl } /archive/export/ ${ exportId } ` ,
43- {
44- headers : getHeaders ( ) ,
45- responseType : 'binary' , // Ensure we get binary data
46- } ,
47- ) ;
122+ export function duplicateResource (
123+ application : string ,
124+ resourceId : string ,
125+ ) : RefinedResponse < ResponseType | undefined > {
126+ const payload = JSON . stringify ( { application , resourceId } ) ;
127+ const res = http . post ( ` ${ rootUrl } /archive/duplicate` , payload , {
128+ headers : getHeaders ( "application/json" ) ,
129+ } ) ;
48130 return res ;
49131}
50132
133+ export function duplicateResourceOrFail (
134+ application : string ,
135+ resourceId : string ,
136+ ) : string {
137+ const res = duplicateResource ( application , resourceId ) ;
138+ if ( res . status !== 200 ) {
139+ fail (
140+ `Failed to launch duplication for resource ${ resourceId } in application ${ application } . Response: ${ res . status } - ${ res . body } ` ,
141+ ) ;
142+ }
143+ return res . json ( "duplicateId" ) as string ;
144+ }
145+
51146/**
52147 * Represents a file entry in a ZIP archive
53148 */
@@ -89,14 +184,23 @@ function readUInt16LE(bytes: Uint8Array, offset: number): number {
89184 * Read a little-endian 32-bit integer from a byte array
90185 */
91186function readUInt32LE ( bytes : Uint8Array , offset : number ) : number {
92- return bytes [ offset ] | ( bytes [ offset + 1 ] << 8 ) | ( bytes [ offset + 2 ] << 16 ) | ( bytes [ offset + 3 ] << 24 ) ;
187+ return (
188+ bytes [ offset ] |
189+ ( bytes [ offset + 1 ] << 8 ) |
190+ ( bytes [ offset + 2 ] << 16 ) |
191+ ( bytes [ offset + 3 ] << 24 )
192+ ) ;
93193}
94194
95195/**
96196 * Convert bytes to string (ASCII/UTF-8)
97197 */
98- function bytesToString ( bytes : Uint8Array , offset : number , length : number ) : string {
99- let str = '' ;
198+ function bytesToString (
199+ bytes : Uint8Array ,
200+ offset : number ,
201+ length : number ,
202+ ) : string {
203+ let str = "" ;
100204 for ( let i = 0 ; i < length ; i ++ ) {
101205 str += String . fromCharCode ( bytes [ offset + i ] ) ;
102206 }
@@ -115,7 +219,11 @@ function toUint8Array(body: any): Uint8Array {
115219 return new Uint8Array ( body ) ;
116220 }
117221 // Check if it's an array-like object with numeric indices
118- if ( typeof body === 'object' && body . length !== undefined && typeof body [ 0 ] === 'number' ) {
222+ if (
223+ typeof body === "object" &&
224+ body . length !== undefined &&
225+ typeof body [ 0 ] === "number"
226+ ) {
119227 const bytes = new Uint8Array ( body . length ) ;
120228 for ( let i = 0 ; i < body . length ; i ++ ) {
121229 bytes [ i ] = body [ i ] ;
@@ -141,9 +249,14 @@ function findEndOfCentralDirectory(bytes: Uint8Array): number {
141249 // Search from the end of the file (EOCD is at the end)
142250 const maxSearchLength = Math . min ( bytes . length , 65536 + 22 ) ; // Maximum comment length + EOCD size
143251 const searchStart = Math . max ( 0 , bytes . length - maxSearchLength ) ;
144-
252+
145253 for ( let i = bytes . length - 22 ; i >= searchStart ; i -- ) {
146- if ( bytes [ i ] === 0x50 && bytes [ i + 1 ] === 0x4b && bytes [ i + 2 ] === 0x05 && bytes [ i + 3 ] === 0x06 ) {
254+ if (
255+ bytes [ i ] === 0x50 &&
256+ bytes [ i + 1 ] === 0x4b &&
257+ bytes [ i + 2 ] === 0x05 &&
258+ bytes [ i + 3 ] === 0x06
259+ ) {
147260 return i ;
148261 }
149262 }
@@ -164,7 +277,9 @@ export function parseZip(data: any): ZipArchive {
164277 // Find End of Central Directory record
165278 const eocdOffset = findEndOfCentralDirectory ( bytes ) ;
166279 if ( eocdOffset === - 1 ) {
167- throw new Error ( "Invalid ZIP file: End of Central Directory record not found" ) ;
280+ throw new Error (
281+ "Invalid ZIP file: End of Central Directory record not found" ,
282+ ) ;
168283 }
169284
170285 // Parse EOCD
@@ -174,21 +289,27 @@ export function parseZip(data: any): ZipArchive {
174289
175290 // Validate offsets
176291 if ( centralDirOffset >= bytes . length ) {
177- throw new Error ( `Invalid ZIP: Central Directory offset (${ centralDirOffset } ) exceeds file size (${ bytes . length } )` ) ;
292+ throw new Error (
293+ `Invalid ZIP: Central Directory offset (${ centralDirOffset } ) exceeds file size (${ bytes . length } )` ,
294+ ) ;
178295 }
179296
180297 // Parse Central Directory entries
181298 let offset = centralDirOffset ;
182299 for ( let i = 0 ; i < totalEntries ; i ++ ) {
183300 // Check if we have enough bytes to read the signature
184301 if ( offset + 4 > bytes . length ) {
185- throw new Error ( `Invalid ZIP: Not enough data for Central Directory entry ${ i } at offset ${ offset } (file size: ${ bytes . length } )` ) ;
302+ throw new Error (
303+ `Invalid ZIP: Not enough data for Central Directory entry ${ i } at offset ${ offset } (file size: ${ bytes . length } )` ,
304+ ) ;
186305 }
187-
306+
188307 // Central Directory File Header signature: 0x02014b50
189308 const signature = readUInt32LE ( bytes , offset ) ;
190309 if ( signature !== 0x02014b50 ) {
191- throw new Error ( `Invalid Central Directory entry ${ i } at offset ${ offset } : expected signature 0x02014b50, got 0x${ signature . toString ( 16 ) . padStart ( 8 , '0' ) } ` ) ;
310+ throw new Error (
311+ `Invalid Central Directory entry ${ i } at offset ${ offset } : expected signature 0x02014b50, got 0x${ signature . toString ( 16 ) . padStart ( 8 , "0" ) } ` ,
312+ ) ;
192313 }
193314
194315 const compressionMethod = readUInt16LE ( bytes , offset + 10 ) ;
@@ -199,7 +320,7 @@ export function parseZip(data: any): ZipArchive {
199320 const fileCommentLength = readUInt16LE ( bytes , offset + 32 ) ;
200321
201322 const filename = bytesToString ( bytes , offset + 46 , filenameLength ) ;
202- const isDirectory = filename . endsWith ( '/' ) ;
323+ const isDirectory = filename . endsWith ( "/" ) ;
203324
204325 if ( isDirectory ) {
205326 directoryCount ++ ;
@@ -231,11 +352,14 @@ export function parseZip(data: any): ZipArchive {
231352 * @param filename - Name of the file to extract
232353 * @returns The file data as Uint8Array, or null if not found
233354 */
234- export function extractFileFromZip ( data : any , filename : string ) : Uint8Array | null {
355+ export function extractFileFromZip (
356+ data : any ,
357+ filename : string ,
358+ ) : Uint8Array | null {
235359 const bytes = toUint8Array ( data ) ;
236360 const archive = parseZip ( data ) ;
237361 const entry = archive . entries . get ( filename ) ;
238-
362+
239363 if ( ! entry || entry . isDirectory ) {
240364 return null ;
241365 }
@@ -247,23 +371,25 @@ export function extractFileFromZip(data: any, filename: string): Uint8Array | nu
247371 const filenameLength = readUInt16LE ( bytes , i + 26 ) ;
248372 const extraFieldLength = readUInt16LE ( bytes , i + 28 ) ;
249373 const localFilename = bytesToString ( bytes , i + 30 , filenameLength ) ;
250-
374+
251375 if ( localFilename === filename ) {
252376 const compressionMethod = readUInt16LE ( bytes , i + 8 ) ;
253377 const compressedSize = readUInt32LE ( bytes , i + 18 ) ;
254378 const dataOffset = i + 30 + filenameLength + extraFieldLength ;
255-
379+
256380 if ( compressionMethod === 0 ) {
257381 // No compression - return data as-is
258382 return bytes . slice ( dataOffset , dataOffset + compressedSize ) ;
259383 } else {
260384 // Compressed data - k6 doesn't have built-in decompression
261- throw new Error ( `File "${ filename } " uses compression method ${ compressionMethod } . Decompression is not supported in k6. Use stored (uncompressed) files only.` ) ;
385+ throw new Error (
386+ `File "${ filename } " uses compression method ${ compressionMethod } . Decompression is not supported in k6. Use stored (uncompressed) files only.` ,
387+ ) ;
262388 }
263389 }
264390 }
265391 }
266-
392+
267393 return null ;
268394}
269395
@@ -290,29 +416,29 @@ export function getZipTree(archive: ZipArchive): string[] {
290416export function validateZipStructure (
291417 archive : ZipArchive ,
292418 expectedFiles : string [ ] ,
293- expectedDirs ?: string [ ]
419+ expectedDirs ?: string [ ] ,
294420) : { valid : boolean ; missing : string [ ] ; unexpected : string [ ] } {
295421 const missing : string [ ] = [ ] ;
296422 const unexpected : string [ ] = [ ] ;
297423 const allExpected = new Set ( [ ...expectedFiles , ...( expectedDirs || [ ] ) ] ) ;
298-
424+
299425 // Check for missing files
300426 for ( const expected of allExpected ) {
301427 if ( ! archive . entries . has ( expected ) ) {
302428 missing . push ( expected ) ;
303429 }
304430 }
305-
431+
306432 // Check for unexpected files (optional - you can skip this if you want)
307433 for ( const [ filename ] of archive . entries ) {
308434 if ( ! allExpected . has ( filename ) ) {
309435 unexpected . push ( filename ) ;
310436 }
311437 }
312-
438+
313439 return {
314440 valid : missing . length === 0 ,
315441 missing,
316442 unexpected,
317443 } ;
318- }
444+ }
0 commit comments