UNPKG

tdpw

Version:

CLI tool for uploading Playwright test reports to TestDino platform with TestDino storage support

211 lines 8.02 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ApiClient = void 0; const types_1 = require("../types"); const version_1 = require("../version"); // Upload endpoint relative to base API URL const UPLOAD_ENDPOINT = '/api/reports/playwright'; /** * Client for communicating with the TestDino API */ class ApiClient { baseUrl; apiKey; constructor(config) { this.baseUrl = config.apiUrl; this.apiKey = config.token; } /** * Headers to include on every API request */ getHeaders() { return { 'Content-Type': 'application/json', 'User-Agent': `tdpw/${version_1.VERSION}`, 'X-API-Key': this.apiKey, }; } /** * Upload a JSON payload to the TestDino API with enhanced error handling * @param payload The data to upload (report + metadata) */ async uploadReport(payload) { const url = `${this.baseUrl}${UPLOAD_ENDPOINT}`; let response; try { response = await fetch(url, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(payload), }); } catch (error) { // Handle network-level errors (DNS, connection refused, etc.) const errorMessage = error instanceof Error ? error.message : 'Unknown network error'; throw new types_1.NetworkError(`Failed to connect to TestDino API: ${errorMessage}`); } // Handle HTTP error responses if (!response.ok) { await this.handleHttpError(response); } // Parse JSON response let json; try { json = await response.json(); } catch (error) { throw new types_1.NetworkError(`Invalid JSON response from API: ${error instanceof Error ? error.message : 'Parse error'}`); } // Extract response data const result = this.parseUploadResponse(json); return result; } /** * Handle HTTP error responses with detailed error messages */ async handleHttpError(response) { let errorBody; try { errorBody = await response.text(); } catch { errorBody = 'Unable to read error response'; } switch (response.status) { case 401: throw new types_1.AuthenticationError('Invalid API key or unauthorized access'); case 403: throw new types_1.AuthenticationError('API key does not have permission to upload reports'); case 400: throw new types_1.NetworkError(`Bad request - Invalid data format: ${errorBody}`); case 413: throw new types_1.NetworkError('Report payload too large - consider uploading without HTML/traces'); case 429: throw new types_1.NetworkError('Rate limit exceeded - please wait before retrying'); case 500: case 502: case 503: case 504: throw new types_1.NetworkError(`TestDino API server error (${response.status}) - please try again later`); default: throw new types_1.NetworkError(`HTTP ${response.status}: ${errorBody || response.statusText}`); } } /** * Parse and validate the upload response */ parseUploadResponse(json) { if (!json || typeof json !== 'object') { throw new types_1.NetworkError('Invalid response format - expected JSON object'); } // Check for usage limit error (HTTP 200 with success: false) this.checkForUsageLimitError(json); // Handle different response structures let responseData; // Check for direct response if ('testRunId' in json) { responseData = json; } // Check for wrapped response else if ('data' in json && json.data && typeof json.data === 'object' && 'testRunId' in json.data) { responseData = json.data; } // Check for success wrapper else if ('success' in json && 'result' in json && json.result && typeof json.result === 'object' && 'testRunId' in json.result) { responseData = json.result; } else { throw new types_1.NetworkError(`Unexpected API response structure: ${JSON.stringify(json)}`); } // Validate required fields if (!responseData.testRunId || typeof responseData.testRunId !== 'string') { throw new types_1.NetworkError('API response missing required testRunId field'); } // Build response object const result = { testRunId: responseData.testRunId, }; // Add optional fields if present if (responseData.viewUrl && typeof responseData.viewUrl === 'string') { result.viewUrl = responseData.viewUrl; } if (responseData.status && typeof responseData.status === 'string') { result.status = responseData.status; } // Extract warnings array if present if (Array.isArray(responseData.warnings)) { result.warnings = responseData.warnings.filter((w) => typeof w === 'string'); } // Include any additional fields Object.keys(responseData).forEach(key => { if (!['testRunId', 'viewUrl', 'status', 'warnings'].includes(key)) { result[key] = responseData[key]; } }); return result; } /** * Check for usage limit error in API response * Server returns HTTP 200 with success: false and either: * - data.code: "USAGE_LIMIT_EXCEEDED" (with full usage data) * - message containing "limit reached" (minimal response) */ checkForUsageLimitError(json) { if (!json || typeof json !== 'object') return; const response = json; // Only check if success is explicitly false if (response.success !== false) return; const message = typeof response.message === 'string' ? response.message : ''; // Check for usage limit error by code or message pattern const data = response.data && typeof response.data === 'object' ? response.data : {}; const isUsageLimitError = data.code === 'USAGE_LIMIT_EXCEEDED' || message.toLowerCase().includes('limit reached') || message.toLowerCase().includes('test case limit'); if (isUsageLimitError) { const usageData = { tier: typeof data.tier === 'string' ? data.tier : 'unknown', limit: typeof data.limit === 'number' ? data.limit : 0, used: typeof data.used === 'number' ? data.used : 0, remaining: typeof data.remaining === 'number' ? data.remaining : 0, resetDate: typeof data.resetDate === 'string' ? data.resetDate : new Date().toISOString(), }; // Add organizationId if present in response if (typeof data.organizationId === 'string') { usageData.organizationId = data.organizationId; } throw new types_1.UsageLimitError(message || 'Monthly test case limit reached.', usageData); } } /** * Health check endpoint to verify API connectivity */ async healthCheck() { try { const response = await fetch(`${this.baseUrl}/api/health`, { method: 'GET', headers: { 'User-Agent': `tdpw/${version_1.VERSION}`, }, }); return response.ok; } catch { return false; } } } exports.ApiClient = ApiClient; //# sourceMappingURL=api.js.map