UNPKG

@snowtop/ent-graphql-tests

Version:

easy ways to test ent and graphql

508 lines (507 loc) 18.6 kB
"use strict"; 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; }