@snowtop/ent-graphql-tests
Version:
easy ways to test ent and graphql
508 lines (507 loc) • 18.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.expectMutation = exports.expectQueryFromRoot = void 0;
const express_1 = __importDefault(require("express"));
const graphql_helix_1 = require("graphql-helix");
const graphql_1 = require("graphql");
const auth_1 = require("@snowtop/ent/auth");
const supertest_1 = __importDefault(require("supertest"));
const fs = __importStar(require("fs"));
const util_1 = require("util");
// TODO need to make it obvious that jest-expect-message is a peer?dependency and setupFilesAfterEnv is a requirement to use this
// or change the usage here.
function server(config) {
const viewer = config.viewer;
if (viewer) {
(0, auth_1.registerAuthHandler)("viewer", {
authViewer: async (_context) => {
// TODO we want to use Context here in tests to get caching etc
return viewer;
},
});
}
let app = (0, express_1.default)();
if (config.init) {
config.init(app);
}
app.use(express_1.default.json());
let handlers = config.customHandlers || [];
handlers.push(async (req, res) => {
const { operationName, query, variables } = (0, graphql_helix_1.getGraphQLParameters)(req);
const result = await (0, graphql_helix_1.processRequest)({
operationName,
query,
variables,
request: req,
schema: config.schema,
contextFactory: async (executionContext) => {
return (0, auth_1.buildContext)(req, res);
},
});
await (0, graphql_helix_1.sendResult)(result, res);
});
app.use(config.graphQLPath || "/graphql", ...handlers);
return app;
}
function getInnerType(typ, list) {
if ((0, graphql_1.isWrappingType)(typ)) {
if (typ instanceof graphql_1.GraphQLList) {
return getInnerType(typ.ofType, true);
}
return getInnerType(typ.ofType, list);
}
return [typ, list];
}
function makeGraphQLRequest(config, query, fieldArgs) {
let test;
if (config.test) {
if (typeof config.test === "function") {
test = config.test(config.server ? config.server : server(config));
}
else {
test = config.test;
}
}
else {
test = (0, supertest_1.default)(config.server ? config.server : server(config));
}
let files = new Map();
// handle files
fieldArgs.forEach((fieldArg) => {
let [typ, list] = getInnerType(fieldArg.type, false);
if (typ instanceof graphql_1.GraphQLScalarType && typ.name == "Upload") {
let value = config.args[fieldArg.name];
if (list) {
expect(Array.isArray(value)).toBe(true);
// clone if we're going to make changes
value = [...value];
for (let i = 0; i < value.length; i++) {
files.set(`${fieldArg.name}.${i}`, value[i]);
value[i] = null;
}
config.args[fieldArg.name] = value;
}
else {
files.set(fieldArg.name, value);
config.args[fieldArg.name] = null;
}
}
});
let variables = {
...config.args,
};
for (const k in config.extraVariables) {
variables[k] = config.extraVariables[k].value;
}
if (files.size) {
let ret = test
.post(config.graphQLPath || "/graphql")
.set(config.headers || {});
ret.field("operations", JSON.stringify({
query: query,
variables: variables,
}));
let m = {};
let idx = 0;
for (const [key] of files) {
m[idx] = [`variables.${key}`];
idx++;
}
ret.field("map", JSON.stringify(m));
idx = 0;
for (let [key, val] of files) {
if (typeof val === "string") {
val = fs.createReadStream(val);
}
ret.attach(`${idx}`, val, key);
idx++;
}
return [test, ret];
}
else {
return [
test,
test
.post(config.graphQLPath || "/graphql")
.set(config.headers || {})
.send({
query: query,
variables: JSON.stringify(variables),
}),
];
}
}
function buildTreeFromQueryPaths(schema, fieldType, ...options) {
let fields;
const [typ] = getInnerType(fieldType, false);
if (typ instanceof graphql_1.GraphQLObjectType) {
fields = typ.getFields();
}
let topLevelTree = {};
options.forEach((option) => {
let path = option[0];
let parts = [];
let match = fragmentRegex.exec(path);
if (match) {
// fragment, keep the part of the fragment e.g. `...on User`, and then split the rest....
parts = [match[0], ...match[2].split(".")];
const typ = schema.getType(match[1]);
if (!typ) {
throw new Error(`can't find type for ${match[1]} in schema`);
}
if (typ instanceof graphql_1.GraphQLObjectType) {
fields = typ.getFields();
}
}
else {
parts = splitPath(path);
}
let tree = topLevelTree;
for (let i = 0; i < parts.length; i++) {
let part = parts[i];
// a list, remove the index in the list building part
let idx = part.indexOf("[");
if (idx !== -1) {
part = part.substr(0, idx);
}
// if this part of the tree doesn't exist, put an empty node there
if (tree[part] === undefined) {
tree[part] = {};
}
if (part !== "") {
tree = tree[part];
}
// TODO this needs to be aware of paths etc so this part works for complicated
// cases but inlineFragmentRoot is a workaround for now.
function handleSubtree(obj, tree, parts) {
let parts2 = [...parts];
if (Array.isArray(obj)) {
for (const obj2 of obj) {
handleSubtree(obj2, tree, parts2);
}
return;
}
for (const key in obj) {
if (tree[key] === undefined) {
tree[key] = {};
}
if (typeof obj[key] === "object") {
let parts2 = [...parts, key];
if (!scalarFieldAtLeaf(parts2)) {
handleSubtree(obj[key], tree[key], parts2);
}
}
}
}
function scalarFieldAtLeaf(pathFromRoot) {
let root = fields;
if (!root) {
return false;
}
let subField;
for (const p of pathFromRoot) {
subField = root === null || root === void 0 ? void 0 : root[p];
if (subField) {
[subField] = getInnerType(subField.type, false);
if (subField instanceof graphql_1.GraphQLObjectType) {
root = subField.getFields();
}
}
}
if (!subField) {
return false;
}
return (0, graphql_1.isScalarType)(subField) || (0, graphql_1.isEnumType)(subField);
}
if (i === parts.length - 1 && typeof option[1] === "object") {
if (!scalarFieldAtLeaf(parts)) {
handleSubtree(option[1], tree, parts);
}
}
}
});
return topLevelTree;
}
function constructQueryFromTreePath(treePath) {
let query = [];
for (let key in treePath) {
let value = treePath[key];
let valueStr = constructQueryFromTreePath(value);
if (!valueStr) {
// leaf node
query.push(key);
}
else {
query.push(`${key}{${valueStr}}`);
}
}
return query.join(",");
}
function expectQueryResult(schema, fieldType, ...options) {
let topLevelTree = buildTreeFromQueryPaths(schema, fieldType, ...options);
// console.log(topLevelTree);
let query = constructQueryFromTreePath(topLevelTree);
// console.log(query);
return query;
}
async function expectQueryFromRoot(config, ...options // TODO queries? expected values
) {
return await expectFromRoot({
...config,
queryPrefix: "query",
querySuffix: "Query",
queryFN: config.schema.getQueryType(),
}, ...options);
}
exports.expectQueryFromRoot = expectQueryFromRoot;
async function expectMutation(config, ...options) {
// wrap args in input because we simplify the args for mutations
// and don't require the input
let args = config.args;
if (!config.disableInputWrapping) {
args = {
input: args,
};
}
return await expectFromRoot({
...config,
args: args,
root: config.mutation,
queryPrefix: "mutation",
querySuffix: "Mutation",
queryFN: config.schema.getMutationType(),
}, ...options);
}
exports.expectMutation = expectMutation;
const fragmentRegex = /^...on (\w+)(.*)/;
function splitPath(path) {
// handle fragment queries. we don't want to compare against this when checking the result
// but we'll make sure to send to server
const match = fragmentRegex.exec(path);
if (!match) {
return path.split(".");
}
return match[2].split(".");
}
async function expectFromRoot(config, ...options) {
let query = config.queryFN;
let fields = query === null || query === void 0 ? void 0 : query.getFields();
if (!fields) {
// TODO custom error?
throw new Error("schema doesn't have query or fields");
}
let field = fields[config.root];
if (!field) {
throw new Error(`could not find field ${config.root} in GraphQL query schema`);
}
let fieldArgs = field.args;
let queryParams = [];
fieldArgs.forEach((fieldArg) => {
const arg = config.args[fieldArg.name];
// let the graphql runtime handle this (it may be optional for example)
if (arg === undefined) {
return;
}
queryParams.push(`$${fieldArg.name}: ${fieldArg.type}`);
});
// add extra variables in queryArgs...
for (const key in config.extraVariables) {
const v = config.extraVariables[key];
queryParams.push(`$${key}: ${v.graphqlType}`);
}
let params = [];
for (let key in config.args) {
params.push(`${key}: $${key}`);
}
let fieldType = field.type;
if (config.inlineFragmentRoot) {
const rootType = config.schema.getType(config.inlineFragmentRoot);
if (!rootType) {
throw new Error(`couldn't find inline fragment root ${config.inlineFragmentRoot} in the schema`);
}
fieldType = rootType;
}
let q = expectQueryResult(config.schema, fieldType, ...options);
let queryVar = "";
let callVar = "";
if (queryParams.length) {
queryVar = `(${queryParams.join(",")})`;
}
if (params.length) {
callVar = `(${params.join(",")})`;
}
let suffix = "";
if (q) {
// if no suffix part of query, don't put it there
suffix = `{${q}}`;
}
if (config.inlineFragmentRoot) {
suffix = `{...on ${config.inlineFragmentRoot}${suffix}}`;
}
q = `${config.queryPrefix} ${config.root}${config.querySuffix} ${queryVar} {
${config.root}${callVar} ${suffix}
}`;
if (config.debugMode) {
console.log(q);
}
let [st, temp] = makeGraphQLRequest(config, q, fieldArgs);
const res = await temp.expect("Content-Type", /json/);
if (config.debugMode) {
console.log((0, util_1.inspect)(res.body, false, 3));
}
// if there's a callback, let everything be done there and we're done
if (config.callback) {
config.callback(res);
return st;
}
if (config.expectedStatus !== undefined) {
expect(res.status).toBe(config.expectedStatus);
}
else {
expect(res.ok, `expected ok response. instead got ${res.status} and result ${JSON.stringify(res.body)}`);
}
// res.ok = true in graphql-helix when there's errors...
// res.ok = false in express-graphql when there's errors...
if (!res.ok || (res.body.errors && res.body.errors.length > 0)) {
let errors = res.body.errors;
expect(errors.length).toBeGreaterThan(0);
if (config.expectedError) {
// todo multiple errors etc
expect(errors[0].message).toMatch(config.expectedError);
}
else {
throw new Error(`unhandled error ${JSON.stringify(errors)}`);
}
return st;
}
let data = res.body.data;
let result = data[config.root];
if (config.rootQueryNull) {
expect(result, "root query wasn't null").toBe(null);
return st;
}
// special case. TODO needs to be handled better
if (options.length === 1) {
const parts = splitPath(options[0][0]);
if (parts.length == 1 && parts[0] === "") {
expect(options[0][1]).toStrictEqual(result);
return st;
}
}
await Promise.all(options.map(async (option) => {
const path = option[0];
const expected = option[1];
const alias = option[2];
let nullPath;
let nullParts = [];
let undefinedPath;
let undefinedParts = [];
if (config.nullQueryPaths) {
for (let i = 0; i < config.nullQueryPaths.length; i++) {
if (path.startsWith(config.nullQueryPaths[i])) {
nullPath = config.nullQueryPaths[i];
nullParts = splitPath(nullPath);
break;
}
}
}
if (config.undefinedQueryPaths) {
for (let i = 0; i < config.undefinedQueryPaths.length; i++) {
if (path.startsWith(config.undefinedQueryPaths[i])) {
undefinedPath = config.undefinedQueryPaths[i];
undefinedParts = splitPath(undefinedPath);
break;
}
}
}
let parts = splitPath(alias !== null && alias !== void 0 ? alias : path);
let current = result;
// possible to make this smarter and better
// e.g. when building up the tree above
for (let i = 0; i < parts.length; i++) {
let part = parts[i];
let idx = part.indexOf("[");
let listIdx;
// list
if (idx !== -1) {
let endIdx = part.indexOf("]");
if (endIdx === -1) {
throw new Error("can't have a beginning index without an end index");
}
// get the idx we care about
listIdx = parseInt(part.substr(idx + 1, endIdx - idx), 10);
// update part
part = part.substr(0, idx);
}
idx = part.indexOf("(");
// function or arg call
if (idx !== -1) {
let endIdx = part.indexOf(")");
if (endIdx === -1) {
throw new Error("can't have a beginning index without an end index");
}
// update part
part = part.substr(0, idx);
}
// "" as root is ok.
if (part !== "") {
current = current[part];
}
if (listIdx !== undefined && (nullPath === null || nullPath === void 0 ? void 0 : nullPath.indexOf("[")) !== -1) {
current = current[listIdx];
}
// at the part of the path where it's expected to be null, confirm it is before proceeding
if (nullParts.length === i + 1) {
expect(current, `path ${nullPath} expected to be null`).toBe(null);
return st;
}
if (undefinedParts.length === i + 1) {
expect(current, `path ${undefinedPath} expected to be undefined`).toBe(undefined);
return st;
}
if (listIdx !== undefined && (nullPath === null || nullPath === void 0 ? void 0 : nullPath.indexOf("[")) === -1) {
current = current[listIdx];
}
if (i === parts.length - 1) {
// leaf node, check the value
if (typeof expected === "function") {
// TODO eventually may need to batch this but fine for now
await expected(current);
}
else {
expect(current, `value of ${part} in path ${path} was not as expected`).toStrictEqual(expected);
}
}
else {
expect(part, `found undefined node in path ${path} at subtree ${part}`).not.toBe(undefined);
}
}
}));
return st;
}