diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 0adaf20c79..810c2d3112 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -43,9 +43,15 @@ export async function expectMetricsResult(expectedMetricsResult: Partial) { const actualMetricsResult = await readMutationTestResult(); + const actualMetrics: Partial = {}; Object.entries(expectedMetrics).forEach(([key, value]) => { - expect(actualMetricsResult.metrics).property(key, value); + if (key === 'mutationScore' || key === 'mutationScoreBasedOnCoveredCode') { + actualMetrics[key] = parseFloat(actualMetricsResult.metrics[key].toFixed(2)); + } else { + actualMetrics[key as keyof Metrics] = value; + } }); + expect(actualMetrics).deep.eq(expectedMetrics); } export function produceMetrics(metrics: Partial): Metrics { diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 1245837e8d..84926f1905 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -2868,6 +2868,15 @@ "node-releases": "^1.1.44" } }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, "bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -8640,6 +8649,12 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -11810,6 +11825,92 @@ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "dev": true }, + "ts-jest": { + "version": "25.4.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.4.0.tgz", + "integrity": "sha512-+0ZrksdaquxGUBwSdTIcdX7VXdwLIlSRsyjivVA9gcO+Cvr6ByqDhu/mi5+HCcb6cMkiQp5xZ8qRO7/eCqLeyw==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "micromatch": "4.x", + "mkdirp": "1.x", + "resolve": "1.x", + "semver": "6.x", + "yargs-parser": "18.x" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "ts-node": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", diff --git a/e2e/package.json b/e2e/package.json index 42c2e76294..8e45e2a1d2 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -27,6 +27,7 @@ "mutation-testing-metrics": "~1.3.0", "rxjs": "~6.5.3", "semver": "~6.3.0", + "ts-jest": "^25.4.0", "ts-node": "~7.0.0", "typescript": "~3.7.2", "webpack": "~4.41.2" diff --git a/e2e/test/jest-with-ts/jest.config.js b/e2e/test/jest-with-ts/jest.config.js new file mode 100644 index 0000000000..beac019525 --- /dev/null +++ b/e2e/test/jest-with-ts/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + + // Example pulled from: https://kulshekhar.github.io/ts-jest/user/config/#example + moduleNameMapper: { + '^@App/(.*)$': '/src/$1', + '^lib/(.*)$': '/common/$1' + } +}; diff --git a/e2e/test/jest-with-ts/package-lock.json b/e2e/test/jest-with-ts/package-lock.json new file mode 100644 index 0000000000..63bbf16f22 --- /dev/null +++ b/e2e/test/jest-with-ts/package-lock.json @@ -0,0 +1,647 @@ +{ + "name": "jest-with-ts", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.1.tgz", + "integrity": "sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA==", + "dev": true, + "requires": { + "jest-diff": "^25.2.1", + "pretty-format": "^25.2.1" + } + }, + "@types/yargs": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz", + "integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "dev": true + }, + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + }, + "azure-storage": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.10.3.tgz", + "integrity": "sha512-IGLs5Xj6kO8Ii90KerQrrwuJKexLgSwYC4oLWmc11mzKe7Jt2E5IVg+ZQ8K53YWZACtVTMBNO3iGuA+4ipjJxQ==", + "requires": { + "browserify-mime": "~1.2.9", + "extend": "^3.0.2", + "json-edm-parser": "0.1.2", + "md5.js": "1.3.4", + "readable-stream": "~2.0.0", + "request": "^2.86.0", + "underscore": "~1.8.3", + "uuid": "^3.0.0", + "validator": "~9.4.1", + "xml2js": "0.2.8", + "xmlbuilder": "^9.0.7" + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "browserify-mime": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", + "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff-sequences": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + } + }, + "jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", + "dev": true + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-edm-parser": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz", + "integrity": "sha1-HmCw/vG8CvZ7wNFG393lSGzWFbQ=", + "requires": { + "jsonparse": "~1.2.0" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonparse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", + "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "validator": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", + "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "xml2js": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz", + "integrity": "sha1-m4FpCTFjH/CdGVdUn69U9PmAs8I=", + "requires": { + "sax": "0.5.x" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + } + } +} diff --git a/e2e/test/jest-with-ts/package.json b/e2e/test/jest-with-ts/package.json new file mode 100644 index 0000000000..e987db70fb --- /dev/null +++ b/e2e/test/jest-with-ts/package.json @@ -0,0 +1,14 @@ +{ + "name": "jest-with-ts", + "description": "A test project for using stryker with jest and jest-ts preprocessor. Inspired by the stryker-dashboard source code.", + "scripts": { + "test": "stryker run", + "posttest": "mocha --require ../../tasks/ts-node-register.js verify/verify.ts" + }, + "dependencies": { + "azure-storage": "^2.10.3" + }, + "devDependencies": { + "@types/jest": "^25.2.1" + } +} diff --git a/e2e/test/jest-with-ts/src/data-access/errors/OptimisticConcurrencyError.ts b/e2e/test/jest-with-ts/src/data-access/errors/OptimisticConcurrencyError.ts new file mode 100644 index 0000000000..cc428de6d0 --- /dev/null +++ b/e2e/test/jest-with-ts/src/data-access/errors/OptimisticConcurrencyError.ts @@ -0,0 +1,2 @@ +export class OptimisticConcurrencyError extends Error { +} diff --git a/e2e/test/jest-with-ts/src/data-access/errors/index.ts b/e2e/test/jest-with-ts/src/data-access/errors/index.ts new file mode 100644 index 0000000000..20ab2646a4 --- /dev/null +++ b/e2e/test/jest-with-ts/src/data-access/errors/index.ts @@ -0,0 +1 @@ +export * from './OptimisticConcurrencyError'; diff --git a/e2e/test/jest-with-ts/src/data-access/mappers/DashboardQuery.ts b/e2e/test/jest-with-ts/src/data-access/mappers/DashboardQuery.ts new file mode 100644 index 0000000000..a0bb8ae2de --- /dev/null +++ b/e2e/test/jest-with-ts/src/data-access/mappers/DashboardQuery.ts @@ -0,0 +1,36 @@ +import { TableQuery } from 'azure-storage'; +import { ModelClass } from '../services/ModelClass'; +import { encodeKey } from '../utils'; + +interface WhereCondition { + condition: string; + params: unknown[]; +} + +export class DashboardQuery { + private constructor(protected ModelClass: ModelClass, private readonly whereConditions: WhereCondition[]) { } + + public whereRowKeyNotEquals(rowKey: Pick): DashboardQuery { + const whereCondition: WhereCondition = { condition: 'not(RowKey eq ?)', params: [encodeKey(this.ModelClass.createRowKey(rowKey) || '')] }; + return new DashboardQuery(this.ModelClass, [...this.whereConditions, whereCondition]); + } + + public wherePartitionKeyEquals(partitionKey: Pick): DashboardQuery { + const whereCondition: WhereCondition = { condition: 'PartitionKey eq ?', params: [encodeKey(this.ModelClass.createPartitionKey(partitionKey))] }; + return new DashboardQuery(this.ModelClass, [...this.whereConditions, whereCondition]); + } + + public static create(ModelClass: ModelClass): DashboardQuery { + return new DashboardQuery(ModelClass, []); + } + + public build(): TableQuery { + return this.whereConditions.reduce((tableQuery, whereCondition, index) => { + if (index === 0) { + return tableQuery.where(whereCondition.condition, ...whereCondition.params); + } else { + return tableQuery.and(whereCondition.condition, whereCondition.params); + } + }, new TableQuery()); + } +} diff --git a/e2e/test/jest-with-ts/src/data-access/mappers/Mapper.ts b/e2e/test/jest-with-ts/src/data-access/mappers/Mapper.ts new file mode 100644 index 0000000000..db8c58e191 --- /dev/null +++ b/e2e/test/jest-with-ts/src/data-access/mappers/Mapper.ts @@ -0,0 +1,12 @@ +import { DashboardQuery } from './DashboardQuery'; + +export interface Result { etag: string; model: TModel; } + +export interface Mapper { + createStorageIfNotExists(): Promise; + insertOrMerge(model: TModel): Promise; + insert(model: TModel): Promise>; + replace(model: TModel, etag: string): Promise>; + findOne(identifier: Pick): Promise | null>; + findAll(query: DashboardQuery): Promise[]>; +} diff --git a/e2e/test/jest-with-ts/src/data-access/mappers/TableStorageMapper.test.ts b/e2e/test/jest-with-ts/src/data-access/mappers/TableStorageMapper.test.ts new file mode 100644 index 0000000000..3b4a817030 --- /dev/null +++ b/e2e/test/jest-with-ts/src/data-access/mappers/TableStorageMapper.test.ts @@ -0,0 +1,191 @@ +import { Constants, TableQuery } from 'azure-storage'; +import TableStorageMapper from './TableStorageMapper'; +import * as TableServiceAsPromisedModule from '../services/TableServiceAsPromised'; +import { StorageError } from '../../../test/helpers/StorageError'; +import { DashboardQuery } from './DashboardQuery'; +import { Result } from './Mapper'; +import { OptimisticConcurrencyError } from '../errors'; + +jest.mock('../services/TableServiceAsPromised'); + +export class FooModel { + public partitionId!: string; + public rowId!: string; + public bar!: number; + + public static createPartitionKey(entity: Pick): string { + return entity.partitionId; + } + public static createRowKey(entity: Pick): string | undefined { + return entity.rowId; + } + public static identify(entity: FooModel, partitionKeyValue: string, rowKeyValue: string): void { + entity.partitionId = partitionKeyValue; + entity.rowId = rowKeyValue; + } + public static readonly persistedFields = ['bar'] as const; + public static readonly tableName = 'FooTable'; +} + +describe(TableStorageMapper.name, () => { + const TableServiceAsPromisedModuleMocked = TableServiceAsPromisedModule as typeof import('../services/__mocks__/TableServiceAsPromised'); + const TableService = TableServiceAsPromisedModuleMocked.default; + + class TestHelper { + public sut = new TableStorageMapper(FooModel); // this will mock the TableService because of Jest's black magic! + } + let helper: TestHelper; + + beforeEach(() => { + helper = new TestHelper(); + TableService.mockClear(); + }); + + describe('createTableIfNotExists', () => { + it('should create table "FooTable"', async () => { + TableServiceAsPromisedModuleMocked.createTableIfNotExistsMock.mockResolvedValueOnce({}); + await helper.sut.createStorageIfNotExists(); + expect(TableServiceAsPromisedModuleMocked.createTableIfNotExistsMock).toHaveBeenCalledWith('FooTable'); + }); + }); + + describe('insertOrMerge', () => { + it('should insert the given model', async () => { + const expected: FooModel = { + partitionId: 'github/owner', + rowId: 'name', + bar: 42 + }; + TableServiceAsPromisedModuleMocked.insertOrMergeEntityMock.mockResolvedValue({}); + await helper.sut.insertOrMerge(expected); + expect(TableServiceAsPromisedModuleMocked.insertOrMergeEntityMock).toHaveBeenCalledWith('FooTable', { + PartitionKey: 'github;owner', + RowKey: 'name', + bar: 42, + ['.metadata']: {} + }); + expect(expected.bar).toEqual(42); + }); + }); + + describe('findOne', () => { + it('should retrieve the entity from storage', async () => { + const result = createEntity(); + TableServiceAsPromisedModuleMocked.retrieveEntityMock.mockResolvedValue(result); + await helper.sut.findOne({ partitionId: 'github/partKey', rowId: 'row/key' }); + expect(TableServiceAsPromisedModuleMocked.retrieveEntityMock).toHaveBeenCalledWith('FooTable', 'github;partKey', 'row;key'); + }); + + it('should return null if it resulted in a 404', async () => { + const error = new StorageError(Constants.StorageErrorCodeStrings.RESOURCE_NOT_FOUND); + TableServiceAsPromisedModuleMocked.retrieveEntityMock.mockRejectedValue(error); + const actualProject = await helper.sut.findOne({ partitionId: 'github/partKey', rowId: 'rowKey' }); + expect(actualProject).toEqual(null); + }); + + it('should return the entity', async () => { + const expected: FooModel = { rowId: 'rowKey', partitionId: 'partKey', bar: 42 }; + TableServiceAsPromisedModuleMocked.retrieveEntityMock.mockResolvedValue(createEntity(expected, 'etagValue')); + const actualProjects = await helper.sut.findOne({ partitionId: 'github/partKey', rowId: 'rowKey' }); + expect(actualProjects).toEqual({ model: expected, etag: 'etagValue' }); + }); + }); + + describe('findAll', () => { + it('should query the underlying storage', async () => { + const expectedQuery = new TableQuery().where('PartitionKey eq ?', 'github;partKey'); + TableServiceAsPromisedModuleMocked.queryEntitiesMock.mockResolvedValue({ entries: [] }); + await helper.sut.findAll(DashboardQuery.create(FooModel) + .wherePartitionKeyEquals({ partitionId: 'github/partKey' }) + ); + expect(TableServiceAsPromisedModuleMocked.queryEntitiesMock).toHaveBeenCalledWith('FooTable', expectedQuery, undefined); + }); + + it('should return the all entities', async () => { + const expectedEntities: FooModel[] = [ + { rowId: 'rowKey', partitionId: 'partKey', bar: 142 }, + { rowId: 'rowKey2', partitionId: 'partKey2', bar: 25 } + ]; + TableServiceAsPromisedModuleMocked.queryEntitiesMock.mockResolvedValue({ entries: expectedEntities.map(entity => createEntity(entity)) }); + const actualProjects = await helper.sut.findAll(DashboardQuery.create(FooModel) + .wherePartitionKeyEquals({ partitionId: 'github/partKey' }) + ); + expect(actualProjects).toEqual(expectedEntities.map(model => ({ model, etag: 'foo-etag' }))); + }); + }); + + describe('replace', () => { + it('should replace entity with given etag', async () => { + TableServiceAsPromisedModuleMocked.replaceEntityMock.mockResolvedValue({ ['.metadata']: { etag: 'next-etag' } }); + const expected: FooModel = { bar: 42, partitionId: 'partId', rowId: 'rowId' }; + const expectedResult: Result = { model: expected, etag: 'next-etag' }; + const result = await helper.sut.replace(expected, 'prev-etag'); + expect(result).toEqual(expectedResult); + const expectedEntity = createRawEntity(expected, 'prev-etag'); + expect(TableServiceAsPromisedModuleMocked.replaceEntityMock).toHaveBeenCalledWith(FooModel.tableName, expectedEntity, {}); + }); + + it('should throw a OptimisticConcurrencyError if the UPDATE_CONDITION_NOT_SATISFIED is thrown', async () => { + TableServiceAsPromisedModuleMocked.replaceEntityMock.mockRejectedValue(new StorageError(Constants.StorageErrorCodeStrings.UPDATE_CONDITION_NOT_SATISFIED)); + await expect(helper.sut.replace({ bar: 24, partitionId: 'part', rowId: 'row' }, 'prev-etag')).rejects.toBeInstanceOf(OptimisticConcurrencyError); + }); + }); + + describe('insert', () => { + it('should insert entity', async () => { + TableServiceAsPromisedModuleMocked.insertEntityMock.mockResolvedValue({ ['.metadata']: { etag: 'next-etag' } }); + const expected: FooModel = { bar: 42, partitionId: 'partId', rowId: 'rowId' }; + const expectedResult: Result = { model: expected, etag: 'next-etag' }; + const result: Result = await helper.sut.insert(expected); + expect(result).toEqual(expectedResult); + expect(TableServiceAsPromisedModuleMocked.insertEntityMock).toHaveBeenCalledWith(FooModel.tableName, createRawEntity(expected), {}); + }); + + it('should throw an OptimisticConcurrencyError if the entity already exists', async () => { + TableServiceAsPromisedModuleMocked.insertEntityMock.mockRejectedValue(new StorageError(Constants.TableErrorCodeStrings.ENTITY_ALREADY_EXISTS)); + await expect(helper.sut.insert({ bar: 24, partitionId: 'part', rowId: 'row' })).rejects.toBeInstanceOf(OptimisticConcurrencyError); + }); + }); + + function createRawEntity(overrides?: Partial, etag?: string) { + const foo: FooModel = { + bar: 42, + partitionId: 'partKey', + rowId: 'rowKey', + ...overrides + }; + function metadata() { + if (etag) { + return { + etag + }; + } else { + return {}; + } + } + return { + PartitionKey: foo.partitionId, + RowKey: foo.rowId, + bar: foo.bar, + ['.metadata']: metadata() + }; + } + + function createEntity(overrides?: Partial, etag = 'foo-etag'): TableServiceAsPromisedModule.Entity { + const foo: FooModel = { + bar: 42, + partitionId: 'partKey', + rowId: 'rowKey', + ...overrides + }; + return { + PartitionKey: { _: foo.partitionId, $: 'Edm.String' }, + RowKey: { _: foo.rowId, $: 'Edm.String' }, + bar: { _: foo.bar, $: 'Edm.Int32' }, + ['.metadata']: { + etag + } + }; + } + +}); diff --git a/e2e/test/jest-with-ts/src/data-access/mappers/TableStorageMapper.ts b/e2e/test/jest-with-ts/src/data-access/mappers/TableStorageMapper.ts new file mode 100644 index 0000000000..2c0fa47f2a --- /dev/null +++ b/e2e/test/jest-with-ts/src/data-access/mappers/TableStorageMapper.ts @@ -0,0 +1,103 @@ +import { Constants } from 'azure-storage'; +import TableServiceAsPromised, { Entity } from '../services/TableServiceAsPromised'; +import { encodeKey, decodeKey, isStorageError } from '../utils'; +import { Mapper, Result } from './Mapper'; +import { OptimisticConcurrencyError } from '../errors'; +import { ModelClass } from '../services/ModelClass'; +import { DashboardQuery } from './DashboardQuery'; + +export default class TableStorageMapper + implements Mapper { + constructor( + private readonly ModelClass: ModelClass, + private readonly tableService: TableServiceAsPromised = new TableServiceAsPromised()) { + } + + public async createStorageIfNotExists(): Promise { + await this.tableService.createTableIfNotExists(this.ModelClass.tableName); + } + + public async insertOrMerge(model: TModel) { + const entity = this.toEntity(model); + await this.tableService.insertOrMergeEntity(this.ModelClass.tableName, entity); + } + + public async findOne(identity: Pick): Promise | null> { + try { + const result = await this.tableService.retrieveEntity>( + this.ModelClass.tableName, + encodeKey(this.ModelClass.createPartitionKey(identity)), + encodeKey(this.ModelClass.createRowKey(identity) || '')); + return this.toModel(result); + } catch (err) { + if (isStorageError(err) && err.code === Constants.StorageErrorCodeStrings.RESOURCE_NOT_FOUND) { + return null; + } + else { + // Oops... didn't mean to catch this one + throw err; + } + } + } + + public async findAll(query: DashboardQuery = DashboardQuery.create(this.ModelClass)): Promise[]> { + const tableQuery = query.build(); + const results = await this.tableService.queryEntities(this.ModelClass.tableName, tableQuery, undefined); + return results.entries.map(entity => this.toModel(entity)); + } + + /** + * Replace an entity of a specific version (throws error otherwise) + * @param model The model to replace + * @param etag The etag (version id) + * @throws {OptimisticConcurrencyError} + */ + public async replace(model: TModel, etag: string): Promise> { + const entity = this.toEntity(model); + entity['.metadata'].etag = etag; + try { + const result = await this.tableService.replaceEntity(this.ModelClass.tableName, entity, {}); + return { model, etag: result['.metadata'].etag }; + } catch (err) { + if (isStorageError(err) && err.code === Constants.StorageErrorCodeStrings.UPDATE_CONDITION_NOT_SATISFIED) { + throw new OptimisticConcurrencyError(`Replace entity with etag ${etag} resulted in ${Constants.StorageErrorCodeStrings.UPDATE_CONDITION_NOT_SATISFIED}`); + } else { + throw err; + } + } + } + + public async insert(model: TModel) { + const entity = this.toEntity(model); + try { + const result = await this.tableService.insertEntity(this.ModelClass.tableName, entity, {}); + return { model, etag: result['.metadata'].etag }; + } catch (err) { + if (isStorageError(err) && err.code === Constants.TableErrorCodeStrings.ENTITY_ALREADY_EXISTS) { + throw new OptimisticConcurrencyError(`Trying to insert "${entity.PartitionKey}" "${entity.RowKey}" which already exists (${Constants.TableErrorCodeStrings.ENTITY_ALREADY_EXISTS})`); + } else { + throw err; + } + } + } + + private toModel(entity: Entity): Result { + const value = new this.ModelClass(); + this.ModelClass.identify(value, decodeKey(entity.PartitionKey._), decodeKey(entity.RowKey._)); + this.ModelClass.persistedFields.forEach(field => (value[field] as any) = (entity as any)[field]._); + return { + etag: entity['.metadata'].etag, + model: value + }; + } + + private toEntity(entity: TModel): Entity { + const data: any = { + PartitionKey: encodeKey(this.ModelClass.createPartitionKey(entity)), + RowKey: encodeKey(this.ModelClass.createRowKey(entity) || ''), + }; + this.ModelClass.persistedFields.forEach(field => data[field] = entity[field]); + data['.metadata'] = {}; + return data; + } +} diff --git a/e2e/test/jest-with-ts/src/data-access/services/ModelClass.ts b/e2e/test/jest-with-ts/src/data-access/services/ModelClass.ts new file mode 100644 index 0000000000..453245182a --- /dev/null +++ b/e2e/test/jest-with-ts/src/data-access/services/ModelClass.ts @@ -0,0 +1,8 @@ +export interface ModelClass { + new(): TModel; + createPartitionKey(entity: Pick): string; + createRowKey(entity: Pick): string | undefined; + identify(entity: Partial, partitionKeyValue: string, rowKeyValue: string): void; + readonly persistedFields: ReadonlyArray>; + readonly tableName: string; +} diff --git a/e2e/test/jest-with-ts/src/data-access/services/TableServiceAsPromised.ts b/e2e/test/jest-with-ts/src/data-access/services/TableServiceAsPromised.ts new file mode 100644 index 0000000000..8810a84ec1 --- /dev/null +++ b/e2e/test/jest-with-ts/src/data-access/services/TableServiceAsPromised.ts @@ -0,0 +1,45 @@ +import { promisify } from 'util'; +import { TableService, TableQuery, createTableService, common } from 'azure-storage'; + +export type Entity = { + [K in Exclude]: { + $: string; + _: T[K]; + }; +} & EntityKey & EntityMetadata; + +export interface EntityKey { + PartitionKey: { + $: 'Edm.String'; + _: string; + }; + RowKey: { + $: 'Edm.String'; + _: string; + }; +} + +export interface EntityMetadata { + ['.metadata']: { + etag: string; + }; +} + +export default class TableServiceAsPromised { + + constructor(tableService = createTableService()) { + this.createTableIfNotExists = promisify(tableService.createTableIfNotExists).bind(tableService); + this.queryEntities = promisify(tableService.queryEntities).bind(tableService) as any; + this.insertOrMergeEntity = promisify(tableService.insertOrMergeEntity).bind(tableService); + this.retrieveEntity = promisify(tableService.retrieveEntity).bind(tableService) as (table: string, partitionKey: string, rowKey: string, options?: TableService.TableEntityRequestOptions) => Promise; + this.replaceEntity = promisify(tableService.replaceEntity).bind(tableService); + this.insertEntity = promisify(tableService.insertEntity).bind(tableService); + } + + public insertEntity: (table: string, entityDescriptor: unknown, options: common.RequestOptions) => Promise; + public replaceEntity: (table: string, entityDescriptor: unknown, options: common.RequestOptions) => Promise; + public createTableIfNotExists: (name: string) => Promise; + public queryEntities: (table: string, tableQuery: TableQuery, cancellationToken: TableService.TableContinuationToken | undefined) => Promise & EntityKey>>; + public insertOrMergeEntity: (table: string, entity: any) => Promise; + public retrieveEntity: (table: string, partitionKey: string, rowKey: string, options?: TableService.TableEntityRequestOptions) => Promise; +} diff --git a/e2e/test/jest-with-ts/src/data-access/services/__mocks__/TableServiceAsPromised.ts b/e2e/test/jest-with-ts/src/data-access/services/__mocks__/TableServiceAsPromised.ts new file mode 100644 index 0000000000..35c04d9318 --- /dev/null +++ b/e2e/test/jest-with-ts/src/data-access/services/__mocks__/TableServiceAsPromised.ts @@ -0,0 +1,19 @@ +export const createTableIfNotExistsMock = jest.fn(); +export const queryEntitiesMock = jest.fn(); +export const insertOrMergeEntityMock = jest.fn(); +export const retrieveEntityMock = jest.fn(); +export const replaceEntityMock = jest.fn(); +export const insertEntityMock = jest.fn(); + +const mock = jest.fn().mockImplementation(() => { + return { + createTableIfNotExists: createTableIfNotExistsMock, + queryEntities: queryEntitiesMock, + insertOrMergeEntity: insertOrMergeEntityMock, + retrieveEntity: retrieveEntityMock, + replaceEntity: replaceEntityMock, + insertEntity: insertEntityMock + }; +}); + +export default mock; diff --git a/e2e/test/jest-with-ts/src/data-access/utils.ts b/e2e/test/jest-with-ts/src/data-access/utils.ts new file mode 100644 index 0000000000..0262eb9e89 --- /dev/null +++ b/e2e/test/jest-with-ts/src/data-access/utils.ts @@ -0,0 +1,13 @@ +import { StorageError } from 'azure-storage'; + +export function encodeKey(inputWithSlashes: string) { + return inputWithSlashes.replace(/\//g, ';'); +} + +export function decodeKey(inputWithSemiColons: string) { + return inputWithSemiColons.replace(/;/g, '/'); +} + +export function isStorageError(maybeStorageError: unknown): maybeStorageError is StorageError { + return maybeStorageError instanceof Error && (maybeStorageError as StorageError).name === 'StorageError'; +} diff --git a/e2e/test/jest-with-ts/stryker.conf.json b/e2e/test/jest-with-ts/stryker.conf.json new file mode 100644 index 0000000000..9587004662 --- /dev/null +++ b/e2e/test/jest-with-ts/stryker.conf.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker/master/packages/api/schema/stryker-core.json", + "mutator": "typescript", + "mutate": [ + "src/data-access/mappers/TableStorageMapper.ts" + ], + "packageManager": "npm", + "testRunner": "jest", + "tempDirName": "stryker-tmp", + "transpilers": [], + "maxConcurrentTestRunners": 2, + "coverageAnalysis": "off", + "reporters": [ + "event-recorder" + ] +} diff --git a/e2e/test/jest-with-ts/test/helpers/StorageError.ts b/e2e/test/jest-with-ts/test/helpers/StorageError.ts new file mode 100644 index 0000000000..005875ae81 --- /dev/null +++ b/e2e/test/jest-with-ts/test/helpers/StorageError.ts @@ -0,0 +1,6 @@ +export class StorageError extends Error { + public readonly name = 'StorageError'; + constructor(public readonly code: string) { + super(code); + } +} diff --git a/e2e/test/jest-with-ts/tsconfig.json b/e2e/test/jest-with-ts/tsconfig.json new file mode 100644 index 0000000000..69ba96b8c4 --- /dev/null +++ b/e2e/test/jest-with-ts/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es2017", + "esModuleInterop": true, + "lib": [ + "es2017" + ], + "types": [ + "jest" + ], + "strict": true, + "typeRoots": [ + "./node_modules/@types" + ], + "outDir": "dist" + }, + // Example pulled from: https://kulshekhar.github.io/ts-jest/user/config/#example + "paths": { + "@App/*": [ + "src/*" + ], + "lib/*": [ + "common/*" + ] + } +} diff --git a/e2e/test/jest-with-ts/verify/verify.ts b/e2e/test/jest-with-ts/verify/verify.ts new file mode 100644 index 0000000000..403ac5e614 --- /dev/null +++ b/e2e/test/jest-with-ts/verify/verify.ts @@ -0,0 +1,17 @@ + +import { expectMetrics } from '../../../helpers'; + +describe('Verify stryker has ran correctly', () => { + + it('should report correct score', async () => { + await expectMetrics({ + ignored: 0, + killed: 26, + mutationScore: 54.55, + noCoverage: 0, + survived: 13, + timeout: 0, + runtimeErrors: 15 + }); + }); +});