API Integration & External Tools

Complete guide to integrating ConductorQA with external tools, CI/CD pipelines, and third-party services for seamless test management workflows.

Overview

ConductorQA provides extensive integration capabilities to connect with your existing development workflow, CI/CD pipelines, and third-party tools. This guide covers all available integrations and how to implement them effectively.

API Integration

REST API Overview

API Architecture

ConductorQA API Structure:
├── Authentication: Secure API key-based access
├── Projects: Project management and configuration
├── Test Suites: Test organization and management
├── Test Cases: Individual test management
├── Test Runs: Execution tracking and results
├── Results: Test result reporting and retrieval
└── Analytics: Metrics and reporting data

Base API Configuration

// API Configuration
const CONDUCTORQA_CONFIG = {
  baseURL: 'https://api.conductorqa.com/v1',
  headers: {
    'Authorization': `Bearer ${process.env.CONDUCTORQA_API_KEY}`,
    'Content-Type': 'application/json',
    'User-Agent': 'MyApp/1.0'
  },
  timeout: 30000,
  retries: 3
};

Authentication and Security

API Key Management

// Secure API Key Usage
class ConductorQAClient {
  constructor(apiKey, options = {}) {
    this.apiKey = apiKey;
    this.baseURL = options.baseURL || 'https://api.conductorqa.com/v1';
    this.timeout = options.timeout || 30000;
  }

  async authenticate() {
    try {
      const response = await this.request('GET', '/auth/verify');
      return response.data;
    } catch (error) {
      throw new Error(`Authentication failed: ${error.message}`);
    }
  }

  async request(method, endpoint, data = null) {
    const config = {
      method,
      url: `${this.baseURL}${endpoint}`,
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      timeout: this.timeout
    };

    if (data) {
      config.data = data;
    }

    try {
      const response = await axios(config);
      return response.data;
    } catch (error) {
      this.handleApiError(error);
    }
  }
}

Security Best Practices

  • Environment Variables: Store API keys securely
  • Key Rotation: Regular API key updates
  • Scope Limitation: Restrict API key permissions
  • Request Validation: Validate all API requests and responses
  • Error Handling: Secure error message handling

Core API Operations

Project Management

// Project Operations
class ProjectAPI {
  constructor(client) {
    this.client = client;
  }

  async listProjects() {
    return await this.client.request('GET', '/projects');
  }

  async getProject(projectId) {
    return await this.client.request('GET', `/projects/${projectId}`);
  }

  async createProject(projectData) {
    return await this.client.request('POST', '/projects', projectData);
  }

  async updateProject(projectId, updates) {
    return await this.client.request('PUT', `/projects/${projectId}`, updates);
  }
}

Test Execution Integration

// Test Run Management
class TestExecutionAPI {
  constructor(client) {
    this.client = client;
  }

  async createTestRun(projectId, testRunData) {
    return await this.client.request(
      'POST', 
      `/projects/${projectId}/test-runs`, 
      testRunData
    );
  }

  async reportResults(testRunId, results) {
    return await this.client.request(
      'POST',
      `/test-runs/${testRunId}/results`,
      { results }
    );
  }

  async bulkReportResults(projectId, bulkData) {
    return await this.client.request(
      'POST',
      `/projects/${projectId}/bulk-results`,
      bulkData
    );
  }
}

CI/CD Pipeline Integration

GitHub Actions Integration

Workflow Configuration

# .github/workflows/test-integration.yml
name: Test Execution with ConductorQA

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test-and-report:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout Code
      uses: actions/checkout@v4
      
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: 'npm'
        
    - name: Install Dependencies
      run: npm ci
      
    - name: Run Tests
      run: npm run test:ci
      env:
        NODE_ENV: test
        
    - name: Report Results to ConductorQA
      uses: ./.github/actions/conductorqa-report
      with:
        api-key: ${{ secrets.CONDUCTORQA_API_KEY }}
        project-id: ${{ secrets.CONDUCTORQA_PROJECT_ID }}
        test-results-path: './test-results.json'
        environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
        build-number: ${{ github.run_number }}
      if: always()

    - name: Create Test Summary
      run: |
        echo "## Test Results" >> $GITHUB_STEP_SUMMARY
        echo "Build: ${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
        echo "Environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}" >> $GITHUB_STEP_SUMMARY
        node scripts/create-test-summary.js >> $GITHUB_STEP_SUMMARY
      if: always()

Custom Action for ConductorQA

// .github/actions/conductorqa-report/index.js
const core = require('@actions/core');
const axios = require('axios');
const fs = require('fs');

async function reportToConductorQA() {
  try {
    const apiKey = core.getInput('api-key', { required: true });
    const projectId = core.getInput('project-id', { required: true });
    const testResultsPath = core.getInput('test-results-path', { required: true });
    const environment = core.getInput('environment') || 'ci';
    const buildNumber = core.getInput('build-number');

    // Read test results
    const testResults = JSON.parse(fs.readFileSync(testResultsPath, 'utf8'));

    // Prepare payload
    const payload = {
      name: `CI Build #${buildNumber} - ${environment}`,
      environment,
      build_number: buildNumber,
      executed_by: 'github-actions',
      started_at: new Date().toISOString(),
      results: testResults.tests.map(test => ({
        test_case_name: test.fullName,
        status: test.status === 'passed' ? 'passed' : 'failed',
        duration: test.duration / 1000,
        error_message: test.failureMessages?.join('\n'),
        file_path: test.ancestorTitles.join(' > ')
      }))
    };

    // Send to ConductorQA
    const response = await axios.post(
      `https://api.conductorqa.com/v1/projects/${projectId}/test-runs`,
      payload,
      {
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json'
        }
      }
    );

    core.setOutput('test-run-id', response.data.id);
    core.setOutput('results-url', response.data.url);
    
    console.log(`✅ Test results reported to ConductorQA: ${response.data.url}`);
  } catch (error) {
    core.setFailed(`Failed to report to ConductorQA: ${error.message}`);
  }
}

reportToConductorQA();

GitLab CI Integration

Pipeline Configuration

# .gitlab-ci.yml
stages:
  - test
  - report

variables:
  CONDUCTORQA_BASE_URL: "https://api.conductorqa.com/v1"

test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm run test:ci
  artifacts:
    reports:
      junit: junit.xml
    paths:
      - test-results.json
      - coverage/
    expire_in: 1 week
    when: always

report-to-conductorqa:
  stage: report
  image: node:20
  dependencies:
    - test
  script:
    - node scripts/report-to-conductorqa.js
  variables:
    ENVIRONMENT: $CI_COMMIT_REF_NAME
    BUILD_NUMBER: $CI_PIPELINE_ID
    COMMIT_SHA: $CI_COMMIT_SHA
  only:
    - main
    - develop
  when: always

GitLab Reporting Script

// scripts/report-to-conductorqa.js
const axios = require('axios');
const fs = require('fs');

async function reportResults() {
  const testResults = JSON.parse(fs.readFileSync('test-results.json', 'utf8'));
  
  const payload = {
    name: `GitLab Pipeline #${process.env.BUILD_NUMBER} - ${process.env.ENVIRONMENT}`,
    environment: process.env.ENVIRONMENT,
    build_number: process.env.BUILD_NUMBER,
    commit_sha: process.env.COMMIT_SHA,
    executed_by: 'gitlab-ci',
    results: testResults.tests.map(test => ({
      test_case_name: test.title,
      status: test.status,
      duration: test.duration,
      error_message: test.error?.message,
      suite: test.suite
    }))
  };

  try {
    const response = await axios.post(
      `${process.env.CONDUCTORQA_BASE_URL}/projects/${process.env.CONDUCTORQA_PROJECT_ID}/test-runs`,
      payload,
      {
        headers: {
          'Authorization': `Bearer ${process.env.CONDUCTORQA_API_KEY}`,
          'Content-Type': 'application/json'
        }
      }
    );

    console.log('✅ Results reported to ConductorQA:', response.data.url);
  } catch (error) {
    console.error('❌ Failed to report to ConductorQA:', error.response?.data || error.message);
    process.exit(1);
  }
}

reportResults();

Jenkins Integration

Pipeline Script

pipeline {
    agent any
    
    environment {
        CONDUCTORQA_API_KEY = credentials('conductorqa-api-key')
        CONDUCTORQA_PROJECT_ID = credentials('conductorqa-project-id')
        CONDUCTORQA_BASE_URL = 'https://api.conductorqa.com/v1'
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Install Dependencies') {
            steps {
                sh 'npm ci'
            }
        }
        
        stage('Run Tests') {
            steps {
                sh 'npm run test:ci'
            }
            post {
                always {
                    publishTestResults testResultsPattern: 'junit.xml'
                    archiveArtifacts artifacts: 'test-results.json', fingerprint: true
                }
            }
        }
        
        stage('Report to ConductorQA') {
            when {
                anyOf {
                    branch 'main'
                    branch 'develop'
                }
            }
            steps {
                script {
                    def testResults = readJSON file: 'test-results.json'
                    def payload = [
                        name: "Jenkins Build #${BUILD_NUMBER} - ${BRANCH_NAME}",
                        environment: BRANCH_NAME == 'main' ? 'production' : 'staging',
                        build_number: BUILD_NUMBER,
                        executed_by: 'jenkins',
                        results: testResults.tests.collect { test ->
                            [
                                test_case_name: test.fullName,
                                status: test.status,
                                duration: test.duration / 1000,
                                error_message: test.failureMessages?.join('\n')
                            ]
                        }
                    ]
                    
                    def response = httpRequest(
                        httpMode: 'POST',
                        url: "${CONDUCTORQA_BASE_URL}/projects/${CONDUCTORQA_PROJECT_ID}/test-runs",
                        requestBody: groovy.json.JsonOutput.toJson(payload),
                        customHeaders: [
                            [name: 'Authorization', value: "Bearer ${CONDUCTORQA_API_KEY}"],
                            [name: 'Content-Type', value: 'application/json']
                        ]
                    )
                    
                    def responseData = readJSON text: response.content
                    echo "✅ Test results reported to ConductorQA: ${responseData.url}"
                }
            }
        }
    }
}

Test Framework Integration

Jest Integration

Jest Reporter

// jest-conductorqa-reporter.js
class ConductorQAReporter {
  constructor(globalConfig, options) {
    this.globalConfig = globalConfig;
    this.options = options;
    this.results = [];
  }

  onTestResult(test, testResult, aggregatedResult) {
    testResult.testResults.forEach(result => {
      this.results.push({
        test_case_name: result.fullName,
        status: result.status === 'passed' ? 'passed' : 'failed',
        duration: result.duration / 1000,
        error_message: result.failureMessages.join('\n'),
        file_path: test.path,
        suite: result.ancestorTitles.join(' > ')
      });
    });
  }

  async onRunComplete(contexts, aggregatedResult) {
    if (process.env.CONDUCTORQA_API_KEY) {
      await this.reportToConductorQA(this.results);
    }
  }

  async reportToConductorQA(results) {
    const axios = require('axios');
    
    const payload = {
      name: `Jest Test Run - ${new Date().toISOString()}`,
      environment: process.env.NODE_ENV || 'test',
      executed_by: 'jest',
      results: results
    };

    try {
      const response = await axios.post(
        `${process.env.CONDUCTORQA_BASE_URL}/projects/${process.env.CONDUCTORQA_PROJECT_ID}/test-runs`,
        payload,
        {
          headers: {
            'Authorization': `Bearer ${process.env.CONDUCTORQA_API_KEY}`,
            'Content-Type': 'application/json'
          }
        }
      );

      console.log(`✅ Results reported to ConductorQA: ${response.data.url}`);
    } catch (error) {
      console.error('❌ Failed to report to ConductorQA:', error.message);
    }
  }
}

module.exports = ConductorQAReporter;

Jest Configuration

// jest.config.js
module.exports = {
  reporters: [
    'default',
    ['./jest-conductorqa-reporter.js', {
      projectId: process.env.CONDUCTORQA_PROJECT_ID,
      environment: process.env.NODE_ENV || 'test'
    }]
  ],
  testResultsProcessor: './scripts/process-test-results.js'
};

Cypress Integration

Cypress Plugin

// cypress/plugins/conductorqa.js
const axios = require('axios');

function reportToConductorQA(results) {
  return new Promise(async (resolve, reject) => {
    try {
      const payload = {
        name: `Cypress E2E Tests - ${new Date().toISOString()}`,
        environment: Cypress.env('ENVIRONMENT') || 'test',
        executed_by: 'cypress',
        results: results.runs[0].tests.map(test => ({
          test_case_name: test.title.join(' > '),
          status: test.state === 'passed' ? 'passed' : 'failed',
          duration: test.duration / 1000,
          error_message: test.err?.message,
          suite: test.title.slice(0, -1).join(' > ')
        }))
      };

      const response = await axios.post(
        `${Cypress.env('CONDUCTORQA_BASE_URL')}/projects/${Cypress.env('CONDUCTORQA_PROJECT_ID')}/test-runs`,
        payload,
        {
          headers: {
            'Authorization': `Bearer ${Cypress.env('CONDUCTORQA_API_KEY')}`,
            'Content-Type': 'application/json'
          }
        }
      );

      console.log('✅ Results reported to ConductorQA:', response.data.url);
      resolve(response.data);
    } catch (error) {
      console.error('❌ Failed to report to ConductorQA:', error.message);
      reject(error);
    }
  });
}

module.exports = { reportToConductorQA };

Cypress Configuration

// cypress.config.js
const { defineConfig } = require('cypress');
const { reportToConductorQA } = require('./cypress/plugins/conductorqa');

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('after:run', async (results) => {
        if (config.env.CONDUCTORQA_API_KEY) {
          await reportToConductorQA(results);
        }
      });
    }
  }
});

Third-Party Tool Integration

Slack Integration

Notification Setup

// slack-integration.js
class SlackNotifier {
  constructor(webhookUrl) {
    this.webhookUrl = webhookUrl;
  }

  async sendTestResults(testRunData) {
    const { name, results, pass_rate, environment } = testRunData;
    const passed = results.filter(r => r.status === 'passed').length;
    const failed = results.filter(r => r.status === 'failed').length;
    
    const color = pass_rate >= 95 ? 'good' : pass_rate >= 80 ? 'warning' : 'danger';
    const emoji = pass_rate >= 95 ? '✅' : pass_rate >= 80 ? '⚠️' : '❌';

    const payload = {
      username: 'ConductorQA',
      icon_emoji: ':test_tube:',
      attachments: [{
        color: color,
        title: `${emoji} Test Run Completed: ${name}`,
        fields: [
          { title: 'Environment', value: environment, short: true },
          { title: 'Pass Rate', value: `${pass_rate.toFixed(1)}%`, short: true },
          { title: 'Passed', value: passed.toString(), short: true },
          { title: 'Failed', value: failed.toString(), short: true }
        ],
        footer: 'ConductorQA',
        ts: Math.floor(Date.now() / 1000)
      }]
    };

    await axios.post(this.webhookUrl, payload);
  }
}

Jira Integration

Issue Creation

// jira-integration.js
class JiraIntegration {
  constructor(config) {
    this.baseURL = config.baseURL;
    this.username = config.username;
    this.apiToken = config.apiToken;
    this.projectKey = config.projectKey;
  }

  async createBugFromFailure(testResult) {
    const payload = {
      fields: {
        project: { key: this.projectKey },
        summary: `Test Failure: ${testResult.test_case_name}`,
        description: this.formatDescription(testResult),
        issuetype: { name: 'Bug' },
        priority: this.mapPriority(testResult.priority),
        labels: ['automated-test', 'qa-found'],
        customfield_10001: testResult.environment // Environment field
      }
    };

    const response = await axios.post(
      `${this.baseURL}/rest/api/3/issue`,
      payload,
      {
        auth: {
          username: this.username,
          password: this.apiToken
        },
        headers: {
          'Content-Type': 'application/json'
        }
      }
    );

    return response.data;
  }

  formatDescription(testResult) {
    return `
h3. Test Case
${testResult.test_case_name}

h3. Environment
${testResult.environment}

h3. Error Message
{code}
${testResult.error_message}
{code}

h3. Test Run Details
* Duration: ${testResult.duration}s
* Executed By: ${testResult.executed_by}
* Build: ${testResult.build_number}

h3. ConductorQA Link
[View Test Result|${testResult.conductorqa_url}]
    `;
  }
}

Webhook Integration

Webhook Configuration

Setting Up Webhooks

// webhook-handler.js
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Webhook verification middleware
function verifyWebhookSignature(req, res, next) {
  const signature = req.headers['x-conductorqa-signature'];
  const payload = JSON.stringify(req.body);
  const hmac = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET);
  const expectedSignature = 'sha256=' + hmac.update(payload).digest('hex');

  if (signature === expectedSignature) {
    next();
  } else {
    res.status(401).send('Unauthorized');
  }
}

// Test run completion webhook
app.post('/webhooks/test-run-completed', verifyWebhookSignature, async (req, res) => {
  const { event, data } = req.body;
  
  if (event === 'test_run_completed') {
    await handleTestRunCompletion(data);
  }
  
  res.status(200).send('OK');
});

async function handleTestRunCompletion(testRunData) {
  // Send Slack notification
  if (process.env.SLACK_WEBHOOK_URL) {
    await sendSlackNotification(testRunData);
  }

  // Create Jira issues for failures
  if (testRunData.failed_tests.length > 0) {
    await createJiraIssues(testRunData.failed_tests);
  }

  // Update external dashboard
  await updateExternalDashboard(testRunData);
}

Webhook Event Types

Supported Webhook Events:
{
  "test_run_started": {
    "description": "Test run execution begins",
    "payload": "test_run_id, project_id, name, started_at"
  },
  "test_run_completed": {
    "description": "Test run execution completes", 
    "payload": "test_run_id, results, metrics, duration"
  },
  "test_case_failed": {
    "description": "Individual test case fails",
    "payload": "test_case_id, failure_reason, environment"
  },
  "flaky_test_detected": {
    "description": "Flaky test pattern identified",
    "payload": "test_case_id, failure_rate, recommendations"
  }
}

Error Handling and Monitoring

Integration Health Monitoring

Health Check Implementation

// integration-health.js
class IntegrationHealthChecker {
  constructor(integrations) {
    this.integrations = integrations;
  }

  async checkAllIntegrations() {
    const results = {};
    
    for (const [name, integration] of Object.entries(this.integrations)) {
      try {
        await integration.healthCheck();
        results[name] = { status: 'healthy', timestamp: new Date() };
      } catch (error) {
        results[name] = { 
          status: 'unhealthy', 
          error: error.message,
          timestamp: new Date()
        };
      }
    }
    
    return results;
  }

  async validateApiConnectivity() {
    try {
      const response = await axios.get(
        `${process.env.CONDUCTORQA_BASE_URL}/health`,
        {
          headers: {
            'Authorization': `Bearer ${process.env.CONDUCTORQA_API_KEY}`
          },
          timeout: 10000
        }
      );
      
      return response.status === 200;
    } catch (error) {
      console.error('API connectivity check failed:', error.message);
      return false;
    }
  }
}

Error Recovery Strategies

Retry Logic Implementation

// retry-handler.js
class RetryHandler {
  constructor(maxRetries = 3, baseDelay = 1000) {
    this.maxRetries = maxRetries;
    this.baseDelay = baseDelay;
  }

  async executeWithRetry(operation, context = {}) {
    let lastError;
    
    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;
        
        if (attempt === this.maxRetries) {
          break;
        }
        
        if (!this.isRetryableError(error)) {
          break;
        }
        
        const delay = this.calculateDelay(attempt);
        console.warn(`Operation failed (attempt ${attempt + 1}), retrying in ${delay}ms:`, error.message);
        await this.sleep(delay);
      }
    }
    
    throw new Error(`Operation failed after ${this.maxRetries + 1} attempts: ${lastError.message}`);
  }

  isRetryableError(error) {
    // Retry on network errors and 5xx status codes
    return error.code === 'ECONNRESET' || 
           error.code === 'ETIMEDOUT' ||
           (error.response && error.response.status >= 500);
  }

  calculateDelay(attempt) {
    // Exponential backoff with jitter
    const delay = this.baseDelay * Math.pow(2, attempt);
    const jitter = delay * 0.1 * Math.random();
    return delay + jitter;
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Integration Best Practices

Security Best Practices

Secure Configuration

// secure-config.js
class SecureIntegrationConfig {
  constructor() {
    this.validateRequiredEnvVars();
    this.setupRateLimiting();
  }

  validateRequiredEnvVars() {
    const required = [
      'CONDUCTORQA_API_KEY',
      'CONDUCTORQA_PROJECT_ID',
      'CONDUCTORQA_BASE_URL'
    ];

    const missing = required.filter(env => !process.env[env]);
    
    if (missing.length > 0) {
      throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
    }
  }

  setupRateLimiting() {
    // Implement rate limiting to avoid API throttling
    this.rateLimiter = new Map();
    this.maxRequestsPerMinute = 100;
  }

  async enforceRateLimit(key = 'default') {
    const now = Date.now();
    const minute = Math.floor(now / 60000);
    const limiterKey = `${key}-${minute}`;

    const count = this.rateLimiter.get(limiterKey) || 0;
    
    if (count >= this.maxRequestsPerMinute) {
      throw new Error('Rate limit exceeded. Please wait before making more requests.');
    }

    this.rateLimiter.set(limiterKey, count + 1);
  }
}

Performance Optimization

Batch Processing

// batch-processor.js
class BatchProcessor {
  constructor(batchSize = 100, delayBetweenBatches = 1000) {
    this.batchSize = batchSize;
    this.delayBetweenBatches = delayBetweenBatches;
  }

  async processBatches(items, processor) {
    const batches = this.createBatches(items);
    const results = [];

    for (let i = 0; i < batches.length; i++) {
      const batch = batches[i];
      console.log(`Processing batch ${i + 1} of ${batches.length} (${batch.length} items)`);

      try {
        const batchResults = await processor(batch);
        results.push(...batchResults);
      } catch (error) {
        console.error(`Batch ${i + 1} failed:`, error.message);
        // Continue with next batch or implement retry logic
      }

      // Delay between batches to avoid overwhelming the API
      if (i < batches.length - 1) {
        await this.sleep(this.delayBetweenBatches);
      }
    }

    return results;
  }

  createBatches(items) {
    const batches = [];
    for (let i = 0; i < items.length; i += this.batchSize) {
      batches.push(items.slice(i, i + this.batchSize));
    }
    return batches;
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Next Steps

To maximize your integration capabilities:

  1. Set Up API Authentication - Configure secure API access
  2. Configure CI/CD Integration - Automate test result reporting
  3. Implement Webhook Handlers - Set up real-time notifications
  4. Monitor Integration Health - Track integration performance and reliability

Ready to integrate ConductorQA with your workflow? Start with a simple CI/CD integration, then gradually add more advanced features like webhooks and third-party tool connections based on your team’s needs.

Last updated: August 28, 2025

Tags

integration api ci-cd automation tools