tdpw
Version:
CLI tool for uploading Playwright test reports to TestDino platform with TestDino storage support
211 lines • 8.02 kB
JavaScript
;
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