Source: test-rail.js

/**
 * @exports TestRail
 * @requires sync-request
 */

const request = require('sync-request');

/**
 * TestRail basic API wrapper
 */
class TestRail {

	/**
	 * @param {Object} options - wdio TestRail specifc configurations
	 * @param {string} options.domain - Domain for TestRail
	 * @param {number} options.projectId - Project identifier
	 * @param {Array.<number>} options.suiteId - List of suites identifiers
	 * @param {number} [options.assignedToId] - User identifier
	 * @param {string} options.username - User email
	 * @param {string} options.password - User API key
	 * @param {Boolean} options.includeAll - Flag to inlcude all tests from a suite in a run
	 * @param {number} [options.updateRun] - Test run identifier for test run to update
	 * @param {number} [options.updatePlan] - Test plan identifier for a test plan to update
	 */
	constructor(options) {
		this._validate(options, 'domain');
		this._validate(options, 'username');
		this._validate(options, 'password');
		this._validate(options, 'projectId');
		this._validate(options, 'suiteId');
		this._validate(options, 'includeAll');

		// compute base url
		this.options = options;
		this.base = `https://${options.domain}/index.php`;
	}

	/**
	 * Verifies if required options exist in webdriverio config file
	 *
	 * @param {Object} options - wdio TestRail specifc configurations
	 * @param {string} options.domain - Domain for TestRail
	 * @param {number} options.projectId - Project identifier
	 * @param {Array.<number>} options.suiteId - List of suites identifiers
	 * @param {number} [options.assignedToId] - User identifier
	 * @param {string} options.username - User email
	 * @param {string} options.password - User API key
	 * @param {Boolean} options.includeAll - Flag to inlcude all tests from a suite in a run
	 * @param {number} [options.updateRun] - Test run identifier for test run to update
	 * @param {number} [options.updatePlan] - Test plan identifier for a test plan to update
	 * @param {string} name - Name of the property
	 * @private
	 */
	_validate(options, name) {
		if (options == null) {
			throw new Error("Missing testRailsOptions in wdio.conf");
		}
		if (options[name] == null) {
			throw new Error(`Missing ${name} value. Please update testRailsOptions in wdio.conf`);
		}
	}

	/**
	 * Construct and returns an API path
	 *
	 * @param {string} path - The path for the API
	 * @return {string} Constructed URL path to TestRail API
	 * @private
	 */
	_url(path) {
		return `${this.base}?${path}`;
	}

	/**
	 * Makes a POST request on a TestRail API
	 *
	 * @param {string} api - API path
	 * @param {*} body - Body of request
	 * @param {callback} error - Callback to handle errors
	 * @return {*} Response object
	 * @private
	 */
	_post(api, body, error = undefined) {
		return this._request("POST", api, body, error);
	}

	/**
	 * Makes a GET request on a TestRail API
	 *
	 * @param {string} api - API path
	 * @param {callback} error - Callback to handle errors
	 * @return {*} Response object
	 * @private
	 */
	_get(api, error = undefined) {
		return this._request("GET", api, null, error);
	}

	/**
	 * Makes a request to the TestRail API
	 *
	 * @param {string} method - Type of request to make
	 * @param {string} api - API path
	 * @param {*} body  Body of request
	 * @param {callback} error
	 * @return {*} API response
	 * @private
	 */
	_request(method, api, body, error = undefined) {
		let options = {
			headers: {
				"Authorization": "Basic " + new Buffer(this.options.username + ":" + this.options.password).toString("base64"),
				"Content-Type": "application/json"
			},
		};
		if (body) {
			options['json'] = body;
		}

		let result = request(method, this._url(`/api/v2/${api}`), options);
		result = JSON.parse(result.getBody('utf8'));
		if (result.error) {
			console.log("Error: %s", JSON.stringify(result.body));
			if (error) {
				error(result.error);
			} else {
				throw new Error(result.error);
			}
		}
		return result;
	}

	/**
	 * Creates a new array of unique data from the data
	 * within 2 existing arrays
	 *
	 * @param {Array.<*>} currArr
	 * @param {Array.<*>} newArr
	 * @returns {array}
	 * @private
	 */
	_createUniqueArray(currArr, newArr) {
		return [...new Set([...newArr, ...currArr])]
	}

	/**
	 * Creates a new test plan
	 *
	 * @param {string} name - Plan name
	 * @param {string} desc - Plane description
	 * @param {Array.<Object>} testRuns - Test runs
	 * @returns {*} API response
	 */
	addPlan(name, description, testRuns) {
		return this._post(`add_plan/${this.options.projectId}`, {
			"name": name,
			"description": description,
			"entries": testRuns
		});
	}

	/**
	 * Retrieves a test plan
	 *
	 * @param {number} planId - Plan identifier
	 * @returns {*} API response
	 */
	getPlan(planId) {
		return this._get(`get_plan/${this.options.updatePlan}`);
	}

	/**
	 * Adds a test plan entry to the current project
	 *
	 * @param {number} planId - Plan identifier
	 * @param {number} suiteId  - Suite identifier
	 * @param {string} name - Plan name
	 * @param {string} desc - Plan name
	 * @param {Array.<Object>} runs - Test runs
	 * @param {Array.<number>} caseIds - Test case identifiers
	 * @return {*} API response
	 */
	addTestPlanEntry(planId, suiteId, name, desc, runs, caseIds) {
		return this._post(
			`add_plan_entry/${planId}`, {
				'include_all': this.options.includeAll,
				'suite_id': suiteId,
				'name': name,
				'description': desc,
				'runs': runs,
				'case_ids': caseIds
			});
	}

	/**
	 * Adds missing case ids to a test plan entry
	 *
	 * @param {number} planId - Plan identifier
	 * @param {number} entryId  - Entry identifier
	 * @param {Array.<number>} caseIds - Test case identifiers
	 * @return {*} API response
	 */
	updateTestPlanEntry(planId, entryId, caseIds) {
		return this._post(`update_plan_entry/${planId}/${entryId}`, {
			case_ids: caseIds
		});
	}

	/**
	 * Gets a suite
	 *
	 * @param {number} suiteId - Suite identifier
	 * @return {*} API response
	 */
	getSuite(suiteId) {
		return this._get(`get_suite/${suiteId}`);
	}

	/**
	 * Gets all the tests in a run
	 *
	 * @param {number} runId - Run identifier
	 * @return {*} API response
	 */
	getTestsForRun(runId) {
		return this._get(`get_tests/${runId}`)
	}

	/**
	 * Adds a test run
	 *
	 * @param {string} name - Test run name
	 * @param {string} description - Test run description
	 * @param {number} suiteId - Suite id for test cases in this run
	 * @return {*} API response
	 */
	addRun(name, description, suiteId, caseIds) {
		return this._post(`add_run/${this.options.projectId}`, {
			"suite_id": suiteId,
			"name": name,
			"description": description,
			"assignedto_id": this.options.assignedToId,
			"include_all": this.options.includeAll,
			"case_ids": caseIds
		});
	}

	/**
	 * Adds test cases to a test run
	 *
	 * @param {number} runId - Run identifier
	 * @param {Array.<Object>} cases - Test case data
	 * @return {*} API response
	 */
	addCasesToRun(runId, cases) {
		const currentCases = this.getTestsForRun(runId).map(c => c.case_id);
		//console.log([...currentCases, ...cases])
		this._post(`update_run/${runId}`, {
			'case_ids': this._createUniqueArray(currentCases, cases)
		});
	}

	/**
	 * Get test cases that belong to a suite
	 *
	 * @param {*} projectId - Project identifier
	 * @param {*} suiteId - Suite identifier
	 * @return {*} API response
	 */
	getTestsForSuite(projectId, suiteId) {
		return this._get(`get_cases/${projectId}&suite_id=${suiteId}`);
	}

	/**
	 * Adds test results for a test cases
	 *
	 * @param {number} runId - Run identifier
	 * @param {Array.<Object>} results - Test case results
	 *
	 * @return {*} API response
	 */
	addResultsForCases(runId, results) {
		if (results.length > 0) {
			return this._post(`add_results_for_cases/${runId}`, {
				results: results
			});
		}
	}

	/**
	 * Publishes results of execution of an automated test run
	 *
	 * @param {string} name - Test run/plan name
	 * @param {string} description
	 * @param {Array.<Object>} results
	 * @param {callback} callback
	 */
	publish(name, description, results, runners, callback = undefined) {
		let runs = [];
		let body = null;
		let plan = null

		if (typeof this.options.suiteId !== 'number') {
			if (this.options.updatePlan) {
				//1. find our existing plan
				plan = this.getPlan(this.options.updatePlan);

				//console.log(plan);
				plan.entries.forEach(entry => {
					//console.log(entry.runs)
					//console.log(entry.runs[0].id)

					const suiteInfo = this.getSuite(entry.runs[0].suite_id);
					const currentCases = this.getTestsForRun(entry.runs[0].id).map(c => c.case_id);
					suiteInfo.cases = this.getTestsForSuite(this.options.projectId, suiteInfo.id).map(c => c.id);
					suiteInfo.newCases = results.filter(r => suiteInfo.cases.includes(r.case_id));

					//reset case id listing
					this.updateTestPlanEntry(
						plan.id,
						entry.id,
						this._createUniqueArray(
							currentCases,
							suiteInfo.newCases.map(r => r.case_id)
						)
					)
					body = [];
					//add new results
					body.push(this.addResultsForCases(entry.runs[0].id, suiteInfo.newCases));
				})
			} else {
				let testPlanEntries = []
				//1. create the test plan
				plan = this.addPlan(name, description, []);
				runners.forEach((runner, runnerIdx) => {
					this.options.suiteId.forEach((suiteId, suiteIdx) => {
						const suiteInfo = this.getSuite(suiteId);
						suiteInfo.cases = this.getTestsForSuite(this.options.projectId, suiteId).map(c => c.id);
						//runs = 
						testPlanEntries.push(
							this.addTestPlanEntry(
								plan.id,
								suiteId,
								`${suiteInfo.name} - ${runner.deviceName||runner.platform} - ${runner.browserName}`,
								description, [],
								results.filter(result => {
									return JSON.stringify(result.runner) === JSON.stringify(runner);
								}).map(result => result.case_id)).runs[0]);
						
						// console.log('\n\n');
						// console.log(testPlanEntries)
						// console.log('-------------');

						body = [];

						body.push(
							this.addResultsForCases(
								testPlanEntries.slice(-1)[0].id,
								results.filter(r => suiteInfo.cases.includes(r.case_id) &&
									JSON.stringify(r.runner) === JSON.stringify(runner))
							)
						);
					})
				})
			}

			console.log(`Results published to ${this.base}?/plans/view/${plan.id}`);
			console.log(`Add updatePlan: ${plan.id} to your config to update this plan.`);
		} else {
			if (this.options.updateRun) {
				//update run here
				runs[0] = {
					id: this.options.updateRun
				};

				//add any missing test case ids to a run
				this.addCasesToRun(runs[0].id, results.map(r => r.case_id));
			} else {
				// console.log('runners')
				// console.log(runners)
				runners.forEach((runner, idx) => {
					const runName = `${name} - (${runner.deviceName||runner.platform} - ${runner.browserName})`
					runs.push(this.addRun(runName, description, this.options.suiteId, results.filter(result => {
						return JSON.stringify(result.runner) === JSON.stringify(runner);
					}).map(result => result.case_id)));
					this.addResultsForCases(runs[idx].id, results);
				})
			}

			//body = this.addResultsForCases(runs[0].id, results);
			console.log(`Results published to ${this.base}?/runs/view/${runs[0].id}`);
			console.log(`Add updateRun: ${runs[0].id} to your config to update this run.`);
		}

		// execute callback if specified
		if (callback) {
			callback(body);
		}
	}
}

module.exports = TestRail;