'use strict'; require('../helper'); var assert = require('assert'); var appServer = require('../../lib/server'); var redisUtils = require('./redis-utils'); var debug = require('debug')('batch-test-client'); var JobStatus = require('../../lib/batch/job-status'); var metadataBackend = require('cartodb-redis')({ pool: redisUtils.getPool() }); var batchFactory = require('../../lib/batch/index'); var Logger = require('../../lib/utils/logger'); function response (code) { return { status: code }; } var RESPONSE = { OK: response(200), CREATED: response(201), BAD_REQUEST: response(400) }; function BatchTestClient (config) { this.config = config || {}; this.server = appServer(); const logger = new Logger(); this.batch = batchFactory(metadataBackend, redisUtils.getPool(), this.config.name, undefined, logger); this.batch.start(); this.pendingJobs = []; this.ready = false; this.batch.on('ready', function () { this.ready = true; this.pendingJobs.forEach(function (pendingJob) { this.createJob(pendingJob.job, pendingJob.override, pendingJob.callback); }.bind(this)); }.bind(this)); } module.exports = BatchTestClient; BatchTestClient.prototype.isReady = function () { return this.ready; }; BatchTestClient.prototype.getExpectedResponse = function (override) { return override.response || this.config.response || RESPONSE.CREATED; }; BatchTestClient.prototype.createJob = function (job, override, callback) { if (!callback) { callback = override; override = {}; } if (!this.isReady()) { this.pendingJobs.push({ job: job, override: override || {}, callback: callback }); return debug('Waiting for Batch service to be ready'); } assert.response( this.server, { url: this.getUrl(override), headers: { host: this.getHost(override), 'Content-Type': 'application/json', authorization: this.getAuthorization(override) }, method: 'POST', data: JSON.stringify(job) }, this.getExpectedResponse(override), function (err, res) { if (err) { return callback(err); } if (res.statusCode < 400) { return callback(null, new JobResult(JSON.parse(res.body), this, override), res); } else { return callback(null, res); } }.bind(this) ); }; BatchTestClient.prototype.getJobStatus = function (jobId, override, callback) { assert.response( this.server, { url: this.getUrl(override, jobId), headers: { host: this.getHost(override), authorization: this.getAuthorization(override) }, method: 'GET', timeout: override.timeout }, RESPONSE.OK, function (err, res) { if (err) { return callback(err); } return callback(null, JSON.parse(res.body)); } ); }; BatchTestClient.prototype.getWorkInProgressJobs = function (override, callback) { if (!callback) { callback = override; override = {}; } assert.response( this.server, { url: '/api/v1/jobs-wip', headers: { host: this.getHost(override) }, method: 'GET' }, RESPONSE.OK, function (err, res) { if (err) { return callback(err); } return callback(null, JSON.parse(res.body)); } ); }; BatchTestClient.prototype.cancelJob = function (jobId, override, callback) { assert.response( this.server, { url: this.getUrl(override, jobId), headers: { host: this.getHost(override) }, method: 'DELETE' }, override.statusCode, function (err, res) { if (err) { return callback(err); } return callback(null, JSON.parse(res.body)); } ); }; BatchTestClient.prototype.drain = function (callback) { this.batch.stop(function () { return redisUtils.clean('batch:*', callback); }); }; BatchTestClient.prototype.getHost = function (override) { return override.host || this.config.host || 'vizzuality.cartodb.com'; }; BatchTestClient.prototype.getAuthorization = function (override) { const auth = override.authorization || this.config.authorization; if (auth) { return `Basic ${Buffer.from(auth).toString('base64')}`; } }; BatchTestClient.prototype.getUrl = function (override, jobId) { var urlParts = ['/api/v2/sql/job']; if (jobId) { urlParts.push(jobId); } return `${urlParts.join('/')}${override.anonymous ? '' : '?api_key=' + this.getApiKey(override)}`; }; BatchTestClient.prototype.getApiKey = function (override) { return override.apiKey || this.config.apiKey || '1234'; }; /** **************** JobResult ******************/ function JobResult (job, batchTestClient, override) { this.job = job; this.batchTestClient = batchTestClient; this.override = override; } JobResult.prototype.getStatus = function (requiredStatus, callback) { if (!callback) { callback = requiredStatus; requiredStatus = undefined; } var self = this; var attempts = 1; self.override.timeout = 1000; var interval = setInterval(function () { self.batchTestClient.getJobStatus(self.job.job_id, self.override, function (err, job) { if (err) { clearInterval(interval); return callback(err); } attempts += 1; if (attempts > 20) { clearInterval(interval); return callback(new Error('Reached maximum number of request (20) to check job status')); } if (hasRequiredStatus(job, requiredStatus)) { clearInterval(interval); self.job = job; return callback(null, job); } else { debug('Job %s [status=%s] waiting to be done', self.job.job_id, job.status); } }); }, 100); }; function hasRequiredStatus (job, requiredStatus) { if (requiredStatus) { return job.status === requiredStatus; } if (JobStatus.isFinal(job.status)) { if (job.fallback_status !== undefined) { if (JobStatus.isFinal(job.fallback_status) || job.fallback_status === JobStatus.SKIPPED) { return true; } } else { return true; } } return false; } JobResult.prototype.cancel = function (callback) { var self = this; this.override.statusCode = response(RESPONSE.OK); this.batchTestClient.cancelJob(this.job.job_id, this.override, function (err, job) { if (err) { return callback(err); } self.job = job; callback(null, job); }); }; JobResult.prototype.tryCancel = function (callback) { var self = this; this.override.statusCode = response(); this.batchTestClient.cancelJob(this.job.job_id, this.override, function (err, job) { if (err) { return callback(err); } self.job = job; callback(null, job); }); }; JobResult.prototype.validateExpectedResponse = function (expected) { var actual = this.job.query; actual.query.forEach(function (actualQuery, index) { var expectedQuery = expected.query[index]; assert.ok(expectedQuery); Object.keys(expectedQuery).forEach(function (expectedKey) { assert.strictEqual( actualQuery[expectedKey], expectedQuery[expectedKey], 'Expected value for key "' + expectedKey + '" does not match: ' + actualQuery[expectedKey] + ' ==' + expectedQuery[expectedKey] + ' at query index=' + index + '. Full response: ' + JSON.stringify(actual, null, 4) ); }); var propsToCheckDate = ['started_at', 'ended_at']; propsToCheckDate.forEach(function (propToCheckDate) { if (Object.prototype.hasOwnProperty.call(actualQuery, propToCheckDate)) { assert.ok(new Date(actualQuery[propToCheckDate])); } }); }); assert.strictEqual(actual.onsuccess, expected.onsuccess); assert.strictEqual(actual.onerror, expected.onerror); };