import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; export type CollectionFormat = 'bru' | 'yml'; export interface GenerateCollectionOptions { /** Name of the collection */ name?: string; /** Total number of requests to generate */ requestCount: number; /** Maximum folder nesting depth (0 = no folders, all requests at root) */ depth?: number; /** Number of folders per level (default: 3) */ foldersPerLevel?: number; /** Collection format: 'bru' or 'yml' */ format: CollectionFormat; /** Number of environments to generate (default: 2) */ environmentCount?: number; /** Include mixed HTTP methods (default: true) */ mixedMethods?: boolean; } export interface GeneratedCollection { /** Path to the generated collection directory */ path: string; /** Cleanup function to remove the collection */ cleanup: () => Promise; /** Collection name */ name: string; /** Number of requests generated */ requestCount: number; /** Number of folders generated */ folderCount: number; /** Format used */ format: CollectionFormat; } const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const; /** * Generate a collection with the specified parameters */ export async function generateCollection( options: GenerateCollectionOptions ): Promise { const { name = `Generated Collection ${Date.now()}`, requestCount, depth = 2, foldersPerLevel = 3, format, environmentCount = 2, mixedMethods = true } = options; // Create temp directory const tempDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'bruno-test-collection-') ); try { // Generate collection root files await generateCollectionRoot(tempDir, name, format); // Generate environments if (environmentCount > 0) { await generateEnvironments(tempDir, environmentCount, format); } // Calculate folder structure const folders = generateFolderStructure(depth, foldersPerLevel); const folderCount = folders.length; // Create folders for (const folder of folders) { await createFolder(tempDir, folder, format); } // Distribute requests across folders const requestsPerLocation = distributeRequests(requestCount, folders); let globalIndex = 0; for (const [folderPath, count] of Object.entries(requestsPerLocation)) { for (let i = 0; i < count; i++) { const method = mixedMethods ? HTTP_METHODS[globalIndex % HTTP_METHODS.length] : 'GET'; // Pass per-folder index (i) for naming, global index for method cycling await createRequest(tempDir, folderPath, i, method, format); globalIndex++; } } return { path: tempDir, cleanup: async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); }, name, requestCount, folderCount, format }; } catch (error) { // Cleanup on error await fs.promises.rm(tempDir, { recursive: true, force: true }); throw error; } } /** * Generate collection root files (bruno.json and collection.bru/opencollection.yml) */ async function generateCollectionRoot( basePath: string, name: string, format: CollectionFormat ): Promise { // Always create bruno.json const brunoJson = { version: '1', name, type: 'collection' }; await fs.promises.writeFile( path.join(basePath, 'bruno.json'), JSON.stringify(brunoJson, null, 2) ); if (format === 'bru') { const collectionBru = `meta { name: ${name} } auth { mode: none } `; await fs.promises.writeFile( path.join(basePath, 'collection.bru'), collectionBru ); } else { const opencollectionYml = `opencollection: "1.0.0" info: name: ${name} `; await fs.promises.writeFile( path.join(basePath, 'opencollection.yml'), opencollectionYml ); } } /** * Generate environment files */ async function generateEnvironments( basePath: string, count: number, format: CollectionFormat ): Promise { const envDir = path.join(basePath, 'environments'); await fs.promises.mkdir(envDir, { recursive: true }); const envNames = ['dev', 'staging', 'prod', 'local', 'test']; for (let i = 0; i < count; i++) { const envName = envNames[i % envNames.length]; const ext = format === 'bru' ? 'bru' : 'yml'; const filePath = path.join(envDir, `${envName}.${ext}`); if (format === 'bru') { const content = `vars { host: https://api.${envName}.example.com apiKey: ${envName}-api-key-${i} } `; await fs.promises.writeFile(filePath, content); } else { const content = `name: ${envName} variables: - name: host value: https://api.${envName}.example.com - name: apiKey value: ${envName}-api-key-${i} `; await fs.promises.writeFile(filePath, content); } } } /** * Generate folder structure paths * Folder names include parent prefix for verifiable parent-child relationships * Uses short names: F1, F1-F1, F1-F1-F1 to avoid OS filename length limits */ function generateFolderStructure( depth: number, foldersPerLevel: number ): string[] { if (depth === 0) { return ['']; // Root only } const folders: string[] = ['']; function addFolders(currentPath: string, parentName: string, currentDepth: number): void { if (currentDepth >= depth) return; for (let i = 0; i < foldersPerLevel; i++) { // Short folder name with parent prefix: F1, F1-F1, F1-F1-F1 const folderName = parentName ? `${parentName}-F${i + 1}` : `F${i + 1}`; const newPath = currentPath ? `${currentPath}/${folderName}` : folderName; folders.push(newPath); addFolders(newPath, folderName, currentDepth + 1); } } addFolders('', '', 0); return folders; } /** * Create a folder with its metadata file */ async function createFolder( basePath: string, folderPath: string, format: CollectionFormat ): Promise { if (!folderPath) return; // Skip root const fullPath = path.join(basePath, folderPath); await fs.promises.mkdir(fullPath, { recursive: true }); const folderName = path.basename(folderPath); const ext = format === 'bru' ? 'bru' : 'yml'; const metaPath = path.join(fullPath, `folder.${ext}`); if (format === 'bru') { const content = `meta { name: ${folderName} } auth { mode: inherit } `; await fs.promises.writeFile(metaPath, content); } else { const content = `info: name: ${folderName} `; await fs.promises.writeFile(metaPath, content); } } /** * Distribute requests across folders */ function distributeRequests( requestCount: number, folders: string[] ): Record { const distribution: Record = {}; // Initialize all folders with 0 for (const folder of folders) { distribution[folder] = 0; } // Distribute requests evenly, with remainder going to first folders const perFolder = Math.floor(requestCount / folders.length); const remainder = requestCount % folders.length; for (let i = 0; i < folders.length; i++) { distribution[folders[i]] = perFolder + (i < remainder ? 1 : 0); } return distribution; } /** * Create a request file * Request names include parent folder prefix for verifiable parent-child relationships * Uses short names: R1, F1-R1, F1-F1-R1 to avoid OS filename length limits */ async function createRequest( basePath: string, folderPath: string, indexInFolder: number, method: typeof HTTP_METHODS[number], format: CollectionFormat ): Promise { // Get parent folder name from path (e.g., "F1/F1-F1" -> "F1-F1") const parentName = folderPath ? path.basename(folderPath) : ''; const requestName = parentName ? `${parentName}-R${indexInFolder + 1}` : `R${indexInFolder + 1}`; const ext = format === 'bru' ? 'bru' : 'yml'; const fullFolderPath = folderPath ? path.join(basePath, folderPath) : basePath; const filePath = path.join(fullFolderPath, `${requestName}.${ext}`); // Ensure folder exists await fs.promises.mkdir(fullFolderPath, { recursive: true }); const hasBody = ['POST', 'PUT', 'PATCH'].includes(method); if (format === 'bru') { let content = `meta { name: ${requestName} type: http seq: ${(indexInFolder % 100) + 1} } ${method.toLowerCase()} { url: {{host}}/api/resource/${indexInFolder + 1} body: ${hasBody ? 'json' : 'none'} auth: none } `; if (hasBody) { content += ` body:json { { "id": ${indexInFolder + 1}, "name": "Resource ${indexInFolder + 1}", "timestamp": "${new Date().toISOString()}" } } `; } await fs.promises.writeFile(filePath, content); } else { let content = `info: name: ${requestName} type: http seq: ${(indexInFolder % 100) + 1} http: method: ${method} url: "{{host}}/api/resource/${indexInFolder + 1}" `; if (hasBody) { content += ` body: mode: json json: | { "id": ${indexInFolder + 1}, "name": "Resource ${indexInFolder + 1}", "timestamp": "${new Date().toISOString()}" } `; } await fs.promises.writeFile(filePath, content); } } /** * Generate collections in both formats for comparison testing */ export async function generateCollectionPair( options: Omit ): Promise<{ bru: GeneratedCollection; yml: GeneratedCollection; cleanup: () => Promise; }> { const bru = await generateCollection({ ...options, format: 'bru' }); const yml = await generateCollection({ ...options, format: 'yml' }).catch(async (err) => { await bru.cleanup(); throw err; }); return { bru, yml, cleanup: async () => { await Promise.all([bru.cleanup(), yml.cleanup()]); } }; } /** * Item definition for sorting test collections */ export interface SortingTestItem { /** Name of the request */ name: string; /** Sequence number (omit to test alphabetical sorting) */ seq?: number; /** HTTP method (default: GET) */ method?: typeof HTTP_METHODS[number]; } /** * Options for generating a sorting test collection */ export interface SortingTestCollectionOptions { /** Name of the collection */ name: string; /** Items to generate with specific names and sequences */ items: SortingTestItem[]; /** Collection format */ format: CollectionFormat; } /** * Generate a collection specifically for sorting tests. * Allows specifying exact item names and sequence numbers. */ export async function generateSortingTestCollection( options: SortingTestCollectionOptions ): Promise { const { name, items, format } = options; // Create temp directory const tempDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'bruno-test-sorting-') ); try { // Generate collection root files await generateCollectionRoot(tempDir, name, format); // Create each request with specified name and sequence for (const item of items) { await createSortingTestRequest(tempDir, item, format); } return { path: tempDir, cleanup: async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); }, name, requestCount: items.length, folderCount: 0, format }; } catch (error) { await fs.promises.rm(tempDir, { recursive: true, force: true }); throw error; } } /** * Create a request file for sorting tests with specific name and sequence */ async function createSortingTestRequest( basePath: string, item: SortingTestItem, format: CollectionFormat ): Promise { const { name, seq, method = 'GET' } = item; const ext = format === 'bru' ? 'bru' : 'yml'; const filePath = path.join(basePath, `${name}.${ext}`); if (format === 'bru') { const seqLine = seq !== undefined ? `\n seq: ${seq}` : ''; const content = `meta { name: ${name} type: http${seqLine} } ${method.toLowerCase()} { url: {{host}}/api/${name.toLowerCase().replace(/\s+/g, '-')} body: none auth: none } `; await fs.promises.writeFile(filePath, content); } else { const seqLine = seq !== undefined ? `\n seq: ${seq}` : ''; const content = `info: name: ${name} type: http${seqLine} http: method: ${method} url: "{{host}}/api/${name.toLowerCase().replace(/\s+/g, '-')}" `; await fs.promises.writeFile(filePath, content); } } /** * Set up a sorting test fixture with collection and user data directory */ export async function setupSortingTestFixture( options: SortingTestCollectionOptions ): Promise { const collection = await generateSortingTestCollection(options); const userDataPath = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'bruno-test-userdata-') ); try { const preferences = { lastOpenedCollections: [collection.path], preferences: { onboarding: { hasLaunchedBefore: true, hasSeenWelcomeModal: true } } }; await fs.promises.writeFile( path.join(userDataPath, 'preferences.json'), JSON.stringify(preferences, null, 2) ); return { collection, userDataPath, cleanup: async () => { await Promise.all([ collection.cleanup(), fs.promises.rm(userDataPath, { recursive: true, force: true }) ]); } }; } catch (error) { await Promise.all([ collection.cleanup(), fs.promises.rm(userDataPath, { recursive: true, force: true }) ]); throw error; } } /** * Options for setting up a test fixture */ export interface TestFixtureOptions extends GenerateCollectionOptions { /** Additional collections to preload (paths) */ additionalCollections?: string[]; } /** * Result of setting up a test fixture */ export interface TestFixture { /** Generated collection */ collection: GeneratedCollection; /** Path to user data directory with preferences */ userDataPath: string; /** Cleanup function to remove all generated files */ cleanup: () => Promise; } /** * Set up a complete test fixture with collection and user data directory. * This creates: * 1. A generated collection based on the provided options * 2. A user data directory with preferences.json configured to preload the collection * * Use this in beforeAll to set up fixtures, and call cleanup() in afterAll. * Note: This does NOT handle app launching or closing - that should be done separately. */ export async function setupTestFixture( options: TestFixtureOptions ): Promise { const { additionalCollections = [], ...collectionOptions } = options; // Generate the collection const collection = await generateCollection(collectionOptions); // Create user data directory const userDataPath = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'bruno-test-userdata-') ); try { // Create preferences.json with collection preloaded const preferences = { lastOpenedCollections: [collection.path, ...additionalCollections], preferences: { onboarding: { hasLaunchedBefore: true, hasSeenWelcomeModal: true } } }; await fs.promises.writeFile( path.join(userDataPath, 'preferences.json'), JSON.stringify(preferences, null, 2) ); return { collection, userDataPath, cleanup: async () => { await Promise.all([ collection.cleanup(), fs.promises.rm(userDataPath, { recursive: true, force: true }) ]); } }; } catch (error) { // Cleanup on error await Promise.all([ collection.cleanup(), fs.promises.rm(userDataPath, { recursive: true, force: true }) ]); throw error; } } /** * Result of setting up a static (committed) collection fixture */ export interface StaticTestFixture { /** Path to the temp copy of the collection that is actually mounted */ collectionPath: string; /** Path to user data directory with preferences */ userDataPath: string; /** Cleanup function — removes the temp copy and user data dir; never touches the source */ cleanup: () => Promise; } /** * Set up a test fixture that preloads a static, committed collection from disk. * * Unlike setupTestFixture (which generates a throwaway collection), this uses an * existing collection directory checked into the repo — for tests that need a * fixed, hand-authored layout rather than generated data. The source is copied * to a temp dir before mounting so the running app never mutates the committed * fixture. cleanup() removes the temp copy and the user data dir. */ export async function setupStaticFixture( sourceCollectionPath: string ): Promise { const userDataPath = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'bruno-test-userdata-') ); const collectionPath = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'bruno-test-collection-') ); const cleanup = async () => { await Promise.all([ fs.promises.rm(userDataPath, { recursive: true, force: true }), fs.promises.rm(collectionPath, { recursive: true, force: true }) ]); }; try { // Copy the committed collection into the temp dir so the app mounts the copy. await fs.promises.cp(sourceCollectionPath, collectionPath, { recursive: true }); const preferences = { lastOpenedCollections: [collectionPath], preferences: { onboarding: { hasLaunchedBefore: true, hasSeenWelcomeModal: true } } }; await fs.promises.writeFile( path.join(userDataPath, 'preferences.json'), JSON.stringify(preferences, null, 2) ); return { collectionPath, userDataPath, cleanup }; } catch (error) { await cleanup(); throw error; } }