diff --git a/package-lock.json b/package-lock.json index f7e68b54..f890c8df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1285,7 +1285,6 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.4.tgz", "integrity": "sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA==", - "optional": true, "requires": { "lodash.camelcase": "^4.3.0", "protobufjs": "^6.8.6" @@ -1583,6 +1582,15 @@ "@hapi/hoek": "^9.0.0" } }, + "@hapi/validate": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-1.1.2.tgz", + "integrity": "sha512-ojg3iE/haKh8aCZFObkOzuJ1vQ8NP+EiuibliJKe01IMstBPXQc4Xl08+8zqAL+iZSZKp1TaWdwaNSzq8HIMKA==", + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0" + } + }, "@hapi/vise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-4.0.0.tgz", @@ -2493,6 +2501,29 @@ } } }, + "@mojaloop/central-services-error-handling": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/@mojaloop/central-services-error-handling/-/central-services-error-handling-10.6.0.tgz", + "integrity": "sha512-93Jbz/CWNxMiA6/x+KmQezf7C/K3etIAwmXdeAjR9BBDM9xJt1nGfRDovXJZzqV5pTgh9ytGen7A3ub6oVqcQA==", + "requires": { + "@mojaloop/sdk-standard-components": "10.3.2", + "lodash": "4.17.19" + }, + "dependencies": { + "@mojaloop/sdk-standard-components": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@mojaloop/sdk-standard-components/-/sdk-standard-components-10.3.2.tgz", + "integrity": "sha512-O5DqUL+ncS718nFDFUMx8QO0pmTmg+/CNYuaXPrFfHDgf8c05mgSjg6Z8wt69Auwph6WXWaNjKTQRqZG2/BDdQ==", + "requires": { + "base64url": "3.0.1", + "fast-safe-stringify": "^2.0.7", + "ilp-packet": "2.2.0", + "jsonwebtoken": "8.5.1", + "jws": "4.0.0" + } + } + } + }, "@mojaloop/central-services-logger": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/@mojaloop/central-services-logger/-/central-services-logger-10.6.0.tgz", @@ -2503,6 +2534,121 @@ "winston": "3.3.3" } }, + "@mojaloop/central-services-metrics": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@mojaloop/central-services-metrics/-/central-services-metrics-9.5.0.tgz", + "integrity": "sha512-4wba5JCNhmevBEHAPl+BmMqTmfT/7lOxbuRlziyAFhcySrZpCQhINMwyGm1CmNlldsDtp8rHaL5inQzKAGsBXA==", + "requires": { + "prom-client": "11.5.3" + } + }, + "@mojaloop/central-services-shared": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@mojaloop/central-services-shared/-/central-services-shared-11.3.2.tgz", + "integrity": "sha512-2ZAZsYpZgp50O9h6cqtUjC3ozIjFDqjCmduyUaxk9BjfInTBTG+8SMZNrzhWKVGm2wgDmTpSpvo778g3GPyGdQ==", + "requires": { + "@hapi/catbox": "11.1.1", + "@hapi/catbox-memory": "5.0.0", + "@mojaloop/central-services-error-handling": "10.6.0", + "@mojaloop/central-services-logger": "10.6.0", + "@mojaloop/central-services-metrics": "9.5.0", + "@mojaloop/event-sdk": "10.6.0", + "ajv": "6.12.4", + "ajv-keywords": "3.5.2", + "axios": "0.20.0", + "base64url": "3.0.1", + "clone": "2.1.2", + "data-urls": "2.0.0", + "dotenv": "8.2.0", + "env-var": "6.3.0", + "immutable": "3.8.2", + "lodash": "4.17.20", + "mustache": "4.0.1", + "openapi-backend": "3.5.2", + "raw-body": "2.4.1", + "uuid4": "2.0.2" + }, + "dependencies": { + "@hapi/catbox": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-11.1.1.tgz", + "integrity": "sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ==", + "requires": { + "@hapi/boom": "9.x.x", + "@hapi/hoek": "9.x.x", + "@hapi/podium": "4.x.x", + "@hapi/validate": "1.x.x" + } + }, + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "openapi-backend": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-3.5.2.tgz", + "integrity": "sha512-B6VptLEvyDstDd2bY+7/Lk6IK1syEzHMyAZG7U4A+UiGLD+/NN39axVhFD+8ulBjo037AcrZ3OeQzoMV+nigPQ==", + "requires": { + "ajv": "^6.10.0", + "bath-es5": "^3.0.3", + "cookie": "^0.4.0", + "lodash": "^4.17.15", + "mock-json-schema": "^1.0.7", + "openapi-schema-validation": "^0.4.2", + "openapi-types": "^1.3.4", + "qs": "^6.9.3", + "swagger-parser": "^9.0.1" + } + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + } + } + }, + "@mojaloop/event-sdk": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/@mojaloop/event-sdk/-/event-sdk-10.6.0.tgz", + "integrity": "sha512-mDVow/3WDILDUF2v32fqcOZAoRQCOZX8D2fJF3kHvZLGthU9ydNPHK118aVibw76XAyq6E6UbxHMXg3ZUPBlhg==", + "requires": { + "@grpc/proto-loader": "0.5.4", + "@mojaloop/central-services-logger": "10.6.0", + "brototype": "0.0.6", + "error-callsites": "2.0.3", + "grpc": "1.24.3", + "lodash": "4.17.19", + "moment": "2.27.0", + "parse-strings-in-object": "2.0.0", + "protobufjs": "6.9.0", + "rc": "1.2.8", + "serialize-error": "4.1.0", + "sinon": "9.0.2", + "traceparent": "1.0.0", + "tslib": "2.0.0", + "uuid4": "2.0.2", + "winston": "3.3.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } + } + }, "@mojaloop/sdk-standard-components": { "version": "11.9.0", "resolved": "https://registry.npmjs.org/@mojaloop/sdk-standard-components/-/sdk-standard-components-11.9.0.tgz", @@ -2614,32 +2760,27 @@ "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", - "optional": true + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" }, "@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "optional": true + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" }, "@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "optional": true + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" }, "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", - "optional": true + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -2648,32 +2789,27 @@ "@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", - "optional": true + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", - "optional": true + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", - "optional": true + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", - "optional": true + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", - "optional": true + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "@sindresorhus/is": { "version": "0.14.0", @@ -2685,7 +2821,6 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", "integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==", - "dev": true, "requires": { "type-detect": "4.0.8" } @@ -2694,11 +2829,34 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", - "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" } }, + "@sinonjs/formatio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" + } + }, + "@sinonjs/samsam": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.1.0.tgz", + "integrity": "sha512-42nyaQOVunX5Pm6GRJobmzbS7iLI+fhERITnETXzzwDZh+TtDr/Au3yAvXVjFmZ4wEUaE4Y3NFZfKv0bV0cbtg==", + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" + }, "@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -2760,6 +2918,15 @@ "integrity": "sha512-PH7bfkt1nu4pnlxz+Ws+wwJJF1HE12W3ia+Iace2JT7q56DLH3hbyjOJyNHJYRxk3PkKaC36fHfHKyeG1rMgCA==", "dev": true }, + "@types/bytebuffer": { + "version": "5.0.41", + "resolved": "https://registry.npmjs.org/@types/bytebuffer/-/bytebuffer-5.0.41.tgz", + "integrity": "sha512-Mdrv4YcaHvpkx25ksqqFaezktx3yZRcd51GZY0rY/9avyaqZdiT/GiWRhfrJhMpgzXqTOSHgGvsumGxJFNiZZA==", + "requires": { + "@types/long": "*", + "@types/node": "*" + } + }, "@types/catbox": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/@types/catbox/-/catbox-10.0.6.tgz", @@ -2963,8 +3130,7 @@ "@types/long": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", - "optional": true + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/mime-db": { "version": "1.43.0", @@ -3186,14 +3352,12 @@ "abab": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", - "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", - "dev": true + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==" }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "abort-controller": { "version": "3.0.0", @@ -3299,6 +3463,11 @@ "uri-js": "^4.2.2" } }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -3359,8 +3528,7 @@ "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "ansi-styles": { "version": "3.2.1", @@ -3384,14 +3552,12 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -3491,6 +3657,15 @@ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", "dev": true }, + "ascli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", + "integrity": "sha1-vPpZdKYvGOgcq660lzKrSoj5Brw=", + "requires": { + "colour": "~0.7.1", + "optjs": "~3.2.2" + } + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -3586,6 +3761,14 @@ "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", "dev": true }, + "axios": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz", + "integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-jest": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.1.0.tgz", @@ -3723,8 +3906,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -3822,6 +4004,11 @@ "file-uri-to-path": "1.0.0" } }, + "bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -3894,7 +4081,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3909,6 +4095,11 @@ "fill-range": "^7.0.1" } }, + "brototype": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/brototype/-/brototype-0.0.6.tgz", + "integrity": "sha1-mz8HNkeDOXuPHEvuehQZk1ZuS0Q=" + }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -3966,6 +4157,26 @@ "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", "dev": true }, + "bytebuffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", + "integrity": "sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0=", + "requires": { + "long": "~3" + }, + "dependencies": { + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=" + } + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, "cacache": { "version": "15.0.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.4.tgz", @@ -4281,6 +4492,11 @@ } } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -4299,8 +4515,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collect-v8-coverage": { "version": "1.0.1", @@ -4364,6 +4579,11 @@ "text-hex": "1.0.x" } }, + "colour": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", + "integrity": "sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g=" + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4430,8 +4650,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -4461,8 +4680,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "contains-path": { "version": "0.1.0", @@ -5125,7 +5343,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, "requires": { "abab": "^2.0.3", "whatwg-mimetype": "^2.3.0", @@ -5148,7 +5365,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -5326,14 +5542,12 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "detect-indent": { "version": "6.0.0", @@ -5341,6 +5555,11 @@ "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==", "dev": true }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5368,8 +5587,7 @@ "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" }, "diff-sequences": { "version": "25.2.6", @@ -5566,12 +5784,22 @@ "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", "dev": true }, + "env-var": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/env-var/-/env-var-6.3.0.tgz", + "integrity": "sha512-gaNzDZuVaJQJlP2SigAZLu/FieZN5MzdN7lgHNehESwlRanHwGQ/WUtJ7q//dhrj3aGBZM45yEaKOuvSJaf4mA==" + }, "err-code": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=", "dev": true }, + "error-callsites": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/error-callsites/-/error-callsites-2.0.3.tgz", + "integrity": "sha512-v036z4IEffZFE5kBkV5/F2MzhLnG0vuDyN+VXpzCf4yWXvX/1WJCI0A+TGTr8HWzBfCw5k8gr9rwAo09V+obTA==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6501,6 +6729,11 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "follow-redirects": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -6577,8 +6810,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.1.3", @@ -6601,7 +6833,6 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -6616,14 +6847,12 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6632,7 +6861,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6643,7 +6871,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7304,7 +7531,6 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -7457,6 +7683,106 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "dev": true }, + "grpc": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.24.3.tgz", + "integrity": "sha512-EDemzuZTfhM0hgrXqC4PtR76O3t+hTIYJYR5vgiW0yt2WJqo4mhxUqZUirzUQz34Psz7dbLp38C6Cl7Ij2vXRQ==", + "requires": { + "@types/bytebuffer": "^5.0.40", + "lodash.camelcase": "^4.3.0", + "lodash.clone": "^4.5.0", + "nan": "^2.13.2", + "node-pre-gyp": "^0.15.0", + "protobufjs": "^5.0.3" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "protobufjs": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.3.tgz", + "integrity": "sha512-55Kcx1MhPZX0zTbVosMQEO5R6/rikNXd9b6RQK4KSPcrSIIwoXTtebIczUrXlwaSrbz4x8XUVThGPob1n8I4QA==", + "requires": { + "ascli": "~1", + "bytebuffer": "~5", + "glob": "^7.0.5", + "yargs": "^3.10.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "requires": { + "camelcase": "^2.0.1", + "cliui": "^3.0.3", + "decamelize": "^1.1.1", + "os-locale": "^1.4.0", + "string-width": "^1.0.1", + "window-size": "^0.1.4", + "y18n": "^3.2.0" + } + } + } + }, "gtoken": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz", @@ -7551,8 +7877,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "has-value": { "version": "1.0.0", @@ -7660,6 +7985,18 @@ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", "dev": true }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, "http-parser-js": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", @@ -7862,7 +8199,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -7877,7 +8213,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "dev": true, "requires": { "minimatch": "^3.0.4" } @@ -7905,6 +8240,11 @@ } } }, + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" + }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -8012,7 +8352,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -8131,6 +8470,11 @@ "loose-envify": "^1.0.0" } }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -12496,6 +12840,11 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", + "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==" + }, "jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -12550,6 +12899,14 @@ "package-json": "^6.3.0" } }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -12910,8 +13267,12 @@ "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", - "optional": true + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=" }, "lodash.clonedeep": { "version": "4.5.0", @@ -12984,8 +13345,7 @@ "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" }, "lodash.template": { "version": "4.5.0", @@ -13145,8 +13505,7 @@ "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "optional": true + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "loose-envify": { "version": "1.4.0", @@ -13461,7 +13820,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -13588,7 +13946,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, "requires": { "minipass": "^2.9.0" }, @@ -13597,7 +13954,6 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -13630,7 +13986,6 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, "requires": { "minimist": "^1.2.5" } @@ -13650,11 +14005,21 @@ "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true }, + "moment": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "mustache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.0.1.tgz", + "integrity": "sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA==" + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -13664,9 +14029,7 @@ "nan": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "dev": true, - "optional": true + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" }, "nanomatch": { "version": "1.2.13", @@ -13699,6 +14062,16 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "needle": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz", + "integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==", + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, "neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", @@ -13717,6 +14090,18 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", + "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, "node-alias": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/node-alias/-/node-alias-1.0.4.tgz", @@ -13900,11 +14285,75 @@ } } }, + "node-pre-gyp": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz", + "integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.3", + "needle": "^2.5.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "requires": { + "minipass": "^2.6.0" + } + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + } + } + }, "nopt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -13966,7 +14415,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "dev": true, "requires": { "npm-normalize-package-bin": "^1.0.1" } @@ -14132,8 +14580,7 @@ "npm-normalize-package-bin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" }, "npm-package-arg": { "version": "8.0.1", @@ -14241,7 +14688,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -14258,8 +14704,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "nwsapi": { "version": "2.2.0", @@ -14276,8 +14721,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", @@ -14468,23 +14912,33 @@ "word-wrap": "~1.2.3" } }, + "optjs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", + "integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4=" + }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -14661,8 +15115,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "2.0.1", @@ -14686,6 +15139,21 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, "path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", @@ -14850,6 +15318,14 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "prom-client": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.3.tgz", + "integrity": "sha512-iz22FmTbtkyL2vt0MdDFY+kWof+S9UB/NACxSn2aJcewtw+EERsen0urSkZ2WrHseNdydsvcxCTAnPcSMZZv4Q==", + "requires": { + "tdigest": "^0.1.1" + } + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -14880,7 +15356,6 @@ "version": "6.9.0", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.9.0.tgz", "integrity": "sha512-LlGVfEWDXoI/STstRDdZZKb/qusoAWUnmLg9R8OLSO473mBLWHowx8clbX5/+mKDEI+v7GzjoK9tRPZMMcoTrg==", - "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -14900,8 +15375,7 @@ "@types/node": { "version": "13.13.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.13.tgz", - "integrity": "sha512-UfvBE9oRCAJVzfR+3eWm/sdLFe/qroAPEXP3GPJ1SehQiEVgZT6NQZWYbPMiJ3UdcKM06v4j+S1lTcdWCmw+3g==", - "optional": true + "integrity": "sha512-UfvBE9oRCAJVzfR+3eWm/sdLFe/qroAPEXP3GPJ1SehQiEVgZT6NQZWYbPMiJ3UdcKM06v4j+S1lTcdWCmw+3g==" } } }, @@ -14988,6 +15462,22 @@ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, + "random-poly-fill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/random-poly-fill/-/random-poly-fill-1.0.1.tgz", + "integrity": "sha512-bMOL0hLfrNs52+EHtIPIXxn2PxYwXb0qjnKruTjXiM/sKfYqj506aB2plFwWW1HN+ri724bAVVGparh4AtlJKw==" + }, + "raw-body": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", + "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -15438,7 +15928,6 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, "requires": { "glob": "^7.1.3" } @@ -15481,8 +15970,7 @@ "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==" }, "sane": { "version": "4.1.0", @@ -15628,8 +16116,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "saxes": { "version": "5.0.1", @@ -15643,8 +16130,7 @@ "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "semver-compare": { "version": "1.0.0", @@ -15681,11 +16167,25 @@ "integrity": "sha512-EjnoLE5OGmDAVV/8YDoN5KiajNadjzIp9BAHOhYeQHt7j0UWxjmgsx4YD48wp4Ue1Qogq38F1GNUJNqF1kKKxA==", "dev": true }, + "serialize-error": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-4.1.0.tgz", + "integrity": "sha512-5j9GgyGsP9vV9Uj1S0lDCvlsd+gc2LEPVK7HHHte7IyPwOD4lVQFeaX143gx3U5AnoCi+wbcb3mvaxVysjpxEw==", + "requires": { + "type-fest": "^0.3.0" + }, + "dependencies": { + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==" + } + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { "version": "2.0.1", @@ -15710,6 +16210,11 @@ } } }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -15772,6 +16277,35 @@ } } }, + "sinon": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.2.tgz", + "integrity": "sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A==", + "requires": { + "@sinonjs/commons": "^1.7.2", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.0.3", + "diff": "^4.0.2", + "nise": "^4.0.1", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -16272,6 +16806,11 @@ } } }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", @@ -16687,6 +17226,14 @@ } } }, + "tdigest": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "requires": { + "bintrees": "1.0.1" + } + }, "teeny-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.3.tgz", @@ -16850,6 +17397,11 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", @@ -16865,11 +17417,18 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", - "dev": true, "requires": { "punycode": "^2.1.1" } }, + "traceparent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/traceparent/-/traceparent-1.0.0.tgz", + "integrity": "sha512-b/hAbgx57pANQ6cg2eBguY3oxD6FGVLI1CC2qoi01RmHR7AYpQHPXTig9FkzbWohEsVuHENZHP09aXuw3/LM+w==", + "requires": { + "random-poly-fill": "^1.0.1" + } + }, "trim-newlines": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", @@ -16996,8 +17555,7 @@ "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" }, "type-fest": { "version": "0.8.1", @@ -17087,6 +17645,11 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -17251,6 +17814,11 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==" }, + "uuid4": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uuid4/-/uuid4-2.0.2.tgz", + "integrity": "sha512-TzsQS8sN1B2m9WojyNp0X/3JL8J2RScnrAJnooNPL6lq3lA02/XdoWysyUgI6rAif0DzkkWk51N6OggujPy2RA==" + }, "v8-compile-cache": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", @@ -17377,14 +17945,12 @@ "whatwg-mimetype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" }, "whatwg-url": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.1.0.tgz", "integrity": "sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==", - "dev": true, "requires": { "lodash.sortby": "^4.7.0", "tr46": "^2.0.2", @@ -17394,8 +17960,7 @@ "webidl-conversions": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" } } }, @@ -17463,7 +18028,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, "requires": { "string-width": "^1.0.2 || 2" }, @@ -17471,14 +18035,12 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -17488,7 +18050,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, "requires": { "ansi-regex": "^3.0.0" } @@ -17504,6 +18065,11 @@ "string-width": "^4.0.0" } }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" + }, "winston": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", diff --git a/package.json b/package.json index 05583695..b76d63aa 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "@hapi/inert": "^6.0.1", "@hapi/vision": "^6.0.0", "@mojaloop/central-services-logger": "^10.6.0", + "@mojaloop/central-services-shared": "^11.3.2", "@mojaloop/sdk-standard-components": "^11.9.0", "@types/uuid": "^8.0.0", "convict": "^6.0.0", diff --git a/src/interface/mojaloop.yaml b/src/interface/mojaloop.yaml index f389026d..0fb8c444 100644 --- a/src/interface/mojaloop.yaml +++ b/src/interface/mojaloop.yaml @@ -270,12 +270,17 @@ paths: $ref: '../../node_modules/@mojaloop/api-snippets/v1.0/openapi3/responses/index.yaml#/501' 503: $ref: '../../node_modules/@mojaloop/api-snippets/v1.0/openapi3/responses/index.yaml#/503' - delete: + patch: tags: - consents - summary: '[Mojaloop Callback] Result of deleting the specified consent' - description: Result of deleting the specified consent - operationId: deleteConsentsById + summary: '[Mojaloop Callback] Result of patching the specified consent (used in unlinking)' + description: Result of patching the specified consent (used in unlinking) + operationId: patchConsentsById + requestBody: + content: + application/json: + schema: + $ref: 'shared.yaml#/components/schemas/PatchConsentsByIdRequest' responses: 200: $ref: '../../node_modules/@mojaloop/api-snippets/v1.0/openapi3/responses/index.yaml#/200' diff --git a/src/interface/shared.yaml b/src/interface/shared.yaml index f1e66450..4a0a6e80 100644 --- a/src/interface/shared.yaml +++ b/src/interface/shared.yaml @@ -98,6 +98,15 @@ components: name: type: string description: Data model for the complex type Participant. + PatchConsentsByIdRequest: + title: PutConsentsByIdRequest + type: object + properties: + status: + type: string + revokedAt: + type: string + description: Data model for the complex type PatchConsentsByIdRequest. PostAuthorizationsRequest: title: PostAuthorizationsRequest type: object diff --git a/src/lib/config.ts b/src/lib/config.ts index c4572a2d..b33b0c6e 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -108,6 +108,13 @@ const config = convict({ default: 'pisp', env: 'MOJALOOP_PARTICIPANT_ID', }, + // TODO: Replace placeholder + pispCallbackUri: { + doc: 'The callback URI sent by PISP deeplinked with the app', + format: String, + default: 'PLACEHOLDER', + env: 'MOJALOOP_CALLBACK_URI', + }, endpoints: { default: { doc: 'Default endpoint to communicate with Mojaloop', diff --git a/src/models/consent.ts b/src/models/consent.ts index 87fb2957..3ecbf1e2 100644 --- a/src/models/consent.ts +++ b/src/models/consent.ts @@ -20,6 +20,7 @@ * Google - Steven Wijaya + - Abhimanyu Kapur -------------- ******/ @@ -40,15 +41,25 @@ export enum ConsentStatus { * Waiting for the user to confirm payee information and provide more * details about the transaction. */ - PENDING_PAYEE_CONFIRMATION = 'PENDING_PAYEE_CONFIRMATION', + PENDING_PARTY_CONFIRMATION = 'PENDING_PARTY_CONFIRMATION', /** * Waiting for the user to authorize the consent. */ - AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED', + AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED', /** - * The consent is authorized and active. + * The consent is granted and active. + */ + CONSENT_GRANTED = 'CONSENT_GRANTED', + + /** + * The consent is ACTIVE and challenge has been generated + */ + CHALLENGE_GENERATED = 'CHALLENGE_GENERATED', + + /** + * The consent is ACTIVE and challenge has been verified */ ACTIVE = 'ACTIVE', @@ -56,6 +67,11 @@ export enum ConsentStatus { * The consent is revoked and no longer valid. */ REVOKED = 'REVOKED', + + /** + * The consent is requested to be revoked for unlinking. + */ + REVOKE_REQUESTED = 'REVOKE_REQUESTED', } export interface Consent { diff --git a/src/models/errors.ts b/src/models/errors.ts index 66f421cb..6a99baf2 100644 --- a/src/models/errors.ts +++ b/src/models/errors.ts @@ -37,3 +37,8 @@ export class MissingConsentFieldsError extends Error { this.consent = consent } } +export class InvalidConsentStatusError extends Error { + public constructor(status: string, id: string) { + super(`Invalid Consent Status Provided! ID: ${id} | Status: ${status}`) + } +} diff --git a/src/repositories/consent.ts b/src/repositories/consent.ts index 2a10730b..9db0d952 100644 --- a/src/repositories/consent.ts +++ b/src/repositories/consent.ts @@ -19,27 +19,47 @@ - Name Surname * Google - - Steven Wijaya + - Abhimanyu Kapur -------------- ******/ -/* eslint-disable @typescript-eslint/no-explicit-any */ +/* istanbul ignore file */ +// TODO: BDD Testing will covered in separate ticket #1702 import firebase from '~/lib/firebase' import { Consent } from '~/models/consent' import { logger } from '~/shared/logger' export interface IConsentRepository { + /** + * Updates a consent document based on a unique identifier. + * + * @param id Id for the consent document that needs to be updated. + * @param data Document fields that are about to be updated. + */ + updateConsentById(id: string, data: Record): Promise + /** * Retrieves a consent document based on its consent ID. * * @param id Consent ID of the document that needs to be retrieved. */ - getByConsentId(id: string): Promise + getConsentById(id: string): Promise + + /** + * Updates one or more consent documents based on the given conditions. + * + * @param conditions Conditions for the documents that need to be updated. + * @param data Document fields that are about to be updated. + */ + updateConsent( + conditions: Record, + data: Record + ): Promise } export class FirebaseConsentRepository implements IConsentRepository { - async getByConsentId(id: string): Promise { + async getConsentById(id: string): Promise { return new Promise((resolve, reject) => { firebase .firestore() @@ -58,6 +78,51 @@ export class FirebaseConsentRepository implements IConsentRepository { }) }) } + + async updateConsentById( + id: string, + data: Record + ): Promise { + await firebase.firestore().collection('consents').doc(id).update(data) + } + + async updateConsent( + conditions: Record, + data: Record + ): Promise { + try { + let firestoreQuery: FirebaseFirestore.Query = firebase + .firestore() + .collection('consents') + + // Chain all of the given conditions to the query + for (const key in conditions) { + firestoreQuery = firestoreQuery.where(key, '==', conditions[key]) + } + + // Find and update all matching documents in Firebase that match the given conditions. + const response = await firestoreQuery.get() + // Create a batch to perform all of the updates using a single request. + // Firebase will also execute the updates atomically according to the + // API specification. + const batch = firebase.firestore().batch() + + // Iterate through all matching documents add them to the processing batch. + response.docs.forEach((doc) => { + batch.update( + // Put a reference to the document. + firebase.firestore().collection('consents').doc(doc.id), + // Specify the updated fields and their new values. + data + ) + }) + + // Commit the updates. + await batch.commit() + } catch (error) { + logger.error(error) + } + } } export const consentRepository: IConsentRepository = new FirebaseConsentRepository() diff --git a/src/repositories/participants.ts b/src/repositories/participants.ts new file mode 100644 index 00000000..754719d9 --- /dev/null +++ b/src/repositories/participants.ts @@ -0,0 +1,78 @@ +/***** + License + -------------- + Copyright © 2020 Mojaloop Foundation + The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the 'License') and you may not use these files except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Contributors + -------------- + This is the official list of the Mojaloop project contributors for this file. + Names of the original copyright holders (individuals or organizations) + should be listed with a '*' in the first column. People who have + contributed from an organization can be listed under the organization + that actually holds the copyright for their contributions (see the + Mojaloop Foundation organization for an example). Those individuals should have + their names indented and be marked with a '-'. Email address can be added + optionally within square brackets . + * Mojaloop Foundation + - Name Surname + + * Google + - Steven Wijaya + - Abhimanyu Kapur + -------------- + ******/ + +/* istanbul ignore file */ +// TODO: BDD Testing will covered in separate ticket #1702 + +import firebase from '~/lib/firebase' +import { logger } from '~/shared/logger' +import { Participant } from '~/shared/ml-thirdparty-client/models/core' + +export interface IParticipantRepository { + /** + * Replace existing participants list with new list. + * + * @param data Documents that are about to be added. + */ + replace(data: Participant[]): Promise +} + +export class FirebaseParticipantRepository implements IParticipantRepository { + async replace(data: Participant[]): Promise { + try { + const collectionRef: FirebaseFirestore.CollectionReference = firebase + .firestore() + .collection('participants') + + const response = await collectionRef.get() + // Create a batch to perform all of the updates using a single request. + // Firebase will also execute the updates atomically according to the + // API specification. + const batch = firebase.firestore().batch() + + const batchSize = response.size + if (batchSize > 0) { + // If previous participants list exists, delete it + + // Iterate through all matching documents add them to the processing batch. + response.docs.forEach((doc) => { + batch.delete(doc.ref) + }) + } + // Iterate through received participants list and add them to the processing batch. + data.forEach((participant: Participant) => { + batch.set(collectionRef.doc(), participant) + }) + + // Commit the updates. + await batch.commit() + } catch (error) { + logger.error(error) + } + } +} + +export const participantRepository: IParticipantRepository = new FirebaseParticipantRepository() diff --git a/src/repositories/transaction.ts b/src/repositories/transaction.ts index 333ee844..c09e4ae4 100644 --- a/src/repositories/transaction.ts +++ b/src/repositories/transaction.ts @@ -23,7 +23,8 @@ -------------- ******/ -/* eslint-disable @typescript-eslint/no-explicit-any */ +/* istanbul ignore file */ +// TODO: BDD Testing will covered in separate ticket #1702 import firebase from '~/lib/firebase' import { logger } from '~/shared/logger' @@ -35,7 +36,7 @@ export interface ITransactionRepository { * @param id Id for the transaction document that needs to be updated. * @param data Document fields that are about to be updated. */ - updateById(id: string, data: Record): Promise + updateById(id: string, data: Record): Promise /** * Updates one or more transaction documents based on the given conditions. @@ -44,54 +45,52 @@ export interface ITransactionRepository { * @param data Document fields that are about to be updated. */ update( - conditions: Record, - data: Record + conditions: Record, + data: Record ): Promise } export class FirebaseTransactionRepository implements ITransactionRepository { - async updateById(id: string, data: Record): Promise { + async updateById(id: string, data: Record): Promise { await firebase.firestore().collection('transactions').doc(id).update(data) } async update( - conditions: Record, - data: Record + conditions: Record, + data: Record ): Promise { - let firestoreQuery: FirebaseFirestore.Query = firebase - .firestore() - .collection('transactions') + try { + let firestoreQuery: FirebaseFirestore.Query = firebase + .firestore() + .collection('transactions') - // Chain all of the given conditions to the query - for (const key in conditions) { - firestoreQuery = firestoreQuery.where(key, '==', conditions[key]) - } - - // Find and update all matching documents in Firebase that match the given conditions. - await firestoreQuery - .get() - .then((response) => { - // Create a batch to perform all of the updates using a single request. - // Firebase will also execute the updates atomically according to the - // API specification. - const batch = firebase.firestore().batch() + // Chain all of the given conditions to the query + for (const key in conditions) { + firestoreQuery = firestoreQuery.where(key, '==', conditions[key]) + } - // Iterate through all matching documents add them to the processing batch. - response.docs.forEach((doc) => { - batch.update( - // Put a reference to the document. - firebase.firestore().collection('transactions').doc(doc.id), - // Specify the updated fields and their new values. - data - ) - }) + // Find and update all matching documents in Firebase that match the given conditions. + const response = await firestoreQuery.get() + // Create a batch to perform all of the updates using a single request. + // Firebase will also execute the updates atomically according to the + // API specification. + const batch = firebase.firestore().batch() - // Commit the updates. - return batch.commit() - }) - .catch((err) => { - logger.error(err) + // Iterate through all matching documents add them to the processing batch. + response.docs.forEach((doc) => { + batch.update( + // Put a reference to the document. + firebase.firestore().collection('transactions').doc(doc.id), + // Specify the updated fields and their new values. + data + ) }) + + // Commit the updates. + await batch.commit() + } catch (error) { + logger.error(error) + } } } diff --git a/src/server/handlers/firestore/consents.ts b/src/server/handlers/firestore/consents.ts new file mode 100644 index 00000000..8e5723e2 --- /dev/null +++ b/src/server/handlers/firestore/consents.ts @@ -0,0 +1,228 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/***** + License + -------------- + Copyright © 2020 Mojaloop Foundation + The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the 'License') and you may not use these files except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Contributors + -------------- + This is the official list of the Mojaloop project contributors for this file. + Names of the original copyright holders (individuals or organizations) + should be listed with a '*' in the first column. People who have + contributed from an organization can be listed under the organization + that actually holds the copyright for their contributions (see the + Mojaloop Foundation organization for an example). Those individuals should have + their names indented and be marked with a '-'. Email address can be added + optionally within square brackets . + * Mojaloop Foundation + - Name Surname + + * Google + - Steven Wijaya + - Abhimanyu Kapur + -------------- + ******/ +/* istanbul ignore file */ +// TODO: BDD Testing will covered in separate ticket #1702 + +import * as uuid from 'uuid' +import { logger } from '~/shared/logger' + +import { ConsentHandler } from '~/server/plugins/internal/firestore' +import { Consent, ConsentStatus } from '~/models/consent' + +import { consentRepository } from '~/repositories/consent' +import * as validator from './consents.validator' +import config from '~/lib/config' +import { MissingConsentFieldsError, InvalidConsentStatusError } from '~/models/errors' + +async function handleNewConsent(_: StateServer, consent: Consent) { + // Assign a consentRequestId to the document and set the initial + // status. This operation will create an event that triggers the execution + // of the onUpdate function. + + // Not await-ing promise to resolve - code is executed asynchronously + consentRepository.updateConsentById(consent.id, { + consentRequestId: uuid.v4(), + status: ConsentStatus.PENDING_PARTY_LOOKUP, + }) +} + +async function initiatePartyLookup(server: StateServer, consent: Consent) { + // Check whether the consent document has all the necessary properties + // to perform a party lookup. + if (!validator.isValidPartyLookup(consent)) { + throw new MissingConsentFieldsError(consent) + } + + // Fields are guaranteed to be non-null by the validator. + try { + server.app.mojaloopClient.getParties( + consent.party!.partyIdInfo.partyIdType, + consent.party!.partyIdInfo.partyIdentifier + ) + } catch (error) { + logger.error(error) + } +} + +async function initiateAuthentication(server: StateServer, consent: Consent) { + if (!validator.isValidAuthentication(consent)) { + throw new MissingConsentFieldsError(consent) + } + + // Fields are guaranteed to be non-null by the validator. + try { + server.app.mojaloopClient.putConsentRequests( + consent.id, + { + initiatorId: consent.initiatorId!, + authChannels: consent.authChannels!, + scopes: consent.scopes!, + authUri: consent.authUri!, + callbackUri: config.get('mojaloop').pispCallbackUri, + authToken: consent.authToken!, + }, + consent.party!.partyIdInfo.fspId! + ) + } catch (error) { + logger.error(error) + } +} + +async function initiateConsentRequest(server: StateServer, consent: Consent) { + if (!validator.isValidConsentRequest(consent)) { + throw new MissingConsentFieldsError(consent) + } + // If the update contains all the necessary fields, process document + + try { + // Fields are guaranteed to be non-null by the validator. + server.app.mojaloopClient.postConsentRequests( + { + initiatorId: consent.initiatorId!, + scopes: consent.scopes!, + authChannels: consent.authChannels!, + id: consent.id, + callbackUri: config.get('mojaloop').pispCallbackUri, + }, + consent.party!.partyIdInfo.fspId! + ) + } catch (err) { + logger.error(err) + } +} + +async function initiateChallengeGeneration(server: StateServer, consent: Consent) { + if (!validator.isValidGenerateChallengeOrRevokeConsent(consent)) { + throw new MissingConsentFieldsError(consent) + } + + try { + // Fields are guaranteed to be non-null by the validator. + server.app.mojaloopClient.postGenerateChallengeForConsent( + consent.consentId!, + consent.party!.partyIdInfo.fspId! + ) + } catch (error) { + logger.error(error) + } +} + +async function handleSignedChallenge(server: StateServer, consent: Consent) { + if (!validator.isValidSignedChallenge(consent)) { + throw new MissingConsentFieldsError(consent) + } + + try { + // Fields are guaranteed to be non-null by the validator. + server.app.mojaloopClient.putConsentId( + consent.consentId!, + { + requestId: consent.id, + initiatorId: consent.initiatorId!, + participantId: consent.participantId!, + scopes: consent.scopes!, + credential: consent.credential!, + }, + consent.party!.partyIdInfo.fspId! + ) + } catch (error) { + logger.error(error) + } +} + +async function initiateRevokingConsent(server: StateServer, consent: Consent) { + if (!validator.isValidGenerateChallengeOrRevokeConsent(consent)) { + throw new MissingConsentFieldsError(consent) + } + + try { + // Fields are guaranteed to be non-null by the validator. + server.app.mojaloopClient.postRevokeConsent( + consent.consentId!, + consent.party!.partyIdInfo.fspId! + ) + } catch (error) { + logger.error(error) + } +} + +export const onCreate: ConsentHandler = async ( + server: StateServer, + consent: Consent +): Promise => { + if (consent.status) { + // Skip transaction that has been processed previously. + // We need this because when the server starts for the first time, + // all existing documents in the Firebase will be treated as a new + // document. + return + } + + await handleNewConsent(server, consent) +} + +export const onUpdate: ConsentHandler = async ( + server: StateServer, + consent: Consent +): Promise => { + if (!consent.status) { + // Status is expected to be null only when the document is created for the first + // time by the user. + logger.error('Invalid consent, undefined status.') + return + } + + switch (consent.status) { + case ConsentStatus.PENDING_PARTY_LOOKUP: + await initiatePartyLookup(server, consent) + break + + case ConsentStatus.PENDING_PARTY_CONFIRMATION: + await initiateConsentRequest(server, consent) + break + + case ConsentStatus.AUTHENTICATION_REQUIRED: + await initiateAuthentication(server, consent) + break + + case ConsentStatus.CONSENT_GRANTED: + await initiateChallengeGeneration(server, consent) + break + + case ConsentStatus.ACTIVE: + await handleSignedChallenge(server, consent) + break + + case ConsentStatus.REVOKE_REQUESTED: + await initiateRevokingConsent(server, consent) + break + + default: + throw new InvalidConsentStatusError(consent.status, consent.id) + } +} diff --git a/src/server/handlers/firestore/consents.validator.ts b/src/server/handlers/firestore/consents.validator.ts new file mode 100644 index 00000000..f8fac167 --- /dev/null +++ b/src/server/handlers/firestore/consents.validator.ts @@ -0,0 +1,105 @@ +/***** + License + -------------- + Copyright © 2020 Mojaloop Foundation + The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the 'License') and you may not use these files except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Contributors + -------------- + This is the official list of the Mojaloop project contributors for this file. + Names of the original copyright holders (individuals or organizations) + should be listed with a '*' in the first column. People who have + contributed from an organization can be listed under the organization + that actually holds the copyright for their contributions (see the + Mojaloop Foundation organization for an example). Those individuals should have + their names indented and be marked with a '-'. Email address can be added + optionally within square brackets . + * Mojaloop Foundation + - Name Surname + + * Google + - Abhimanyu Kapur + -------------- + ******/ + +import { Consent } from '~/models/consent' + +/** + * Checks whether a consent document has all the necessary fields to perform + * a party lookup. + * + * @param consent the object representation of a consent that is stored + * on Firebase. + */ +export const isValidPartyLookup = (consent: Consent): boolean => { + return !!( + consent?.party?.partyIdInfo?.partyIdType && + consent.party.partyIdInfo.partyIdentifier + ) +} + +/** + * Checks whether a consent document has all the necessary fields to be + * processed as an authenticated consent request. + * + * @param consent the object representation of a consent that is stored + * on Firebase. + */ +export const isValidAuthentication = (consent: Consent): boolean => { + return !!( + consent?.party?.partyIdInfo?.fspId && + consent.consentRequestId && + consent.consentId && + consent.initiatorId && + consent.authChannels && + consent.authToken + ) +} + +/** + * Checks whether a consent document has all the necessary fields to be + * processed as a consent request. + * + * @param consent the object representation of a consent that is stored + * on Firebase. + */ +export const isValidConsentRequest = (consent: Consent): boolean => { + return !!( + consent?.party?.partyIdInfo?.fspId && + consent.authChannels && + consent.scopes && + consent.initiatorId && + consent.authUri + ) +} + +/** + * Checks whether a consent document has all the necessary fields to be + * processed as a signed consent request. + * + * @param consent the object representation of a consent that is stored + * on Firebase. + */ +export const isValidSignedChallenge = (consent: Consent): boolean => { + return !!( + consent?.party?.partyIdInfo?.fspId && + consent.credential && + consent.scopes && + consent.initiatorId && + consent.participantId + ) +} + +/** + * Checks whether a consent document has all the necessary fields to be + * processed as revoke consent request or a request to generate challenge for a consent. + * + * @param consent the object representation of a consent that is stored + * on Firebase. + */ +export const isValidGenerateChallengeOrRevokeConsent = ( + consent: Consent +): boolean => { + return !!(consent?.party?.partyIdInfo?.fspId && consent.consentId) +} diff --git a/src/server/handlers/firestore/index.ts b/src/server/handlers/firestore/index.ts index 1868d73f..8fe453c9 100644 --- a/src/server/handlers/firestore/index.ts +++ b/src/server/handlers/firestore/index.ts @@ -20,11 +20,14 @@ * Google - Steven Wijaya + - Abhimanyu Kapur -------------- ******/ import * as transactionHandlers from './transactions' +import * as consentHandlers from './consents' export default { - transactions: transactionHandlers + transactions: transactionHandlers, + consents: consentHandlers, } diff --git a/src/server/handlers/firestore/transactions.ts b/src/server/handlers/firestore/transactions.ts index 891b9a00..db28b588 100644 --- a/src/server/handlers/firestore/transactions.ts +++ b/src/server/handlers/firestore/transactions.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ /***** License -------------- @@ -23,6 +24,8 @@ - Abhimanyu Kapur -------------- ******/ +/* istanbul ignore file */ +// TODO: Testing will covered in separate ticket import * as uuid from 'uuid' @@ -40,13 +43,14 @@ import { transactionRepository } from '~/repositories/transaction' import * as validator from './transactions.validator' import { consentRepository } from '~/repositories/consent' -// TODO: Replace once decided how to implement +// TODO: Replace once design decision made on how we should be obtaining this const destParticipantId = 'PLACEHOLDER' async function handleNewTransaction(_: StateServer, transaction: Transaction) { // Assign a transactionRequestId to the document and set the initial // status. This operation will create an event that triggers the execution // of the onUpdate function. + // Not await-ing promise to resolve - code is executed asynchronously transactionRepository.updateById(transaction.id, { transactionRequestId: uuid.v4(), status: Status.PENDING_PARTY_LOOKUP, @@ -86,7 +90,7 @@ async function handlePartyConfirmation( // The optional values are guaranteed to exist by the validator. // eslint-disable @typescript-eslint/no-non-null-assertion - const consent = await consentRepository.getByConsentId( + const consent = await consentRepository.getConsentById( transaction.consentId! ) diff --git a/src/server/handlers/openapi/app/index.ts b/src/server/handlers/openapi/app/index.ts index 5f7f0570..671750ad 100644 --- a/src/server/handlers/openapi/app/index.ts +++ b/src/server/handlers/openapi/app/index.ts @@ -20,6 +20,7 @@ * Google - Steven Wijaya + - Abhimanyu Kapur -------------- ******/ diff --git a/src/server/handlers/openapi/mojaloop/authorizations.ts b/src/server/handlers/openapi/mojaloop/authorizations.ts index f21b0bba..67e79901 100644 --- a/src/server/handlers/openapi/mojaloop/authorizations.ts +++ b/src/server/handlers/openapi/mojaloop/authorizations.ts @@ -32,8 +32,9 @@ import { transactionRepository } from '~/repositories/transaction' import { Status } from '~/models/transaction' export const post: Handler = async (context: Context, _: Request, h: ResponseToolkit) => { - let body = context.request.body as AuthorizationsPostRequest + const body = context.request.body as AuthorizationsPostRequest + // Not await-ing promise to resolve - code is executed asynchronously transactionRepository.update( { transactionRequestId: body.transactionRequestId, diff --git a/src/server/handlers/openapi/mojaloop/consentRequests/{ID}.ts b/src/server/handlers/openapi/mojaloop/consentRequests/{ID}.ts index c6d47a85..1fd0e11a 100644 --- a/src/server/handlers/openapi/mojaloop/consentRequests/{ID}.ts +++ b/src/server/handlers/openapi/mojaloop/consentRequests/{ID}.ts @@ -20,14 +20,26 @@ * Google - Steven Wijaya + - Abhimanyu Kapur -------------- ******/ import { Request, ResponseToolkit } from '@hapi/hapi' import { Handler, Context } from 'openapi-backend' -import { logger } from '~/shared/logger' +import { consentRepository } from '~/repositories/consent' +import { ConsentStatus } from '~/models/consent' -export const put: Handler = async (context: Context, request: Request, h: ResponseToolkit) => { - logger.logRequest(context, request, h) +export const put: Handler = async ( + context: Context, + _request: Request, + h: ResponseToolkit +) => { + const { authChannels, authUri } = context.request.body + // Not await-ing promise to resolve - code is executed asynchronously + consentRepository.updateConsentById(context.request.params.ID as string, { + authChannels, + authUri, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) return h.response().code(200) } diff --git a/src/server/handlers/openapi/mojaloop/consents.ts b/src/server/handlers/openapi/mojaloop/consents.ts index 2efd4197..3490f28d 100644 --- a/src/server/handlers/openapi/mojaloop/consents.ts +++ b/src/server/handlers/openapi/mojaloop/consents.ts @@ -20,14 +20,28 @@ * Google - Steven Wijaya + - Abhimanyu Kapur -------------- ******/ import { Request, ResponseToolkit } from '@hapi/hapi' import { Handler, Context } from 'openapi-backend' -import { logger } from '~/shared/logger' +import { consentRepository } from '~/repositories/consent' +import { ConsentStatus } from '~/models/consent' -export const post: Handler = async (context: Context, request: Request, h: ResponseToolkit) => { - logger.logRequest(context, request, h) +export const post: Handler = async ( + context: Context, + _request: Request, + h: ResponseToolkit +) => { + const { id, initiatorId, participantId, scopes } = context.request.body + + // Not await-ing promise to resolve - code is executed asynchronously + consentRepository.updateConsentById(id, { + initiatorId, + participantId, + scopes, + status: ConsentStatus.CONSENT_GRANTED, + }) return h.response().code(202) } diff --git a/src/server/handlers/openapi/mojaloop/consents/{ID}.ts b/src/server/handlers/openapi/mojaloop/consents/{ID}.ts index a14d392c..e07df5ad 100644 --- a/src/server/handlers/openapi/mojaloop/consents/{ID}.ts +++ b/src/server/handlers/openapi/mojaloop/consents/{ID}.ts @@ -20,19 +20,38 @@ * Google - Steven Wijaya + - Abhimanyu Kapur -------------- ******/ import { Request, ResponseToolkit } from '@hapi/hapi' import { Handler, Context } from 'openapi-backend' -import { logger } from '~/shared/logger' +import { consentRepository } from '~/repositories/consent' -export const put: Handler = async (context: Context, request: Request, h: ResponseToolkit) => { - logger.logRequest(context, request, h) +export const put: Handler = async ( + context: Context, + _request: Request, + h: ResponseToolkit +) => { + // Updates consent fields + // Not await-ing promise to resolve - code is executed asynchronously + consentRepository.updateConsentById( + context.request.params.ID as string, + context.request.body + ) return h.response().code(200) } -export const remove: Handler = async (context: Context, request: Request, h: ResponseToolkit) => { - logger.logRequest(context, request, h) +export const patch: Handler = async ( + context: Context, + _request: Request, + h: ResponseToolkit +) => { + // Updates consent fields patched + // Not await-ing promise to resolve - code is executed asynchronously + consentRepository.updateConsentById( + context.request.params.ID as string, + context.request.body + ) return h.response().code(200) } diff --git a/src/server/handlers/openapi/mojaloop/index.ts b/src/server/handlers/openapi/mojaloop/index.ts index 9a74fc5b..4deab839 100644 --- a/src/server/handlers/openapi/mojaloop/index.ts +++ b/src/server/handlers/openapi/mojaloop/index.ts @@ -20,6 +20,7 @@ * Google - Steven Wijaya + - Abhimanyu Kapur -------------- ******/ @@ -41,7 +42,7 @@ export const apiHandlers = { postAuthorizations: MojaloopAuthorizations.post, postConsents: MojaloopConsents.post, putConsentsById: MojaloopConsentsById.put, - deleteConsentsById: MojaloopConsentsById.remove, + patchConsentsById: MojaloopConsentsById.patch, putConsentRequestsById: MojaloopConsentRequestsById.put, putParticipants: MojaloopParticipants.put, putParticipantsError: MojaloopParticipantsError.put, diff --git a/src/server/handlers/openapi/mojaloop/participants.ts b/src/server/handlers/openapi/mojaloop/participants.ts index c6d47a85..2d01e7b3 100644 --- a/src/server/handlers/openapi/mojaloop/participants.ts +++ b/src/server/handlers/openapi/mojaloop/participants.ts @@ -20,14 +20,23 @@ * Google - Steven Wijaya + - Abhimanyu Kapur -------------- ******/ import { Request, ResponseToolkit } from '@hapi/hapi' import { Handler, Context } from 'openapi-backend' -import { logger } from '~/shared/logger' +import { participantRepository } from '~/repositories/participants' -export const put: Handler = async (context: Context, request: Request, h: ResponseToolkit) => { - logger.logRequest(context, request, h) +export const put: Handler = async ( + context: Context, + _request: Request, + h: ResponseToolkit +) => { + const participants = context.request.body.participants + + // Replace existing participants list with received list + // Not await-ing promise to resolve - code is executed asynchronously + participantRepository.replace(participants) return h.response().code(200) } diff --git a/src/server/handlers/openapi/mojaloop/parties/{Type}/{ID}.ts b/src/server/handlers/openapi/mojaloop/parties/{Type}/{ID}.ts index 6654c711..56199178 100644 --- a/src/server/handlers/openapi/mojaloop/parties/{Type}/{ID}.ts +++ b/src/server/handlers/openapi/mojaloop/parties/{Type}/{ID}.ts @@ -20,6 +20,7 @@ * Google - Steven Wijaya + - Abhimanyu Kapur -------------- ******/ @@ -30,37 +31,68 @@ import { PartiesTypeIDPutRequest } from '~/shared/ml-thirdparty-client/models/op import { Status } from '~/models/transaction' import { transactionRepository } from '~/repositories/transaction' +import { consentRepository } from '~/repositories/consent' +import { ConsentStatus } from '~/models/consent' +import { PartyIdType } from '~/shared/ml-thirdparty-client/models/core' /** * Handles callback from Mojaloop that specifies detailed info about a requested party. - * + * * @param context an object that contains detailed information about the incoming request. * @param request original request object as defined by the hapi library. * @param h original request toolkit as defined by the hapi libary. */ -export const put: Handler = async (context: Context, _: Request, h: ResponseToolkit): Promise => { +export const put: Handler = async ( + context: Context, + _: Request, + h: ResponseToolkit +): Promise => { // Retrieve the data that have been validated by the openapi-backend library. - let body = context.request.body as PartiesTypeIDPutRequest - let partyIdType = context.request.params.Type - let partyIdentifier = context.request.params.ID + const body = context.request.body as PartiesTypeIDPutRequest + const partyIdType = context.request.params.Type + const partyIdentifier = context.request.params.ID // Find all matching documents in Firebase that are waiting for the result of - // party lookup with the specified type and identifier. The execution of this - // function is expected to run asynchronously, so the server could quickly + // party lookup with the specified type and identifier. The execution of this + // function is expected to run asynchronously, so the server could quickly // give a response to Mojaloop. - transactionRepository.update( - // Conditions for the documents that need to be updated - { - "payee.partyIdInfo.partyIdType": partyIdType, - "payee.partyIdInfo.partyIdentifier": partyIdentifier, - "status": Status.PENDING_PARTY_LOOKUP, - }, - // Update the given field by their new values - { - payee: body.party, - status: Status.PENDING_PAYEE_CONFIRMATION, - } - ) + + if (partyIdType === PartyIdType.OPAQUE) { + // Update Consents as OPAQUE is the type during linking when we're fetching the accounts + // available for linking from a pre-determined DFSP + + // Not await-ing promise to resolve - code is executed asynchronously + consentRepository.updateConsent( + // Conditions for the documents that need to be updated + { + 'payee.partyIdInfo.partyIdType': partyIdType, + 'payee.partyIdInfo.partyIdentifier': partyIdentifier, + status: ConsentStatus.PENDING_PARTY_LOOKUP, + }, + // Update the given field by their new values + { + party: body.party, + accounts: body.accounts, + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + } + ) + } else { + // Update Transactions + // Not await-ing promise to resolve - code is executed asynchronously + transactionRepository.update( + // Conditions for the documents that need to be updated + { + 'payee.partyIdInfo.partyIdType': partyIdType, + 'payee.partyIdInfo.partyIdentifier': partyIdentifier, + status: Status.PENDING_PARTY_LOOKUP, + }, + // Update the given field by their new values + { + payee: body.party, + status: Status.PENDING_PAYEE_CONFIRMATION, + } + ) + } // Return "200 OK" as defined by the Mojaloop API for successful request. return h.response().code(200) diff --git a/src/server/handlers/openapi/mojaloop/transfers/{ID}.ts b/src/server/handlers/openapi/mojaloop/transfers/{ID}.ts index ae948f51..328be552 100644 --- a/src/server/handlers/openapi/mojaloop/transfers/{ID}.ts +++ b/src/server/handlers/openapi/mojaloop/transfers/{ID}.ts @@ -32,8 +32,9 @@ import { Status } from '~/models/transaction' import { transactionRepository } from '~/repositories/transaction' export const put: Handler = async (context: Context, _: Request, h: ResponseToolkit) => { - let body = context.request.body as TransferIDPutRequest + const body = context.request.body as TransferIDPutRequest + // Not await-ing promise to resolve - code is executed asynchronously transactionRepository.update( { transactionId: body.transactionId, diff --git a/src/server/plugins/internal/firestore.ts b/src/server/plugins/internal/firestore.ts index 9615eeba..eb42b3bb 100644 --- a/src/server/plugins/internal/firestore.ts +++ b/src/server/plugins/internal/firestore.ts @@ -28,12 +28,18 @@ import { Plugin, Server } from '@hapi/hapi' import firebase from '~/lib/firebase' import { Transaction } from '~/models/transaction' +import { Consent } from '~/models/consent' export type TransactionHandler = ( server: StateServer, transaction: Transaction ) => Promise +export type ConsentHandler = ( + server: StateServer, + consent: Consent +) => Promise + /** * An interface definition for options that need to be specfied to use this plugin. */ @@ -44,6 +50,11 @@ export interface Options { onUpdate?: TransactionHandler onRemove?: TransactionHandler } + consents: { + onCreate?: ConsentHandler + onUpdate?: ConsentHandler + onRemove?: ConsentHandler + } } } @@ -89,6 +100,48 @@ const listenToTransactions = ( }) } +/** + * Listens to events that happen in the "consents" collection. + * Note that when the server starts running, all existing documents in Firestore + * will be treated as a document that is created for the first time. The handler for + * `onCreate` consent must be able to differentiate whether a document is created in + * realtime or because it has persisted in the database when the server starts. + * + * @param server a server object as defined in the hapi library. + * @param options a configuration object for the plugin. + * @returns a function to unsubscribe the listener. + */ +const listenToConsents = ( + server: StateServer, + options: Options +): (() => void) => { + const consentHandlers = options.handlers.consents + + return firebase + .firestore() + .collection('consents') + .onSnapshot((querySnapshot) => { + querySnapshot.docChanges().forEach((change) => { + if (change.type === 'added' && consentHandlers.onCreate) { + consentHandlers.onCreate(server, { + id: change.doc.id, + ...change.doc.data(), + }) + } else if (change.type === 'modified' && consentHandlers.onUpdate) { + consentHandlers.onUpdate(server, { + id: change.doc.id, + ...change.doc.data(), + }) + } else if (change.type === 'removed' && consentHandlers.onRemove) { + consentHandlers.onRemove(server, { + id: change.doc.id, + ...change.doc.data(), + }) + } + }) + }) +} + /** * A plugin that enables the hapi server to listen to changes in the Firestore * collections that are relevant for the PISP demo. @@ -97,6 +150,7 @@ export const Firestore: Plugin = { name: 'PispDemoFirestore', version: '1.0.0', register: async (server: Server, options: Options) => { + const unsubscribeConsents = listenToConsents(server as StateServer, options) const unsubscribeTransactions = listenToTransactions( server as StateServer, options @@ -105,6 +159,7 @@ export const Firestore: Plugin = { // Unsubscribe to the changes in Firebase when the server stops running. server.ext('onPreStop', (_: Server) => { unsubscribeTransactions() + unsubscribeConsents() }) }, } diff --git a/src/server/run.ts b/src/server/run.ts index cccc4d92..78e5a43e 100644 --- a/src/server/run.ts +++ b/src/server/run.ts @@ -23,12 +23,11 @@ -------------- ******/ -import { Server } from '@hapi/hapi' import { ServiceConfig } from '../lib/config' import create from './create' import start from './start' -export default async function run(config: ServiceConfig): Promise { +export default async function run(config: ServiceConfig): Promise { const server = await create(config) - return start(server) + return start(server) as Promise } diff --git a/src/shared/logger/index.ts b/src/shared/logger/index.ts index 7f52baf1..8e6c7d49 100644 --- a/src/shared/logger/index.ts +++ b/src/shared/logger/index.ts @@ -40,8 +40,8 @@ export interface ResponseLogged extends ResponseObject { } export interface BaseLogger { - info: (message: string, ...meta: any[]) => any - error: (messsage: string, ...meta: any[]) => any + info: (message: string, ...meta: unknown[]) => unknown + error: (messsage: string, ...meta: unknown[]) => unknown } export class Logger { @@ -74,11 +74,11 @@ export class Logger { } } - info(message: string, ...meta: any[]) { + info(message: string, ...meta: unknown[]) { this._logger.info(message, ...meta) } - error(message: string, ...meta: any[]) { + error(message: string, ...meta: unknown[]) { this._logger.error(message, ...meta) } } diff --git a/src/shared/ml-thirdparty-client/index.ts b/src/shared/ml-thirdparty-client/index.ts index 870ddd95..21fd4736 100644 --- a/src/shared/ml-thirdparty-client/index.ts +++ b/src/shared/ml-thirdparty-client/index.ts @@ -24,6 +24,8 @@ - Abhimanyu Kapur -------------- ******/ +/* istanbul ignore file */ +// TODO: BDD Testing will covered in separate ticket #1702 import { Simulator } from '~/shared/ml-thirdparty-simulator' import { PartyIdType } from './models/core' @@ -34,12 +36,8 @@ import { ThirdPartyTransactionRequest, } from './models/openapi' -// import Logger from '@mojaloop/central-services-logger' -import { logger as Logger } from '~/shared/logger' - import SDKStandardComponents, { - // TODO: Once implemented in sdk-standard-components, use this logger - // Logger, + Logger, ThirdpartyRequests, MojaloopRequests, } from '@mojaloop/sdk-standard-components' @@ -112,12 +110,14 @@ export class Client { * @param _id the party identifier */ public async getParties( - _type: PartyIdType, - _id: string + idType: PartyIdType, + idValue: string, + idSubValue?: string ): Promise { - // TODO: Implement communication with Mojaloop. - // Placeholder below - throw new NotImplementedError() + if (idSubValue) { + return this.mojaloopRequests.getParties(idType, idValue, idSubValue) + } + return this.mojaloopRequests.getParties(idType, idValue) } /** @@ -149,8 +149,10 @@ export class Client { _requestBody: AuthorizationsPutIdRequest, _destParticipantId: string ): Promise { - // TODO: Implement communication with Mojaloop. - // Placeholder below + // TODO: Replace placeholder with commented implementation + // once implemented in sdk-standard-components + + // Placeholder throw new NotImplementedError() // return this.thirdpartyRequests.putThirdpartyRequestsTransactionsAuthorizations( @@ -210,10 +212,12 @@ export class Client { * Performs a request to generate a challenge for FIDO registration * * @param _consentId identifier of consent as defined by Mojaloop API. + * @param destParticipantId ID of destination - to be used when sending request */ public async postGenerateChallengeForConsent( // eslint-disable-next-line @typescript-eslint/no-unused-vars - _consentId: string + _consentId: string, + _destParticipantId: string ): Promise { // TODO: Add once implemented in sdk-standard components // Placeholder below @@ -243,10 +247,12 @@ export class Client { * Performs a request to revoke the Consent object and unlink * * @param _consentId identifier of consent as defined by Mojaloop API. + * @param destParticipantId ID of destination - to be used when sending request */ // eslint-disable-next-line @typescript-eslint/no-unused-vars public async postRevokeConsent( - _consentId: string + _consentId: string, + _destParticipantId: string ): Promise { // TODO: Add once implemented in sdk-standard components // Placeholder below diff --git a/src/shared/ml-thirdparty-client/models/core/parties.ts b/src/shared/ml-thirdparty-client/models/core/parties.ts index c68ecd98..bf9be42e 100644 --- a/src/shared/ml-thirdparty-client/models/core/parties.ts +++ b/src/shared/ml-thirdparty-client/models/core/parties.ts @@ -23,7 +23,7 @@ -------------- ******/ -import { Currency } from './transactions'; +import { Currency } from './transactions' /** * Data model for the complex type Account. @@ -91,6 +91,13 @@ export interface PartyComplexName { * The allowed values for the enumeration of party identifier type. */ export enum PartyIdType { + // TODO: Confirm other possible uses for OPAQUE and + // fill out docstring + /** + * Type for Consent Requests + */ + OPAQUE = 'OPAQUE', + /** * An MSISDN (Mobile Station International Subscriber Directory Number, * that is, the phone number) is used as reference to a participant. diff --git a/src/shared/ml-thirdparty-client/models/openapi/index.ts b/src/shared/ml-thirdparty-client/models/openapi/index.ts index 28e6966b..e48c3fc0 100644 --- a/src/shared/ml-thirdparty-client/models/openapi/index.ts +++ b/src/shared/ml-thirdparty-client/models/openapi/index.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ /***** License -------------- diff --git a/src/shared/ml-thirdparty-simulator/factories/consents.ts b/src/shared/ml-thirdparty-simulator/factories/consents.ts index e0538e71..4d427d6c 100644 --- a/src/shared/ml-thirdparty-simulator/factories/consents.ts +++ b/src/shared/ml-thirdparty-simulator/factories/consents.ts @@ -23,6 +23,8 @@ - Abhimanyu Kapur -------------- ******/ +/* istanbul ignore file */ +// TODO: Confirm if testing necessary for factory methods import * as faker from 'faker' diff --git a/src/shared/ml-thirdparty-simulator/factories/participant.ts b/src/shared/ml-thirdparty-simulator/factories/participant.ts index d4f99dbd..ea1ee4b4 100644 --- a/src/shared/ml-thirdparty-simulator/factories/participant.ts +++ b/src/shared/ml-thirdparty-simulator/factories/participant.ts @@ -22,6 +22,8 @@ - Steven Wijaya -------------- ******/ +/* istanbul ignore file */ +// TODO: Confirm if testing necessary for factory methods import * as faker from 'faker' @@ -34,12 +36,12 @@ export class ParticipantFactory { /** * Number of participants that will be generated by the simulator. The value * must be a positive integer. If not set, the default value is 10. - * - * Note that this variable must be set before `getParticipants()` is called, - * otherwise, changing the value will not have any effect since the list of + * + * Note that this variable must be set before `getParticipants()` is called, + * otherwise, changing the value will not have any effect since the list of * participants is stored within the `participants` field. */ - public static numOfParticipants: number = 10 + public static numOfParticipants = 10 /** * List of participants that is stored internally within the `ParticipantFactory`. @@ -49,7 +51,7 @@ export class ParticipantFactory { /** * Returns the list of participants for the simulator. * The participants will only be generated once when this function is called - * for the first time. Afterward, the list will be stored internally for + * for the first time. Afterward, the list will be stored internally for * subsequent usage. */ public static getParticipants() { @@ -60,11 +62,11 @@ export class ParticipantFactory { } private static createParticipants() { - for (var i = 0; i < ParticipantFactory.numOfParticipants; i++) { + for (let i = 0; i < ParticipantFactory.numOfParticipants; i++) { // Generate a random participant - let participant: Participant = { + const participant: Participant = { fspId: faker.finance.bic(), - name: faker.company.companyName() + name: faker.company.companyName(), } ParticipantFactory.participants.push(participant) diff --git a/src/shared/ml-thirdparty-simulator/factories/party.ts b/src/shared/ml-thirdparty-simulator/factories/party.ts index 724c09ec..fd00abe4 100644 --- a/src/shared/ml-thirdparty-simulator/factories/party.ts +++ b/src/shared/ml-thirdparty-simulator/factories/party.ts @@ -23,6 +23,9 @@ -------------- ******/ +/* istanbul ignore file */ +// TODO: Confirm if testing necessary for factory methods + import * as faker from 'faker' import { PartyIdType, Party, Account, Currency } from '~/shared/ml-thirdparty-client/models/core' import { PartiesTypeIDPutRequest } from '~/shared/ml-thirdparty-client/models/openapi' diff --git a/src/shared/ml-thirdparty-simulator/factories/transfer.ts b/src/shared/ml-thirdparty-simulator/factories/transfer.ts index 2718da07..f889aa26 100644 --- a/src/shared/ml-thirdparty-simulator/factories/transfer.ts +++ b/src/shared/ml-thirdparty-simulator/factories/transfer.ts @@ -28,21 +28,24 @@ import * as faker from 'faker' import { AuthorizationsPutIdRequest, TransferIDPutRequest, -} from '~/shared/ml-thirdparty-client/models/openapi'; +} from '~/shared/ml-thirdparty-client/models/openapi' -import { TransferState } from '~/shared/ml-thirdparty-client/models/core'; +import { TransferState } from '~/shared/ml-thirdparty-client/models/core' export class TransferFactory { /** * Creates a `PUT /transfers/{ID}` request body that is normally sent * by Mojaloop as a callback to inform about the transfer result. - * + * * @param _ transaction request id of the corresponsing authorization. * @param __ an authorization object as defined by the Mojaloop API. * @param transactionId transaction id to be associated with the transfer object. */ - public static createTransferIdPutRequest(_: string, - __: AuthorizationsPutIdRequest, transactionId: string): TransferIDPutRequest { + public static createTransferIdPutRequest( + _: string, + __: AuthorizationsPutIdRequest, + transactionId: string + ): TransferIDPutRequest { return { transactionId, fulfilment: faker.random.alphaNumeric(43), diff --git a/src/shared/ml-thirdparty-simulator/index.ts b/src/shared/ml-thirdparty-simulator/index.ts index 87429f16..3953d5d8 100644 --- a/src/shared/ml-thirdparty-simulator/index.ts +++ b/src/shared/ml-thirdparty-simulator/index.ts @@ -23,6 +23,8 @@ - Abhimanyu Kapur -------------- ******/ +/* istanbul ignore file */ +// TODO: BDD Testing will covered in separate ticket #1702 import { ServerInjectResponse } from '@hapi/hapi' import * as faker from 'faker' diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 8579f2b4..7927ed76 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -25,7 +25,6 @@ import index from '~/index' import Config from '~/lib/config' -import { Server } from '@hapi/hapi' // Mock firebase to prevent server from listening to the changes. jest.mock('~/lib/firebase') @@ -37,10 +36,10 @@ describe('index', (): void => { }) describe('api routes', (): void => { - let server: Server + let server: StateServer beforeAll( - async (): Promise => { + async (): Promise => { server = await index.server.run(Config) return server } diff --git a/test/unit/server/handlers/firestore/consents.test.ts b/test/unit/server/handlers/firestore/consents.test.ts new file mode 100644 index 00000000..07b92330 --- /dev/null +++ b/test/unit/server/handlers/firestore/consents.test.ts @@ -0,0 +1,662 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/***** + License + -------------- + Copyright © 2020 Mojaloop Foundation + The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the 'License') and you may not use these files except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Contributors + -------------- + This is the official list of the Mojaloop project contributors for this file. + Names of the original copyright holders (individuals or organizations) + should be listed with a '*' in the first column. People who have + contributed from an organization can be listed under the organization + that actually holds the copyright for their contributions (see the + Mojaloop Foundation organization for an example). Those individuals should have + their names indented and be marked with a '-'. Email address can be added + optionally within square brackets . + * Mojaloop Foundation + - Name Surname + + * Google + - Abhimanyu Kapur + -------------- + ******/ + +import config from '~/lib/config' + +import createServer from '~/server/create' +import * as consentsHandler from '~/server/handlers/firestore/consents' + +import { consentRepository } from '~/repositories/consent' +import * as Validator from '~/server/handlers/firestore/consents.validator' +import { Consent, ConsentStatus } from '~/models/consent' +import { + PartyIdType, + Currency, +} from '~/shared/ml-thirdparty-client/models/core' +import SDKStandardComponents from '@mojaloop/sdk-standard-components' +import { logger } from '~/shared/logger' +import { MissingConsentFieldsError, InvalidConsentStatusError } from '~/models/errors' + +// Mock firebase to prevent server from listening to the changes. +jest.mock('~/lib/firebase') + +// Mock uuid to consistently return the provided value. +jest.mock('uuid', () => ({ + v4: jest.fn().mockImplementation(() => '12345'), +})) + +// Mock logger to prevent handlers from logging incoming request +jest.mock('~/shared/logger', () => ({ + logger: { + error: jest.fn().mockImplementation(), + }, +})) + +const documentId = '111' + +describe('Handlers for consent documents in Firebase', () => { + let server: StateServer + // let loggerErrorSpy: jest.SpyInstance + + beforeAll(async () => { + server = await createServer(config) + }) + + beforeEach(() => { + jest.clearAllMocks() + // loggerErrorSpy = jest.spyOn(logger, 'error') + }) + + afterEach(() => { + jest.clearAllTimers() + }) + + describe('OnCreate', () => { + // Set spies + const consentRepositorySpy = jest.spyOn( + consentRepository, + 'updateConsentById' + ) + + it('Should log an error and return, if status field exists', async () => { + const consentWithStatus = { + id: '111', + consentId: 'acv', + userId: 'bob123', + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + } + + await consentsHandler.onCreate(server, consentWithStatus) + + expect(consentRepositorySpy).not.toBeCalled() + }) + + it('Should set status and consentRequestId for new consent', () => { + consentsHandler.onCreate(server, { + id: '111', + userId: 'bob123', + party: { + partyIdInfo: { + partyIdType: PartyIdType.OPAQUE, + partyIdentifier: 'bob1234', + fspId: 'fspb', + }, + }, + }) + + expect(consentRepositorySpy).toBeCalledWith('111', { + consentRequestId: '12345', + status: ConsentStatus.PENDING_PARTY_LOOKUP, + }) + }) + }) + + describe('OnUpdate', () => { + it('Should log an error and return, if status field is missing', async () => { + const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation() + const consentNoStatus = { + id: '111', + consentId: 'acv', + userId: 'bob123', + } + + await consentsHandler.onUpdate(server, consentNoStatus) + + expect(loggerErrorSpy).toBeCalledWith( + 'Invalid consent, undefined status.' + ) + }) + + it('Should throw a InvalidConsentStatusErrro, if status field does not match any ConsentStatus enum', async () => { + const consentInvalidStatus = { + id: '111', + status: 'invalid' as ConsentStatus, + consentId: 'acv', + userId: 'bob123', + } + + expect( + consentsHandler.onUpdate(server, consentInvalidStatus) + ).rejects.toThrow( + new InvalidConsentStatusError( + consentInvalidStatus.status, + consentInvalidStatus.id + ) + ) + }) + + describe('Party Lookup', () => { + // Mocked Methods + let mojaloopClientSpy: jest.SpyInstance + + const validatorSpy = jest + .spyOn(Validator, 'isValidPartyLookup') + .mockReturnValue(true) + + // Mock consent data that would be given by Firebase + const consentPartyLookup: Consent = { + id: documentId, + userId: 'bob123', + party: { + partyIdInfo: { + partyIdType: PartyIdType.OPAQUE, + partyIdentifier: 'bob1234', + }, + }, + consentRequestId: '12345', + status: ConsentStatus.PENDING_PARTY_LOOKUP, + } + + beforeAll(() => { + mojaloopClientSpy = jest + .spyOn(server.app.mojaloopClient, 'getParties') + .mockImplementation() + }) + + it('Should perform party lookup when all necessary fields are set', async () => { + await consentsHandler.onUpdate(server, consentPartyLookup) + + expect(validatorSpy).toBeCalledWith(consentPartyLookup) + expect(mojaloopClientSpy).toBeCalledWith(PartyIdType.OPAQUE, 'bob1234') + }) + + it('Should throw an error if Validator returns false', async () => { + validatorSpy.mockReturnValueOnce(false) + + const consentPartyLookup: Consent = { + id: documentId, + userId: 'bob123', + party: { + partyIdInfo: { + partyIdType: PartyIdType.OPAQUE, + partyIdentifier: 'bob1234', + }, + }, + status: ConsentStatus.PENDING_PARTY_LOOKUP, + } + + await expect( + consentsHandler.onUpdate(server, consentPartyLookup) + ).rejects.toThrow(new MissingConsentFieldsError(consentPartyLookup)) + + expect(validatorSpy).toBeCalledWith(consentPartyLookup) + expect(mojaloopClientSpy).not.toBeCalled() + }) + + it('Should log an error if mojaloop client throws error', async () => { + const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation() + mojaloopClientSpy.mockImplementationOnce(() => { + throw Error('Client not connected') + }) + + await consentsHandler.onUpdate(server, consentPartyLookup) + + expect(validatorSpy).toBeCalledWith(consentPartyLookup) + expect(mojaloopClientSpy).toBeCalledWith(PartyIdType.OPAQUE, 'bob1234') + expect(loggerErrorSpy).toBeCalledWith(new Error('Client not connected')) + }) + }) + + describe('Authentication', () => { + let mojaloopClientSpy: jest.SpyInstance + + const validatorSpy = jest + .spyOn(Validator, 'isValidAuthentication') + .mockReturnValue(true) + + // Mock consent data that would be given by Firebase + const consentAuthentication: Consent = { + id: documentId, + userId: 'bob123', + initiatorId: 'pispa', + party: { + partyIdInfo: { + partyIdType: PartyIdType.MSISDN, + partyIdentifier: 'bob1234', + fspId: 'fspb', + }, + }, + scopes: [ + { + accountId: 'as2342', + actions: ['account.getAccess', 'account.transferMoney'], + }, + { + accountId: 'as22', + actions: ['account.getAccess'], + }, + ], + consentRequestId: '12345', + authChannels: ['WEB'], + authUri: 'http//auth.com', + authToken: '', + accounts: [ + { id: 'bob.aaaaa.fspb', currency: Currency.SGD }, + { id: 'bob.bbbbb.fspb', currency: Currency.USD }, + ], + status: ConsentStatus.AUTHENTICATION_REQUIRED, + } + + // Mock the expected transaction request being sent. + const request: SDKStandardComponents.PutConsentRequestsRequest = { + initiatorId: consentAuthentication.initiatorId!, + scopes: consentAuthentication.scopes!, + authChannels: consentAuthentication.authChannels!, + callbackUri: config.get('mojaloop').pispCallbackUri, + authToken: consentAuthentication.authToken!, + authUri: consentAuthentication.authUri!, + } + + beforeAll(() => { + mojaloopClientSpy = jest + .spyOn(server.app.mojaloopClient, 'putConsentRequests') + .mockImplementation() + }) + + it('Should initiate consent request request when all necessary fields are set', async () => { + await consentsHandler.onUpdate(server, consentAuthentication) + + expect(mojaloopClientSpy).toBeCalledWith( + consentAuthentication.id, + request, + consentAuthentication.party?.partyIdInfo.fspId + ) + expect(validatorSpy).toBeCalledWith(consentAuthentication) + }) + + it('Should throw an error if Validator returns false', async () => { + validatorSpy.mockReturnValueOnce(false) + + await expect( + consentsHandler.onUpdate(server, consentAuthentication) + ).rejects.toThrow(new MissingConsentFieldsError(consentAuthentication)) + + expect(validatorSpy).toBeCalledWith(consentAuthentication) + expect(mojaloopClientSpy).not.toBeCalled() + }) + + it('Should log an error if mojaloop client throws error', async () => { + const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation() + mojaloopClientSpy.mockImplementation(() => { + throw new Error('Client not connected') + }) + + await consentsHandler.onUpdate(server, consentAuthentication) + + expect(mojaloopClientSpy).toBeCalledWith( + consentAuthentication.id, + request, + consentAuthentication.party?.partyIdInfo.fspId + ) + expect(validatorSpy).toBeCalledWith(consentAuthentication) + expect(loggerErrorSpy).toBeCalledWith(new Error('Client not connected')) + }) + }) + + describe('Consent Request', () => { + let mojaloopClientSpy: jest.SpyInstance + + const validatorSpy = jest + .spyOn(Validator, 'isValidConsentRequest') + .mockReturnValue(true) + + // Mock consent data that would be given by Firebase + const consentConsentRequest: Consent = { + id: documentId, + userId: 'bob123', + party: { + partyIdInfo: { + partyIdType: PartyIdType.OPAQUE, + partyIdentifier: 'bob1234', + fspId: 'fspb', + }, + }, + scopes: [ + { + accountId: 'as2342', + actions: ['account.getAccess', 'account.transferMoney'], + }, + { + accountId: 'as22', + actions: ['account.getAccess'], + }, + ], + consentRequestId: '12345', + authChannels: ['WEB'], + accounts: [ + { id: 'bob.aaaaa.fspb', currency: Currency.SGD }, + { id: 'bob.bbbbb.fspb', currency: Currency.USD }, + ], + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + } + + // Mock the expected request being sent. + const consentRequest: SDKStandardComponents.PostConsentRequestsRequest = { + initiatorId: consentConsentRequest.initiatorId!, + id: consentConsentRequest.id, + scopes: consentConsentRequest.scopes!, + authChannels: consentConsentRequest.authChannels!, + callbackUri: config.get('mojaloop').pispCallbackUri, + } + + beforeAll(() => { + mojaloopClientSpy = jest + .spyOn(server.app.mojaloopClient, 'postConsentRequests') + .mockImplementation() + }) + + it('Should initiate consent request request when all necessary fields are set', async () => { + await consentsHandler.onUpdate(server, consentConsentRequest) + + expect(mojaloopClientSpy).toBeCalledWith( + consentRequest, + consentConsentRequest.party?.partyIdInfo.fspId + ) + expect(validatorSpy).toBeCalledWith(consentConsentRequest) + }) + + it('Should throw an error if Validator returns false', async () => { + validatorSpy.mockReturnValueOnce(false) + + await expect( + consentsHandler.onUpdate(server, consentConsentRequest) + ).rejects.toThrow(new MissingConsentFieldsError(consentConsentRequest)) + + expect(validatorSpy).toBeCalledWith(consentConsentRequest) + expect(mojaloopClientSpy).not.toBeCalled() + }) + + it('Should log an error if mojaloop client throws error', async () => { + const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation() + mojaloopClientSpy.mockImplementation(() => { + throw new Error('Client not connected') + }) + + await consentsHandler.onUpdate(server, consentConsentRequest) + + expect(mojaloopClientSpy).toBeCalledWith( + consentRequest, + consentConsentRequest.party?.partyIdInfo.fspId + ) + expect(validatorSpy).toBeCalledWith(consentConsentRequest) + expect(loggerErrorSpy).toBeCalledWith(new Error('Client not connected')) + }) + }) + + describe('Challenge Generation Request', () => { + // Mocked Methods + let mojaloopClientSpy: jest.SpyInstance + + const validatorSpy = jest + .spyOn(Validator, 'isValidGenerateChallengeOrRevokeConsent') + .mockReturnValue(true) + + // Mock consent data that would be given by Firebase + const consentGenerateChallenge: Consent = { + id: '111', + consentId: '2323', + party: { + partyIdInfo: { + partyIdType: PartyIdType.MSISDN, + partyIdentifier: '+1-222-222-2222', + fspId: 'fspb', + }, + }, + status: ConsentStatus.CONSENT_GRANTED, + scopes: [ + { + accountId: 'as2342', + actions: ['account.getAccess', 'account.transferMoney'], + }, + { + accountId: 'as22', + actions: ['account.getAccess'], + }, + ], + consentRequestId: '12345', + authChannels: ['WEB'], + accounts: [ + { id: 'bob.aaaaa.fspb', currency: Currency.SGD }, + { id: 'bob.bbbbb.fspb', currency: Currency.USD }, + ], + } + + beforeAll(() => { + mojaloopClientSpy = jest + .spyOn(server.app.mojaloopClient, 'postGenerateChallengeForConsent') + .mockImplementation() + }) + + it('Should initiate challenge generation request when all necessary fields are set', async () => { + await consentsHandler.onUpdate(server, consentGenerateChallenge) + + expect(validatorSpy).toBeCalledWith(consentGenerateChallenge) + expect(mojaloopClientSpy).toBeCalledWith( + consentGenerateChallenge.consentId, + consentGenerateChallenge.party?.partyIdInfo.fspId + ) + }) + + it('Should throw an error if Validator returns false', async () => { + validatorSpy.mockReturnValueOnce(false) + + await expect( + consentsHandler.onUpdate(server, consentGenerateChallenge) + ).rejects.toThrow( + new MissingConsentFieldsError(consentGenerateChallenge) + ) + + expect(validatorSpy).toBeCalledWith(consentGenerateChallenge) + expect(mojaloopClientSpy).not.toBeCalled() + }) + + it('Should log an error if mojaloop client throws error', async () => { + const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation() + mojaloopClientSpy.mockImplementation(() => { + throw new Error('Client not connected') + }) + + await consentsHandler.onUpdate(server, consentGenerateChallenge) + + expect(validatorSpy).toBeCalledWith(consentGenerateChallenge) + expect(mojaloopClientSpy).toBeCalledWith( + consentGenerateChallenge.consentId, + consentGenerateChallenge.party?.partyIdInfo.fspId + ) + expect(loggerErrorSpy).toBeCalledWith(new Error('Client not connected')) + }) + }) + + describe('Signed Challenge', () => { + let mojaloopClientSpy: jest.SpyInstance + + const validatorSpy = jest + .spyOn(Validator, 'isValidSignedChallenge') + .mockReturnValue(true) + + // Mock the expected transaction request being sent. + const consentVerifiedChallenge = { + id: '111', + consentId: '2323', + initiatorId: 'pispa', + participantId: 'pispb', + scopes: [ + { + accountId: 'as2342', + actions: ['account.getAccess', 'account.transferMoney'], + }, + { + accountId: 'as22', + actions: ['account.getAccess'], + }, + ], + party: { + partyIdInfo: { + partyIdType: PartyIdType.MSISDN, + partyIdentifier: '+1-222-222-2222', + fspId: 'fspb', + }, + }, + status: ConsentStatus.ACTIVE, + credential: { + id: '9876', + credentialType: 'FIDO' as const, + status: 'VERIFIED' as const, + challenge: { + payload: 'string_representing_challenge_payload', + signature: 'string_representing_challenge_signature', + }, + payload: 'string_representing_credential_payload', + }, + } + + beforeAll(() => { + mojaloopClientSpy = jest + .spyOn(server.app.mojaloopClient, 'putConsentId') + .mockImplementation() + }) + + it('Should initiate PUT consent/{ID} request when challenge has been signed and all necessary fields are set', async () => { + await consentsHandler.onUpdate(server, consentVerifiedChallenge) + + expect(validatorSpy).toBeCalledWith(consentVerifiedChallenge) + expect(mojaloopClientSpy).toBeCalledWith( + consentVerifiedChallenge.consentId, + { + requestId: consentVerifiedChallenge.id, + initiatorId: consentVerifiedChallenge.initiatorId, + participantId: consentVerifiedChallenge.participantId, + scopes: consentVerifiedChallenge.scopes, + credential: consentVerifiedChallenge.credential, + }, + consentVerifiedChallenge.party.partyIdInfo.fspId + ) + }) + + it('Should throw an error if Validator returns false', async () => { + validatorSpy.mockReturnValueOnce(false) + + await expect( + consentsHandler.onUpdate(server, consentVerifiedChallenge) + ).rejects.toThrow( + new MissingConsentFieldsError(consentVerifiedChallenge) + ) + + expect(validatorSpy).toBeCalledWith(consentVerifiedChallenge) + expect(mojaloopClientSpy).not.toBeCalled() + }) + + it('Should log an error if mojaloop client throws error', async () => { + const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation() + mojaloopClientSpy.mockImplementation(() => { + throw new Error('Client not connected') + }) + + await consentsHandler.onUpdate(server, consentVerifiedChallenge) + + expect(validatorSpy).toBeCalledWith(consentVerifiedChallenge) + expect(mojaloopClientSpy).toBeCalledWith( + consentVerifiedChallenge.consentId, + { + requestId: consentVerifiedChallenge.id, + initiatorId: consentVerifiedChallenge.initiatorId, + participantId: consentVerifiedChallenge.participantId, + scopes: consentVerifiedChallenge.scopes, + credential: consentVerifiedChallenge.credential, + }, + consentVerifiedChallenge.party.partyIdInfo.fspId + ) + expect(loggerErrorSpy).toBeCalledWith(new Error('Client not connected')) + }) + }) + + describe('Request to Revoke Consent ', () => { + // Mocked Methods + let mojaloopClientSpy: jest.SpyInstance + + const validatorSpy = jest + .spyOn(Validator, 'isValidGenerateChallengeOrRevokeConsent') + .mockReturnValue(true) + + // Mock the expected transaction request being sent. + const consentRevokeRequested = { + id: '111', + consentId: '2323', + party: { + partyIdInfo: { + partyIdType: PartyIdType.MSISDN, + partyIdentifier: '+1-222-222-2222', + fspId: 'fspb', + }, + }, + status: ConsentStatus.REVOKE_REQUESTED, + } + + beforeAll(() => { + mojaloopClientSpy = jest + .spyOn(server.app.mojaloopClient, 'postRevokeConsent') + .mockImplementation() + }) + + it('Should initiate consent revoke request when all necessary fields are set', async () => { + await consentsHandler.onUpdate(server, consentRevokeRequested) + + expect(validatorSpy).toBeCalledWith(consentRevokeRequested) + expect(mojaloopClientSpy).toBeCalledWith( + consentRevokeRequested.consentId, + consentRevokeRequested.party.partyIdInfo.fspId + ) + }) + + it('Should throw an error if Validator returns false', async () => { + validatorSpy.mockReturnValueOnce(false) + + await expect( + consentsHandler.onUpdate(server, consentRevokeRequested) + ).rejects.toThrow(new MissingConsentFieldsError(consentRevokeRequested)) + + expect(validatorSpy).toBeCalledWith(consentRevokeRequested) + expect(mojaloopClientSpy).not.toBeCalled() + }) + + it('Should log an error if mojaloop client throws error', async () => { + const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation() + mojaloopClientSpy.mockImplementation(() => { + throw new Error('Client not connected') + }) + + await consentsHandler.onUpdate(server, consentRevokeRequested) + + expect(validatorSpy).toBeCalledWith(consentRevokeRequested) + expect(mojaloopClientSpy).toBeCalledWith( + consentRevokeRequested.consentId, + consentRevokeRequested.party.partyIdInfo.fspId + ) + expect(loggerErrorSpy).toBeCalledWith(new Error('Client not connected')) + }) + }) + }) +}) diff --git a/test/unit/server/handlers/firestore/consents.validator.test.ts b/test/unit/server/handlers/firestore/consents.validator.test.ts new file mode 100644 index 00000000..866e97bd --- /dev/null +++ b/test/unit/server/handlers/firestore/consents.validator.test.ts @@ -0,0 +1,711 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/***** + License + -------------- + Copyright © 2020 Mojaloop Foundation + The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the 'License') and you may not use these files except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Contributors + -------------- + This is the official list of the Mojaloop project contributors for this file. + Names of the original copyright holders (individuals or organizations) + should be listed with a '*' in the first column. People who have + contributed from an organization can be listed under the organization + that actually holds the copyright for their contributions (see the + Mojaloop Foundation organization for an example). Those individuals should have + their names indented and be marked with a '-'. Email address can be added + optionally within square brackets . + * Mojaloop Foundation + - Name Surname + + * Google + - Abhimanyu Kapur + -------------- + ******/ + +import * as Validator from '~/server/handlers/firestore/consents.validator' +import { ConsentStatus } from '~/models/consent' +import { + PartyIdType, + Currency, +} from '~/shared/ml-thirdparty-client/models/core' +import SDKStandardComponents from '@mojaloop/sdk-standard-components' + +const id = '111' +const consentId = 'abc123' +const userId = 'bob123' +const scopes = [ + { + accountId: 'as2342', + actions: ['account.getAccess', 'account.transferMoney'], + }, + { + accountId: 'as22', + actions: ['account.getAccess'], + }, +] +const party = { + partyIdInfo: { + partyIdType: PartyIdType.OPAQUE, + partyIdentifier: 'bob1234', + }, +} +const partyWithFSPId = { + partyIdInfo: { + partyIdType: PartyIdType.MSISDN, + partyIdentifier: '+1-222-222-2222', + fspId: 'fspb', + }, +} +const consentRequestId = '12345' +const authChannels: SDKStandardComponents.TAuthChannel[] = ['WEB'] +const accounts = [ + { id: 'bob.aaaaa.fspb', currency: Currency.SGD }, + { id: 'bob.bbbbb.fspb', currency: Currency.USD }, +] +const initiatorId = 'pispa' +const authUri = 'http//auth.com' +const authToken = '' +const participantId = 'pispb' +const credential = { + id: '9876', + credentialType: 'FIDO' as const, + status: 'VERIFIED' as const, + challenge: { + payload: 'string_representing_challenge_payload', + signature: 'string_representing_challenge_signature', + }, + payload: 'string_representing_credential_payload', +} + +describe('Validators for different consents used in requests', () => { + describe('isValidPartyLookup', () => { + it('Should return true if all necessary fields are present', () => { + expect( + Validator.isValidPartyLookup({ + id, + userId, + party, + status: ConsentStatus.PENDING_PARTY_LOOKUP, + }) + ).toBe(true) + }) + + it('Should return false if party, partyIdInfo, partyIdType and/or partyIdentifier is not present', () => { + expect( + Validator.isValidPartyLookup({ + id, + userId, + status: ConsentStatus.PENDING_PARTY_LOOKUP, + }) + ).toBe(false) + + expect( + Validator.isValidPartyLookup({ + id, + userId, + // @ts-ignore + party: {}, + status: ConsentStatus.PENDING_PARTY_LOOKUP, + }) + ).toBe(false) + + expect( + Validator.isValidPartyLookup({ + id, + userId, + party: { + // @ts-ignore + partyIdInfo: {}, + }, + status: ConsentStatus.PENDING_PARTY_LOOKUP, + }) + ).toBe(false) + + expect( + Validator.isValidPartyLookup({ + id, + userId, + party: { + // @ts-ignore + partyIdInfo: { + partyIdentifier: 'bob1234', + }, + }, + status: ConsentStatus.PENDING_PARTY_LOOKUP, + }) + ).toBe(false) + + expect( + Validator.isValidPartyLookup({ + id, + userId, + party: { + // @ts-ignore + partyIdInfo: { + partyIdType: PartyIdType.OPAQUE, + }, + }, + status: ConsentStatus.PENDING_PARTY_LOOKUP, + }) + ).toBe(false) + }) + }) + + describe('isValidConsentRequest', () => { + it('Should return true if all necessary fields are present', () => { + expect( + Validator.isValidConsentRequest({ + id, + userId, + initiatorId, + party: partyWithFSPId, + scopes, + authUri, + consentRequestId, + authChannels, + accounts, + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + }) + ).toBe(true) + }) + + it('Should return false if party or partyIdInfo or fspId is not present', () => { + expect( + Validator.isValidConsentRequest({ + id, + initiatorId, + authUri, + userId, + party, + scopes, + consentRequestId, + authChannels, + accounts, + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + }) + ).toBe(false) + + expect( + Validator.isValidConsentRequest({ + id, + initiatorId, + userId, + scopes, + authUri, + consentRequestId, + authChannels, + accounts, + // @ts-ignore + party: {}, + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + }) + ).toBe(false) + + expect( + Validator.isValidConsentRequest({ + id, + userId, + initiatorId, + scopes, + authUri, + consentRequestId, + authChannels, + accounts, + party: { + // @ts-ignore + partyIdInfo: {}, + }, + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + }) + ).toBe(false) + }) + + it('Should return false if authChannels is not present', () => { + expect( + Validator.isValidConsentRequest({ + id, + initiatorId, + userId, + authUri, + party: partyWithFSPId, + scopes, + consentRequestId, + accounts, + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + }) + ).toBe(false) + }) + + it('Should return false if scopes are not present', () => { + expect( + Validator.isValidConsentRequest({ + id, + userId, + authUri, + initiatorId, + party: partyWithFSPId, + authChannels, + consentRequestId, + accounts, + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + }) + ).toBe(false) + }) + + it('Should return false if initiator ID is not present', () => { + expect( + Validator.isValidConsentRequest({ + id, + userId, + authUri, + scopes, + party: partyWithFSPId, + authChannels, + consentRequestId, + accounts, + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + }) + ).toBe(false) + }) + + it('Should return false if authURI is not present', () => { + expect( + Validator.isValidConsentRequest({ + id, + userId, + scopes, + party: partyWithFSPId, + authChannels, + consentRequestId, + accounts, + status: ConsentStatus.PENDING_PARTY_CONFIRMATION, + }) + ).toBe(false) + }) + }) + + describe('isValidAuthentication', () => { + it('Should return true if all necessary fields are present', () => { + expect( + Validator.isValidAuthentication({ + id, + userId, + consentId, + initiatorId, + party: partyWithFSPId, + scopes, + authToken, + consentRequestId, + authChannels, + accounts, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) + ).toBe(true) + }) + + it('Should return false if party or partyIdInfo or fspId is not present', () => { + expect( + Validator.isValidAuthentication({ + id, + initiatorId, + authUri, + consentId, + authToken, + userId, + party, + scopes, + consentRequestId, + authChannels, + accounts, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) + ).toBe(false) + + expect( + Validator.isValidAuthentication({ + id, + initiatorId, + userId, + scopes, + authUri, + consentRequestId, + authChannels, + accounts, + // @ts-ignore + party: {}, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) + ).toBe(false) + + expect( + Validator.isValidConsentRequest({ + id, + userId, + initiatorId, + scopes, + consentId, + authToken, + authUri, + consentRequestId, + authChannels, + accounts, + party: { + // @ts-ignore + partyIdInfo: {}, + }, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) + ).toBe(false) + }) + + it('Should return false if authChannels is not present', () => { + expect( + Validator.isValidAuthentication({ + id, + initiatorId, + userId, + consentId, + authToken, + party: partyWithFSPId, + scopes, + consentRequestId, + accounts, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) + ).toBe(false) + }) + + it('Should return false if auth token are not present', () => { + expect( + Validator.isValidAuthentication({ + id, + userId, + authUri, + consentId, + initiatorId, + party: partyWithFSPId, + authChannels, + consentRequestId, + accounts, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) + ).toBe(false) + }) + + it('Should return false if consent ID is not present', () => { + expect( + Validator.isValidAuthentication({ + id, + userId, + scopes, + authToken, + initiatorId, + party: partyWithFSPId, + authChannels, + consentRequestId, + accounts, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) + ).toBe(false) + }) + + it('Should return false if initiator ID is not present', () => { + expect( + Validator.isValidAuthentication({ + id, + userId, + scopes, + authToken, + consentId, + party: partyWithFSPId, + authChannels, + consentRequestId, + accounts, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) + ).toBe(false) + }) + }) + + describe('isValidChallengeGeneration', () => { + it('Should return true if all necessary fields are present', () => { + expect( + Validator.isValidGenerateChallengeOrRevokeConsent({ + id, + consentId, + party: partyWithFSPId, + status: ConsentStatus.CONSENT_GRANTED, + scopes, + consentRequestId, + authChannels, + accounts, + }) + ).toBe(true) + }) + + it('Should return false if party or partyIdInfo or fspId is not present', () => { + expect( + Validator.isValidGenerateChallengeOrRevokeConsent({ + id, + initiatorId, + consentId, + userId, + party, + scopes, + consentRequestId, + authChannels, + accounts, + status: ConsentStatus.CONSENT_GRANTED, + }) + ).toBe(false) + + expect( + Validator.isValidGenerateChallengeOrRevokeConsent({ + id, + initiatorId, + consentId, + userId, + scopes, + consentRequestId, + authChannels, + accounts, + // @ts-ignore + party: {}, + status: ConsentStatus.CONSENT_GRANTED, + }) + ).toBe(false) + + expect( + Validator.isValidGenerateChallengeOrRevokeConsent({ + id, + userId, + initiatorId, + scopes, + consentId, + consentRequestId, + authChannels, + accounts, + party: { + // @ts-ignore + partyIdInfo: {}, + }, + status: ConsentStatus.CONSENT_GRANTED, + }) + ).toBe(false) + }) + + it('Should return false if consent ID is not present', () => { + expect( + Validator.isValidGenerateChallengeOrRevokeConsent({ + id, + userId, + initiatorId, + party: partyWithFSPId, + status: ConsentStatus.CONSENT_GRANTED, + }) + ).toBe(false) + }) + }) + + describe('isValidSignedChallenge', () => { + it('Should return true if all necessary fields are present', () => { + expect( + Validator.isValidSignedChallenge({ + id, + consentId, + initiatorId, + party: partyWithFSPId, + scopes, + participantId, + credential, + status: ConsentStatus.CHALLENGE_GENERATED, + }) + ).toBe(true) + }) + + it('Should return false if party or partyIdInfo or fspId is not present', () => { + expect( + Validator.isValidSignedChallenge({ + id, + initiatorId, + authUri, + consentId, + credential, + participantId, + authToken, + userId, + party, + scopes, + consentRequestId, + status: ConsentStatus.CHALLENGE_GENERATED, + }) + ).toBe(false) + + expect( + Validator.isValidSignedChallenge({ + id, + initiatorId, + userId, + scopes, + credential, + participantId, + consentRequestId, + authChannels, + accounts, + // @ts-ignore + party: {}, + status: ConsentStatus.CHALLENGE_GENERATED, + }) + ).toBe(false) + + expect( + Validator.isValidSignedChallenge({ + id, + userId, + initiatorId, + scopes, + consentId, + credential, + participantId, + consentRequestId, + authChannels, + accounts, + party: { + // @ts-ignore + partyIdInfo: {}, + }, + status: ConsentStatus.CHALLENGE_GENERATED, + }) + ).toBe(false) + }) + + it('Should return false if credential is not present', () => { + expect( + Validator.isValidSignedChallenge({ + id, + initiatorId, + userId, + consentId, + participantId, + party: partyWithFSPId, + scopes, + consentRequestId, + accounts, + status: ConsentStatus.CHALLENGE_GENERATED, + }) + ).toBe(false) + }) + + it('Should return false if initiator ID is not present', () => { + expect( + Validator.isValidSignedChallenge({ + id, + userId, + authUri, + scopes, + party: partyWithFSPId, + authChannels, + consentRequestId, + accounts, + status: ConsentStatus.CHALLENGE_GENERATED, + }) + ).toBe(false) + }) + + it('Should return false if participant ID is not present', () => { + expect( + Validator.isValidAuthentication({ + id, + userId, + scopes, + authToken, + credential, + initiatorId, + party: partyWithFSPId, + consentRequestId, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) + ).toBe(false) + }) + }) + + describe('isValidRevokeConsent', () => { + it('Should return true if all necessary fields are present', () => { + expect( + Validator.isValidGenerateChallengeOrRevokeConsent({ + id, + userId, + consentId, + initiatorId, + party: partyWithFSPId, + status: ConsentStatus.REVOKE_REQUESTED, + }) + ).toBe(true) + }) + + it('Should return false if party or partyIdInfo or fspId is not present', () => { + expect( + Validator.isValidGenerateChallengeOrRevokeConsent({ + id, + initiatorId, + consentId, + userId, + party, + scopes, + consentRequestId, + authChannels, + accounts, + status: ConsentStatus.REVOKE_REQUESTED, + }) + ).toBe(false) + + expect( + Validator.isValidGenerateChallengeOrRevokeConsent({ + id, + initiatorId, + consentId, + userId, + scopes, + consentRequestId, + authChannels, + accounts, + // @ts-ignore + party: {}, + status: ConsentStatus.REVOKE_REQUESTED, + }) + ).toBe(false) + + expect( + Validator.isValidConsentRequest({ + id, + userId, + initiatorId, + scopes, + consentId, + consentRequestId, + authChannels, + accounts, + party: { + // @ts-ignore + partyIdInfo: {}, + }, + status: ConsentStatus.REVOKE_REQUESTED, + }) + ).toBe(false) + }) + + it('Should return false if consent ID is not present', () => { + expect( + Validator.isValidGenerateChallengeOrRevokeConsent({ + id, + userId, + initiatorId, + party: partyWithFSPId, + status: ConsentStatus.REVOKE_REQUESTED, + }) + ).toBe(false) + }) + }) +}) diff --git a/test/unit/server/handlers/firestore/transactions.test.ts b/test/unit/server/handlers/firestore/transactions.test.ts index 7de4eb58..02c18c37 100644 --- a/test/unit/server/handlers/firestore/transactions.test.ts +++ b/test/unit/server/handlers/firestore/transactions.test.ts @@ -185,7 +185,7 @@ describe('Handlers for transaction documents in Firebase', () => { const consentData = createStubConsentData() const consentRepositorySpy = jest - .spyOn(consentRepository, 'getByConsentId') + .spyOn(consentRepository, 'getConsentById') .mockImplementation(() => new Promise((resolve) => resolve(consentData))) // Mock the expected transaction request being sent. diff --git a/test/unit/server/handlers/openapi/mojaloop/authorizations.test.ts b/test/unit/server/handlers/openapi/mojaloop/authorizations.test.ts index 55d8cee5..cf22cc66 100644 --- a/test/unit/server/handlers/openapi/mojaloop/authorizations.test.ts +++ b/test/unit/server/handlers/openapi/mojaloop/authorizations.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /***** License -------------- @@ -25,6 +26,7 @@ import { ResponseToolkit, ResponseObject } from '@hapi/hapi' import { Context } from 'openapi-backend' +import { Enum } from '@mojaloop/central-services-shared' import { PartyFactory } from '~/shared/ml-thirdparty-simulator/factories/party' @@ -54,15 +56,18 @@ jest.mock('~/lib/firebase') const mockRequest = jest.fn().mockImplementation() -const mockResponseToolkit = { +// @ts-ignore +const mockResponseToolkit: ResponseToolkit = { response: (): ResponseObject => { - return { + return ({ code: (num: number): ResponseObject => { - return num as unknown as ResponseObject - } - } as unknown as ResponseObject - } -} as unknown as ResponseToolkit + return ({ + statusCode: num, + } as unknown) as ResponseObject + }, + } as unknown) as ResponseObject + }, +} /** * Mock data for transaction request. @@ -129,7 +134,7 @@ describe('/authorizations', () => { .spyOn(transactionRepository, 'update') .mockImplementation() - it('Should return 200 and update data in Firebase', async () => { + it('Should return 202 and update data in Firebase', async () => { const response = await Authorizations.post( context, mockRequest, @@ -151,7 +156,7 @@ describe('/authorizations', () => { } ) - expect(response).toBe(202) + expect(response.statusCode).toBe(Enum.Http.ReturnCodes.ACCEPTED.CODE) }) }) }) diff --git a/test/unit/server/handlers/openapi/mojaloop/consentRequests/{ID}.test.ts b/test/unit/server/handlers/openapi/mojaloop/consentRequests/{ID}.test.ts new file mode 100644 index 00000000..9b2f21ac --- /dev/null +++ b/test/unit/server/handlers/openapi/mojaloop/consentRequests/{ID}.test.ts @@ -0,0 +1,129 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/***** + License + -------------- + Copyright © 2020 Mojaloop Foundation + The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the 'License') and you may not use these files except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Contributors + -------------- + This is the official list of the Mojaloop project contributors for this file. + Names of the original copyright holders (individuals or organizations) + should be listed with a '*' in the first column. People who have + contributed from an organization can be listed under the organization + that actually holds the copyright for their contributions (see the + Mojaloop Foundation organization for an example). Those individuals should have + their names indented and be marked with a '-'. Email address can be added + optionally within square brackets . + * Mojaloop Foundation + - Name Surname + + * Google + - Abhimanyu Kapur + -------------- + ******/ + +import { ResponseToolkit, ResponseObject } from '@hapi/hapi' + +import { consentRepository } from '~/repositories/consent' +import { Context } from 'openapi-backend' +import { Enum } from '@mojaloop/central-services-shared' + +import * as ConsentHandlers from '~/server/handlers/openapi/mojaloop/consentRequests/{ID}' + +import config from '~/lib/config' +import { ConsentFactory } from '~/shared/ml-thirdparty-simulator/factories/consents' +import SDKStandardComponents from '@mojaloop/sdk-standard-components' +import { ConsentStatus } from '~/models/consent' + +// Mock the factories to consistently return the hardcoded values. +jest.mock('~/shared/ml-thirdparty-simulator/factories/consents') + +// Mock logger to prevent handlers from logging incoming request +jest.mock('~/shared/logger', () => ({ + logger: { + logRequest: jest.fn().mockImplementation(), + }, +})) + +// Mock firebase to prevent transaction repository from opening the connection. +jest.mock('~/lib/firebase') + +const mockRequest = jest.fn().mockImplementation() + +// @ts-ignore +const mockResponseToolkit: ResponseToolkit = { + response: (): ResponseObject => { + return ({ + code: (num: number): ResponseObject => { + return ({ + statusCode: num, + } as unknown) as ResponseObject + }, + } as unknown) as ResponseObject + }, +} +const postConsentRequestRequest: SDKStandardComponents.PostConsentRequestsRequest = { + id: '111', + initiatorId: 'pispA', + authChannels: ['WEB', 'OTP'], + scopes: [ + { + accountId: 'as2342', + actions: ['account.getAccess', 'account.transferMoney'], + }, + { + accountId: 'as22', + actions: ['account.getAccess'], + }, + ], + callbackUri: config.get('mojaloop').pispCallbackUri, +} + +describe('/consentRequests/{ID}', () => { + beforeEach(() => { + jest.clearAllMocks() + jest.clearAllTimers() + }) + + describe('PUT operation', () => { + const requestBody = ConsentFactory.createPutConsentRequestIdRequest( + postConsentRequestRequest + ) + + const context = ({ + request: { + headers: { + host: 'mojaloop.' + config.get('hostname'), + 'content-type': 'application/json', + 'content-length': JSON.stringify(requestBody).length, + }, + params: { id: '99' }, + body: requestBody, + }, + } as unknown) as Context + + const consentRepositorySpy = jest + .spyOn(consentRepository, 'updateConsentById') + .mockImplementation() + + it('Should return 200 and update data in Firebase', async () => { + const response = await ConsentHandlers.put( + context, + mockRequest, + mockResponseToolkit + ) + + const { authChannels, authUri } = requestBody + + expect(consentRepositorySpy).toBeCalledWith(context.request.params.ID, { + authChannels, + authUri, + status: ConsentStatus.AUTHENTICATION_REQUIRED, + }) + + expect(response.statusCode).toBe(Enum.Http.ReturnCodes.OK.CODE) + }) + }) +}) diff --git a/test/unit/server/handlers/openapi/mojaloop/consents.test.ts b/test/unit/server/handlers/openapi/mojaloop/consents.test.ts new file mode 100644 index 00000000..58bcc59a --- /dev/null +++ b/test/unit/server/handlers/openapi/mojaloop/consents.test.ts @@ -0,0 +1,135 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/***** + License + -------------- + Copyright © 2020 Mojaloop Foundation + The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the 'License') and you may not use these files except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Contributors + -------------- + This is the official list of the Mojaloop project contributors for this file. + Names of the original copyright holders (individuals or organizations) + should be listed with a '*' in the first column. People who have + contributed from an organization can be listed under the organization + that actually holds the copyright for their contributions (see the + Mojaloop Foundation organization for an example). Those individuals should have + their names indented and be marked with a '-'. Email address can be added + optionally within square brackets . + * Mojaloop Foundation + - Name Surname + + * Google + - Abhimanyu Kapur + -------------- + ******/ + +import { ResponseToolkit, ResponseObject } from '@hapi/hapi' + +import { consentRepository } from '~/repositories/consent' +import { Context } from 'openapi-backend' +import { Enum } from '@mojaloop/central-services-shared' + +import * as ConsentHandlers from '~/server/handlers/openapi/mojaloop/consents' + +import config from '~/lib/config' +import { ConsentFactory } from '~/shared/ml-thirdparty-simulator/factories/consents' +import SDKStandardComponents from '@mojaloop/sdk-standard-components' +import { ConsentStatus } from '~/models/consent' + +// Mock the factories to consistently return the hardcoded values. +jest.mock('~/shared/ml-thirdparty-simulator/factories/consents') + +// Mock logger to prevent handlers from logging incoming request +jest.mock('~/shared/logger', () => ({ + logger: { + logRequest: jest.fn().mockImplementation(), + }, +})) + +// Mock firebase to prevent transaction repository from opening the connection. +jest.mock('~/lib/firebase') + +const mockRequest = jest.fn().mockImplementation() + +// @ts-ignore +const mockResponseToolkit: ResponseToolkit = { + response: (): ResponseObject => { + return ({ + code: (num: number): ResponseObject => { + return ({ + statusCode: num, + } as unknown) as ResponseObject + }, + } as unknown) as ResponseObject + }, +} + +const consentRequestId = '111' + +const putConsentRequestRequest: SDKStandardComponents.PutConsentRequestsRequest = { + initiatorId: 'pispA', + authChannels: ['WEB', 'OTP'], + scopes: [ + { + accountId: 'as2342', + actions: ['account.getAccess', 'account.transferMoney'], + }, + { + accountId: 'as22', + actions: ['account.getAccess'], + }, + ], + callbackUri: config.get('mojaloop').pispCallbackUri, + authUri: 'https://dfspAuth.com', + authToken: 'secret-token', +} + +describe('/consents', () => { + beforeEach(() => { + jest.clearAllMocks() + jest.clearAllTimers() + }) + + describe('POST operation', () => { + const requestBody = ConsentFactory.createPostConsentRequest( + consentRequestId, + putConsentRequestRequest + ) + + const context = ({ + request: { + headers: { + host: 'mojaloop.' + config.get('hostname'), + 'content-type': 'application/json', + 'content-length': JSON.stringify(requestBody).length, + }, + params: {}, + body: requestBody, + }, + } as unknown) as Context + + const consentRepositorySpy = jest + .spyOn(consentRepository, 'updateConsentById') + .mockImplementation() + + it('Should return 200 and update data in Firebase', async () => { + const response = await ConsentHandlers.post( + context, + mockRequest, + mockResponseToolkit + ) + + const { id, initiatorId, participantId, scopes } = context.request.body + + expect(consentRepositorySpy).toBeCalledWith(id, { + initiatorId, + participantId, + scopes, + status: ConsentStatus.CONSENT_GRANTED, + }) + + expect(response.statusCode).toBe(Enum.Http.ReturnCodes.ACCEPTED.CODE) + }) + }) +}) diff --git a/test/unit/server/handlers/openapi/mojaloop/consents/{ID}.test.ts b/test/unit/server/handlers/openapi/mojaloop/consents/{ID}.test.ts new file mode 100644 index 00000000..f45c0dfd --- /dev/null +++ b/test/unit/server/handlers/openapi/mojaloop/consents/{ID}.test.ts @@ -0,0 +1,141 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/***** + License + -------------- + Copyright © 2020 Mojaloop Foundation + The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the 'License') and you may not use these files except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Contributors + -------------- + This is the official list of the Mojaloop project contributors for this file. + Names of the original copyright holders (individuals or organizations) + should be listed with a '*' in the first column. People who have + contributed from an organization can be listed under the organization + that actually holds the copyright for their contributions (see the + Mojaloop Foundation organization for an example). Those individuals should have + their names indented and be marked with a '-'. Email address can be added + optionally within square brackets . + * Mojaloop Foundation + - Name Surname + + * Google + - Abhimanyu Kapur + -------------- + ******/ + +import { ResponseToolkit, ResponseObject } from '@hapi/hapi' + +import { consentRepository } from '~/repositories/consent' +import { Context } from 'openapi-backend' +import { Enum } from '@mojaloop/central-services-shared' + +import * as ConsentHandlers from '~/server/handlers/openapi/mojaloop/consents/{ID}' + +import config from '~/lib/config' +import { ConsentFactory } from '~/shared/ml-thirdparty-simulator/factories/consents' + +// Mock the factories to consistently return the hardcoded values. +jest.mock('~/shared/ml-thirdparty-simulator/factories/consents') + +// Mock logger to prevent handlers from logging incoming request +jest.mock('~/shared/logger', () => ({ + logger: { + logRequest: jest.fn().mockImplementation(), + }, +})) + +// Mock firebase to prevent transaction repository from opening the connection. +jest.mock('~/lib/firebase') + +const mockRequest = jest.fn().mockImplementation() + +// @ts-ignore +const mockResponseToolkit: ResponseToolkit = { + response: (): ResponseObject => { + return ({ + code: (num: number): ResponseObject => { + return ({ + statusCode: num, + } as unknown) as ResponseObject + }, + } as unknown) as ResponseObject + }, +} + +describe('/consents/{ID}', () => { + beforeEach(() => { + jest.clearAllMocks() + jest.clearAllTimers() + }) + + describe('PUT operation', () => { + const requestBody = ConsentFactory.createPutConsentIdRequest() + + const context = ({ + request: { + headers: { + host: 'mojaloop.' + config.get('hostname'), + 'content-type': 'application/json', + 'content-length': JSON.stringify(requestBody).length, + }, + params: { id: '99' }, + body: requestBody, + }, + } as unknown) as Context + + const consentRepositorySpy = jest + .spyOn(consentRepository, 'updateConsentById') + .mockImplementation() + + it('Should return 200 and update data in Firebase', async () => { + const response = await ConsentHandlers.put( + context, + mockRequest, + mockResponseToolkit + ) + + expect(consentRepositorySpy).toBeCalledWith( + context.request.params.ID, + requestBody + ) + + expect(response.statusCode).toBe(Enum.Http.ReturnCodes.OK.CODE) + }) + }) + + describe('PATCH operation', () => { + const requestBody = ConsentFactory.createPatchConsentRevokeRequest() + + const context = ({ + request: { + headers: { + host: 'mojaloop.' + config.get('hostname'), + 'content-type': 'application/json', + 'content-length': JSON.stringify(requestBody).length, + }, + params: { id: '99' }, + body: requestBody, + }, + } as unknown) as Context + + const consentRepositorySpy = jest + .spyOn(consentRepository, 'updateConsentById') + .mockImplementation() + + it('Should return 200 and update data in Firebase', async () => { + const response = await ConsentHandlers.put( + context, + mockRequest, + mockResponseToolkit + ) + + expect(consentRepositorySpy).toBeCalledWith( + context.request.params.ID, + requestBody + ) + + expect(response.statusCode).toBe(Enum.Http.ReturnCodes.OK.CODE) + }) + }) +}) diff --git a/test/unit/server/handlers/openapi/mojaloop/participants.test.ts b/test/unit/server/handlers/openapi/mojaloop/participants.test.ts new file mode 100644 index 00000000..3e8af26b --- /dev/null +++ b/test/unit/server/handlers/openapi/mojaloop/participants.test.ts @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/***** + License + -------------- + Copyright © 2020 Mojaloop Foundation + The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the 'License') and you may not use these files except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + Contributors + -------------- + This is the official list of the Mojaloop project contributors for this file. + Names of the original copyright holders (individuals or organizations) + should be listed with a '*' in the first column. People who have + contributed from an organization can be listed under the organization + that actually holds the copyright for their contributions (see the + Mojaloop Foundation organization for an example). Those individuals should have + their names indented and be marked with a '-'. Email address can be added + optionally within square brackets . + * Mojaloop Foundation + - Name Surname + + * Google + - Abhimanyu Kapur + -------------- + ******/ + +import { ResponseToolkit, ResponseObject } from '@hapi/hapi' + +import { participantRepository } from '~/repositories/participants' +import { Context } from 'openapi-backend' +import { Enum } from '@mojaloop/central-services-shared' + +import * as ParticipantHandlers from '~/server/handlers/openapi/mojaloop/participants' + +import config from '~/lib/config' +import { ParticipantFactory } from '~/shared/ml-thirdparty-simulator/factories/participant' + +// Mock the factories to consistently return the hardcoded values. +jest.mock('~/shared/ml-thirdparty-simulator/factories/participant') + +// Mock logger to prevent handlers from logging incoming request +jest.mock('~/shared/logger', () => ({ + logger: { + logRequest: jest.fn().mockImplementation(), + }, +})) + +// Mock firebase to prevent transaction repository from opening the connection. +jest.mock('~/lib/firebase') + +const mockRequest = jest.fn().mockImplementation() + +// @ts-ignore +const mockResponseToolkit: ResponseToolkit = { + response: (): ResponseObject => { + return ({ + code: (num: number): ResponseObject => { + return ({ + statusCode: num, + } as unknown) as ResponseObject + }, + } as unknown) as ResponseObject + }, +} + +describe('/parties/{Type}/{ID}', () => { + beforeEach(() => { + jest.clearAllMocks() + jest.clearAllTimers() + }) + + describe('PUT operation', () => { + const requestBody = { participants: ParticipantFactory.getParticipants() } + + const context = ({ + request: { + headers: { + host: 'mojaloop.' + config.get('hostname'), + 'content-type': 'application/json', + 'content-length': JSON.stringify(requestBody).length, + }, + params: {}, + body: requestBody, + }, + } as unknown) as Context + + const participantRepositorySpy = jest + .spyOn(participantRepository, 'replace') + .mockImplementation() + + it('Should return 200 and update data in Firebase', async () => { + const response = await ParticipantHandlers.put( + context, + mockRequest, + mockResponseToolkit + ) + + expect(participantRepositorySpy).toBeCalledWith(requestBody.participants) + + expect(response.statusCode).toBe(Enum.Http.ReturnCodes.OK.CODE) + }) + }) +}) diff --git a/test/unit/server/handlers/openapi/mojaloop/parties/{Type}/{ID}.test.ts b/test/unit/server/handlers/openapi/mojaloop/parties/{Type}/{ID}.test.ts index c37a83f5..e876b659 100644 --- a/test/unit/server/handlers/openapi/mojaloop/parties/{Type}/{ID}.test.ts +++ b/test/unit/server/handlers/openapi/mojaloop/parties/{Type}/{ID}.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /***** License -------------- @@ -27,6 +28,7 @@ import { ResponseToolkit, ResponseObject } from '@hapi/hapi' import { PartyFactory } from '~/shared/ml-thirdparty-simulator/factories/party' import { PartyIdType } from '~/shared/ml-thirdparty-client/models/core' +import { Enum } from '@mojaloop/central-services-shared' import { transactionRepository } from '~/repositories/transaction' import * as PartiesByTypeAndIdHandlers from '~/server/handlers/openapi/mojaloop/parties/{Type}/{ID}' @@ -51,16 +53,18 @@ jest.mock('~/lib/firebase') const mockRequest = jest.fn().mockImplementation() -const mockResponseToolkit = { +// @ts-ignore +const mockResponseToolkit: ResponseToolkit = { response: (): ResponseObject => { - return { + return ({ code: (num: number): ResponseObject => { - return num as unknown as ResponseObject - } - } as unknown as ResponseObject - } -} as unknown as ResponseToolkit - + return ({ + statusCode: num, + } as unknown) as ResponseObject + }, + } as unknown) as ResponseObject + }, +} describe('/parties/{Type}/{ID}', () => { beforeEach(() => { jest.clearAllMocks() @@ -102,7 +106,7 @@ describe('/parties/{Type}/{ID}', () => { } ) - expect(response).toBe(200) + expect(response.statusCode).toBe(Enum.Http.ReturnCodes.OK.CODE) }) }) }) diff --git a/test/unit/server/handlers/openapi/mojaloop/transfers/{ID}.test.ts b/test/unit/server/handlers/openapi/mojaloop/transfers/{ID}.test.ts index d0bcde2b..ba45ab05 100644 --- a/test/unit/server/handlers/openapi/mojaloop/transfers/{ID}.test.ts +++ b/test/unit/server/handlers/openapi/mojaloop/transfers/{ID}.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /***** License -------------- @@ -25,6 +26,7 @@ import { ResponseToolkit, ResponseObject } from '@hapi/hapi' import { Context } from 'openapi-backend' +import { Enum } from '@mojaloop/central-services-shared' import { AuthenticationResponseType, AuthenticationType } from '~/shared/ml-thirdparty-client/models/core' import { AuthorizationsPutIdRequest } from '~/shared/ml-thirdparty-client/models/openapi' @@ -50,15 +52,18 @@ jest.mock('~/lib/firebase') const mockRequest = jest.fn().mockImplementation() -const mockResponseToolkit = { +// @ts-ignore +const mockResponseToolkit: ResponseToolkit = { response: (): ResponseObject => { - return { + return ({ code: (num: number): ResponseObject => { - return num as unknown as ResponseObject - } - } as unknown as ResponseObject - } -} as unknown as ResponseToolkit + return ({ + statusCode: num, + } as unknown) as ResponseObject + }, + } as unknown) as ResponseObject + }, +} describe('/transfers/{ID}', () => { beforeEach(() => { @@ -107,7 +112,7 @@ describe('/transfers/{ID}', () => { } ) - expect(response).toBe(200) + expect(response.statusCode).toBe(Enum.Http.ReturnCodes.OK.CODE) }) }) }) diff --git a/test/unit/shared/ml-thirdparty-client/index.test.ts b/test/unit/shared/ml-thirdparty-client/index.test.ts index db786323..c88d4275 100644 --- a/test/unit/shared/ml-thirdparty-client/index.test.ts +++ b/test/unit/shared/ml-thirdparty-client/index.test.ts @@ -39,6 +39,7 @@ import { ThirdPartyTransactionRequest, } from '~/shared/ml-thirdparty-client/models/openapi' import SDKStandardComponents from '@mojaloop/sdk-standard-components' +import config from '~/lib/config' import { NotImplementedError } from '~/shared/errors' const transactionRequestData: ThirdPartyTransactionRequest = { @@ -117,7 +118,7 @@ const putConsentRequestRequest: SDKStandardComponents.PutConsentRequestsRequest initiatorId: 'pispA', authChannels: ['WEB', 'OTP'], scopes, - callbackUri: 'https://pisp.com', + callbackUri: config.get('mojaloop').pispCallbackUri, authUri: 'https://dfspAuth.com', authToken: 'secret-token', } @@ -152,24 +153,19 @@ describe('Mojaloop third-party client', () => { }) }) - it('Should throw Not Implemented error, attempting to perform party lookup', (): void => { - expect( - client.getParties(PartyIdType.MSISDN, '+1-111-111-1111') - ).rejects.toThrow(new NotImplementedError()) - - // TODO: Use this test once implemented - // // Arrange - // const getPartiesSpy = jest - // .spyOn(client.mojaloopRequests.get, 'getParties') - // .mockImplementation() - // const type = PartyIdType.MSISDN - // const identifier = '+1-111-111-1111' + it('Should perform party lookup', (): void => { + // Arrange + const getPartiesSpy = jest + .spyOn(client.mojaloopRequests, 'getParties') + .mockImplementation() + const type = PartyIdType.MSISDN + const identifier = '+1-111-111-1111' - // // Act - // client.getParties(PartyIdType.MSISDN, '+1-111-111-1111') + // Act + client.getParties(PartyIdType.MSISDN, '+1-111-111-1111') - // // Assert - // expect(getPartiesSpy).toBeCalledWith(type, identifier) + // Assert + expect(getPartiesSpy).toBeCalledWith(type, identifier) }) it('Should perform transaction request', (): void => { @@ -262,9 +258,9 @@ describe('Mojaloop third-party client', () => { }) it('Should throw Not Implemented error, attempting to perform a request to generate a challenge for consent,', (): void => { - expect(client.postGenerateChallengeForConsent(consentId)).rejects.toThrow( - new NotImplementedError() - ) + expect( + client.postGenerateChallengeForConsent(consentId, destParticipantId) + ).rejects.toThrow(new NotImplementedError()) // TODO: Use this test once implemented // // Arrange @@ -297,9 +293,9 @@ describe('Mojaloop third-party client', () => { }) it('Should throw Not Implemented error, attempting to perform a post request to revoke a given consent,', (): void => { - expect(client.postRevokeConsent(consentId)).rejects.toThrow( - new NotImplementedError() - ) + expect( + client.postRevokeConsent(consentId, destParticipantId) + ).rejects.toThrow(new NotImplementedError()) // TODO: Use this test once implemented // // Arrange diff --git a/test/unit/shared/ml-thirdparty-simulator/index.test.ts b/test/unit/shared/ml-thirdparty-simulator/index.test.ts index 353ec59c..a6fdef43 100644 --- a/test/unit/shared/ml-thirdparty-simulator/index.test.ts +++ b/test/unit/shared/ml-thirdparty-simulator/index.test.ts @@ -104,7 +104,7 @@ const postConsentRequestRequest: SDKStandardComponents.PostConsentRequestsReques initiatorId: 'pispA', authChannels: ['WEB', 'OTP'], scopes, - callbackUri: 'https://pisp.com', + callbackUri: config.get('mojaloop').pispCallbackUri, } const putConsentRequestRequest: SDKStandardComponents.PutConsentRequestsRequest = { @@ -112,7 +112,7 @@ const putConsentRequestRequest: SDKStandardComponents.PutConsentRequestsRequest initiatorId: 'pispA', authChannels: ['WEB', 'OTP'], scopes, - callbackUri: 'https://pisp.com', + callbackUri: config.get('mojaloop').pispCallbackUri, authUri: 'https://dfspAuth.com', authToken: 'secret-token', }