UNPKG

@devicecloud.dev/dcd

Version:

Better cloud maestro testing

423 lines (422 loc) 17.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ApiGateway = void 0; const path = require("node:path"); exports.ApiGateway = { /** * Enhances generic "fetch failed" errors with more specific diagnostic information * @param error - The original TypeError from fetch * @param url - The URL that was being fetched * @returns Enhanced error with diagnostic information */ enhanceFetchError(error, url) { const urlObj = new URL(url); const { hostname, origin } = urlObj; let message = `Network request failed: ${url}\n\n`; message += `Possible causes:\n`; message += ` 1. No internet connection - check your network connectivity\n`; message += ` 2. DNS resolution failed - unable to resolve "${hostname}"\n`; message += ` 3. Firewall or proxy blocking the request\n`; message += ` 4. API server is down or unreachable\n`; message += ` 5. SSL/TLS certificate validation failed\n\n`; message += `Troubleshooting steps:\n`; message += ` • Check internet connection: ping google.com\n`; message += ` • Test API reachability: curl ${origin}\n`; message += ` • Verify API URL is correct: ${origin}\n`; message += ` • Check for proxy/VPN interference\n`; message += ` • Try again in a few moments if server is temporarily down\n\n`; message += `Original error: ${error.message}`; const enhancedError = new Error(message); enhancedError.name = 'NetworkError'; enhancedError.stack = error.stack; return enhancedError; }, /** * Standardized error handling for API responses * @param res - The fetch response object * @param operation - Description of the operation that failed * @returns Never returns, always throws */ async handleApiError(res, operation) { const errorText = await res.text(); let userMessage; // Parse common API error formats try { const errorData = JSON.parse(errorText); userMessage = errorData.message || errorData.error || errorText; } catch { userMessage = errorText; } // Add context and improve readability switch (res.status) { case 400: { throw new Error(`Invalid request: ${userMessage}`); } case 401: { throw new Error(`Authentication failed. Please check your API key.`); } case 403: { // For 403, use the server's error message directly as it's now detailed // If the message suggests an API key issue, provide additional guidance if (userMessage.toLowerCase().includes('api key')) { throw new Error(`${userMessage}\n\nTroubleshooting steps:\n` + ` 1. Verify DEVICE_CLOUD_API_KEY environment variable is set\n` + ` 2. Check you're using the correct API key for this environment\n` + ` 3. Ensure the API key hasn't been deleted or revoked\n` + ` 4. Confirm you're connecting to the correct API URL`); } throw new Error(`Access denied. ${userMessage}`); } case 404: { throw new Error(`Resource not found. ${userMessage}`); } case 429: { throw new Error(`Rate limit exceeded. Please try again later.`); } case 500: { throw new Error(`Server error occurred. Please try again or contact support.`); } default: { throw new Error(`${operation} failed: ${userMessage} (HTTP ${res.status})`); } } }, async checkForExistingUpload(baseUrl, apiKey, sha) { try { const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, { body: JSON.stringify({ sha }), headers: { 'content-type': 'application/json', 'x-app-api-key': apiKey, }, method: 'POST', }); if (!res.ok) { await this.handleApiError(res, 'Failed to check for existing upload'); } return res.json(); } catch (error) { // Handle network-level errors (DNS, connection refused, timeout, etc.) if (error instanceof TypeError && error.message === 'fetch failed') { throw this.enhanceFetchError(error, `${baseUrl}/uploads/checkForExistingUpload`); } throw error; } }, async downloadArtifactsZip(baseUrl, apiKey, uploadId, results, artifactsPath = './artifacts.zip') { try { const res = await fetch(`${baseUrl}/results/${uploadId}/download`, { body: JSON.stringify({ results }), headers: { 'content-type': 'application/json', 'x-app-api-key': apiKey, }, method: 'POST', }); if (!res.ok) { await this.handleApiError(res, 'Failed to download artifacts'); } // Handle tilde expansion for home directory if (artifactsPath.startsWith('~/') || artifactsPath === '~') { artifactsPath = artifactsPath.replace(/^~(?=$|\/|\\)/, // eslint-disable-next-line unicorn/prefer-module require('node:os').homedir()); } // Create directory structure if it doesn't exist // eslint-disable-next-line unicorn/prefer-module const { dirname } = require('node:path'); // eslint-disable-next-line unicorn/prefer-module const { createWriteStream, mkdirSync } = require('node:fs'); // eslint-disable-next-line unicorn/prefer-module const { finished } = require('node:stream/promises'); // eslint-disable-next-line unicorn/prefer-module const { Readable } = require('node:stream'); const directory = dirname(artifactsPath); if (directory !== '.') { try { mkdirSync(directory, { recursive: true }); } catch (error) { // Ignore if directory already exists if (error.code !== 'EEXIST') { throw error; } } } const fileStream = createWriteStream(artifactsPath, { flags: 'w' }); await finished(Readable.fromWeb(res.body).pipe(fileStream)); } catch (error) { if (error instanceof TypeError && error.message === 'fetch failed') { throw this.enhanceFetchError(error, `${baseUrl}/results/${uploadId}/download`); } throw error; } }, async finaliseUpload(config) { const { baseUrl, apiKey, id, metadata, path, sha, supabaseSuccess, backblazeSuccess } = config; try { const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, { body: JSON.stringify({ backblazeSuccess, id, metadata, path, // This is tempPath for TUS uploads sha, supabaseSuccess, }), headers: { 'content-type': 'application/json', 'x-app-api-key': apiKey, }, method: 'POST', }); if (!res.ok) { await this.handleApiError(res, 'Failed to finalize upload'); } return res.json(); } catch (error) { if (error instanceof TypeError && error.message === 'fetch failed') { throw this.enhanceFetchError(error, `${baseUrl}/uploads/finaliseUpload`); } throw error; } }, async getBinaryUploadUrl(baseUrl, apiKey, platform, fileSize) { try { const res = await fetch(`${baseUrl}/uploads/getBinaryUploadUrl`, { body: JSON.stringify({ platform, fileSize }), headers: { 'content-type': 'application/json', 'x-app-api-key': apiKey, }, method: 'POST', }); if (!res.ok) { await this.handleApiError(res, 'Failed to get upload URL'); } return res.json(); } catch (error) { if (error instanceof TypeError && error.message === 'fetch failed') { throw this.enhanceFetchError(error, `${baseUrl}/uploads/getBinaryUploadUrl`); } throw error; } }, async finishLargeFile(baseUrl, apiKey, fileId, partSha1Array) { try { const res = await fetch(`${baseUrl}/uploads/finishLargeFile`, { body: JSON.stringify({ fileId, partSha1Array }), headers: { 'content-type': 'application/json', 'x-app-api-key': apiKey, }, method: 'POST', }); if (!res.ok) { await this.handleApiError(res, 'Failed to finish large file'); } return res.json(); } catch (error) { if (error instanceof TypeError && error.message === 'fetch failed') { throw this.enhanceFetchError(error, `${baseUrl}/uploads/finishLargeFile`); } throw error; } }, async getResultsForUpload(baseUrl, apiKey, uploadId) { // TODO: merge with getUploadStatus try { const res = await fetch(`${baseUrl}/results/${uploadId}`, { headers: { 'x-app-api-key': apiKey }, }); if (!res.ok) { await this.handleApiError(res, 'Failed to get results'); } return res.json(); } catch (error) { if (error instanceof TypeError && error.message === 'fetch failed') { throw this.enhanceFetchError(error, `${baseUrl}/results/${uploadId}`); } throw error; } }, async getUploadStatus(baseUrl, apiKey, options) { const queryParams = new URLSearchParams(); if (options.uploadId) { queryParams.append('uploadId', options.uploadId); } if (options.name) { queryParams.append('name', options.name); } try { const response = await fetch(`${baseUrl}/uploads/status?${queryParams}`, { headers: { 'x-app-api-key': apiKey, }, }); if (!response.ok) { await this.handleApiError(response, 'Failed to get upload status'); } return response.json(); } catch (error) { if (error instanceof TypeError && error.message === 'fetch failed') { throw this.enhanceFetchError(error, `${baseUrl}/uploads/status?${queryParams}`); } throw error; } }, async listUploads(baseUrl, apiKey, options = {}) { const queryParams = new URLSearchParams(); if (options.name) { queryParams.append('name', options.name); } if (options.from) { queryParams.append('from', options.from); } if (options.to) { queryParams.append('to', options.to); } if (options.limit !== undefined) { queryParams.append('limit', String(options.limit)); } if (options.offset !== undefined) { queryParams.append('offset', String(options.offset)); } const url = `${baseUrl}/uploads/list?${queryParams}`; try { const response = await fetch(url, { headers: { 'x-app-api-key': apiKey, }, }); if (!response.ok) { await this.handleApiError(response, 'Failed to list uploads'); } return response.json(); } catch (error) { if (error instanceof TypeError && error.message === 'fetch failed') { throw this.enhanceFetchError(error, url); } throw error; } }, async uploadFlow(baseUrl, apiKey, testFormData) { try { const res = await fetch(`${baseUrl}/uploads/flow`, { body: testFormData, headers: { 'x-app-api-key': apiKey, }, method: 'POST', }); if (!res.ok) { await this.handleApiError(res, 'Failed to upload test flows'); } return res.json(); } catch (error) { if (error instanceof TypeError && error.message === 'fetch failed') { throw this.enhanceFetchError(error, `${baseUrl}/uploads/flow`); } throw error; } }, /** * Generic report download method that handles both junit and allure reports * @param baseUrl - API base URL * @param apiKey - API key for authentication * @param uploadId - Upload ID to download report for * @param reportType - Type of report to download ('junit' or 'allure') * @param reportPath - Optional custom path for the downloaded report * @returns Promise that resolves when download is complete */ async downloadReportGeneric(baseUrl, apiKey, uploadId, reportType, reportPath) { // Define endpoint and default filename mappings const config = { junit: { endpoint: `/results/${uploadId}/report`, defaultFilename: `report-${uploadId}.xml`, notFoundMessage: `Upload ID '${uploadId}' not found or no results available for this upload`, errorPrefix: 'Failed to download report', }, allure: { endpoint: `/allure/${uploadId}/download`, defaultFilename: `report-${uploadId}.html`, notFoundMessage: `Upload ID '${uploadId}' not found or no Allure report available for this upload`, errorPrefix: 'Failed to download Allure report', }, html: { endpoint: `/results/${uploadId}/html-report`, defaultFilename: `report-${uploadId}.html`, notFoundMessage: `Upload ID '${uploadId}' not found or no HTML report available for this upload`, errorPrefix: 'Failed to download HTML report', }, }; const { endpoint, defaultFilename, notFoundMessage, errorPrefix } = config[reportType]; const finalReportPath = reportPath || path.resolve(process.cwd(), defaultFilename); const url = `${baseUrl}${endpoint}`; try { // Make the download request const res = await fetch(url, { headers: { 'x-app-api-key': apiKey, }, method: 'GET', }); if (!res.ok) { const errorText = await res.text(); if (res.status === 404) { throw new Error(notFoundMessage); } throw new Error(`${errorPrefix}: ${res.status} ${errorText}`); } // Handle tilde expansion for home directory (applies to all report types) let expandedPath = finalReportPath; if (finalReportPath.startsWith('~/') || finalReportPath === '~') { expandedPath = finalReportPath.replace(/^~/, // eslint-disable-next-line unicorn/prefer-module require('node:os').homedir()); } // Create directory structure if it doesn't exist // eslint-disable-next-line unicorn/prefer-module const { dirname } = require('node:path'); // eslint-disable-next-line unicorn/prefer-module const { createWriteStream, mkdirSync } = require('node:fs'); // eslint-disable-next-line unicorn/prefer-module const { finished } = require('node:stream/promises'); // eslint-disable-next-line unicorn/prefer-module const { Readable } = require('node:stream'); const directory = dirname(expandedPath); if (directory !== '.') { try { mkdirSync(directory, { recursive: true }); } catch (error) { // Ignore EEXIST errors (directory already exists) if (error.code !== 'EEXIST') { throw error; } } } // Write the file using streaming for better memory efficiency // Use 'w' flag to overwrite existing files instead of failing const fileStream = createWriteStream(expandedPath, { flags: 'w' }); await finished(Readable.fromWeb(res.body).pipe(fileStream)); } catch (error) { if (error instanceof TypeError && error.message === 'fetch failed') { throw this.enhanceFetchError(error, url); } throw error; } }, };