How can I run additional assertion after Botium Binding is completed?
const bb = new BotiumBindings({ convodirs: ['/spec'] })
BotiumBindings.helper.mocha().setupMochaTestSuite({ bb })
// TODO: GET request to an external API.
The recommended way to add your custom asserter logic to Botium is by adding your own asserter module to Botium. You can find the details here, but in short:
Create a file MyCustomAsserter.js:
module.exports = class CustomAsserter {
constructor (context, caps, globalArgs) {
this.context = context
this.caps = caps
this.globalArgs = globalArgs
}
assertConvoStep ({convo, convoStep, args, isGlobal, botMsg}) {
if (botMsg.message !== 'hugo') throw new Error('expected hugo')
return Promise.resolve()
}
}
Register it in botium.json
{
"botium": {
"Capabilities": {
...
"ASSERTERS": [
{
"ref": "MY-ASSERTER-NAME",
"src": "./MyCustomAsserter.js",
"global": false,
"args": {
"my-arg-1": "something"
}
}
]
}
}
}
Use this asserter in your convo files:
my-test-case
#me
hi
#bot
hi
MY-ASSERTER-NAME some-arg1|some-arg2
Related
I recently have spotted jscodeshift and leveraging it for refactoring.
But I can't find a way to add await before the node that I am seeking.
// AS-IS
userEvent.click(a);
await userEvent.click(b);
// TO-BE
await userEvent.click(a);
await userEvent.click(b);
This is how I query userEvent.click
const getFunctionCall = (obj:string, prop:string) => {
return source.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
object: {
name: obj
},
property: {
name:prop
}
}
})
}
const clicks = getFunctionCall('userEvent', 'click');
clicks.forEach(i=> {
if (i.parentPath.value.type !== 'AwaitExpression') {
// code goes here
}
})
How can I add await before userEvent and let the rest code stay same in this case?
I checked out the document to find out how to build a statement or expression, but it wasn't understandable for me as I've just started to use this. I would appreciate if you can introduce any good materials for this library.
Thanks in advance!
It is simple, you need to wrap the existing CallExpression inside an await expression like this:
// Press ctrl+space for code completion
export default function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
const body = root.get().value.program.body;
root.find(j.CallExpression, {
callee: {
object: {
name: "userEvent"
},
property: {
name: "click"
}
}
}).filter(path => {
return path.value.arguments[0].name === "a" ;
}).replaceWith(path => {
return j.awaitExpression(path.value)
});
return root.toSource();
}
I'm playing around learning XState and wanted to include an action in a machine that would just log the current state to console.
Defining a simple example machine like so, how would I go about this? Also note the questions in the comments in the code.
import { createMachine, interpret } from "xstate"
const sm = createMachine({
initial: 'foo',
states: {
foo: {
entry: 'logState', // Can I only reference an action by string?
// Or can I add arguments here somehow?
on: {
TOGGLE: {target: 'bar'}
}
},
bar: {
entry: 'logState',
on: {
TOGGLE: {target: 'foo'}
}
}
}
},
{
actions: {
logState(/* What arguments can go here? */) => {
// What do I do here?
}
}
});
I know that actions are called with context and event as arguments but I don't see a way to get the current state from either of those. Am I missing something here?
For a simple use case like yours, you could try recording the state on transition.
let currentState;
const service = interpret(machine).onTransition(state => {
if (state.value != currentState) {
// TODO: terminate timer if any and start a new one
currentState = state.value;
}
});
Then use the value in your actions.
See more here: https://github.com/statelyai/xstate/discussions/1294
Actions receive three arguments - context, event and meta. meta have property state, which is current state.
import { createMachine } from "xstate";
let metaDemo = createMachine(
{
id: "meta-demo",
initial: "ping",
states: {
ping: {
entry: ["logStateValues"],
after: { TIMEOUT: "pong" },
},
pong: {
entry: ["logStateValues"],
after: { TIMEOUT: "ping" },
},
},
},
{
delays: {
TIMEOUT: 3000,
},
actions: {
logStateValues(ctx, event, meta) {
if (meta.state.matches("ping")) {
console.log("It's PING!");
} else if (meta.state.matches("pong")) {
console.log("And now it's PONG");
} else {
console.log(
`This is not supposed to happen. State is: ${meta.state
.toStrings()
.join(".")}`
);
}
},
},
}
);
I'm using cognito authentication,
I create a middlware
const { email } = payload;
req.headers['user-email'] = email as string;
I want to write this kind of function
public async httpCheck(query: any, args: any, context: any,
resolveInfo: any) {
console.log('authhealth');
console.log("context "+ context.userEmail);
console.log("query : "+ query.userEmail);
(context.userEmail === query.userEmail ) ? console.log("authorized successfully") : console.log("authorization failed");
return 'OK';
}
This is my file structure, I want to write wrap resolver
From your example, it looks like you are wanting to reject the whole request if the email from the request header does not match an email being provided as an argument to a field in the GraphQL query.
So given the following query:
query MyQuery($userEmail:String!) {
userByEmail(email: $userEmail) {
id
email
familyName
givenName
}
}
If you want to check that the header email equals the email argument of userByEmail BEFORE Postgraphile executes the operation, you need to use a Postgraphile Server Plugin which adds a dynamic validation rule that implements the check:
import type { PostGraphilePlugin } from "postgraphile";
import type { ValidationRule } from "graphql";
import { GraphQLError } from "graphql";
import type { IncomingMessage } from "http";
import type { Plugin } from "graphile-build";
// Defines a graphile plugin that uses a field argument build hook to add
// metadata as an extension to the "email" argument of the "userByEmail" field
const AddEmailMatchPlugin: Plugin = (builder) => {
builder.hook(
"GraphQLObjectType:fields:field:args",
(args, build, context) => {
// access whatever data you need from the field context. The scope contains
// basically any information you might desire including the database metadata
// e.g table name, primary key.
const {
scope: { fieldName, isRootQuery },
} = context;
if (!isRootQuery && fieldName !== "userByEmail") {
return args;
}
if (args.email) {
return {
...args,
email: {
...args.email,
// add an extensions object to the email argument
// this will be accessible from the finalized GraphQLSchema object
extensions: {
// include any existing extension data
...args.email.extensions,
// this can be whatetever you want, but it's best to create
// an object using a consistent key for any
// GraphQL fields/types/args that you modify
myApp: {
matchToUserEmail: true,
},
},
},
};
}
return args;
}
);
};
// define the server plugin
const matchRequestorEmailWithEmailArgPlugin: PostGraphilePlugin = {
// this hook enables the addition of dynamic validation rules
// where we can access the underlying http request
"postgraphile:validationRules": (
rules,
context: { req: IncomingMessage; variables?: Record<string, unknown> }
) => {
const {
variables,
// get your custom user context/jwt/headers from the request object
// this example assumes you've done this in some upstream middleware
req: { reqUser },
} = context;
if (!reqUser) {
throw Error("No user found!");
}
const { email, role } = reqUser;
const vr: ValidationRule = (validationContext) => {
return {
Argument: {
// this fires when an argument node has been found in query AST
enter(node, key) {
if (typeof key === "number") {
// get the schema definition of the argument
const argDef = validationContext.getFieldDef()?.args[key];
if (argDef?.extensions?.myApp?.matchToUserEmail) {
// restrict check to a custom role
if (role === "standard") {
const nodeValueKind = node.value.kind;
let emailsMatch = false;
// basic case
if (nodeValueKind === "StringValue") {
if (node.value.value === email) {
emailsMatch = true;
}
}
// must account for the value being provided by a variable
else if (nodeValueKind === "Variable") {
const varName = node.value.name.value;
if (variables && variables[varName] === email) {
emailsMatch = true;
}
}
if (!emailsMatch) {
validationContext.reportError(
new GraphQLError(
`Field "${
validationContext.getFieldDef()?.name
}" argument "${
argDef.name
}" must match your user email.`,
node
)
);
}
}
}
}
},
},
};
};
return [...rules, vr];
},
// This hook appends the AddEmailMatchPlugin graphile plugin that
// this server plugin depends on for its custom extension.
"postgraphile:options": (options) => {
return {
...options,
appendPlugins: [...(options.appendPlugins || []), AddEmailMatchPlugin],
};
},
};
export default matchRequestorEmailWithEmailArgPlugin;
Then you need to register the server plugin in the Postgraphile middleware options:
const pluginHook = makePluginHook([MatchRequestorEmailWithEmailArgPlugin]);
const postGraphileMiddleware = postgraphile(databaseUrl, "my_schema", {
pluginHook,
// ...
});
If you just want to reject the userByEmail field in the query and don't care about rejecting before any resolution of any other parts of the request occur, you can use the makeWrapResolversPlugin to wrap the resolver and do the check there.
Does anyone know how to use the javascript-obfuscator (or similar) in Ember ?
I guess it needs to be called inside ember-cli-build.js but I don't know where and how.
Thank you in advance for any help :)
I don't think there's a really straightforward answer to this. If you're running on embroider, then all your Javascript will be handled by webpack, so you could try using webpack-obfuscator -- in your ember-cli-build.js something like
return require('#embroider/compat').compatBuild(app, Webpack, {
plugins: [
new WebpackObfuscator(/*whatever args*/)
],
rules: [
{
test: /\.js$/,
enforce: 'post',
use: {
loader: WebpackObfuscator.loader,
}
}
]
});
The other options I know of would be to write a broccoli plugin. What you're doing is analogous to what ember-cli-terser does -- post-process Javascript files before they get concatenated together -- so you could use that as reference.
You would need to write a broccoli plugin that actually performs the transformations (the analog is broccoli-terser-sourcemap) and an Ember addon to hook it into ember-cli's build pipeline (the analog is ember-cli-terser).
Broccoli plugin
Looking at broccoli-terser-sourcemap's index.js, which is only 114 lines of code, I would think you could adapt it to something like this:
module.exports = class TerserWriter extends Plugin {
constructor(_inputNodes, options = {}) {
let inputNodes = Array.isArray(_inputNodes) ? _inputNodes : [_inputNodes];
super(inputNodes, {
name: options.name,
annotation: options.annotation,
needsCache: false,
});
this.options = defaults(options, {
obfuscator: {},
});
let exclude = this.options.exclude;
if (Array.isArray(exclude)) {
this.excludes = new MatcherCollection(exclude);
} else {
this.excludes = MatchNothing;
}
}
async build() {
let pendingWork = [];
this.inputPaths.forEach(inputPath => {
walkSync(inputPath).forEach(relativePath => {
if (relativePath.slice(-1) === '/') {
return;
}
let inFile = path.join(inputPath, relativePath);
let outFile = path.join(this.outputPath, relativePath);
fs.mkdirSync(path.dirname(outFile), { recursive: true });
if (this._isJSExt(relativePath) && !this.excludes.match(relativePath)) {
pendingWork.push(() => this.processFile(inFile, outFile, relativePath, this.outputPath));
} else {
symlinkOrCopy.sync(inFile, outFile);
}
});
});
for (let fn of pendingWork) {
await fn();
}
}
_isJSExt(relativePath) {
return relativePath.slice(-3) === '.js' || relativePath.slice(-4) === '.mjs';
}
async processFile(inFile, outFile, relativePath, outDir) {
let input = await readFile(inFile).toString();
let result = obfuscate(input, this.options.obfuscator);
await writeFile(outFile, result.getObfuscatedCode());
}
};
You could also do the worker pooling this that broccoli-terser-sourcemaps does, and if you care about source maps you'd need to handle them as well, but broccoli-terser-sourcemaps does just that, so you could use it as reference.
ember-cli addon
ember-cli-terser has even less code -- looking at its index.js, you could adapt it to something like
'use strict';
module.exports = {
name: require('./package').name,
included(app) {
this._super.included.apply(this, arguments);
let defaultOptions = {
enabled: app.env === 'production',
obfuscator: {
// default `javascript-obfuscator` options
},
};
let addonOptions = app.options['ember-cli-obfuscator'];
this._obfuscatorOptions = Object.assign({}, defaultOptions, addonOptions);
},
postprocessTree(type, tree) {
if (this._obfuscatorOptions.enabled === true && type === 'all') {
// Import the plugin code above
const Obfuscator = require('./broccoli-obfuscator');
return new Obfuscator(tree, this._obfuscatorOptions);
} else {
return tree;
}
}
};
Then you'd have to install the above addon in your app (it could be an in-repo addon), and it should do its thing!
This would definitely take some doing, but what you're doing is so similar to what ember-cli-terser is doing, just using the obfuscator API instead of the terser API, that you have a really good starting point.
BUT, if embroider is an option for you, I'd definitely try that route first because it might just be a matter of configuration, rather than writing a bunch of code.
In my Webpack configuration, I am using UglifyJsPlugin, which registers a callback to "normal-module-loader", where it sets context.minimize = true. I wanted to set it to false, so I wrote another plugin to my config:
plugins: [
...
{
apply: compiler => {
console.log('apply was called');
compiler.plugin('normal-module-loader', (context) => {
console.log('callback was invoked');
context.minimize = false;
});
}
}
But the callback never gets invoked and I don't know why. When I register callback to some other event (eg. "done"), it gets invoked, only this "normal-module-loader" doesn't.
Does anyone know what I'm doing wrong?
It seems you need to access Compilation instance first.
plugins: [
...
{
apply: compiler => {
console.log('apply was called');
compiler.plugin('compilation', compilation => {
compilation.plugin('normal-module-loader', (context) => {
console.log('callback was invoked');
context.minimize = false;
});
});
}
}