Jest Twilio Mock - twilio

I would like to know how do I mock a Twilio function inside a lib.
I need help taking the test
If anyone has any ideas thanks
I have this method:
joinSession(data: ICallConfig) {
const token = data.providerData.Token.toString();
this._sessionId = data.sessionId;
this._localConnectionId = data.providerData.CustomerId;
const videoCallOptions = {
name: data.providerData.RoomName,
tracks: this._stream.getTracks(),
};
try {
this._room = await connect(token, videoCallOptions);
} catch (e) {
console.log('Twilio video error: ' + e);
}
I need to mock twilio's connect() function
My test:
describe('joinSession', () => {
test('connect user in room', () => {
// Create object
const data: ICallConfig = {
providerData: {
Token: '',
CustomerId: '',
RoomName: 'Room Test',
},
sessionId: '',
};
mockMediaStream.addTrack(new MockTrack('audio'));
mockMediaStream.addTrack(new MockTrack('video'));
twilioService.setMediaStream(mockMediaStream);
twilioService.joinSession(data);
});

Related

Loopback 4 implementing Microsoft Graph API

I am currently building a microservice that is responsible to communicate with Microsoft Graph, I have already made one with Loopback 3 and this was not a problem.
Except now, I am trying to do the same thing but with Loopback 4, but since the language changes from JavaScript to TypeScript I don't know if it's still possible to achieve this.
This was the code I used for Loopback 3 in my root server file:
'use strict';
const express = require('express');
const erouter = require('express').Router();
var session = require('express-session');
var passport = require('passport');
var OIDCStrategy = require('passport-azure-ad').OIDCStrategy;
const request = require('request');
var querystring = require('querystring');
const graph = require('./graph.service');
const getBookings = require('./getBookings.service');
const cors = require('cors');
var compression = require('compression');
module.exports = function(server) {
// Install a `/` route that returns server status
var router = server.loopback.Router();
router.get('/', server.loopback.status());
// Configure simple-oauth2
const oauth2 = require('simple-oauth2').create({
client: {
id: process.env.OAUTH_APP_ID,
secret: process.env.OAUTH_APP_PASSWORD
},
auth: {
tokenHost: process.env.OAUTH_AUTHORITY,
authorizePath: process.env.OAUTH_AUTHORIZE_ENDPOINT,
tokenPath: process.env.OAUTH_TOKEN_ENDPOINT
}
});
passport.serializeUser(function(user, done) {
var MSUser = server.models.MSUser;
var id = user.profile.oid;
MSUser.find({ where: { oid: id } }, function(err, msu) {
if (err) return done(err, null);
if (!msu) {
MSUser.create(user);
} else {
done(null, id);
}
});
});
passport.deserializeUser(function(id, done) {
var MSUser = server.models.MSUser;
MSUser.findById(id, function(err, user) {
if (err) return next(err);
done(null, user);
});
});
async function signInComplete(iss, sub, profile, accessToken, refreshToken, params, done) {
if (!profile.oid) {
return done(new Error("No OID found in user profile."), null);
}
try {
const user = await graph.getUserDetails(accessToken);
if (user) {
profile['email'] = user.mail ? user.mail.toLowerCase() : user.userPrincipalName.toLowerCase();
}
} catch (err) {
done(err, null);
}
let oauthToken = oauth2.accessToken.create(params);
var AuthUser = server.models.AuthUser;
var user = {};
AuthUser.find({ where: { email: profile['email'] } }, function(err, au) {
if (err) return done(err, null);
if (au.length != 1) return done(new Error("User was not found with that email address."), null);
user = au[0];
const dataMsAuth = querystring.stringify({
"created": new Date().toDateString(),
"token_type": oauthToken.token.token_type,
"expires_in": oauthToken.token.expires_in,
"access_token": oauthToken.token.access_token,
"scope": oauthToken.token.scope,
"ext_expires_in": oauthToken.token.ext_expires_in,
"refresh_token": oauthToken.token.refresh_token,
"id_token": oauthToken.token.id_token,
"expires_at": new Date(oauthToken.token.expires_at).toDateString()
});
const postMSAuth = {
url: process.env.API_URL + "api/Companies/" + user.companyId + "/msauth",
method: 'POST',
body: dataMsAuth,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
request(postMSAuth, function(err, resp, body) {
if (err) return done(err, null);
var MSUser = server.models.MSUser;
var id = profile.oid;
var msuser = { profile, oauthToken, oid: id, email: profile.email }
MSUser.findById(id, function(err, msu) {
if (err) return done(err, null);
if (!msu) {
MSUser.create(msuser);
}
});
return done(null, msuser);
});
});
}
passport.use(new OIDCStrategy({
identityMetadata: `${process.env.OAUTH_AUTHORITY}${process.env.OAUTH_ID_METADATA}`,
clientID: process.env.OAUTH_APP_ID,
responseType: 'code id_token',
responseMode: 'form_post',
redirectUrl: process.env.OAUTH_REDIRECT_URI,
allowHttpForRedirectUrl: true,
clientSecret: process.env.OAUTH_APP_PASSWORD,
validateIssuer: false,
passReqToCallback: false,
scope: process.env.OAUTH_SCOPES.split(' ')
},
signInComplete
));
var app = express();
app.use(compression());
app.use(session({
secret: process.env.BOOKINGS_LOOPBACK_SECRET,
resave: false,
saveUninitialized: false,
unset: 'destroy'
}));
app.use("/result", express.static('client'));
app.use(passport.initialize());
app.use(passport.session());
app.use(cors({
origin: '*'
}));
erouter.get('/API/bookings/:companyid', getBookings());
erouter.get('/auth/signin',
function(req, res, next) {
passport.authenticate('azuread-openidconnect', {
response: res,
prompt: 'login',
state: req.query.state,
failureRedirect: process.env.WEBSITE_URL + 'settings?error=incorrect_request',
successRedirect: process.env.WEBSITE_URL + 'settings?auth=success'
})(req, res, next);
}
);
erouter.post('/auth/callback',
function(req, res, next) {
passport.authenticate('azuread-openidconnect', {
response: res,
failureRedirect: process.env.WEBSITE_URL + 'settings?error=permission_denied',
successRedirect: process.env.WEBSITE_URL + 'settings?auth=success'
})(req, res, next);
}
);
app.use(erouter);
server.use(app);
server.use(router);
};
So my question is: "Is it possible to implement Microsoft Graph API in TypeScript using Loopback 4 or should I keep using Loopback 3 In JavaScript?"
Thanks in advance,
Billy Cottrell

How to get link to composition by using status callbacks twilio

Here is what I tried but it's not working.
This guy is responsible for creating a composition after the meeting is ended.
app.post('/api/endMeeting', (req, res) => {
const roomSid = req.body.roomSid;
userEmail = req.body.userEmail;
const client = require('twilio')(config.twilio.apiKey, config.twilio.apiSecret, {accountSid: config.twilio.accountSid});
client.video.rooms(roomSid).update({ status: 'completed' });
client.video.compositions.create({
roomSid: roomSid,
audioSources: '*',
videoLayout: {
grid : {
video_sources: ['*']
}
},
statusCallback: `${process.env.REACT_APP_BASE_URL}/api/getMeeting`,
statusCallbackMethod: 'POST',
format: 'mp4'
}).then(() => {
// sendRecordingEmail(composition.sid, userEmail);
res.status(200).send({
message: 'success'
});
}).catch(err => {
res.status(500).send({
message: err.message
});
});
});
And this guy will send the download link of the composition to the participant when it's available.
app.post('/api/getMeeting', (req, res) => {
if (req.query.StatusCallbackEvent === 'composition-available') {
const client = require('twilio')(config.twilio.apiKey, config.twilio.apiSecret, {accountSid: config.twilio.accountSid});
const compositionSid = req.query.CompositionSid;
const uri = "https://video.twilio.com/v1/Compositions/" + compositionSid + "/Media?Ttl=3600";
client.request({
method: "GET",
uri: uri,
}).then((response) => {
const requestUrl = request(response.data.redirect_to);
sendRecordingEmail(requestUrl, userEmail);
res.status(200).send("success");
}).catch((error) => {
res.status(500).send("Error fetching /Media resource " + error);
});
}
});
I can confirm that the composition is created exactly in the Twilio console.
But it seems the status callback guy is not working and I can see the below issue.
It seems I made mistakes in using the status callback.
Please let me know what is the problem and how I can solve this.
Thank you.
Thank you very much for #philnash's help in solving this problem.👍
I solved the above issue and I can get the download link of the composition for now.
The problem was in the status callback function and I should use req.body instead of req.query because of the status callback method. (It's POST on my code.)
Here is the code that is fixed.
app.post('/api/getMeeting', (req, res) => {
if (req.body.StatusCallbackEvent === 'composition-available') {
const client = require('twilio')(config.twilio.apiKey, config.twilio.apiSecret, {accountSid: config.twilio.accountSid});
const compositionSid = req.body.CompositionSid;
const uri = "https://video.twilio.com/v1/Compositions/" + compositionSid + "/Media?Ttl=3600";
client.request({
method: "GET",
uri: uri,
}).then((response) => {
const requestUrl = response.body.redirect_to; // Getting the redirect link that user can download composition
sendRecordingEmail(requestUrl, userEmail); // Send URL via email to the user
res.status(200).send("success");
}).catch((error) => {
res.status(500).send("Error fetching /Media resource " + error);
});
} else {
res.status(204).send('compositioin is not available');
}
});

AWS Lambda HTTPS post to Paypal IPN error

I have been trying to implement Paypal's IPN using AWS Api Gateway to get an IPN handler url. the api is integrated with a Lambda function as the "receiver".
I have tested the api gateway url using Paypal's IPN simulator.It works for the first step and I get this message "IPN was sent and the handshake was verified".
My problem is now with the next step,where I have to send the recived message back to Paypal using HTTPS post. I have tried a number of times and keep getting this error:
{
"code": "ECONNREFUSED",
"errno": "ECONNREFUSED",
"syscall": "connect",
"address": "127.0.0.1",
"port": 443
}
I really would appreciate some help in getting this to work.
I'm using node.js 8.10.Here's my Lambda function:
exports.handler = (event, context, callback) => {
console.log('Received event:', JSON.stringify(event, null, 2));
// Return 200 to caller
console.log('sending 200 back to paypal');
callback(null, {
statusCode: '200'
});
// Read the IPN message sent from PayPal and prepend 'cmd=_notify-validate'
console.log('modifying return body...');
var body = 'cmd=_notify-validate&' + event.body;
callHttps(body, context);};
function callHttps(body, context) {
console.log('in callHttp()....');
var https = require('https');
var options = {
url: 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr',
method: 'POST',
headers: {
"user-agent": "Nodejs-IPN-VerificationScript"
},
body: body
};
const req = https.request(options, (res) => {
res.on('data', (chunk) => {
// code to execute
console.log("on data - can execute code here....");
});
res.on('end', () => {
// code to execute
console.log("on end - can execute code here....");
});
});
req.on('error', (e) => {
console.log("Error has occured: ", JSON.stringify(e, null, 2));
});
req.end();}
managed to sort it out.i was using url instead of breaking it down to host and path.here's the full code that worked for me:
exports.handler = (event, context, callback) => {
console.log('Received event:', JSON.stringify(event, null, 2));
// Return 200 to caller
console.log('sending 200 back to paypal');
callback(null, {
statusCode: '200'
});
callHttps(event.body, context);};
function callHttps(body, context) {
console.log('in callHttp()....');
// Read the IPN message sent from PayPal and prepend 'cmd=_notify-validate'
console.log('modifying return body...');
var bodyModified = 'cmd=_notify-validate&' + body;
var https = require('https');
var options = {
host: "ipnpb.sandbox.paypal.com",
path: "/cgi-bin/webscr",
method: 'POST',
headers: {
'user-agent': 'Nodejs-IPN-VerificationScript',
'Content-Length': bodyModified.length,
}
};
const req = https.request(options, (res) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
var result = '';
res.on('data', (d) => {
// get the result here
result += d;
});
res.on('end', (end) => {
// check the result
if (result === 'VERIFIED') {
// process the message
// split the message
var res = body.split("&");
// create an object
var paypalMessageObject = new Object();
// loop through split array
res.forEach(element => {
// split element
var temp = (element.toString()).split("=");
// add to the object
paypalMessageObject[temp[0]] = temp[1];
});
console.log('paypalMessageObject: ' + JSON.stringify(paypalMessageObject, null, 2));
var checkItems = {
payment_status: paypalMessageObject.payment_status,
mc_gross: paypalMessageObject.mc_gross,
mc_currency: paypalMessageObject.mc_currency,
txn_id: paypalMessageObject.txn_id,
receiver_email: paypalMessageObject.receiver_email,
item_number: paypalMessageObject.item_number,
item_name: paypalMessageObject.item_name
};
console.log('checkItems: ', JSON.stringify(checkItems, null, 2));
}
else { console.log('not verified, now what?'); }
});
});
req.on('error', (e) => {
console.log("Error has occured: ", JSON.stringify(e, null, 2));
});
req.write(bodyModified);
req.end();}

sessionConfig.perform not being called

I am trying to write a session authentication mechanism for my application, which goes like that:
import { ZObject, Bundle } from "zapier-platform-core";
import IAuthenticationScheme from "../interfaces/authentication/IAuthenticationScheme";
const getSessionKey = (z: ZObject, bundle: Bundle) => {
console.log('GET SESSION called');
const { username: auth_login, password: auth_password } = bundle.authData;
return z.request({
method: 'POST',
url: 'http://******/perl/auth/login',
body: { auth_login, auth_password }
}).then(response => {
z.console.log(response);
console.log(response);
if (response.status === 401) {
throw new Error('The username/password you supplied is invalid');
} else {
return {
sessionKey: z.JSON.parse(response.content).session_id
};
}
});
};
const includeSessionKeyHeader = (request: any, z: ZObject, bundle: Bundle) => {
console.log('includeSessionKeyHeader called');
if (bundle.authData.sessionKey) {
request.headers = Object.assign({}, request.headers);
let { Cookie: cookie = '' } = request.headers;
cookie = `${bundle.authData.sessionKey};${cookie}`;
request.headers['Cookie'] = cookie;
}
return request;
};
const sessionRefreshIf401 = (response: any, z: ZObject, bundle: Bundle) => {
console.warn('sessionRefreshIf401 called');
if (bundle.authData.sessionKey) {
if (response.status === 401) {
throw new z.errors.RefreshAuthError(); // ask for a refresh & retry
}
}
return response;
};
const test = (z: ZObject, bundle: Bundle) => {
console.log('test called');
return z.request({
url: 'http://******/ruby/features'
}).then((response) => {
z.console.log(response);
if (response.status === 401) {
throw new Error('The API Key you supplied is invalid');
}
return response
});
};
const authentication: IAuthenticationScheme<any> = {
type: 'session',
test,
fields: [
{
key: 'username',
type: 'string',
required: true,
helpText: 'Your login username.'
},
{
key: 'password',
type: 'string',
required: true,
helpText: 'Your login password.'
}
],
connectionLabel: (z, bundle) => {
return bundle.inputData.username;
},
sessionConfig: {
perform: getSessionKey
}
};
export default {
authentication,
beforeRequest: { includeSessionKeyHeader },
afterRequest: { sessionRefreshIf401 }
};
As you can see, I put console.log markers at the beginning of each function here so I can see in which order they are getting called.
Here is my test configuration:
import { should } from "should";
import { describe } from "mocha";
const { version } = require("../../package.json");
import { version as platformVersion } from "zapier-platform-core";
import { createAppTester } from "zapier-platform-core";
import PlackSession from "../authentication/PlackSession";
const App = {
version,
platformVersion,
authentication: PlackSession.authentication,
beforeRequest: [PlackSession.beforeRequest.includeSessionKeyHeader],
afterResponse: [PlackSession.afterRequest.sessionRefreshIf401],
};
const appTester = createAppTester(App);
export default () => {
describe('PlackSession authentication', () => {
it('should authenticate', done => {
console.log(`AUTHENTICATE!!`)
const bundle = {
authData: {
username: 'dev#******.com',
password: 'abc123'
}
};
appTester(App.authentication.test, bundle)
.then(response => {
console.log('BBBBBBBB')
done();
})
.catch(a => {
console.log('CCCCCC');
done(a)
});
});
});
};
And I can see the test output the logs in the following order:
authentication
PlackSession authentication
AUTHENTICATE!!
test called
includeSessionKeyHeader called
CCCCCC
1) should authenticate
That means sessionConfig.perform (getSessionKey) is never called, and this is where the credentials should be exchanged for authentication through the login API call, which I can also see in my server logs it never gets called, it skips straight to the test call and fails.
David here, from the Zapier Platform team. Great question!
I think the problem lies in your test. There should be two function. One should call App.authentication.sessionConfig.perform and tests exchanging username & password for a token. Another should call App.authentication.test, which tests fetching a protected resource with a valid key. Though these may be able to be chained together, they can also be written separately.
There's a more complete example here: https://github.com/zapier/zapier-platform-example-app-session-auth/blob/cc63ca67cbc8933439577b2362d026ba2a701e36/test/basic.js

Can I run `stencil push` command without prompt?

I'm trying to configure bitbucket pipelines with bigcommerce stencil.
The problem is the stencil push command asks some questions. I would like to auto-respond those questions.
Is that possible?
These are the questions prompted:
* Would you like to apply your theme to your store at http://xxxxxxx/? (y/N)
* Which variation would you like to apply?
- Light
- Bold
- Warm
You will need to make changes to the existing stencil-cli to make this work.
Stencil-cli uses the Commander package. My solution was to create an additional flag that would skip all the prompts if you supplied a variant name. This was created from stencil-cli version 1.13.1 so you may need to modify the example.
Inside /bin/stencil-push:
#!/usr/bin/env node
require('colors');
const apiHost = 'https://api.bigcommerce.com';
const dotStencilFilePath = './.stencil';
const options = { dotStencilFilePath };
const pkg = require('../package.json');
const Program = require('commander');
const stencilPush = require('../lib/stencil-push');
const versionCheck = require('../lib/version-check');
Program
.version(pkg.version)
.option('--host [hostname]', 'specify the api host', apiHost)
.option('-f, --file [filename]', 'specify the filename of the bundle to upload')
.option('-a, --activate [variationname]', 'specify the variation of the theme to activate')
.parse(process.argv);
if (!versionCheck()) {
return;
}
stencilPush(Object.assign({}, options, {
apiHost: Program.host || apiHost,
bundleZipPath: Program.file,
activate: Program.activate,
}), (err, result) => {
if (err) {
console.log('not ok'.red + ` -- ${err}`);
console.log('Please try again. If this error persists, please visit https://github.com/bigcommerce/stencil-cli/issues and submit an issue.');
} else {
console.log('ok'.green + ` -- ${result}`);
}
});
Inside /lib/stencil-push.js:
'use strict';
const _ = require('lodash');
const async = require('async');
const Bundle = require('./stencil-bundle');
const fs = require('fs');
const Inquirer = require('inquirer');
const os = require('os');
const ProgressBar = require('progress');
const themeApiClient = require('./theme-api-client');
const themePath = process.cwd();
const themeConfig = require('./theme-config').getInstance(themePath);
const uuid = require('uuid4');
const utils = {};
const Wreck = require('wreck');
const bar = new ProgressBar('Processing [:bar] :percent; ETA: :etas', {
complete: '=',
incomplete: ' ',
total: 100,
});
module.exports = utils;
function validateOptions(options, fields) {
options = options || {};
fields = fields || [];
fields.forEach(field => {
if (!_.has(options, field)) {
throw new Error(`${field} is required!`);
}
});
return options;
}
utils.readStencilConfigFile = (options, callback) => {
options = validateOptions(options, ['dotStencilFilePath']);
fs.readFile(options.dotStencilFilePath, { encoding: 'utf8' }, (err, data) => {
if (err) {
err.name = 'StencilConfigReadError';
return callback(err);
}
callback(null, Object.assign({}, options, {
config: JSON.parse(data),
}));
});
};
utils.getStoreHash = (options, callback) => {
options = validateOptions(options, ['config.normalStoreUrl']);
Wreck.get(`${options.config.normalStoreUrl}/admin/oauth/info`, { json: true, rejectUnauthorized: false }, (error, response, payload) => {
if (error) {
error.name = 'StoreHashReadError';
return callback(error);
}
if (response.statusCode !== 200 || !payload.store_hash) {
const err = new Error('Failed to retrieve store hash');
err.name = 'StoreHashReadError';
return callback(err);
}
callback(null, Object.assign({}, options, { storeHash: payload.store_hash }));
});
};
utils.getThemes = (options, callback) => {
const config = options.config;
themeApiClient.getThemes({
accessToken: config.accessToken,
apiHost: options.apiHost,
clientId: config.clientId,
storeHash: options.storeHash,
}, (error, result) => {
if (error) {
return callback(error);
}
callback(null, Object.assign({}, options, {
themes: result.themes,
}));
});
};
utils.generateBundle = (options, callback) => {
let bundle;
if (options.bundleZipPath) {
return async.nextTick(callback.bind(null, null, options));
}
bundle = new Bundle(themePath, themeConfig, themeConfig.getRawConfig(), {
dest: os.tmpdir(),
name: uuid(),
});
bundle.initBundle((err, bundleZipPath) => {
if (err) {
err.name = 'BundleInitError';
return callback(err);
}
callback(null, Object.assign(options, { bundleZipPath: options.bundleZipPath || bundleZipPath }));
});
};
utils.uploadBundle = (options, callback) => {
const config = options.config;
themeApiClient.postTheme({
accessToken: config.accessToken,
apiHost: options.apiHost,
bundleZipPath: options.bundleZipPath,
clientId: config.clientId,
storeHash: options.storeHash,
}, (error, result) => {
if (error) {
error.name = 'ThemeUploadError';
return callback(error);
}
callback(null, Object.assign({}, options, {
jobId: result.jobId,
themeLimitReached: !!result.themeLimitReached,
}));
});
};
utils.notifyUserOfThemeLimitReachedIfNecessary = (options, callback) => {
if (options.themeLimitReached) {
console.log('warning'.yellow + ` -- You have reached your upload limit. In order to proceed, you'll need to delete at least one theme.`);
}
return async.nextTick(callback.bind(null, null, options));
};
utils.promptUserToDeleteThemesIfNecessary = (options, callback) => {
if (!options.themeLimitReached) {
return async.nextTick(callback.bind(null, null, options));
}
const questions = [{
choices: options.themes.map(theme => ({
disabled: theme.is_active || !theme.is_private,
name: theme.name,
value: theme.uuid,
})),
message: 'Which theme(s) would you like to delete?',
name: 'themeIdsToDelete',
type: 'checkbox',
validate: (val) => {
if (val.length > 0) {
return true;
} else {
return 'You must delete at least one theme';
}
},
}];
Inquirer.prompt(questions, (answers) => {
callback(null, Object.assign({}, options, answers));
});
};
utils.deleteThemesIfNecessary = (options, callback) => {
const config = options.config;
if (!options.themeLimitReached) {
return async.nextTick(callback.bind(null, null, options));
}
async.parallel(options.themeIdsToDelete.map(themeId => {
return cb => {
themeApiClient.deleteThemeById(Object.assign({
accessToken: config.accessToken,
apiHost: options.apiHost,
clientId: config.clientId,
storeHash: options.storeHash,
themeId,
}, options), cb);
}
}), err => {
if (err) {
err.name = 'ThemeDeletionError';
return callback(err);
}
callback(null, options);
})
};
utils.uploadBundleAgainIfNecessary = (options, callback) => {
if (!options.themeLimitReached) {
return async.nextTick(callback.bind(null, null, options));
}
utils.uploadBundle(options, callback);
};
utils.notifyUserOfThemeUploadCompletion = (options, callback) => {
console.log('ok'.green + ' -- Theme Upload Finished');
return async.nextTick(callback.bind(null, null, options));
};
utils.markJobProgressPercentage = percentComplete => {
bar.update(percentComplete / 100);
};
utils.markJobComplete = () => {
utils.markJobProgressPercentage(100);
console.log('ok'.green + ' -- Theme Processing Finished');
};
utils.pollForJobCompletion = () => {
return async.retryable({
interval: 1000,
errorFilter: err => {
if (err.name === "JobCompletionStatusCheckPendingError") {
utils.markJobProgressPercentage(err.message);
return true;
}
return false;
},
times: Number.POSITIVE_INFINITY,
}, utils.checkIfJobIsComplete);
};
utils.checkIfJobIsComplete = (options, callback) => {
const config = options.config;
themeApiClient.getJob({
accessToken: config.accessToken,
apiHost: options.apiHost,
clientId: config.clientId,
storeHash: options.storeHash,
bundleZipPath: options.bundleZipPath,
jobId: options.jobId,
}, (error, result) => {
if (error) {
return callback(error);
}
utils.markJobComplete();
callback(null, Object.assign({}, options, result));
});
};
utils.promptUserWhetherToApplyTheme = (options, callback) => {
if (options.activate) {
callback(null, Object.assign({}, options, { applyTheme: true }));
} else {
const questions = [{
type: 'confirm',
name: 'applyTheme',
message: `Would you like to apply your theme to your store at ${options.config.normalStoreUrl}?`,
default: false,
}];
Inquirer.prompt(questions, answers => {
callback(null, Object.assign({}, options, { applyTheme: answers.applyTheme }));
});
};
};
utils.getVariations = (options, callback) => {
if (!options.applyTheme) {
return async.nextTick(callback.bind(null, null, options));
}
themeApiClient.getVariationsByThemeId({
accessToken: options.accessToken,
apiHost: options.apiHost,
clientId: options.clientId,
themeId: options.themeId,
storeHash: options.storeHash,
}, (error, result) => {
if (error) {
return callback(error);
};
if (options.activate !== true && options.activate !== undefined) {
const findVariation = result.variations.find(item => item.name === options.activate);
callback(null, Object.assign({}, options, { variationId: findVariation.uuid }));
} else if (options.activate === true) {
callback(null, Object.assign({}, options, { variationId: result.variations[0].uuid }));
} else {
callback(null, Object.assign({}, options, result));
};
});
};
utils.promptUserForVariation = (options, callback) => {
if (!options.applyTheme) {
return async.nextTick(callback.bind(null, null, options))
}
if (options.variationId) {
callback(null, options);
} else {
const questions = [{
type: 'list',
name: 'variationId',
message: 'Which variation would you like to apply?',
choices: options.variations.map(variation => ({ name: variation.name, value: variation.uuid })),
}];
Inquirer.prompt(questions, answers => {
console.log(answers);
callback(null, Object.assign({}, options, answers));
});
};
};
utils.requestToApplyVariationWithRetrys = () => {
return async.retryable({
interval: 1000,
errorFilter: err => {
if (err.name === "VariationActivationTimeoutError") {
console.log('warning'.yellow + ` -- Theme Activation Timed Out; Retrying...`);
return true;
}
return false;
},
times: 3,
}, utils.requestToApplyVariation);
};
utils.requestToApplyVariation = (options, callback) => {
if (!options.applyTheme) {
return async.nextTick(callback.bind(null, null, options));
}
themeApiClient.activateThemeByVariationId({
accessToken: options.accessToken,
apiHost: options.apiHost,
clientId: options.clientId,
storeHash: options.storeHash,
variationId: options.variationId,
}, (error, result) => {
if (error) {
return callback(error);
}
callback(null, Object.assign({}, options, result));
});
};
utils.notifyUserOfCompletion = (options, callback) => {
callback(null, 'Stencil Push Finished');
};
This allowed me to use something like stencil push --activate bold to specify a variation and skip all of the prompts.
As of version 1.15.1 this seems to be available with the -a, --activate [variationname] switch for stencil push
> stencil push -a "My Variant" worked for me
Thanks Nikita Puza!
It works like a charm. I applied the changes on stencil 1.14.1 version and the source code looks exactly the same.
The only difference is the second file is called stencil-push.utils.js instead of stencil-push.js

Resources