From 397f9f10eebb1c4f42acc93238c480449b3c0c2a Mon Sep 17 00:00:00 2001 From: Barleen Dhaliwal Date: Tue, 14 Sep 2021 10:54:38 +0530 Subject: [PATCH] test(core): add tests for AuditRepositoryMixin GH-10 --- package-lock.json | 783 +++++++++++++++++- package.json | 7 +- .../acceptance/audit.mixin.acceptance.ts | 728 ++++++++++++++++ .../fixtures/datasources/audit.datasource.ts | 19 + .../fixtures/datasources/test.datasource.ts | 17 + .../acceptance/fixtures/models/audit.model.ts | 76 ++ .../acceptance/fixtures/models/test.model.ts | 32 + .../repositories/audit-error.repository.ts | 10 + .../fixtures/repositories/audit.repository.ts | 11 + .../fixtures/repositories/test.repository.ts | 33 + src/__tests__/unit/audit.mixin.unit.ts | 542 ++++++++++++ src/__tests__/unit/fixtures/mockClass.ts | 175 ++++ src/__tests__/unit/fixtures/mockData.ts | 116 +++ src/__tests__/unit/fixtures/mockModel.ts | 22 + 14 files changed, 2532 insertions(+), 39 deletions(-) create mode 100644 src/__tests__/acceptance/audit.mixin.acceptance.ts create mode 100644 src/__tests__/acceptance/fixtures/datasources/audit.datasource.ts create mode 100644 src/__tests__/acceptance/fixtures/datasources/test.datasource.ts create mode 100644 src/__tests__/acceptance/fixtures/models/audit.model.ts create mode 100644 src/__tests__/acceptance/fixtures/models/test.model.ts create mode 100644 src/__tests__/acceptance/fixtures/repositories/audit-error.repository.ts create mode 100644 src/__tests__/acceptance/fixtures/repositories/audit.repository.ts create mode 100644 src/__tests__/acceptance/fixtures/repositories/test.repository.ts create mode 100644 src/__tests__/unit/audit.mixin.unit.ts create mode 100644 src/__tests__/unit/fixtures/mockClass.ts create mode 100644 src/__tests__/unit/fixtures/mockData.ts create mode 100644 src/__tests__/unit/fixtures/mockModel.ts diff --git a/package-lock.json b/package-lock.json index 141786f..76e042e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -585,6 +585,14 @@ } } }, + "@exlinc/keycloak-passport": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@exlinc/keycloak-passport/-/keycloak-passport-1.0.2.tgz", + "integrity": "sha512-mUZdBq5peaHRNf8pd6XxgNWk4aY477Gy9XqF0WGHBmCygA2tX4EOvnoI/yHhu0ceAY08g3ng9BbEFKlSMRHWcg==", + "requires": { + "passport-oauth2": "^1.4.0" + } + }, "@exodus/schemasafe": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.0.0-rc.3.tgz", @@ -1408,6 +1416,12 @@ "@types/superagent": "*" } }, + "@types/uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.1.tgz", @@ -1542,7 +1556,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, "requires": { "debug": "4" } @@ -1561,7 +1574,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1665,6 +1677,30 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "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" + } + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "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=" + }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -1679,8 +1715,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "at-least-node": { "version": "1.0.0", @@ -1688,16 +1723,39 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true }, + "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.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "bcp47": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", "integrity": "sha1-NUvjMH/9CEM6ePXh4glYRfifx/4=" }, + "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" + } + }, "before-after-hook": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", @@ -1724,6 +1782,11 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -1795,6 +1858,11 @@ "fill-range": "^7.0.1" } }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -1823,18 +1891,65 @@ "fast-json-stable-stringify": "2.x" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true }, + "cache-manager": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-2.10.2.tgz", + "integrity": "sha512-VueuJaWPfugreadk88pZ23g0nGVd4qqG4Y8X8y9LQePEN+EhaDlXbJM5pbJB0CdpuTmqVPCmcWgtEDxLqbT1zw==", + "requires": { + "async": "1.5.2", + "lodash.clonedeep": "4.5.0", + "lru-cache": "4.0.0" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "lru-cache": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.0.tgz", + "integrity": "sha1-tcvwFVbBaWb+vlTO7A+03JDfbCg=", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, "cachedir": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.2.0.tgz", @@ -1934,6 +2049,11 @@ "redeyed": "~2.1.0" } }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, "chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -2108,7 +2228,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -2531,6 +2650,14 @@ "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", "dev": true }, + "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" + } + }, "dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -2632,8 +2759,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "depd": { "version": "2.0.0", @@ -2706,6 +2832,15 @@ "is-obj": "^2.0.0" } }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -2715,6 +2850,23 @@ "readable-stream": "^2.0.2" } }, + "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" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "editor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/editor/-/editor-1.0.0.tgz", @@ -2733,6 +2885,20 @@ "integrity": "sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg==", "dev": true }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2787,6 +2953,19 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3091,6 +3270,11 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -3102,11 +3286,15 @@ "tmp": "^0.0.33" } }, + "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.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.2.5", @@ -3125,8 +3313,7 @@ "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==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-levenshtein": { "version": "2.0.6", @@ -3307,6 +3494,11 @@ "signal-exit": "^3.0.2" } }, + "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": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -3423,6 +3615,14 @@ "pump": "^3.0.0" } }, + "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" + } + }, "git-log-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.0.tgz", @@ -3609,6 +3809,20 @@ } } }, + "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.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, "hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -3634,6 +3848,15 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -3659,6 +3882,16 @@ "tslib": "^2.0.3" } }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -3727,6 +3960,16 @@ "debug": "4" } }, + "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" + } + }, "http2-client": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.3.tgz", @@ -3737,7 +3980,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, "requires": { "agent-base": "6", "debug": "4" @@ -4065,8 +4307,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-utf8": { "version": "0.2.1", @@ -4090,6 +4331,11 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, "issue-parser": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz", @@ -4240,6 +4486,11 @@ "esprima": "^4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -4258,11 +4509,15 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "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==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -4273,8 +4528,7 @@ "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=", - "dev": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { "version": "2.2.0", @@ -4301,12 +4555,76 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "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" + } + }, "just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jwk-to-pem": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz", + "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==", + "requires": { + "asn1.js": "^5.3.0", + "elliptic": "^6.5.4", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -4390,8 +4708,7 @@ "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, "lodash.escaperegexp": { "version": "4.1.2", @@ -4417,23 +4734,41 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", "dev": true }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", - "dev": true + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" }, "lodash.map": { "version": "4.6.0", @@ -4441,6 +4776,11 @@ "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.toarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", @@ -4507,6 +4847,26 @@ "uuid": "^8.3.1" } }, + "loopback4-authentication": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/loopback4-authentication/-/loopback4-authentication-4.6.0.tgz", + "integrity": "sha512-u4/R8PigTAzvhApC+L4g7Ph/HoiEyF4Sm9mDgaFkIobF5U7oSE7TV1WJpuZQfmjToa8LDAaV0imBR2BffwG7rQ==", + "requires": { + "@exlinc/keycloak-passport": "^1.0.2", + "@loopback/context": "^3.16.0", + "@loopback/core": "^2.16.0", + "https-proxy-agent": "^5.0.0", + "passport": "^0.4.1", + "passport-apple": "^2.0.1", + "passport-azure-ad": "^4.3.0", + "passport-facebook": "^3.0.0", + "passport-google-oauth20": "^2.0.0", + "passport-http-bearer": "^1.0.1", + "passport-instagram": "^1.0.0", + "passport-local": "^1.0.0", + "passport-oauth2-client-password": "^0.1.2" + } + }, "lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -4693,14 +5053,12 @@ "mime-db": { "version": "1.47.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", - "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==", - "dev": true + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" }, "mime-types": { "version": "2.1.30", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", - "dev": true, "requires": { "mime-db": "1.47.0" } @@ -4716,6 +5074,16 @@ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -4727,8 +5095,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minimist-options": { "version": "4.1.0", @@ -4843,6 +5210,12 @@ "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "optional": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4865,6 +5238,56 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "optional": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, + "nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "optional": true + }, "nanoid": { "version": "2.1.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", @@ -4876,6 +5299,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -7330,6 +7759,16 @@ "yaml": "^1.10.0" } }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, "object-inspect": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz", @@ -7568,6 +8007,149 @@ "tslib": "^2.0.3" } }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-apple": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/passport-apple/-/passport-apple-2.0.1.tgz", + "integrity": "sha512-+ssWcwgg/PWyHNSgNn4d1dbsgQeEb13Xgu7TRb+FlHggbCTDvCb2jzm+M+hQ0vmU9y2QOmiRPqD27b3TCRc6PQ==", + "requires": { + "jsonwebtoken": "^8.5.1", + "passport-oauth2": "^1.5.0" + } + }, + "passport-azure-ad": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/passport-azure-ad/-/passport-azure-ad-4.3.0.tgz", + "integrity": "sha512-bCY9wQns7JxbRVa9ez1ZeGXRGVpqpi0GvOiR5YC2tv5S9NbgZdkWS70xXBPGQx8rua6n6LFD5oVM5fZ2t9MM6w==", + "requires": { + "async": "^1.5.2", + "base64url": "^3.0.0", + "bunyan": "^1.8.14", + "cache-manager": "2.10.2", + "https-proxy-agent": "^2.2.2", + "jwk-to-pem": "^2.0.4", + "jws": "^3.1.3", + "lodash": "^4.11.2", + "oauth": "0.9.15", + "passport": "^0.3.2", + "request": "^2.72.0", + "valid-url": "^1.0.6" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, + "passport": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz", + "integrity": "sha1-ndAJ+RXo/glbASSgG4+C2gdRAQI=", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + } + } + }, + "passport-facebook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-3.0.0.tgz", + "integrity": "sha512-K/qNzuFsFISYAyC1Nma4qgY/12V3RSLFdFVsPKXiKZt434wOvthFW1p7zKa1iQihQMRhaWorVE1o3Vi1o+ZgeQ==", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-http-bearer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/passport-http-bearer/-/passport-http-bearer-1.0.1.tgz", + "integrity": "sha1-FHRp6jZp4qhMYWfvmdu3fh8AmKg=", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-instagram": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-instagram/-/passport-instagram-1.0.0.tgz", + "integrity": "sha1-6qK0LRFHO8/aUZDyYjTPSF9kVlY=", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-oauth2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.6.0.tgz", + "integrity": "sha512-emXPLqLcVEcLFR/QvQXZcwLmfK8e9CqvMgmOFJxcNT3okSFMtUbRRKpY20x5euD+01uHsjjCa07DYboEeLXYiw==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-oauth2-client-password": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/passport-oauth2-client-password/-/passport-oauth2-client-password-0.1.2.tgz", + "integrity": "sha1-TzeLZ4uS0W270jOmxwZSAJPlYbo=", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "path-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", @@ -7611,6 +8193,16 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, "picomatch": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", @@ -7774,6 +8366,16 @@ "ipaddr.js": "1.9.1" } }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -7786,8 +8388,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "q": { "version": "1.5.1", @@ -8046,6 +8647,55 @@ "es6-error": "^4.0.1" } }, + "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" + }, + "dependencies": { + "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" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8184,11 +8834,16 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semantic-release": { "version": "17.4.4", @@ -8723,6 +9378,22 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "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" + } + }, "stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -9077,6 +9748,15 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "dev": true }, + "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" + } + }, "traverse": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", @@ -9116,6 +9796,19 @@ } } }, + "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=" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9169,6 +9862,11 @@ "dev": true, "optional": true }, + "uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -9216,7 +9914,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "requires": { "punycode": "^2.1.0" } @@ -9244,8 +9941,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { "version": "8.3.2", @@ -9263,6 +9959,11 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -9279,6 +9980,16 @@ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", "dev": true }, + "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" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index aa2d505..0a232de 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,13 @@ "eslint": "lb-eslint --report-unused-disable-directives .", "eslint:fix": "npm run eslint -- --fix", "pretest": "npm run build", - "test": "echo 'No Tests'", + "test": "lb-mocha --allow-console-logs \"dist/__tests__\"", "posttest": "npm run lint", "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", "clean": "lb-clean dist *.tsbuildinfo .eslintcache", "prepublishOnly": "npm run build && npm run lint", - "prepare": "husky install" + "prepare": "husky install", + "coverage":"lb-nyc npm run test" }, "author": "", "license": "", @@ -91,4 +92,4 @@ ], "repositoryUrl": "git@github.com:sourcefuse/loopback4-audit-log.git" } -} +} \ No newline at end of file diff --git a/src/__tests__/acceptance/audit.mixin.acceptance.ts b/src/__tests__/acceptance/audit.mixin.acceptance.ts new file mode 100644 index 0000000..e662816 --- /dev/null +++ b/src/__tests__/acceptance/audit.mixin.acceptance.ts @@ -0,0 +1,728 @@ +import {TestDataSource} from './fixtures/datasources/test.datasource'; +import { + testAuditOpts, + TestRepository, +} from './fixtures/repositories/test.repository'; +import {expect, sinon} from '@loopback/testlab'; +import {TestAuditLogRepository} from './fixtures/repositories/audit.repository'; +import {TestAuditDataSource} from './fixtures/datasources/audit.datasource'; +import {TestModel} from './fixtures/models/test.model'; +import {Action} from '../..'; +import {v4 as uuidv4} from 'uuid'; +import {TestAuditLogErrorRepository} from './fixtures/repositories/audit-error.repository'; + +export let consoleMessage: string; +console.error = (message: string) => { + consoleMessage = message; +}; + +const mockUser = { + id: 'testId', + name: 'testName', +}; + +describe('Audit Mixin', () => { + const testDataSourceInstance = new TestDataSource(); + const getCurrentUser = sinon.stub().resolves(mockUser); + const auditLogRepositoryInstance = new TestAuditLogRepository( + new TestAuditDataSource(), + ); + const getAuditLogRepository = sinon + .stub() + .resolves(auditLogRepositoryInstance); + const testRepositoryInstance = new TestRepository( + testDataSourceInstance, + getCurrentUser, + getAuditLogRepository, + ); + + beforeEach(async () => { + await auditLogRepositoryInstance.deleteAll(); + }); + it('should create audit log when new item is created', async () => { + const mockItem = getMockItem(); + const createMethodResponse = await testRepositoryInstance.create(mockItem); + const auditLog = await auditLogRepositoryInstance.find(); + const storedRecords = await testRepositoryInstance.find({ + where: { + id: mockItem.id, + description: mockItem.description, + itemName: mockItem.itemName, + }, + }); + const expectedAuditLog = { + action: Action.INSERT_ONE, + actedOn: TestModel.name, + actionKey: testAuditOpts.actionKey, + entityId: mockItem.id, + actor: mockUser.id, + before: undefined, + after: { + id: mockItem.id, + itemName: mockItem.itemName, + description: mockItem.description, + }, + }; + + expect(createMethodResponse).to.match(mockItem); + expect(auditLog.length).to.equal(1); + expect(auditLog[0]).to.containDeep(expectedAuditLog); + expect(auditLog[0].actedAt).to.be.Date(); + + //check if stores in db + expect(storedRecords.length).to.equal(1); + }); + + it('should not create audit log when options.noAudit is set to true on creating new item', async () => { + const mockItem = getMockItem(); + const createMethodResponse = await testRepositoryInstance.create(mockItem, { + noAudit: true, + }); + expect(createMethodResponse).to.match(mockItem); + const storedRecords = await testRepositoryInstance.find({ + where: { + id: mockItem.id, + description: mockItem.description, + itemName: mockItem.itemName, + }, + }); + + const auditLog = await auditLogRepositoryInstance.find(); + + expect(auditLog.length).to.equal(0); + //check if stores in db + expect(storedRecords.length).to.equal(1); + }); + + it('should create audit log when new items are created on calling createAll', async () => { + const mockItemArray = getMockItemArray(); + const createMethodResponse = await testRepositoryInstance.createAll( + mockItemArray, + ); + expect(createMethodResponse).to.match(mockItemArray); + + //check if stored in db + const storedRecords = await testRepositoryInstance.find({ + where: { + or: [ + {id: mockItemArray[0].id}, + {id: mockItemArray[1].id}, + {id: mockItemArray[2].id}, + ], + }, + }); + expect(storedRecords.length).to.equal(3); + + //check if audit log is stored + const auditLog = await auditLogRepositoryInstance.find(); + auditLog.sort((a, b) => (a.entityId > b.entityId ? 1 : -1)); + mockItemArray.sort((a, b) => (a.id > b.id ? 1 : -1)); + expect(auditLog.length).to.equal(mockItemArray.length); + auditLog.forEach((auditData, index) => { + const expectedAuditLog = { + action: Action.INSERT_MANY, + actedOn: TestModel.name, + actionKey: testAuditOpts.actionKey, + entityId: mockItemArray[index].id, + actor: mockUser.id, + before: undefined, + after: mockItemArray[index], + }; + expect(auditData).to.containDeep(expectedAuditLog); + expect(auditData.actedAt).to.be.Date(); + }); + }); + + it('should not create audit log when options.noAudit is set to true on creating new items on calling createAll', async () => { + const mockItemArray = getMockItemArray(); + const createMethodResponse = await testRepositoryInstance.createAll( + mockItemArray, + {noAudit: true}, + ); + expect(createMethodResponse).to.match(mockItemArray); + + //check if stored in db + const storedRecords = await testRepositoryInstance.find({ + where: { + or: [ + {id: mockItemArray[0].id}, + {id: mockItemArray[1].id}, + {id: mockItemArray[2].id}, + ], + }, + }); + expect(storedRecords.length).to.equal(3); + + //check if audit log is not stored + const auditLog = await auditLogRepositoryInstance.find(); + expect(auditLog.length).to.equal(0); + }); + it('should create audit log when items are deleted on calling deleteAll', async () => { + //create dummy data to be deleted + const mockItemArray = getMockItemArray(); + await createDummyDataFromArray(mockItemArray); + + const deleteAllMethodResponse = await testRepositoryInstance.deleteAll({ + id: { + inq: mockItemArray.map(item => item.id), + }, + }); + expect(deleteAllMethodResponse.count).to.equal(mockItemArray.length); + + //check if deleted from db + const storedRecords = await testRepositoryInstance.find({ + where: { + id: { + inq: mockItemArray.map(item => item.id), + }, + }, + }); + expect(storedRecords.length).to.equal(0); + + //check if audit log is stored + const auditLog = await auditLogRepositoryInstance.find(); + auditLog.sort((a, b) => (a.entityId > b.entityId ? 1 : -1)); + mockItemArray.sort((a, b) => (a.id > b.id ? 1 : -1)); + expect(auditLog.length).to.equal(mockItemArray.length); + auditLog.forEach((auditData, index) => { + const expectedAuditLog = { + action: Action.DELETE_MANY, + actedOn: TestModel.name, + actionKey: testAuditOpts.actionKey, + entityId: mockItemArray[index].id, + actor: mockUser.id, + before: mockItemArray[index], + after: undefined, + }; + expect(auditData).to.containDeep(expectedAuditLog); + expect(auditData.actedAt).to.be.Date(); + }); + }); + it('should not create audit log when options.noAudit is set to true on deleteing new items on calling deleteAll', async () => { + //create dummy data to be deleted + const mockItemArray = getMockItemArray(); + await createDummyDataFromArray(mockItemArray); + + const deleteAllMethodResponse = await testRepositoryInstance.deleteAll( + { + id: { + inq: mockItemArray.map(item => item.id), + }, + }, + {noAudit: true}, + ); + expect(deleteAllMethodResponse.count).to.equal(mockItemArray.length); + + //check if deleted from db + const storedRecords = await testRepositoryInstance.find({ + where: { + id: { + inq: mockItemArray.map(item => item.id), + }, + }, + }); + expect(storedRecords.length).to.equal(0); + + //check if audit log is not stored + const auditLog = await auditLogRepositoryInstance.find(); + expect(auditLog.length).to.equal(0); + }); + it('should create audit log for deleted item on calling deleteById', async () => { + //create dummy data to be deleted + const mockItem = getMockItem(); + await createDummyData(mockItem); + + await testRepositoryInstance.deleteById(mockItem.id); + + //check if deleted from db + const storedRecords = await testRepositoryInstance.find({ + where: {id: mockItem.id}, + }); + expect(storedRecords.length).to.equal(0); + + //check if Audit log is stored + const auditLog = await auditLogRepositoryInstance.find(); + const expectedAuditLog = { + action: Action.DELETE_ONE, + actedOn: TestModel.name, + actionKey: testAuditOpts.actionKey, + entityId: mockItem.id, + actor: mockUser.id, + before: mockItem, + after: undefined, + }; + expect(auditLog.length).to.equal(1); + expect(auditLog[0]).to.containDeep(expectedAuditLog); + expect(auditLog[0].actedAt).to.be.Date(); + }); + it('should not create audit log for deleted item when options.noAudit is set on calling deleteById', async () => { + //create dummy data to be deleted + const mockItem = getMockItem(); + await createDummyData(mockItem); + + await testRepositoryInstance.deleteById(mockItem.id, {noAudit: true}); + + //check if deleted from db + const storedRecords = await testRepositoryInstance.find({ + where: {id: mockItem.id}, + }); + expect(storedRecords.length).to.equal(0); + + //check if Audit log is stored + const auditLog = await auditLogRepositoryInstance.find(); + expect(auditLog.length).to.equal(0); + }); + + it('should create audit log for replaced item on calling replaceById', async () => { + //create dummy data to be replaced + const mockItem = getMockItem(); + await createDummyData(mockItem); + + await testRepositoryInstance.replaceById(mockItem.id, { + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }); + + //check if replaced in db + const storedRecords = await testRepositoryInstance.find({ + where: { + id: mockItem.id, + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }, + }); + expect(storedRecords.length).to.equal(1); + + //check if audit log is stored + const auditLog = await auditLogRepositoryInstance.find(); + expect(auditLog.length).to.equal(1); + const expectedAuditLog = { + action: Action.UPDATE_ONE, + actedOn: TestModel.name, + actionKey: testAuditOpts.actionKey, + entityId: mockItem.id, + actor: mockUser.id, + before: mockItem, + after: { + id: mockItem.id, + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }, + }; + expect(auditLog[0]).to.containDeep(expectedAuditLog); + expect(auditLog[0].actedAt).to.be.Date(); + }); + + it('should not create audit log for replaced item when options.noAudit is set on calling replaceById', async () => { + //create dummy data to be replaced + const mockItem = getMockItem(); + await createDummyData(mockItem); + + await testRepositoryInstance.replaceById( + mockItem.id, + {itemName: 'replacedTestItem', description: 'replacedTestDescription'}, + {noAudit: true}, + ); + + //check if replaced in db + const storedRecords = await testRepositoryInstance.find({ + where: { + id: mockItem.id, + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }, + }); + expect(storedRecords.length).to.equal(1); + + //check if audit log is not stored + const auditLog = await auditLogRepositoryInstance.find(); + expect(auditLog.length).to.equal(0); + }); + + it('should create audit log for updated item on calling updateById', async () => { + //create dummy data to be updated + const mockItem = getMockItem(); + await createDummyData(mockItem); + + await testRepositoryInstance.updateById(mockItem.id, { + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }); + + //check if replaced in db + const storedRecords = await testRepositoryInstance.find({ + where: { + id: mockItem.id, + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }, + }); + expect(storedRecords.length).to.equal(1); + + //check if audit log is stored + const auditLog = await auditLogRepositoryInstance.find(); + expect(auditLog.length).to.equal(1); + const expectedAuditLog = { + action: Action.UPDATE_ONE, + actedOn: TestModel.name, + actionKey: testAuditOpts.actionKey, + entityId: mockItem.id, + actor: mockUser.id, + before: mockItem, + after: { + id: mockItem.id, + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }, + }; + + expect(auditLog[0]).to.containDeep(expectedAuditLog); + expect(auditLog[0].actedAt).to.be.Date(); + }); + + it('should not create audit log for updated item when options.noAudit is set on calling updateById', async () => { + //create dummy data to be replaced + const mockItem = getMockItem(); + await createDummyData(mockItem); + + await testRepositoryInstance.updateById( + mockItem.id, + {itemName: 'replacedTestItem', description: 'replacedTestDescription'}, + {noAudit: true}, + ); + + //check if replaced in db + const storedRecords = await testRepositoryInstance.find({ + where: { + id: mockItem.id, + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }, + }); + expect(storedRecords.length).to.equal(1); + + //check if audit log is not stored + const auditLog = await auditLogRepositoryInstance.find(); + expect(auditLog.length).to.equal(0); + }); + + it('should create audit log for updated items on calling updateAll', async () => { + //create dummy data to be updated + const mockItemArray = getMockItemArray(); + await createDummyDataFromArray(mockItemArray); + + await testRepositoryInstance.updateAll( + {itemName: 'replacedTestItem', description: 'replacedTestDescription'}, + { + id: { + inq: mockItemArray.map(item => item.id), + }, + }, + ); + //check if updated in db + const storedRecords = await testRepositoryInstance.find({ + where: { + id: { + inq: mockItemArray.map(item => item.id), + }, + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }, + }); + expect(storedRecords.length).to.equal(3); + + //check if audit log is stored + const auditLog = await auditLogRepositoryInstance.find(); + auditLog.sort((a, b) => (a.entityId > b.entityId ? 1 : -1)); + mockItemArray.sort((a, b) => (a.id > b.id ? 1 : -1)); + expect(auditLog.length).to.equal(mockItemArray.length); + auditLog.forEach((auditData, index) => { + const expectedAuditLog = { + action: Action.UPDATE_MANY, + actedOn: TestModel.name, + actionKey: testAuditOpts.actionKey, + entityId: mockItemArray[index].id, + actor: mockUser.id, + before: mockItemArray[index], + after: { + id: mockItemArray[index].id, + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }, + }; + expect(auditData).to.containDeep(expectedAuditLog); + expect(auditData.actedAt).to.be.Date(); + }); + }); + it('should not create audit log for updated items when options.noAudit is set on calling updateAll', async () => { + //create dummy data to be updated + const mockItemArray = getMockItemArray(); + await createDummyDataFromArray(mockItemArray); + + await testRepositoryInstance.updateAll( + {itemName: 'replacedTestItem', description: 'replacedTestDescription'}, + { + id: { + inq: mockItemArray.map(item => item.id), + }, + }, + {noAudit: true}, + ); + + //check if updated in db + const storedRecords = await testRepositoryInstance.find({ + where: { + id: { + inq: mockItemArray.map(item => item.id), + }, + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }, + }); + expect(storedRecords.length).to.equal(3); + + //check if audit log is stored + const auditLog = await auditLogRepositoryInstance.find(); + expect(auditLog.length).to.equal(0); + }); + + const auditLogErrorRepositoryInstance = new TestAuditLogErrorRepository(); + const auditLogErrorRepository = sinon + .stub() + .resolves(auditLogErrorRepositoryInstance); //repository which will cause catch statements to execute + const testRepositoryAuditLogErrorInstance = new TestRepository( + testDataSourceInstance, + getCurrentUser, + auditLogErrorRepository, + ); + + it(`should log appropriate message when audit log can't be created on calling create method`, async () => { + const mockItem = getMockItem(); + await testRepositoryAuditLogErrorInstance.create(mockItem); + + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.INSERT_ONE}"`, + `"after":`, + `"id":`, + `"itemName":"${mockItem.itemName}"`, + `"description":"${mockItem.description}"`, + `"entityId":`, + `"actedOn":"${TestModel.name}"`, + `"actionKey":"${testAuditOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + it(`should log appropriate message when audit log can't be created on calling createAll method`, async () => { + const mockItemArray = getMockItemArray(); + await testRepositoryAuditLogErrorInstance.createAll(mockItemArray); + + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.INSERT_MANY}"`, + `"after":`, + `"id":`, + `"itemName":`, + `"description":`, + `"entityId":`, + `"actedOn":"${TestModel.name}"`, + `"actionKey":"${testAuditOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + it(`should log appropriate message when audit log can't be created on calling deleteAll method`, async () => { + //create dummy data to be deleted + const mockItemArray = getMockItemArray(); + await createDummyDataFromArrayFalse(mockItemArray); + + await testRepositoryAuditLogErrorInstance.deleteAll({ + id: { + inq: mockItemArray.map(item => item.id), + }, + }); + + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.DELETE_MANY}"`, + `"before":`, + `"id":`, + `"itemName":`, + `"description":`, + `"entityId":`, + `"actedOn":"${TestModel.name}"`, + `"actionKey":"${testAuditOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + + it(`should log appropriate message when audit log can't be created on calling updateAll method`, async () => { + //create dummy data to be updated + const mockItemArray = getMockItemArray(); + await createDummyDataFromArrayFalse(mockItemArray); + + await testRepositoryAuditLogErrorInstance.updateAll( + {itemName: 'replacedTestItem', description: 'replacedTestDescription'}, + { + id: { + inq: mockItemArray.map(item => item.id), + }, + }, + ); + + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.UPDATE_MANY}"`, + `"before":`, + `"after":`, + `"id":`, + `"itemName":`, + `"description":`, + `"entityId":`, + `"actedOn":"${TestModel.name}"`, + `"actionKey":"${testAuditOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + + it(`should log appropriate message when audit log can't be created on calling updateById method`, async () => { + //create dummy data to be replaced + const mockItem = getMockItem(); + await createDummyDataFalse(mockItem); + + await testRepositoryAuditLogErrorInstance.updateById(mockItem.id, { + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }); + + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.UPDATE_ONE}"`, + `"before":`, + `"after":`, + `"id":`, + `"itemName":"${mockItem.itemName}"`, + `"description":"${mockItem.description}"`, + `"entityId":`, + `"actedOn":"${TestModel.name}"`, + `"actionKey":"${testAuditOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + it(`should log appropriate message when audit log can't be created on calling replaceById method`, async () => { + //create dummy data to be replaced + const mockItem = getMockItem(); + await createDummyDataFalse(mockItem); + + await testRepositoryAuditLogErrorInstance.replaceById(mockItem.id, { + itemName: 'replacedTestItem', + description: 'replacedTestDescription', + }); + + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.UPDATE_ONE}"`, + `"before":`, + `"after":`, + `"id":`, + `"itemName":"${mockItem.itemName}"`, + `"description":"${mockItem.description}"`, + `"entityId":`, + `"actedOn":"${TestModel.name}"`, + `"actionKey":"${testAuditOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + it(`should log appropriate message when audit log can't be created on calling deleteById method`, async () => { + //create dummy data to be deleted + const mockItem = getMockItem(); + await createDummyDataFalse(mockItem); + + await testRepositoryAuditLogErrorInstance.deleteById(mockItem.id); + + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.DELETE_ONE}"`, + `"before":`, + `"id":`, + `"itemName":"${mockItem.itemName}"`, + `"description":"${mockItem.description}"`, + `"entityId":`, + `"actedOn":"${TestModel.name}"`, + `"actionKey":"${testAuditOpts.actionKey}"`, + ]; + + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + + async function createDummyDataFromArrayFalse( + mockItemArray: {id: string; itemName: string; description: string}[], + ) { + await testRepositoryAuditLogErrorInstance.createAll(mockItemArray, { + noAudit: true, + }); + } + async function createDummyDataFalse(mockItem: { + id: string; + itemName: string; + description: string; + }) { + await testRepositoryAuditLogErrorInstance.create(mockItem, {noAudit: true}); + } + + async function createDummyDataFromArray( + mockItemArray: {id: string; itemName: string; description: string}[], + ) { + await testRepositoryInstance.createAll(mockItemArray, {noAudit: true}); + } + async function createDummyData(mockItem: { + id: string; + itemName: string; + description: string; + }) { + await testRepositoryInstance.create(mockItem, {noAudit: true}); + } + function getMockItem() { + const item = { + id: uuidv4(), + itemName: 'testItem', + description: 'testDescription', + }; + return item; + } + function getMockItemArray() { + const item = [ + {id: uuidv4(), itemName: 'testItem1', description: 'testDescription1'}, + {id: uuidv4(), itemName: 'testItem2', description: 'testDescription2'}, + {id: uuidv4(), itemName: 'testItem3', description: 'testDescription3'}, + ]; + return item; + } +}); diff --git a/src/__tests__/acceptance/fixtures/datasources/audit.datasource.ts b/src/__tests__/acceptance/fixtures/datasources/audit.datasource.ts new file mode 100644 index 0000000..bce9862 --- /dev/null +++ b/src/__tests__/acceptance/fixtures/datasources/audit.datasource.ts @@ -0,0 +1,19 @@ +import {LifeCycleObserver} from '@loopback/core'; +import {juggler} from '@loopback/repository'; +import {AuditDbSourceName} from '../../../../types'; + +const config = { + name: 'testAudit', + connector: 'memory', +}; + +export class TestAuditDataSource + extends juggler.DataSource + implements LifeCycleObserver { + static dataSourceName = AuditDbSourceName; + static readonly defaultConfig = config; + + constructor(dsConfig: object = config) { + super(dsConfig); + } +} diff --git a/src/__tests__/acceptance/fixtures/datasources/test.datasource.ts b/src/__tests__/acceptance/fixtures/datasources/test.datasource.ts new file mode 100644 index 0000000..f95fac8 --- /dev/null +++ b/src/__tests__/acceptance/fixtures/datasources/test.datasource.ts @@ -0,0 +1,17 @@ +import {LifeCycleObserver} from '@loopback/core'; +import {juggler} from '@loopback/repository'; + +const config = { + name: 'test', + connector: 'memory', +}; +export class TestDataSource + extends juggler.DataSource + implements LifeCycleObserver { + static dataSourceName = 'test'; + static readonly defaultConfig = config; + + constructor(dsConfig: object = config) { + super(dsConfig); + } +} diff --git a/src/__tests__/acceptance/fixtures/models/audit.model.ts b/src/__tests__/acceptance/fixtures/models/audit.model.ts new file mode 100644 index 0000000..8317d3f --- /dev/null +++ b/src/__tests__/acceptance/fixtures/models/audit.model.ts @@ -0,0 +1,76 @@ +import {Entity, model, property} from '@loopback/repository'; +import {Action} from '../../../..'; + +@model({ + name: 'audit_logs', + settings: { + strict: false, + }, +}) +export class TestAuditLog extends Entity { + @property({ + type: 'string', + id: true, + generated: false, + }) + id?: string; + + @property({ + type: 'string', + required: true, + }) + action: Action; + + @property({ + name: 'acted_at', + type: 'date', + required: true, + }) + actedAt: Date; + + @property({ + name: 'acted_on', + type: 'string', + }) + actedOn?: string; + + @property({ + name: 'action_key', + type: 'string', + required: true, + }) + actionKey: string; + + @property({ + name: 'entity_id', + type: 'string', + required: true, + }) + entityId: string; + + @property({ + type: 'string', + required: true, + }) + actor: string; + + @property({ + type: 'object', + }) + before?: object; + + @property({ + type: 'object', + }) + after?: object; + + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/src/__tests__/acceptance/fixtures/models/test.model.ts b/src/__tests__/acceptance/fixtures/models/test.model.ts new file mode 100644 index 0000000..b282d9d --- /dev/null +++ b/src/__tests__/acceptance/fixtures/models/test.model.ts @@ -0,0 +1,32 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class TestModel extends Entity { + @property({ + type: 'string', + id: true, + }) + id: string; + + @property({ + type: 'string', + required: true, + }) + itemName: string; + + @property({ + type: 'string', + required: true, + }) + description: string; + + constructor(data?: Partial) { + super(data); + } +} + +export interface TestModelRelations { + // describe navigational properties here +} + +export type TestModelWithRelations = TestModel & TestModelRelations; diff --git a/src/__tests__/acceptance/fixtures/repositories/audit-error.repository.ts b/src/__tests__/acceptance/fixtures/repositories/audit-error.repository.ts new file mode 100644 index 0000000..2fcbced --- /dev/null +++ b/src/__tests__/acceptance/fixtures/repositories/audit-error.repository.ts @@ -0,0 +1,10 @@ +import {TestAuditLog} from '../models/audit.model'; + +export class TestAuditLogErrorRepository { + create(_audit: TestAuditLog) { + return Promise.reject('Error'); + } + createAll(_audit: TestAuditLog[]) { + return Promise.reject('Error'); + } +} diff --git a/src/__tests__/acceptance/fixtures/repositories/audit.repository.ts b/src/__tests__/acceptance/fixtures/repositories/audit.repository.ts new file mode 100644 index 0000000..38a2e3f --- /dev/null +++ b/src/__tests__/acceptance/fixtures/repositories/audit.repository.ts @@ -0,0 +1,11 @@ +import {DefaultCrudRepository, juggler} from '@loopback/repository'; +import {TestAuditLog} from '../models/audit.model'; + +export class TestAuditLogRepository extends DefaultCrudRepository< + TestAuditLog, + typeof TestAuditLog.prototype.id +> { + constructor(dataSource: juggler.DataSource) { + super(TestAuditLog, dataSource); + } +} diff --git a/src/__tests__/acceptance/fixtures/repositories/test.repository.ts b/src/__tests__/acceptance/fixtures/repositories/test.repository.ts new file mode 100644 index 0000000..24ff48a --- /dev/null +++ b/src/__tests__/acceptance/fixtures/repositories/test.repository.ts @@ -0,0 +1,33 @@ +import {Constructor, Getter} from '@loopback/core'; +import {DefaultCrudRepository} from '@loopback/repository'; +import {TestDataSource} from '../datasources/test.datasource'; +import {TestModel, TestModelRelations} from '../models/test.model'; +import {IAuthUser} from 'loopback4-authentication'; +import {AuditRepositoryMixin, IAuditMixinOptions} from '../../../..'; +import {TestAuditLogRepository} from './audit.repository'; + +export const testAuditOpts: IAuditMixinOptions = { + actionKey: 'Item_Logs', +}; + +export class TestRepository extends AuditRepositoryMixin< + TestModel, + typeof TestModel.prototype.id, + TestModelRelations, + number | string, + Constructor< + DefaultCrudRepository< + TestModel, + typeof TestModel.prototype.id, + TestModelRelations + > + > +>(DefaultCrudRepository, testAuditOpts) { + constructor( + dataSource: TestDataSource, + public getCurrentUser: Getter, + public getAuditLogRepository: Getter, + ) { + super(TestModel, dataSource); + } +} diff --git a/src/__tests__/unit/audit.mixin.unit.ts b/src/__tests__/unit/audit.mixin.unit.ts new file mode 100644 index 0000000..5bcd056 --- /dev/null +++ b/src/__tests__/unit/audit.mixin.unit.ts @@ -0,0 +1,542 @@ +import {Constructor} from '@loopback/core'; +import {expect, sinon} from '@loopback/testlab'; +import { + Action, + AuditLog, + AuditRepositoryMixin, + IAuditMixinOptions, +} from '../..'; +import { + MockClass, + mockClassMethodCall, + resetMethodCalls, +} from './fixtures/mockClass'; +import {MockModel} from './fixtures/mockModel'; +import {EntityCrudRepository} from '@loopback/repository'; +import {mockData, mockDataArray, resetMockData} from './fixtures/mockData'; +import {consoleMessage} from '../acceptance/audit.mixin.acceptance'; + +let auditData: AuditLog; +let auditDataArray: AuditLog[]; +let auditCreateCalled = false; +let auditCreateAllCalled = false; + +class MockAuditRepo { + create(audit: AuditLog) { + auditData = audit; + auditCreateCalled = true; + return Promise.resolve('Audit Created'); + } + createAll(auditArray: AuditLog[]) { + auditDataArray = auditArray; + auditCreateAllCalled = true; + return Promise.resolve('Audit Created'); + } +} + +class MockAuditRepoError { + create(audit: AuditLog) { + auditCreateCalled = true; + return Promise.reject('Error'); + } + createAll(auditArray: AuditLog[]) { + auditCreateAllCalled = true; + return Promise.reject('Error'); + } +} + +const mockOpts: IAuditMixinOptions = { + actionKey: 'Test_Logs', +}; +const mockUser = { + id: 'testCurrentUserId', + name: 'testCurrentUserName', +}; + +describe('Audit Mixin', () => { + const returnedMixedClass = AuditRepositoryMixin< + MockModel, + typeof MockModel.prototype.id, + {}, + string, + Constructor< + EntityCrudRepository + > + >(MockClass, mockOpts); + + const returnedMixedClassInstance = new returnedMixedClass(); + returnedMixedClassInstance.getCurrentUser = sinon.stub().resolves(mockUser); + returnedMixedClassInstance.getAuditLogRepository = sinon + .stub() + .resolves(new MockAuditRepo()); + + //for checking message in case Audit can't be made due to any reason + + const returnedMixedClassErrorInstance = new returnedMixedClass(); + returnedMixedClassErrorInstance.getCurrentUser = sinon + .stub() + .resolves(mockUser); + returnedMixedClassErrorInstance.getAuditLogRepository = sinon + .stub() + .resolves(new MockAuditRepoError()); + + beforeEach(() => { + auditCreateCalled = false; + auditCreateAllCalled = false; + resetMethodCalls(); + resetMockData(); + }); + + it('should return newly created record on calling create method with noAudit option set', async () => { + const result = await returnedMixedClassInstance.create(mockData, { + noAudit: true, + }); + expect(result).to.be.equal(mockData); + expect(auditCreateCalled).to.be.false(); + //check if super class method called + expect(mockClassMethodCall.create).to.be.true(); + }); + it('should return newly created record and create appropriate Audit Log on calling create method', async () => { + const result = await returnedMixedClassInstance.create(mockData); + expect(result).to.be.equal(mockData); + expect(auditCreateCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.create).to.be.true(); + + // checking audit data sent + const expectedAuditLog = { + action: Action.INSERT_ONE, + actedOn: MockModel.name, + actionKey: mockOpts.actionKey, + entityId: mockData.id, + actor: mockUser.id, + before: undefined, + after: mockData.toObject(), + }; + expect(auditData).to.containDeep(expectedAuditLog); + expect(auditData.actedAt).to.be.Date(); + }); + + it('should return newly created records on calling createAll method with noAudit option set', async () => { + const result = await returnedMixedClassInstance.createAll(mockDataArray, { + noAudit: true, + }); + expect(result).to.be.equal(mockDataArray); + expect(auditCreateAllCalled).to.be.false(); + //check if super class method called + expect(mockClassMethodCall.createAll).to.be.true(); + }); + + it('should return newly created records and create appropriate Audit Logs on calling createAll method', async () => { + const result = await returnedMixedClassInstance.createAll(mockDataArray); + expect(result).to.be.equal(mockDataArray); + expect(auditCreateAllCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.createAll).to.be.true(); + + //checking audit data sent + + auditDataArray.forEach((audit, index) => { + const expectedAuditLog = { + action: Action.INSERT_MANY, + actedOn: MockModel.name, + actionKey: mockOpts.actionKey, + entityId: mockDataArray[index].id, + actor: mockUser.id, + before: undefined, + after: mockDataArray[index].toObject(), + }; + expect(audit).to.containDeep(expectedAuditLog); + expect(audit.actedAt).to.be.Date(); + }); + }); + + it('should delete record on calling deleteById method with noAudit option set', async () => { + await returnedMixedClassInstance.deleteById(mockData.id, {noAudit: true}); + expect(auditCreateCalled).to.be.false(); + //check if super class method called + expect(mockClassMethodCall.deleteById).to.be.true(); + }); + + it('should delete record and create appropriate Audit Log on calling deleteById method', async () => { + await returnedMixedClassInstance.deleteById(mockData.id); + expect(auditCreateCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.deleteById).to.be.true(); + + //checking audit data sent + + const expectedAuditLog = { + action: Action.DELETE_ONE, + actedOn: MockModel.name, + actionKey: mockOpts.actionKey, + entityId: mockData.id, + actor: mockUser.id, + before: mockData.toObject(), + after: undefined, + }; + expect(auditData).to.containDeep(expectedAuditLog); + expect(auditData.actedAt).to.be.Date(); + }); + it('should delete records on calling deleteAll method with noAudit option set', async () => { + const result = await returnedMixedClassInstance.deleteAll( + {}, + {noAudit: true}, + ); + expect(result.count).to.be.equal(mockDataArray.length); + expect(auditCreateAllCalled).to.be.false(); + + //check if super class method called + expect(mockClassMethodCall.deleteAll).to.be.true(); + }); + + it('should delete records and create appropriate Audit Logs on calling deleteAll method', async () => { + await returnedMixedClassInstance.deleteAll(); + expect(auditCreateAllCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.deleteAll).to.be.true(); + + //checking audit data sent + + auditDataArray.forEach((audit, index) => { + const expectedAuditLog = { + action: Action.DELETE_MANY, + actedOn: MockModel.name, + actionKey: mockOpts.actionKey, + entityId: mockDataArray[index].id, + actor: mockUser.id, + before: mockDataArray[index].toObject(), + after: undefined, + }; + expect(audit.actedAt).to.be.Date(); + expect(audit).to.containDeep(expectedAuditLog); + }); + }); + it('should update record on calling updateById method with noAudit option set', async () => { + await returnedMixedClassInstance.updateById( + mockData.id, + { + itemName: 'replacedTestItemName', + description: 'replacedTestItemDescription', + }, + {noAudit: true}, + ); + + expect(auditCreateCalled).to.be.false(); + + //check if super class method called + expect(mockClassMethodCall.updateById).to.be.true(); + }); + it('should update record and create appropriate Audit Log on calling updateById method', async () => { + const beforeMockData = Object.assign({}, mockData); + + await returnedMixedClassInstance.updateById(mockData.id, { + itemName: 'replacedTestItemName', + description: 'replacedTestItemDescription', + }); //this will update mock data + + expect(auditCreateCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.updateById).to.be.true(); + + //checking audit data sent + + const expectedAuditLog = { + action: Action.UPDATE_ONE, + actedOn: MockModel.name, + actionKey: mockOpts.actionKey, + entityId: mockData.id, + actor: mockUser.id, + before: beforeMockData.toObject(), + after: mockData.toObject(), + }; + expect(auditData).to.containDeep(expectedAuditLog); + expect(auditData.actedAt).to.be.Date(); + }); + + it('should update record on calling replaceById method with noAudit option set', async () => { + await returnedMixedClassInstance.replaceById( + mockData.id, + { + itemName: 'replacedTestItemName', + description: 'replacedTestItemDescription', + }, + {noAudit: true}, + ); + + expect(auditCreateCalled).to.be.false(); + + //check if super class method called + expect(mockClassMethodCall.replaceById).to.be.true(); + }); + it('should update record and create appropriate Audit Log on calling replaceById method', async () => { + const beforeMockData = Object.assign({}, mockData); + + await returnedMixedClassInstance.replaceById(mockData.id, { + itemName: 'replacedTestItemName', + description: 'replacedTestItemDescription', + }); //this will update mock data + + expect(auditCreateCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.replaceById).to.be.true(); + + //checking audit data sent + const expectedAuditLog = { + action: Action.UPDATE_ONE, + actedOn: MockModel.name, + actionKey: mockOpts.actionKey, + entityId: mockData.id, + actor: mockUser.id, + before: beforeMockData.toObject(), + after: mockData.toObject(), + }; + expect(auditData).to.containDeep(expectedAuditLog); + expect(auditData.actedAt).to.be.Date(); + }); + + it('should update records on calling updateAll method with noAudit option set', async () => { + const result = await returnedMixedClassInstance.updateAll( + { + itemName: 'replacedTestItemName', + description: 'replacedTestItemDescription', + }, + { + id: { + inq: mockDataArray.map(item => item.id), + }, + }, + {noAudit: true}, + ); + expect(result.count).to.be.equal(mockDataArray.length); + expect(auditCreateAllCalled).to.be.false(); + + //check if super class method called + expect(mockClassMethodCall.updateAll).to.be.true(); + }); + + it('should update records and create appropriate Audit Logs on calling updateAll method', async () => { + const beforeMockDataArray = mockDataArray.map(d => { + return d.toObject(); + }); + await returnedMixedClassInstance.updateAll( + { + itemName: 'replacedTestItemName', + description: 'replacedTestItemDescription', + }, + { + id: { + inq: mockDataArray.map(item => item.id), + }, + }, + ); + expect(auditCreateAllCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.updateAll).to.be.true(); + + auditDataArray.forEach((audit, index) => { + const expectedAuditLog = { + action: Action.UPDATE_MANY, + actedOn: MockModel.name, + actionKey: mockOpts.actionKey, + entityId: mockDataArray[index].id, + actor: mockUser.id, + before: beforeMockDataArray[index], + after: mockDataArray[index].toObject(), + }; + expect(audit).to.containDeep(expectedAuditLog); + expect(audit.actedAt).to.be.Date(); + }); + }); + + it(`should log appropriate message when audit log can't be created on calling create method`, async () => { + await returnedMixedClassErrorInstance.create(mockData); + + expect(auditCreateCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.create).to.be.true(); + + //checking audit message when create audit fails + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.INSERT_ONE}"`, + `"after":${JSON.stringify(mockData.toObject())}`, + `"entityId":"${mockData.id}"`, + `"actedOn":"${MockModel.name}"`, + `"actionKey":"${mockOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + + it(`should log appropriate message when audit log can't be created on calling createAll method`, async () => { + await returnedMixedClassErrorInstance.createAll(mockDataArray); + + expect(auditCreateAllCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.createAll).to.be.true(); + + //check audit message when create audit fails + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.INSERT_MANY}"`, + `"after":`, + `"entityId":`, + `"actedOn":"${MockModel.name}"`, + `"actionKey":"${mockOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + + it(`should log appropriate message when audit log can't be created on calling deleteAll method`, async () => { + await returnedMixedClassErrorInstance.deleteAll(); + expect(auditCreateAllCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.deleteAll).to.be.true(); + + //check audit message when create audit fails + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.DELETE_MANY}"`, + `"before":`, + `"entityId":`, + `"actedOn":"${MockModel.name}"`, + `"actionKey":"${mockOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + + it(`should log appropriate message when audit log can't be created on calling updateAll method`, async () => { + await returnedMixedClassErrorInstance.updateAll( + { + itemName: 'replacedTestItemName', + description: 'replacedTestDescription', + }, + { + id: { + inq: mockDataArray.map(item => item.id), + }, + }, + ); + expect(auditCreateAllCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.updateAll).to.be.true(); + + //check audit message when create audit fails + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.UPDATE_MANY}"`, + `"before":`, + `"after":`, + `"entityId":`, + `"actedOn":"${MockModel.name}"`, + `"actionKey":"${mockOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + + it(`should log appropriate message when audit log can't be created on calling updateById method`, async () => { + const beforeMockData = Object.assign({}, mockData); + await returnedMixedClassErrorInstance.updateById(mockData.id, { + itemName: 'replacedTestItemName', + description: 'replacedTestDescription', + }); + + expect(auditCreateCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.updateById).to.be.true(); + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.UPDATE_ONE}"`, + `"before":${JSON.stringify(beforeMockData.toObject())}`, + `"after":${JSON.stringify(mockData.toObject())}`, + `"entityId":"${mockData.id}"`, + `"actedOn":"${MockModel.name}"`, + `"actionKey":"${mockOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + + it(`should log appropriate message when audit log can't be created on calling replaceById method`, async () => { + const beforeMockData = Object.assign({}, mockData); + await returnedMixedClassErrorInstance.replaceById(mockData.id, { + itemName: 'replacedTestItemName', + description: 'replacedTestDescription', + }); + + expect(auditCreateCalled).to.be.true(); + + //check if super class method called + expect(mockClassMethodCall.replaceById).to.be.true(); + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.UPDATE_ONE}"`, + `"before":${JSON.stringify(beforeMockData.toObject())}`, + `"after":${JSON.stringify(mockData.toObject())}`, + `"entityId":"${mockData.id}"`, + `"actedOn":"${MockModel.name}"`, + `"actionKey":"${mockOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); + + it(`should log appropriate message when audit log can't be created on calling deleteById method`, async () => { + resetMockData(); + const beforeMockData = Object.assign({}, mockData); + await returnedMixedClassErrorInstance.deleteById(mockData.id); + expect(auditCreateCalled).to.be.true(); + + //check if super class method called + + expect(mockClassMethodCall.deleteById).to.be.true(); + + const expectedConsoleMessageValues = [ + `Audit failed for data =>`, + `"actedAt":`, + `"actor":"${mockUser.id}"`, + `"action":"${Action.DELETE_ONE}"`, + `"before":${JSON.stringify(beforeMockData.toObject())}`, + `"entityId":"${mockData.id}"`, + `"actedOn":"${MockModel.name}"`, + `"actionKey":"${mockOpts.actionKey}"`, + ]; + expectedConsoleMessageValues.forEach(expectedValue => { + expect(consoleMessage.includes(expectedValue)).to.be.true(); + }); + }); +}); diff --git a/src/__tests__/unit/fixtures/mockClass.ts b/src/__tests__/unit/fixtures/mockClass.ts new file mode 100644 index 0000000..b8ac5fa --- /dev/null +++ b/src/__tests__/unit/fixtures/mockClass.ts @@ -0,0 +1,175 @@ +import { + EntityCrudRepository, + DataObject, + AnyObject, + Command, + Count, + Entity, + Filter, + FilterExcludingWhere, + InclusionResolver, + PositionalParameters, + Where, +} from '@loopback/repository'; +import {mockData, mockDataArray} from './mockData'; +import {MockModel} from './mockModel'; + +export class MockClass + implements EntityCrudRepository { + entityClass: typeof Entity & {prototype: MockModel} = MockModel; + inclusionResolvers: Map>; + + save(entity: DataObject, options?: AnyObject): Promise { + throw new Error('Method not implemented.'); + } + update(entity: DataObject, options?: AnyObject): Promise { + throw new Error('Method not implemented.'); + } + delete(entity: DataObject, options?: AnyObject): Promise { + throw new Error('Method not implemented.'); + } + findById( + id: string | undefined, + filter?: FilterExcludingWhere, + options?: AnyObject, + ): Promise { + return new Promise(resolve => { + const mockDataToReturn = Object.assign({}, mockData); + resolve(mockDataToReturn); + }); + } + updateById( + id: string | undefined, + data: DataObject, + options?: AnyObject, + ): Promise { + mockClassMethodCall.updateById = true; + + if (data.id) { + mockData.id = data.id; + } + if (data.itemName) { + mockData.itemName = data.itemName; + } + if (data.description) { + mockData.description = data.description; + } + + return new Promise(resolve => { + resolve(); + }); + } + replaceById( + id: string | undefined, + data: DataObject, + options?: AnyObject, + ): Promise { + mockClassMethodCall.replaceById = true; + + if (data.id) { + mockData.id = data.id; + } + if (data.itemName) { + mockData.itemName = data.itemName; + } + if (data.description) { + mockData.description = data.description; + } + + return new Promise(resolve => { + resolve(); + }); + } + deleteById(id: string | undefined, options?: AnyObject): Promise { + mockClassMethodCall.deleteById = true; + return new Promise(resolve => { + resolve(); + }); + } + exists(id: string | undefined, options?: AnyObject): Promise { + throw new Error('Method not implemented.'); + } + execute( + command: Command, + parameters: AnyObject | PositionalParameters, + options?: AnyObject, + ): Promise { + throw new Error('Method not implemented.'); + } + create( + dataObject: DataObject, + options?: AnyObject, + ): Promise { + mockClassMethodCall.create = true; + return new Promise(resolve => { + resolve(mockData); + }); + } + createAll( + dataObjects: DataObject[], + options?: AnyObject, + ): Promise { + mockClassMethodCall.createAll = true; + return new Promise(resolve => { + resolve(mockDataArray); + }); + } + find(filter?: Filter, options?: AnyObject): Promise { + return new Promise(resolve => { + const mockDataArrayToReturn: MockModel[] = []; + mockDataArray.forEach(data => { + mockDataArrayToReturn.push(Object.assign({}, data)); + }); + resolve(mockDataArrayToReturn); + }); + } + updateAll( + dataObject: DataObject, + where?: Where, + options?: AnyObject, + ): Promise { + mockClassMethodCall.updateAll = true; + + mockDataArray.forEach(data => { + if (dataObject.id) { + data.id = dataObject.id; + } + if (dataObject.itemName) { + data.itemName = dataObject.itemName; + } + if (dataObject.description) { + data.description = dataObject.description; + } + }); + return new Promise(resolve => { + resolve({count: mockDataArray.length}); + }); + } + deleteAll(where?: Where, options?: AnyObject): Promise { + mockClassMethodCall.deleteAll = true; + return new Promise(resolve => { + resolve({count: mockDataArray.length}); + }); + } + count(where?: Where, options?: AnyObject): Promise { + throw new Error('Method not implemented.'); + } +} +export const mockClassMethodCall = { + create: false, + createAll: false, + deleteById: false, + deleteAll: false, + updateById: false, + replaceById: false, + updateAll: false, +}; +export function resetMethodCalls() { + mockClassMethodCall.create = false; + mockClassMethodCall.createAll = false; + mockClassMethodCall.deleteById = false; + mockClassMethodCall.deleteAll = false; + mockClassMethodCall.updateById = false; + mockClassMethodCall.replaceById = false; + mockClassMethodCall.updateAll = false; +} diff --git a/src/__tests__/unit/fixtures/mockData.ts b/src/__tests__/unit/fixtures/mockData.ts new file mode 100644 index 0000000..29c81e2 --- /dev/null +++ b/src/__tests__/unit/fixtures/mockData.ts @@ -0,0 +1,116 @@ +import {MockModel} from './mockModel'; + +export const mockData: MockModel = { + id: 'testId', + itemName: 'testItemName', + description: 'testDescription', + getId: function () { + return this.id; + }, + getIdObject: function (): Object { + return {id: this.id}; + }, + toJSON: function (): Object { + return { + id: this.id, + itemName: this.itemName, + description: this.description, + }; + }, + toObject: function (): Object { + return { + id: this.id, + itemName: this.itemName, + description: this.description, + }; + }, +}; + +export const mockDataArray: MockModel[] = [ + { + id: 'testId1', + itemName: 'testItemName1', + description: 'testDescription1', + getId: function () { + return this.id; + }, + getIdObject: function (): Object { + return {id: this.id}; + }, + toJSON: function (): Object { + return { + id: this.id, + itemName: this.itemName, + description: this.description, + }; + }, + toObject: function (): Object { + return { + id: this.id, + itemName: this.itemName, + description: this.description, + }; + }, + }, + { + id: 'testId2', + itemName: 'testItemName2', + description: 'testDescription2', + getId: function () { + return this.id; + }, + getIdObject: function (): Object { + return {id: this.id}; + }, + toJSON: function (): Object { + return { + id: this.id, + itemName: this.itemName, + description: this.description, + }; + }, + toObject: function (): Object { + return { + id: this.id, + itemName: this.itemName, + description: this.description, + }; + }, + }, + { + id: 'testId3', + itemName: 'testItemName3', + description: 'testDescription3', + getId: function () { + return this.id; + }, + getIdObject: function (): Object { + return {id: this.id}; + }, + toJSON: function (): Object { + return { + id: this.id, + itemName: this.itemName, + description: this.description, + }; + }, + toObject: function (): Object { + return { + id: this.id, + itemName: this.itemName, + description: this.description, + }; + }, + }, +]; + +export function resetMockData() { + mockData.id = 'testId'; + mockData.itemName = 'testItemName'; + mockData.description = 'testItemDescription'; + mockDataArray.forEach((data, index) => { + data.id = 'testId' + (index + 1); + data.itemName = 'testItemName' + (index + 1); + data.description = 'testItemDescription' + (index + 1); + }); +} diff --git a/src/__tests__/unit/fixtures/mockModel.ts b/src/__tests__/unit/fixtures/mockModel.ts new file mode 100644 index 0000000..f83643b --- /dev/null +++ b/src/__tests__/unit/fixtures/mockModel.ts @@ -0,0 +1,22 @@ +import {Entity, property, model} from '@loopback/repository'; +@model() +export class MockModel extends Entity { + @property({ + type: 'string', + id: true, + generated: false, + }) + id: string; + @property({ + type: 'string', + id: true, + generated: false, + }) + itemName: string; + @property({ + type: 'string', + id: true, + generated: false, + }) + description: string; +}